@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/mcp.js CHANGED
@@ -133,8 +133,12 @@ var init_session_types = __esm({
133
133
  title: z.string().optional(),
134
134
  commitHash: z.string().optional(),
135
135
  risk: z.string().optional(),
136
- realizedRisk: z.string().optional()
136
+ realizedRisk: z.string().optional(),
137
+ startedAt: z.string().optional(),
138
+ completedAt: z.string().optional()
137
139
  })).default([]),
140
+ // T-187: Per-ticket timing -- set when ticket is picked, cleared on commit
141
+ ticketStartedAt: z.string().nullable().default(null),
138
142
  // FINALIZE checkpoint
139
143
  finalizeCheckpoint: z.enum(["staged", "staged_override", "precommit_passed", "committed"]).nullable().default(null),
140
144
  // Git state
@@ -196,6 +200,10 @@ var init_session_types = __esm({
196
200
  lastGuideCall: z.string().optional(),
197
201
  startedAt: z.string(),
198
202
  guideCallCount: z.number().default(0),
203
+ // ISS-098: Codex availability cache -- skip codex after failure
204
+ // ISS-110: Changed from boolean to ISO timestamp with 10-minute TTL
205
+ codexUnavailable: z.boolean().optional(),
206
+ codexUnavailableSince: z.string().optional(),
199
207
  // Supersession tracking
200
208
  supersededBy: z.string().optional(),
201
209
  supersededSession: z.string().optional(),
@@ -555,12 +563,13 @@ var init_project_state = __esm({
555
563
  totalTicketCount;
556
564
  openTicketCount;
557
565
  completeTicketCount;
558
- openIssueCount;
566
+ activeIssueCount;
559
567
  issuesBySeverity;
560
568
  activeNoteCount;
561
569
  archivedNoteCount;
562
570
  activeLessonCount;
563
571
  deprecatedLessonCount;
572
+ lessonTags;
564
573
  constructor(input) {
565
574
  this.tickets = input.tickets;
566
575
  this.issues = input.issues;
@@ -641,19 +650,19 @@ var init_project_state = __esm({
641
650
  lByID.set(l.id, l);
642
651
  }
643
652
  this.lessonsByID = lByID;
644
- this.totalTicketCount = input.tickets.length;
645
- this.openTicketCount = input.tickets.filter(
653
+ this.totalTicketCount = this.leafTickets.length;
654
+ this.openTicketCount = this.leafTickets.filter(
646
655
  (t) => t.status !== "complete"
647
656
  ).length;
648
- this.completeTicketCount = input.tickets.filter(
657
+ this.completeTicketCount = this.leafTickets.filter(
649
658
  (t) => t.status === "complete"
650
659
  ).length;
651
- this.openIssueCount = input.issues.filter(
652
- (i) => i.status === "open"
660
+ this.activeIssueCount = input.issues.filter(
661
+ (i) => i.status !== "resolved"
653
662
  ).length;
654
663
  const bySev = /* @__PURE__ */ new Map();
655
664
  for (const i of input.issues) {
656
- if (i.status === "open") {
665
+ if (i.status !== "resolved") {
657
666
  bySev.set(i.severity, (bySev.get(i.severity) ?? 0) + 1);
658
667
  }
659
668
  }
@@ -670,6 +679,7 @@ var init_project_state = __esm({
670
679
  this.deprecatedLessonCount = this.lessons.filter(
671
680
  (l) => l.status === "deprecated" || l.status === "superseded"
672
681
  ).length;
682
+ this.lessonTags = [...new Set(this.lessons.flatMap((l) => l.tags ?? []))].sort();
673
683
  }
674
684
  // --- Query Methods ---
675
685
  isUmbrella(ticket) {
@@ -1843,7 +1853,7 @@ function formatStatus(state, format, activeSessions = []) {
1843
1853
  completeTickets: state.completeLeafTicketCount,
1844
1854
  openTickets: state.leafTicketCount - state.completeLeafTicketCount,
1845
1855
  blockedTickets: state.blockedCount,
1846
- openIssues: state.openIssueCount,
1856
+ openIssues: state.activeIssueCount,
1847
1857
  activeNotes: state.activeNoteCount,
1848
1858
  archivedNotes: state.archivedNoteCount,
1849
1859
  activeLessons: state.activeLessonCount,
@@ -1865,7 +1875,7 @@ function formatStatus(state, format, activeSessions = []) {
1865
1875
  `# ${escapeMarkdownInline(state.config.project)}`,
1866
1876
  "",
1867
1877
  `Tickets: ${state.completeLeafTicketCount}/${state.leafTicketCount} complete, ${state.blockedCount} blocked`,
1868
- `Issues: ${state.openIssueCount} open`,
1878
+ `Issues: ${state.activeIssueCount} open`,
1869
1879
  `Notes: ${state.activeNoteCount} active, ${state.archivedNoteCount} archived`,
1870
1880
  `Lessons: ${state.activeLessonCount} active, ${state.deprecatedLessonCount} deprecated`,
1871
1881
  `Handovers: ${state.handoverFilenames.length}`,
@@ -2291,7 +2301,7 @@ function formatRecap(recap, state, format) {
2291
2301
  lines.push("No snapshot found. Run `claudestory snapshot` to enable session diffs.");
2292
2302
  lines.push("");
2293
2303
  lines.push(`Tickets: ${state.completeLeafTicketCount}/${state.leafTicketCount} complete, ${state.blockedCount} blocked`);
2294
- lines.push(`Issues: ${state.openIssueCount} open`);
2304
+ lines.push(`Issues: ${state.activeIssueCount} open`);
2295
2305
  } else {
2296
2306
  lines.push(`# ${escapeMarkdownInline(state.config.project)} \u2014 Recap`);
2297
2307
  lines.push("");
@@ -2299,6 +2309,13 @@ function formatRecap(recap, state, format) {
2299
2309
  if (recap.partial) {
2300
2310
  lines.push("**Note:** Snapshot was taken from a project with integrity warnings. Diff may be incomplete.");
2301
2311
  }
2312
+ if (recap.staleness) {
2313
+ if (recap.staleness.status === "diverged") {
2314
+ lines.push("**Warning:** Snapshot commit is not an ancestor of current HEAD (history diverged; possible rebase, force-push, or branch switch).");
2315
+ } else if (recap.staleness.status === "behind" && recap.staleness.commitsBehind) {
2316
+ lines.push(`**Warning:** Snapshot is ${recap.staleness.commitsBehind} commit(s) behind HEAD -- context may be stale.`);
2317
+ }
2318
+ }
2302
2319
  const changes = recap.changes;
2303
2320
  const hasChanges = hasAnyChanges(changes);
2304
2321
  if (!hasChanges) {
@@ -2560,7 +2577,7 @@ function formatFullExport(state, format) {
2560
2577
  lines.push(`# ${escapeMarkdownInline(state.config.project)} \u2014 Full Export`);
2561
2578
  lines.push("");
2562
2579
  lines.push(`Tickets: ${state.completeLeafTicketCount}/${state.leafTicketCount} complete`);
2563
- lines.push(`Issues: ${state.openIssueCount} open`);
2580
+ lines.push(`Issues: ${state.activeIssueCount} open`);
2564
2581
  lines.push(`Notes: ${state.activeNoteCount} active, ${state.archivedNoteCount} archived`);
2565
2582
  lines.push(`Lessons: ${state.activeLessonCount} active, ${state.deprecatedLessonCount} deprecated`);
2566
2583
  lines.push("");
@@ -3460,6 +3477,204 @@ var init_issue2 = __esm({
3460
3477
  }
3461
3478
  });
3462
3479
 
3480
+ // src/autonomous/git-inspector.ts
3481
+ import { execFile } from "child_process";
3482
+ async function git(cwd, args, parse) {
3483
+ return new Promise((resolve10) => {
3484
+ execFile("git", args, { cwd, timeout: GIT_TIMEOUT, maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
3485
+ if (err) {
3486
+ const message = stderr?.trim() || err.message || "unknown git error";
3487
+ resolve10({ ok: false, reason: "git_error", message });
3488
+ return;
3489
+ }
3490
+ try {
3491
+ resolve10({ ok: true, data: parse(stdout) });
3492
+ } catch (parseErr) {
3493
+ resolve10({ ok: false, reason: "parse_error", message: parseErr.message });
3494
+ }
3495
+ });
3496
+ });
3497
+ }
3498
+ async function gitStatus(cwd) {
3499
+ return git(
3500
+ cwd,
3501
+ ["status", "--porcelain"],
3502
+ (out) => out.split("\n").filter((l) => l.length > 0)
3503
+ );
3504
+ }
3505
+ async function gitHead(cwd) {
3506
+ const hashResult = await git(cwd, ["rev-parse", "HEAD"], (out) => out.trim());
3507
+ if (!hashResult.ok) return hashResult;
3508
+ const branchResult = await gitBranch(cwd);
3509
+ return {
3510
+ ok: true,
3511
+ data: {
3512
+ hash: hashResult.data,
3513
+ branch: branchResult.ok ? branchResult.data : null
3514
+ }
3515
+ };
3516
+ }
3517
+ async function gitBranch(cwd) {
3518
+ return git(cwd, ["symbolic-ref", "--short", "HEAD"], (out) => out.trim());
3519
+ }
3520
+ async function gitMergeBase(cwd, base) {
3521
+ return git(cwd, ["merge-base", "HEAD", base], (out) => out.trim());
3522
+ }
3523
+ async function gitDiffStat(cwd, base) {
3524
+ return git(cwd, ["diff", "--numstat", base], parseDiffNumstat);
3525
+ }
3526
+ async function gitDiffNames(cwd, base) {
3527
+ return git(
3528
+ cwd,
3529
+ ["diff", "--name-only", base],
3530
+ (out) => out.split("\n").filter((l) => l.length > 0)
3531
+ );
3532
+ }
3533
+ async function gitBlobHash(cwd, file) {
3534
+ return git(cwd, ["hash-object", file], (out) => out.trim());
3535
+ }
3536
+ async function gitDiffCachedNames(cwd) {
3537
+ return git(
3538
+ cwd,
3539
+ ["diff", "--cached", "--name-only"],
3540
+ (out) => out.split("\n").filter((l) => l.length > 0)
3541
+ );
3542
+ }
3543
+ async function gitStash(cwd, message) {
3544
+ const pushResult = await git(cwd, ["stash", "push", "-m", message], () => void 0);
3545
+ if (!pushResult.ok) return { ok: false, reason: pushResult.reason, message: pushResult.message };
3546
+ const hashResult = await git(cwd, ["rev-parse", "stash@{0}"], (out) => out.trim());
3547
+ if (!hashResult.ok) {
3548
+ const listResult = await git(
3549
+ cwd,
3550
+ ["stash", "list", "--format=%gd %s"],
3551
+ (out) => out.split("\n").filter((l) => l.includes(message))
3552
+ );
3553
+ if (listResult.ok && listResult.data.length > 0) {
3554
+ const ref = listResult.data[0].split(" ")[0];
3555
+ const refHash = await git(cwd, ["rev-parse", ref], (out) => out.trim());
3556
+ if (refHash.ok) return { ok: true, data: refHash.data };
3557
+ }
3558
+ 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." };
3559
+ }
3560
+ return { ok: true, data: hashResult.data };
3561
+ }
3562
+ async function gitStashPop(cwd, commitHash) {
3563
+ if (!commitHash) {
3564
+ return git(cwd, ["stash", "pop"], () => void 0);
3565
+ }
3566
+ const listResult = await git(
3567
+ cwd,
3568
+ ["stash", "list", "--format=%gd %H"],
3569
+ (out) => out.split("\n").filter((l) => l.length > 0).map((l) => {
3570
+ const [ref, hash] = l.split(" ", 2);
3571
+ return { ref, hash };
3572
+ })
3573
+ );
3574
+ if (!listResult.ok) {
3575
+ return { ok: false, reason: "stash_list_failed", message: `Cannot list stash entries to find ${commitHash}. Run \`git stash list\` and pop manually.` };
3576
+ }
3577
+ const match = listResult.data.find((e) => e.hash === commitHash);
3578
+ if (!match) {
3579
+ return { ok: false, reason: "stash_not_found", message: `No stash entry with commit hash ${commitHash}` };
3580
+ }
3581
+ return git(cwd, ["stash", "pop", match.ref], () => void 0);
3582
+ }
3583
+ async function gitDiffTreeNames(cwd, commitHash) {
3584
+ return git(
3585
+ cwd,
3586
+ ["diff-tree", "--name-only", "--no-commit-id", "-r", commitHash],
3587
+ (out) => out.split("\n").filter((l) => l.trim().length > 0)
3588
+ );
3589
+ }
3590
+ async function gitIsAncestor(cwd, ancestor, descendant) {
3591
+ if (!SAFE_REF.test(ancestor) || !SAFE_REF.test(descendant)) {
3592
+ return { ok: false, reason: "git_error", message: "invalid ref format" };
3593
+ }
3594
+ return new Promise((resolve10) => {
3595
+ execFile(
3596
+ "git",
3597
+ ["merge-base", "--is-ancestor", ancestor, descendant],
3598
+ { cwd, timeout: GIT_TIMEOUT },
3599
+ (err) => {
3600
+ if (!err) {
3601
+ resolve10({ ok: true, data: true });
3602
+ return;
3603
+ }
3604
+ const code = err.code;
3605
+ if (code === 1) {
3606
+ resolve10({ ok: true, data: false });
3607
+ return;
3608
+ }
3609
+ resolve10({ ok: false, reason: "git_error", message: err.message });
3610
+ }
3611
+ );
3612
+ });
3613
+ }
3614
+ async function gitLogRange(cwd, from, to, limit = 20) {
3615
+ if (from && !SAFE_REF.test(from)) {
3616
+ return { ok: false, reason: "invalid_ref", message: `Invalid git ref: ${from}` };
3617
+ }
3618
+ if (to && !SAFE_REF.test(to)) {
3619
+ return { ok: false, reason: "invalid_ref", message: `Invalid git ref: ${to}` };
3620
+ }
3621
+ if (!from || !to) {
3622
+ return { ok: true, data: [] };
3623
+ }
3624
+ return git(
3625
+ cwd,
3626
+ ["log", "--oneline", `-${limit}`, `${from}..${to}`],
3627
+ (out) => out.split("\n").filter((l) => l.trim().length > 0)
3628
+ );
3629
+ }
3630
+ async function gitHeadHash(cwd) {
3631
+ return new Promise((resolve10) => {
3632
+ execFile("git", ["rev-parse", "HEAD"], { cwd, timeout: 3e3 }, (err, stdout) => {
3633
+ if (err) {
3634
+ const message = err.stderr?.trim() || err.message || "unknown git error";
3635
+ resolve10({ ok: false, reason: "git_error", message });
3636
+ return;
3637
+ }
3638
+ resolve10({ ok: true, data: stdout.trim() });
3639
+ });
3640
+ });
3641
+ }
3642
+ async function gitCommitDistance(cwd, fromSha, toSha) {
3643
+ if (!SAFE_REF.test(fromSha) || !SAFE_REF.test(toSha)) {
3644
+ return { ok: false, reason: "git_error", message: "invalid ref format" };
3645
+ }
3646
+ return git(cwd, ["rev-list", "--count", `${fromSha}..${toSha}`], (out) => {
3647
+ const n = parseInt(out.trim(), 10);
3648
+ if (Number.isNaN(n)) throw new Error(`unexpected rev-list output: ${out}`);
3649
+ return n;
3650
+ });
3651
+ }
3652
+ function parseDiffNumstat(out) {
3653
+ const lines = out.split("\n").filter((l) => l.length > 0);
3654
+ let insertions = 0;
3655
+ let deletions = 0;
3656
+ let filesChanged = 0;
3657
+ for (const line of lines) {
3658
+ const parts = line.split(" ");
3659
+ if (parts.length < 3) continue;
3660
+ const added = parseInt(parts[0], 10);
3661
+ const removed = parseInt(parts[1], 10);
3662
+ if (!Number.isNaN(added)) insertions += added;
3663
+ if (!Number.isNaN(removed)) deletions += removed;
3664
+ filesChanged++;
3665
+ }
3666
+ return { filesChanged, insertions, deletions, totalLines: insertions + deletions };
3667
+ }
3668
+ var GIT_TIMEOUT, SAFE_REF;
3669
+ var init_git_inspector = __esm({
3670
+ "src/autonomous/git-inspector.ts"() {
3671
+ "use strict";
3672
+ init_esm_shims();
3673
+ GIT_TIMEOUT = 1e4;
3674
+ SAFE_REF = /^[0-9a-f]{4,40}$/i;
3675
+ }
3676
+ });
3677
+
3463
3678
  // src/core/snapshot.ts
3464
3679
  var snapshot_exports = {};
3465
3680
  __export(snapshot_exports, {
@@ -3499,6 +3714,10 @@ async function saveSnapshot(root, loadResult) {
3499
3714
  }))
3500
3715
  } : {}
3501
3716
  };
3717
+ const headResult = await gitHeadHash(absRoot);
3718
+ if (headResult.ok) {
3719
+ snapshot.gitHead = headResult.data;
3720
+ }
3502
3721
  const json = JSON.stringify(snapshot, null, 2) + "\n";
3503
3722
  const targetPath = join11(snapshotsDir, filename);
3504
3723
  const wrapDir = join11(absRoot, ".story");
@@ -3694,7 +3913,7 @@ function diffStates(snapshotState, currentState) {
3694
3913
  handovers: { added: handoversAdded, removed: handoversRemoved }
3695
3914
  };
3696
3915
  }
3697
- function buildRecap(currentState, snapshotInfo) {
3916
+ async function buildRecap(currentState, snapshotInfo, root) {
3698
3917
  const next = nextTicket(currentState);
3699
3918
  const nextTicketAction = next.kind === "found" ? { id: next.ticket.id, title: next.ticket.title, phase: next.ticket.phase } : null;
3700
3919
  const highSeverityIssues = currentState.issues.filter(
@@ -3724,6 +3943,30 @@ function buildRecap(currentState, snapshotInfo) {
3724
3943
  });
3725
3944
  const changes = diffStates(snapshotState, currentState);
3726
3945
  const recentlyClearedBlockers = changes.blockers.cleared;
3946
+ let staleness;
3947
+ if (snapshot.gitHead) {
3948
+ const currentHeadResult = await gitHeadHash(root);
3949
+ if (currentHeadResult.ok) {
3950
+ const snapshotSha = snapshot.gitHead;
3951
+ const currentSha = currentHeadResult.data;
3952
+ if (snapshotSha !== currentSha) {
3953
+ const ancestorResult = await gitIsAncestor(root, snapshotSha, currentSha);
3954
+ if (ancestorResult.ok && ancestorResult.data) {
3955
+ const distResult = await gitCommitDistance(root, snapshotSha, currentSha);
3956
+ if (distResult.ok) {
3957
+ staleness = {
3958
+ status: "behind",
3959
+ snapshotSha,
3960
+ currentSha,
3961
+ commitsBehind: distResult.data
3962
+ };
3963
+ }
3964
+ } else if (ancestorResult.ok && !ancestorResult.data) {
3965
+ staleness = { status: "diverged", snapshotSha, currentSha };
3966
+ }
3967
+ }
3968
+ }
3969
+ }
3727
3970
  return {
3728
3971
  snapshot: { filename, createdAt: snapshot.createdAt },
3729
3972
  changes,
@@ -3732,7 +3975,8 @@ function buildRecap(currentState, snapshotInfo) {
3732
3975
  highSeverityIssues,
3733
3976
  recentlyClearedBlockers
3734
3977
  },
3735
- partial: (snapshot.warnings ?? []).length > 0
3978
+ partial: (snapshot.warnings ?? []).length > 0,
3979
+ ...staleness ? { staleness } : {}
3736
3980
  };
3737
3981
  }
3738
3982
  function formatSnapshotFilename(date) {
@@ -3779,6 +4023,7 @@ var init_snapshot = __esm({
3779
4023
  init_project_state();
3780
4024
  init_queries();
3781
4025
  init_project_loader();
4026
+ init_git_inspector();
3782
4027
  LoadWarningSchema = z9.object({
3783
4028
  type: z9.string(),
3784
4029
  file: z9.string(),
@@ -3795,7 +4040,8 @@ var init_snapshot = __esm({
3795
4040
  notes: z9.array(NoteSchema).optional().default([]),
3796
4041
  lessons: z9.array(LessonSchema).optional().default([]),
3797
4042
  handoverFilenames: z9.array(z9.string()).optional().default([]),
3798
- warnings: z9.array(LoadWarningSchema).optional()
4043
+ warnings: z9.array(LoadWarningSchema).optional(),
4044
+ gitHead: z9.string().optional()
3799
4045
  });
3800
4046
  MAX_SNAPSHOTS = 20;
3801
4047
  }
@@ -4172,9 +4418,9 @@ var init_session = __esm({
4172
4418
 
4173
4419
  // src/mcp/index.ts
4174
4420
  init_esm_shims();
4175
- import { realpathSync as realpathSync3, existsSync as existsSync13 } from "fs";
4176
- import { resolve as resolve9, join as join24, isAbsolute } from "path";
4177
- import { z as z11 } from "zod";
4421
+ import { realpathSync as realpathSync3, existsSync as existsSync14 } from "fs";
4422
+ import { resolve as resolve9, join as join26, isAbsolute } from "path";
4423
+ import { z as z12 } from "zod";
4178
4424
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4179
4425
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4180
4426
 
@@ -4223,7 +4469,8 @@ function checkRoot(candidate) {
4223
4469
  init_esm_shims();
4224
4470
  init_session_types();
4225
4471
  import { z as z10 } from "zod";
4226
- import { join as join22 } from "path";
4472
+ import { readFileSync as readFileSync12, writeFileSync as writeFileSync7, mkdirSync as mkdirSync5 } from "fs";
4473
+ import { join as join23 } from "path";
4227
4474
 
4228
4475
  // src/autonomous/review-lenses/mcp-handlers.ts
4229
4476
  init_esm_shims();
@@ -5593,6 +5840,9 @@ function validateFindings(raw, lensName) {
5593
5840
  continue;
5594
5841
  }
5595
5842
  const normalized = normalizeFields(item);
5843
+ if (typeof normalized.lens !== "string" && typeof lensName === "string") {
5844
+ normalized.lens = lensName;
5845
+ }
5596
5846
  const reason = checkFinding(normalized, lensName);
5597
5847
  if (reason) {
5598
5848
  invalid.push({ raw: item, reason });
@@ -6318,6 +6568,58 @@ function redactArtifactSecrets(artifact, redactedLines) {
6318
6568
  return lines.map((line, i) => linesToRedact.has(i) ? "[REDACTED -- potential secret]" : line).join("\n");
6319
6569
  }
6320
6570
 
6571
+ // src/autonomous/review-lenses/diff-scope.ts
6572
+ init_esm_shims();
6573
+ function parseDiffScope(diff) {
6574
+ const changedFiles = /* @__PURE__ */ new Set();
6575
+ const addedLines = /* @__PURE__ */ new Map();
6576
+ if (!diff) return { changedFiles, addedLines };
6577
+ const lines = diff.split("\n");
6578
+ let currentFile = null;
6579
+ let currentLineNum = 0;
6580
+ for (const line of lines) {
6581
+ if (line.startsWith("+++ ")) {
6582
+ if (line.startsWith("+++ /dev/null")) {
6583
+ currentFile = null;
6584
+ continue;
6585
+ }
6586
+ const rawPath = line.startsWith("+++ b/") ? line.slice(6) : line.slice(4);
6587
+ currentFile = normalizePath(rawPath);
6588
+ changedFiles.add(currentFile);
6589
+ if (!addedLines.has(currentFile)) {
6590
+ addedLines.set(currentFile, /* @__PURE__ */ new Set());
6591
+ }
6592
+ currentLineNum = 0;
6593
+ continue;
6594
+ }
6595
+ const hunkMatch = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)/);
6596
+ if (hunkMatch) {
6597
+ currentLineNum = parseInt(hunkMatch[1], 10) - 1;
6598
+ continue;
6599
+ }
6600
+ if (!currentFile) continue;
6601
+ if (line.startsWith("-")) continue;
6602
+ currentLineNum++;
6603
+ if (line.startsWith("+")) {
6604
+ addedLines.get(currentFile).add(currentLineNum);
6605
+ }
6606
+ }
6607
+ return { changedFiles, addedLines };
6608
+ }
6609
+ function normalizePath(p) {
6610
+ return p.startsWith("./") ? p.slice(2) : p;
6611
+ }
6612
+ function classifyOrigin(finding, scope, stage) {
6613
+ if (stage === "PLAN_REVIEW") return "introduced";
6614
+ if (!finding.file) return "introduced";
6615
+ const file = normalizePath(finding.file);
6616
+ if (!scope.changedFiles.has(file)) return "pre-existing";
6617
+ if (finding.line == null) return "introduced";
6618
+ const fileLines = scope.addedLines.get(file);
6619
+ if (!fileLines || !fileLines.has(finding.line)) return "pre-existing";
6620
+ return "introduced";
6621
+ }
6622
+
6321
6623
  // src/autonomous/review-lenses/mcp-handlers.ts
6322
6624
  var MAX_PROMPT_SIZE = 1e4;
6323
6625
  function handlePrepare(input) {
@@ -6415,6 +6717,7 @@ function handleSynthesize(input) {
6415
6717
  }
6416
6718
  }
6417
6719
  const stage = input.stage ?? "CODE_REVIEW";
6720
+ const diffScope = input.diff && input.changedFiles && stage === "CODE_REVIEW" ? parseDiffScope(input.diff) : null;
6418
6721
  const lensesCompleted = [];
6419
6722
  const lensesFailed = [];
6420
6723
  const lensesInsufficientContext = [];
@@ -6447,7 +6750,8 @@ function handleSynthesize(input) {
6447
6750
  const enriched = {
6448
6751
  ...f,
6449
6752
  issueKey: generateIssueKey(f),
6450
- blocking: computeBlocking(f, stage, policy)
6753
+ blocking: computeBlocking(f, stage, policy),
6754
+ origin: diffScope ? classifyOrigin(f, diffScope, stage) : void 0
6451
6755
  };
6452
6756
  allFindings.push(enriched);
6453
6757
  }
@@ -6462,6 +6766,9 @@ function handleSynthesize(input) {
6462
6766
  lensesFailed.push(lens);
6463
6767
  }
6464
6768
  }
6769
+ const preExistingFindings = allFindings.filter(
6770
+ (f) => f.origin === "pre-existing" && f.severity !== "suggestion"
6771
+ );
6465
6772
  const lensMetadata = buildLensMetadata(lensesCompleted, lensesFailed, lensesInsufficientContext);
6466
6773
  const mergerPrompt = buildMergerPrompt(allFindings, lensMetadata, stage);
6467
6774
  return {
@@ -6471,7 +6778,9 @@ function handleSynthesize(input) {
6471
6778
  lensesFailed,
6472
6779
  lensesInsufficientContext,
6473
6780
  droppedFindings: droppedTotal,
6474
- droppedDetails: dropReasons.slice(0, 5)
6781
+ droppedDetails: dropReasons.slice(0, 5),
6782
+ preExistingFindings,
6783
+ preExistingCount: preExistingFindings.length
6475
6784
  };
6476
6785
  }
6477
6786
  function handleJudge(input) {
@@ -6496,8 +6805,8 @@ function handleJudge(input) {
6496
6805
  [...input.lensesSkipped]
6497
6806
  );
6498
6807
  if (input.convergenceHistory && input.convergenceHistory.length > 0) {
6499
- const sanitize = (s) => s.replace(/[|\n\r#>`*_~\[\]]/g, " ").slice(0, 50);
6500
- const historyTable = input.convergenceHistory.map((h) => `| R${h.round} | ${sanitize(h.verdict)} | ${h.blocking} | ${h.important} | ${sanitize(h.newCode)} |`).join("\n");
6808
+ const sanitize2 = (s) => s.replace(/[|\n\r#>`*_~\[\]]/g, " ").slice(0, 50);
6809
+ const historyTable = input.convergenceHistory.map((h) => `| R${h.round} | ${sanitize2(h.verdict)} | ${h.blocking} | ${h.important} | ${sanitize2(h.newCode)} |`).join("\n");
6501
6810
  judgePrompt += `
6502
6811
 
6503
6812
  ## Convergence History
@@ -6846,7 +7155,7 @@ init_output_formatter();
6846
7155
  init_snapshot();
6847
7156
  async function handleRecap(ctx) {
6848
7157
  const snapshotInfo = await loadLatestSnapshot(ctx.root);
6849
- const recap = buildRecap(ctx.state, snapshotInfo);
7158
+ const recap = await buildRecap(ctx.state, snapshotInfo, ctx.root);
6850
7159
  return { output: formatRecap(recap, ctx.state, ctx.format) };
6851
7160
  }
6852
7161
 
@@ -7814,8 +8123,8 @@ init_handover();
7814
8123
  init_esm_shims();
7815
8124
  init_session_types();
7816
8125
  init_session();
7817
- import { readFileSync as readFileSync10, writeFileSync as writeFileSync5, readdirSync as readdirSync4 } from "fs";
7818
- import { join as join19 } from "path";
8126
+ import { readFileSync as readFileSync10, writeFileSync as writeFileSync6, readdirSync as readdirSync4 } from "fs";
8127
+ import { join as join20 } from "path";
7819
8128
 
7820
8129
  // src/autonomous/state-machine.ts
7821
8130
  init_esm_shims();
@@ -7835,15 +8144,17 @@ var TRANSITIONS = {
7835
8144
  // advance → IMPLEMENT, retry stays, exhaustion → PLAN, no-op → COMPLETE (ISS-069)
7836
8145
  TEST: ["CODE_REVIEW", "IMPLEMENT", "TEST"],
7837
8146
  // pass → CODE_REVIEW, fail → IMPLEMENT, retry
7838
- CODE_REVIEW: ["VERIFY", "FINALIZE", "IMPLEMENT", "PLAN", "CODE_REVIEW", "SESSION_END"],
7839
- // approve → VERIFY/FINALIZE, reject → IMPLEMENT/PLAN, stay for next round; SESSION_END for tiered exit
7840
- VERIFY: ["FINALIZE", "IMPLEMENT", "VERIFY"],
8147
+ CODE_REVIEW: ["VERIFY", "BUILD", "FINALIZE", "IMPLEMENT", "PLAN", "CODE_REVIEW", "SESSION_END", "ISSUE_FIX"],
8148
+ // approve → VERIFY/BUILD/FINALIZE, reject → IMPLEMENT/PLAN, stay for next round; SESSION_END for tiered exit; T-208: ISSUE_FIX for issue-fix reviews
8149
+ VERIFY: ["BUILD", "FINALIZE", "IMPLEMENT", "VERIFY"],
8150
+ // pass → BUILD/FINALIZE, fail → IMPLEMENT, retry
8151
+ BUILD: ["FINALIZE", "IMPLEMENT", "BUILD"],
7841
8152
  // pass → FINALIZE, fail → IMPLEMENT, retry
7842
8153
  FINALIZE: ["COMPLETE", "PICK_TICKET"],
7843
8154
  // ISS-084: issues now route through COMPLETE too; PICK_TICKET kept for in-flight session compat
7844
8155
  COMPLETE: ["PICK_TICKET", "HANDOVER", "ISSUE_SWEEP", "SESSION_END"],
7845
- ISSUE_FIX: ["FINALIZE", "PICK_TICKET", "ISSUE_FIX"],
7846
- // T-153: fix done → FINALIZE, cancel → PICK_TICKET, retry self
8156
+ ISSUE_FIX: ["FINALIZE", "PICK_TICKET", "ISSUE_FIX", "CODE_REVIEW"],
8157
+ // T-153: fix done → FINALIZE, cancel → PICK_TICKET, retry self; T-208: optional code review
7847
8158
  LESSON_CAPTURE: ["ISSUE_SWEEP", "HANDOVER", "LESSON_CAPTURE"],
7848
8159
  // advance → ISSUE_SWEEP, retry self, done → HANDOVER
7849
8160
  ISSUE_SWEEP: ["ISSUE_SWEEP", "HANDOVER", "PICK_TICKET"],
@@ -7933,189 +8244,55 @@ function requiredRounds(risk) {
7933
8244
  return 3;
7934
8245
  }
7935
8246
  }
7936
- function nextReviewer(previousRounds, backends) {
7937
- if (backends.length === 0) return "agent";
7938
- if (backends.length === 1) return backends[0];
7939
- if (previousRounds.length === 0) return backends[0];
8247
+ var CODEX_CACHE_TTL_MS = 10 * 60 * 1e3;
8248
+ function isCodexUnavailable(codexUnavailableSince) {
8249
+ if (!codexUnavailableSince) return false;
8250
+ const since = new Date(codexUnavailableSince).getTime();
8251
+ if (Number.isNaN(since)) return false;
8252
+ return Date.now() - since < CODEX_CACHE_TTL_MS;
8253
+ }
8254
+ function nextReviewer(previousRounds, backends, codexUnavailable, codexUnavailableSince) {
8255
+ const unavailable = codexUnavailableSince ? isCodexUnavailable(codexUnavailableSince) : !!codexUnavailable;
8256
+ const effective = unavailable ? backends.filter((b) => b !== "codex") : backends;
8257
+ if (effective.length === 0) return "agent";
8258
+ if (effective.length === 1) return effective[0];
8259
+ if (previousRounds.length === 0) return effective[0];
7940
8260
  const lastReviewer = previousRounds[previousRounds.length - 1].reviewer;
7941
- const lastIndex = backends.indexOf(lastReviewer);
7942
- if (lastIndex === -1) return backends[0];
7943
- return backends[(lastIndex + 1) % backends.length];
8261
+ const lastIndex = effective.indexOf(lastReviewer);
8262
+ if (lastIndex === -1) return effective[0];
8263
+ return effective[(lastIndex + 1) % effective.length];
7944
8264
  }
7945
8265
 
7946
- // src/autonomous/git-inspector.ts
8266
+ // src/autonomous/guide.ts
8267
+ init_git_inspector();
8268
+
8269
+ // src/autonomous/recipes/loader.ts
7947
8270
  init_esm_shims();
7948
- import { execFile } from "child_process";
7949
- var GIT_TIMEOUT = 1e4;
7950
- async function git(cwd, args, parse) {
7951
- return new Promise((resolve10) => {
7952
- execFile("git", args, { cwd, timeout: GIT_TIMEOUT, maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
7953
- if (err) {
7954
- const message = stderr?.trim() || err.message || "unknown git error";
7955
- resolve10({ ok: false, reason: "git_error", message });
7956
- return;
7957
- }
7958
- try {
7959
- resolve10({ ok: true, data: parse(stdout) });
7960
- } catch (parseErr) {
7961
- resolve10({ ok: false, reason: "parse_error", message: parseErr.message });
7962
- }
7963
- });
7964
- });
7965
- }
7966
- async function gitStatus(cwd) {
7967
- return git(
7968
- cwd,
7969
- ["status", "--porcelain"],
7970
- (out) => out.split("\n").filter((l) => l.length > 0)
7971
- );
7972
- }
7973
- async function gitHead(cwd) {
7974
- const hashResult = await git(cwd, ["rev-parse", "HEAD"], (out) => out.trim());
7975
- if (!hashResult.ok) return hashResult;
7976
- const branchResult = await gitBranch(cwd);
7977
- return {
7978
- ok: true,
7979
- data: {
7980
- hash: hashResult.data,
7981
- branch: branchResult.ok ? branchResult.data : null
7982
- }
7983
- };
7984
- }
7985
- async function gitBranch(cwd) {
7986
- return git(cwd, ["symbolic-ref", "--short", "HEAD"], (out) => out.trim());
7987
- }
7988
- async function gitMergeBase(cwd, base) {
7989
- return git(cwd, ["merge-base", "HEAD", base], (out) => out.trim());
7990
- }
7991
- async function gitDiffStat(cwd, base) {
7992
- return git(cwd, ["diff", "--numstat", base], parseDiffNumstat);
7993
- }
7994
- async function gitDiffNames(cwd, base) {
7995
- return git(
7996
- cwd,
7997
- ["diff", "--name-only", base],
7998
- (out) => out.split("\n").filter((l) => l.length > 0)
7999
- );
8000
- }
8001
- async function gitBlobHash(cwd, file) {
8002
- return git(cwd, ["hash-object", file], (out) => out.trim());
8003
- }
8004
- async function gitDiffCachedNames(cwd) {
8005
- return git(
8006
- cwd,
8007
- ["diff", "--cached", "--name-only"],
8008
- (out) => out.split("\n").filter((l) => l.length > 0)
8009
- );
8010
- }
8011
- async function gitStash(cwd, message) {
8012
- const pushResult = await git(cwd, ["stash", "push", "-m", message], () => void 0);
8013
- if (!pushResult.ok) return { ok: false, reason: pushResult.reason, message: pushResult.message };
8014
- const hashResult = await git(cwd, ["rev-parse", "stash@{0}"], (out) => out.trim());
8015
- if (!hashResult.ok) {
8016
- const listResult = await git(
8017
- cwd,
8018
- ["stash", "list", "--format=%gd %s"],
8019
- (out) => out.split("\n").filter((l) => l.includes(message))
8020
- );
8021
- if (listResult.ok && listResult.data.length > 0) {
8022
- const ref = listResult.data[0].split(" ")[0];
8023
- const refHash = await git(cwd, ["rev-parse", ref], (out) => out.trim());
8024
- if (refHash.ok) return { ok: true, data: refHash.data };
8025
- }
8026
- 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." };
8027
- }
8028
- return { ok: true, data: hashResult.data };
8029
- }
8030
- async function gitStashPop(cwd, commitHash) {
8031
- if (!commitHash) {
8032
- return git(cwd, ["stash", "pop"], () => void 0);
8033
- }
8034
- const listResult = await git(
8035
- cwd,
8036
- ["stash", "list", "--format=%gd %H"],
8037
- (out) => out.split("\n").filter((l) => l.length > 0).map((l) => {
8038
- const [ref, hash] = l.split(" ", 2);
8039
- return { ref, hash };
8040
- })
8041
- );
8042
- if (!listResult.ok) {
8043
- return { ok: false, reason: "stash_list_failed", message: `Cannot list stash entries to find ${commitHash}. Run \`git stash list\` and pop manually.` };
8044
- }
8045
- const match = listResult.data.find((e) => e.hash === commitHash);
8046
- if (!match) {
8047
- return { ok: false, reason: "stash_not_found", message: `No stash entry with commit hash ${commitHash}` };
8048
- }
8049
- return git(cwd, ["stash", "pop", match.ref], () => void 0);
8050
- }
8051
- async function gitDiffTreeNames(cwd, commitHash) {
8052
- return git(
8053
- cwd,
8054
- ["diff-tree", "--name-only", "--no-commit-id", "-r", commitHash],
8055
- (out) => out.split("\n").filter((l) => l.trim().length > 0)
8056
- );
8057
- }
8058
- var SAFE_REF = /^[0-9a-f]{4,40}$/i;
8059
- async function gitLogRange(cwd, from, to, limit = 20) {
8060
- if (from && !SAFE_REF.test(from)) {
8061
- return { ok: false, reason: "invalid_ref", message: `Invalid git ref: ${from}` };
8062
- }
8063
- if (to && !SAFE_REF.test(to)) {
8064
- return { ok: false, reason: "invalid_ref", message: `Invalid git ref: ${to}` };
8065
- }
8066
- if (!from || !to) {
8067
- return { ok: true, data: [] };
8068
- }
8069
- return git(
8070
- cwd,
8071
- ["log", "--oneline", `-${limit}`, `${from}..${to}`],
8072
- (out) => out.split("\n").filter((l) => l.trim().length > 0)
8073
- );
8074
- }
8075
- function parseDiffNumstat(out) {
8076
- const lines = out.split("\n").filter((l) => l.length > 0);
8077
- let insertions = 0;
8078
- let deletions = 0;
8079
- let filesChanged = 0;
8080
- for (const line of lines) {
8081
- const parts = line.split(" ");
8082
- if (parts.length < 3) continue;
8083
- const added = parseInt(parts[0], 10);
8084
- const removed = parseInt(parts[1], 10);
8085
- if (!Number.isNaN(added)) insertions += added;
8086
- if (!Number.isNaN(removed)) deletions += removed;
8087
- filesChanged++;
8088
- }
8089
- return { filesChanged, insertions, deletions, totalLines: insertions + deletions };
8090
- }
8091
-
8092
- // src/autonomous/recipes/loader.ts
8093
- init_esm_shims();
8094
- import { readFileSync as readFileSync7 } from "fs";
8095
- import { join as join14, dirname as dirname3 } from "path";
8096
- import { fileURLToPath as fileURLToPath2 } from "url";
8097
- var DEFAULT_PIPELINE = [
8098
- "PICK_TICKET",
8099
- "PLAN",
8100
- "PLAN_REVIEW",
8101
- "IMPLEMENT",
8102
- "CODE_REVIEW",
8103
- "FINALIZE",
8104
- "COMPLETE"
8105
- ];
8106
- var DEFAULT_DEFAULTS = {
8107
- maxTicketsPerSession: 3,
8108
- compactThreshold: "high",
8109
- reviewBackends: ["codex", "agent"]
8110
- };
8111
- function loadRecipe(recipeName) {
8112
- if (!/^[A-Za-z0-9_-]+$/.test(recipeName)) {
8113
- throw new Error(`Invalid recipe name: ${recipeName}`);
8114
- }
8115
- const recipesDir = join14(dirname3(fileURLToPath2(import.meta.url)), "..", "recipes");
8116
- const path2 = join14(recipesDir, `${recipeName}.json`);
8117
- const raw = readFileSync7(path2, "utf-8");
8118
- return JSON.parse(raw);
8271
+ import { readFileSync as readFileSync7 } from "fs";
8272
+ import { join as join14, dirname as dirname3 } from "path";
8273
+ import { fileURLToPath as fileURLToPath2 } from "url";
8274
+ var DEFAULT_PIPELINE = [
8275
+ "PICK_TICKET",
8276
+ "PLAN",
8277
+ "PLAN_REVIEW",
8278
+ "IMPLEMENT",
8279
+ "CODE_REVIEW",
8280
+ "FINALIZE",
8281
+ "COMPLETE"
8282
+ ];
8283
+ var DEFAULT_DEFAULTS = {
8284
+ maxTicketsPerSession: 3,
8285
+ compactThreshold: "high",
8286
+ reviewBackends: ["codex", "agent"]
8287
+ };
8288
+ function loadRecipe(recipeName) {
8289
+ if (!/^[A-Za-z0-9_-]+$/.test(recipeName)) {
8290
+ throw new Error(`Invalid recipe name: ${recipeName}`);
8291
+ }
8292
+ const recipesDir = join14(dirname3(fileURLToPath2(import.meta.url)), "..", "recipes");
8293
+ const path2 = join14(recipesDir, `${recipeName}.json`);
8294
+ const raw = readFileSync7(path2, "utf-8");
8295
+ return JSON.parse(raw);
8119
8296
  }
8120
8297
  function resolveRecipe(recipeName, projectOverrides) {
8121
8298
  let raw;
@@ -8169,6 +8346,13 @@ function resolveRecipe(recipeName, projectOverrides) {
8169
8346
  pipeline.splice(implementIdx + 1, 0, "TEST");
8170
8347
  }
8171
8348
  }
8349
+ if (stages2.ISSUE_FIX?.enableCodeReview) {
8350
+ if (pipeline.includes("VERIFY") || pipeline.includes("BUILD")) {
8351
+ throw new Error(
8352
+ "ISSUE_FIX.enableCodeReview is incompatible with VERIFY/BUILD in the pipeline (issue fixes use goto transitions, not pipeline walker)"
8353
+ );
8354
+ }
8355
+ }
8172
8356
  const postComplete = raw.postComplete ? [...raw.postComplete] : [];
8173
8357
  const recipeDefaults = raw.defaults ?? {};
8174
8358
  const defaults = {
@@ -8466,7 +8650,15 @@ function buildTargetedStuckHandover(candidatesText, sessionId) {
8466
8650
  var PickTicketStage = class {
8467
8651
  id = "PICK_TICKET";
8468
8652
  async enter(ctx) {
8469
- const { state: projectState } = await ctx.loadProject();
8653
+ let projectState;
8654
+ try {
8655
+ ({ state: projectState } = await ctx.loadProject());
8656
+ } catch (err) {
8657
+ return {
8658
+ action: "retry",
8659
+ 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.`
8660
+ };
8661
+ }
8470
8662
  if (isTargetedMode(ctx.state)) {
8471
8663
  const remaining = getRemainingTargets(ctx.state);
8472
8664
  if (remaining.length === 0) {
@@ -8566,7 +8758,12 @@ var PickTicketStage = class {
8566
8758
  }
8567
8759
  const targetReject = this.enforceTargetList(ctx, ticketId);
8568
8760
  if (targetReject) return targetReject;
8569
- const { state: projectState } = await ctx.loadProject();
8761
+ let projectState;
8762
+ try {
8763
+ ({ state: projectState } = await ctx.loadProject());
8764
+ } catch (err) {
8765
+ return { action: "retry", instruction: `Failed to load project state: ${err instanceof Error ? err.message : String(err)}. Check .story/ files for corruption.` };
8766
+ }
8570
8767
  const ticket = projectState.ticketByID(ticketId);
8571
8768
  if (!ticket) {
8572
8769
  return { action: "retry", instruction: `Ticket ${ticketId} not found. Pick a valid ticket.` };
@@ -8588,7 +8785,8 @@ var PickTicketStage = class {
8588
8785
  ctx.updateDraft({
8589
8786
  ticket: { id: ticket.id, title: ticket.title, claimed: true },
8590
8787
  reviews: { plan: [], code: [] },
8591
- finalizeCheckpoint: null
8788
+ finalizeCheckpoint: null,
8789
+ ticketStartedAt: (/* @__PURE__ */ new Date()).toISOString()
8592
8790
  });
8593
8791
  return {
8594
8792
  action: "advance",
@@ -8619,7 +8817,12 @@ ${ticket.description}` : "",
8619
8817
  async handleIssuePick(ctx, issueId) {
8620
8818
  const targetReject = this.enforceTargetList(ctx, issueId);
8621
8819
  if (targetReject) return targetReject;
8622
- const { state: projectState } = await ctx.loadProject();
8820
+ let projectState;
8821
+ try {
8822
+ ({ state: projectState } = await ctx.loadProject());
8823
+ } catch (err) {
8824
+ return { action: "retry", instruction: `Failed to load project state: ${err instanceof Error ? err.message : String(err)}. Check .story/ files for corruption.` };
8825
+ }
8623
8826
  const issue = projectState.issues.find((i) => i.id === issueId);
8624
8827
  if (!issue) {
8625
8828
  return { action: "retry", instruction: `Issue ${issueId} not found. Pick a valid issue or ticket.` };
@@ -8628,11 +8831,16 @@ ${ticket.description}` : "",
8628
8831
  if (issue.status !== "open" && !(targeted && issue.status === "inprogress")) {
8629
8832
  return { action: "retry", instruction: `Issue ${issueId} is ${issue.status}. Pick an open issue.` };
8630
8833
  }
8834
+ const transitionId = `issue-pick-${issueId}-${Date.now()}`;
8835
+ ctx.writeState({
8836
+ pendingProjectMutation: { type: "issue_update", target: issueId, field: "status", value: "inprogress", expectedCurrent: issue.status, transitionId }
8837
+ });
8631
8838
  try {
8632
8839
  const { handleIssueUpdate: handleIssueUpdate2 } = await Promise.resolve().then(() => (init_issue2(), issue_exports));
8633
- await handleIssueUpdate2({ id: issueId, status: "inprogress" }, "json", ctx.root);
8840
+ await handleIssueUpdate2(issueId, { status: "inprogress" }, "json", ctx.root);
8634
8841
  } catch {
8635
8842
  }
8843
+ ctx.writeState({ pendingProjectMutation: null });
8636
8844
  ctx.updateDraft({
8637
8845
  currentIssue: { id: issue.id, title: issue.title, severity: issue.severity },
8638
8846
  ticket: void 0,
@@ -8773,7 +8981,7 @@ var PlanReviewStage = class {
8773
8981
  const backends = ctx.state.config.reviewBackends;
8774
8982
  const existingReviews = ctx.state.reviews.plan;
8775
8983
  const roundNum = existingReviews.length + 1;
8776
- const reviewer = nextReviewer(existingReviews, backends);
8984
+ const reviewer = nextReviewer(existingReviews, backends, ctx.state.codexUnavailable, ctx.state.codexUnavailableSince);
8777
8985
  const risk = ctx.state.ticket?.risk ?? "low";
8778
8986
  const minRounds = requiredRounds(risk);
8779
8987
  if (reviewer === "lenses") {
@@ -8784,10 +8992,11 @@ var PlanReviewStage = class {
8784
8992
  "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.",
8785
8993
  "",
8786
8994
  "1. Read the plan file",
8787
- "2. Call `prepareLensReview()` with the plan text (stage: PLAN_REVIEW)",
8788
- "3. Spawn all lens subagents in parallel",
8789
- "4. Collect results and pass through the merger and judge pipeline",
8790
- "5. Report the final SynthesisResult verdict and findings",
8995
+ "2. Call `claudestory_review_lenses_prepare` with the plan text as diff, stage: PLAN_REVIEW, and ticketDescription",
8996
+ "3. Spawn all lens subagents in parallel (each prompt is returned by the prepare tool)",
8997
+ "4. Collect results and call `claudestory_review_lenses_synthesize` with the lens results",
8998
+ "5. Run the merger agent with the returned mergerPrompt, then call `claudestory_review_lenses_judge`",
8999
+ "6. Run the judge agent and report the final SynthesisResult verdict and findings",
8791
9000
  "",
8792
9001
  "When done, call `claudestory_autonomous_guide` with:",
8793
9002
  "```json",
@@ -8814,7 +9023,11 @@ var PlanReviewStage = class {
8814
9023
  `{ "sessionId": "${ctx.state.sessionId}", "action": "report", "report": { "completedAction": "plan_review_round", "verdict": "<approve|revise|request_changes|reject>", "findings": [...] } }`,
8815
9024
  "```"
8816
9025
  ].join("\n"),
8817
- reminders: ["Report the exact verdict and findings from the reviewer."],
9026
+ reminders: [
9027
+ "Report the exact verdict and findings from the reviewer.",
9028
+ "IMPORTANT: After the review, file ANY pre-existing issues discovered using claudestory_issue_create with severity and impact. Do NOT skip this step.",
9029
+ ...reviewer === "codex" ? ["If codex is unavailable (usage limit, error, etc.), fall back to agent review and include 'codex unavailable' in your report notes."] : []
9030
+ ],
8818
9031
  transitionedFrom: ctx.state.previousState ?? void 0
8819
9032
  };
8820
9033
  }
@@ -8827,7 +9040,8 @@ var PlanReviewStage = class {
8827
9040
  const roundNum = planReviews.length + 1;
8828
9041
  const findings = report.findings ?? [];
8829
9042
  const backends = ctx.state.config.reviewBackends;
8830
- const reviewerBackend = nextReviewer(planReviews, backends);
9043
+ const computedReviewer = nextReviewer(planReviews, backends, ctx.state.codexUnavailable, ctx.state.codexUnavailableSince);
9044
+ const reviewerBackend = report.reviewer ?? (computedReviewer === "codex" && report.notes && /codex\b.*\b(unavail|limit|failed|down|error|usage)/i.test(report.notes) ? "agent" : null) ?? computedReviewer;
8831
9045
  planReviews.push({
8832
9046
  round: roundNum,
8833
9047
  reviewer: reviewerBackend,
@@ -8839,6 +9053,9 @@ var PlanReviewStage = class {
8839
9053
  codexSessionId: report.reviewerSessionId,
8840
9054
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
8841
9055
  });
9056
+ if (report.notes && /codex\b.*\b(unavail|limit|failed|down|error|usage)/i.test(report.notes)) {
9057
+ ctx.writeState({ codexUnavailable: true, codexUnavailableSince: (/* @__PURE__ */ new Date()).toISOString() });
9058
+ }
8842
9059
  const risk = ctx.state.ticket?.risk ?? "low";
8843
9060
  const minRounds = requiredRounds(risk);
8844
9061
  const hasCriticalOrMajor = findings.some(
@@ -8926,7 +9143,7 @@ var PlanReviewStage = class {
8926
9143
  }
8927
9144
  return { action: "advance" };
8928
9145
  }
8929
- const nextReviewerName = nextReviewer(planReviews, backends);
9146
+ const nextReviewerName = nextReviewer(planReviews, backends, ctx.state.codexUnavailable, ctx.state.codexUnavailableSince);
8930
9147
  return {
8931
9148
  action: "retry",
8932
9149
  instruction: [
@@ -8943,6 +9160,7 @@ var PlanReviewStage = class {
8943
9160
 
8944
9161
  // src/autonomous/stages/implement.ts
8945
9162
  init_esm_shims();
9163
+ init_git_inspector();
8946
9164
  var ImplementStage = class {
8947
9165
  id = "IMPLEMENT";
8948
9166
  async enter(ctx) {
@@ -9048,7 +9266,7 @@ var WriteTestsStage = class {
9048
9266
  const exitMatch = notes.match(EXIT_CODE_REGEX);
9049
9267
  const exitCode = exitMatch ? parseInt(exitMatch[1], 10) : -1;
9050
9268
  const failMatch = notes.match(FAIL_COUNT_REGEX);
9051
- const currentFailCount = failMatch ? parseInt(failMatch[1], 10) : -1;
9269
+ const currentFailCount = failMatch ? parseInt(failMatch[1], 10) : exitCode === 0 ? 0 : -1;
9052
9270
  const baseline = ctx.state.testBaseline;
9053
9271
  const baselineFailCount = baseline?.failCount ?? -1;
9054
9272
  const nextRetry = retryCount + 1;
@@ -9224,26 +9442,29 @@ var CodeReviewStage = class {
9224
9442
  const backends = ctx.state.config.reviewBackends;
9225
9443
  const codeReviews = ctx.state.reviews.code;
9226
9444
  const roundNum = codeReviews.length + 1;
9227
- const reviewer = nextReviewer(codeReviews, backends);
9445
+ const reviewer = nextReviewer(codeReviews, backends, ctx.state.codexUnavailable, ctx.state.codexUnavailableSince);
9228
9446
  const risk = ctx.state.ticket?.realizedRisk ?? ctx.state.ticket?.risk ?? "low";
9229
9447
  const rounds = requiredRounds(risk);
9230
9448
  const mergeBase = ctx.state.git.mergeBase;
9449
+ const isIssueFix = !!ctx.state.currentIssue;
9450
+ const issueHeader = isIssueFix ? `Issue Fix Code Review (${ctx.state.currentIssue.id})` : "Code Review";
9231
9451
  const diffCommand = mergeBase ? `\`git diff ${mergeBase}\`` : `\`git diff HEAD\` AND \`git ls-files --others --exclude-standard\``;
9232
9452
  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.";
9233
9453
  if (reviewer === "lenses") {
9234
9454
  return {
9235
9455
  instruction: [
9236
- `# Multi-Lens Code Review \u2014 Round ${roundNum} of ${rounds} minimum`,
9456
+ `# Multi-Lens ${issueHeader} \u2014 Round ${roundNum} of ${rounds} minimum`,
9237
9457
  "",
9238
9458
  `Capture the diff with: ${diffCommand}`,
9239
9459
  "",
9240
9460
  "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.",
9241
9461
  "",
9242
- "1. Capture the full diff",
9243
- "2. Call `prepareLensReview()` with the diff and changed file list",
9244
- "3. Spawn all lens subagents in parallel (each prompt is provided by the orchestrator)",
9245
- "4. Collect results and pass through the merger and judge pipeline",
9246
- "5. Report the final SynthesisResult verdict and findings",
9462
+ "1. Capture the full diff and changed file list (`git diff --name-only`)",
9463
+ "2. Call `claudestory_review_lenses_prepare` with the diff, changedFiles, stage: CODE_REVIEW, and ticketDescription",
9464
+ "3. Spawn all lens subagents in parallel (each prompt is returned by the prepare tool)",
9465
+ "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)",
9466
+ "5. Run the merger agent with the returned mergerPrompt, then call `claudestory_review_lenses_judge`",
9467
+ "6. Run the judge agent and report the final SynthesisResult verdict and findings",
9247
9468
  "",
9248
9469
  "When done, report verdict and findings."
9249
9470
  ].join("\n"),
@@ -9251,14 +9472,14 @@ var CodeReviewStage = class {
9251
9472
  diffReminder,
9252
9473
  "Do NOT compress or summarize the diff.",
9253
9474
  "Lens subagents run in parallel with read-only tools (Read, Grep, Glob).",
9254
- "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."
9475
+ "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."
9255
9476
  ],
9256
9477
  transitionedFrom: ctx.state.previousState ?? void 0
9257
9478
  };
9258
9479
  }
9259
9480
  return {
9260
9481
  instruction: [
9261
- `# Code Review \u2014 Round ${roundNum} of ${rounds} minimum`,
9482
+ `# ${issueHeader} \u2014 Round ${roundNum} of ${rounds} minimum`,
9262
9483
  "",
9263
9484
  `Capture the diff with: ${diffCommand}`,
9264
9485
  "",
@@ -9270,7 +9491,8 @@ var CodeReviewStage = class {
9270
9491
  reminders: [
9271
9492
  diffReminder,
9272
9493
  "Do NOT compress or summarize the diff.",
9273
- "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."
9494
+ "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.",
9495
+ ...reviewer === "codex" ? ["If codex is unavailable (usage limit, error, etc.), fall back to agent review and include 'codex unavailable' in your report notes."] : []
9274
9496
  ],
9275
9497
  transitionedFrom: ctx.state.previousState ?? void 0
9276
9498
  };
@@ -9284,7 +9506,8 @@ var CodeReviewStage = class {
9284
9506
  const roundNum = codeReviews.length + 1;
9285
9507
  const findings = report.findings ?? [];
9286
9508
  const backends = ctx.state.config.reviewBackends;
9287
- const reviewerBackend = nextReviewer(codeReviews, backends);
9509
+ const computedReviewer = nextReviewer(codeReviews, backends, ctx.state.codexUnavailable, ctx.state.codexUnavailableSince);
9510
+ const reviewerBackend = report.reviewer ?? (computedReviewer === "codex" && report.notes && /codex\b.*\b(unavail|limit|failed|down|error|usage)/i.test(report.notes) ? "agent" : null) ?? computedReviewer;
9288
9511
  codeReviews.push({
9289
9512
  round: roundNum,
9290
9513
  reviewer: reviewerBackend,
@@ -9296,6 +9519,9 @@ var CodeReviewStage = class {
9296
9519
  codexSessionId: report.reviewerSessionId,
9297
9520
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
9298
9521
  });
9522
+ if (report.notes && /codex\b.*\b(unavail|limit|failed|down|error|usage)/i.test(report.notes)) {
9523
+ ctx.writeState({ codexUnavailable: true, codexUnavailableSince: (/* @__PURE__ */ new Date()).toISOString() });
9524
+ }
9299
9525
  const risk = ctx.state.ticket?.realizedRisk ?? ctx.state.ticket?.risk ?? "low";
9300
9526
  const minRounds = requiredRounds(risk);
9301
9527
  const hasCriticalOrMajor = findings.some(
@@ -9320,6 +9546,7 @@ var CodeReviewStage = class {
9320
9546
  } else {
9321
9547
  nextAction = "CODE_REVIEW";
9322
9548
  }
9549
+ const isIssueFix = !!ctx.state.currentIssue;
9323
9550
  if (nextAction === "PLAN") {
9324
9551
  clearCache(ctx.dir);
9325
9552
  ctx.writeState({
@@ -9331,9 +9558,12 @@ var CodeReviewStage = class {
9331
9558
  round: roundNum,
9332
9559
  verdict,
9333
9560
  findingCount: findings.length,
9334
- redirectedTo: "PLAN"
9561
+ redirectedTo: isIssueFix ? "ISSUE_FIX" : "PLAN"
9335
9562
  });
9336
9563
  await ctx.fileDeferredFindings(findings, "code");
9564
+ if (isIssueFix) {
9565
+ return { action: "goto", target: "ISSUE_FIX" };
9566
+ }
9337
9567
  return { action: "back", target: "PLAN", reason: "plan_redirect" };
9338
9568
  }
9339
9569
  const stateUpdate = {
@@ -9356,6 +9586,9 @@ var CodeReviewStage = class {
9356
9586
  });
9357
9587
  await ctx.fileDeferredFindings(findings, "code");
9358
9588
  if (nextAction === "IMPLEMENT") {
9589
+ if (isIssueFix) {
9590
+ return { action: "goto", target: "ISSUE_FIX" };
9591
+ }
9359
9592
  return { action: "back", target: "IMPLEMENT", reason: "request_changes" };
9360
9593
  }
9361
9594
  if (nextAction === "FINALIZE") {
@@ -9382,7 +9615,7 @@ var CodeReviewStage = class {
9382
9615
  }
9383
9616
  return { action: "advance" };
9384
9617
  }
9385
- const nextReviewerName = nextReviewer(codeReviews, backends);
9618
+ const nextReviewerName = nextReviewer(codeReviews, backends, ctx.state.codexUnavailable, ctx.state.codexUnavailableSince);
9386
9619
  const mergeBase = ctx.state.git.mergeBase;
9387
9620
  return {
9388
9621
  action: "retry",
@@ -9481,6 +9714,7 @@ var BuildStage = class {
9481
9714
 
9482
9715
  // src/autonomous/stages/verify.ts
9483
9716
  init_esm_shims();
9717
+ init_git_inspector();
9484
9718
  var MAX_VERIFY_RETRIES = 3;
9485
9719
  var APP_ROUTER_RE = /^(?:src\/)?app\/((?:.*\/)?api\/.*?)\/route\.[jt]sx?$/;
9486
9720
  var PAGES_ROUTER_RE = /^(?:src\/)?pages\/(api\/.*?)\.[jt]sx?$/;
@@ -9663,19 +9897,53 @@ function detectEndpoints(changedFiles) {
9663
9897
 
9664
9898
  // src/autonomous/stages/finalize.ts
9665
9899
  init_esm_shims();
9900
+ init_git_inspector();
9666
9901
  var FinalizeStage = class {
9667
9902
  id = "FINALIZE";
9668
9903
  async enter(ctx) {
9904
+ if (ctx.state.finalizeCheckpoint === "committed") {
9905
+ return { action: "advance" };
9906
+ }
9907
+ const previousHead = ctx.state.git.expectedHead ?? ctx.state.git.initHead;
9908
+ if (previousHead) {
9909
+ const headResult = await gitHead(ctx.root);
9910
+ if (headResult.ok && headResult.data.hash !== previousHead) {
9911
+ const treeResult = await gitDiffTreeNames(ctx.root, headResult.data.hash);
9912
+ const ticketId = ctx.state.ticket?.id;
9913
+ if (ticketId) {
9914
+ const ticketPath = `.story/tickets/${ticketId}.json`;
9915
+ if (treeResult.ok && !treeResult.data.includes(ticketPath)) {
9916
+ } else {
9917
+ ctx.writeState({ finalizeCheckpoint: "precommit_passed" });
9918
+ return this.handleCommit(ctx, { completedAction: "commit_done", commitHash: headResult.data.hash });
9919
+ }
9920
+ }
9921
+ const issueId = ctx.state.currentIssue?.id;
9922
+ if (issueId) {
9923
+ const issuePath = `.story/issues/${issueId}.json`;
9924
+ if (treeResult.ok && !treeResult.data.includes(issuePath)) {
9925
+ } else {
9926
+ ctx.writeState({ finalizeCheckpoint: "precommit_passed" });
9927
+ return this.handleCommit(ctx, { completedAction: "commit_done", commitHash: headResult.data.hash });
9928
+ }
9929
+ }
9930
+ if (!ticketId && !issueId) {
9931
+ ctx.writeState({ finalizeCheckpoint: "precommit_passed" });
9932
+ return this.handleCommit(ctx, { completedAction: "commit_done", commitHash: headResult.data.hash });
9933
+ }
9934
+ }
9935
+ }
9669
9936
  return {
9670
9937
  instruction: [
9671
9938
  "# Finalize",
9672
9939
  "",
9673
9940
  "Code review passed. Time to commit.",
9674
9941
  "",
9675
- ctx.state.ticket ? `1. Update ticket ${ctx.state.ticket.id} status to "complete" in .story/` : "",
9676
- ctx.state.currentIssue ? `1. Ensure issue ${ctx.state.currentIssue.id} status is "resolved" in .story/issues/` : "",
9677
- "2. Stage only the files you created or modified for this work (code + .story/ changes). Do NOT use `git add -A` or `git add .`",
9678
- '3. Call me with completedAction: "files_staged"'
9942
+ "1. Run `git reset` to clear the staging area (ensures no stale files from prior operations)",
9943
+ ctx.state.ticket ? `2. Update ticket ${ctx.state.ticket.id} status to "complete" in .story/` : "",
9944
+ ctx.state.currentIssue ? `2. Ensure .story/issues/${ctx.state.currentIssue.id}.json is updated with status: "resolved"` : "",
9945
+ "3. Stage only the files you modified for this fix (code + .story/ changes). Do NOT use `git add -A` or `git add .`",
9946
+ '4. Call me with completedAction: "files_staged"'
9679
9947
  ].filter(Boolean).join("\n"),
9680
9948
  reminders: [
9681
9949
  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."
@@ -9696,6 +9964,9 @@ var FinalizeStage = class {
9696
9964
  return this.handlePrecommit(ctx);
9697
9965
  }
9698
9966
  if (action === "commit_done") {
9967
+ if (!checkpoint) {
9968
+ ctx.writeState({ finalizeCheckpoint: "precommit_passed" });
9969
+ }
9699
9970
  return this.handleCommit(ctx, report);
9700
9971
  }
9701
9972
  return {
@@ -9709,12 +9980,12 @@ var FinalizeStage = class {
9709
9980
  return {
9710
9981
  action: "retry",
9711
9982
  instruction: [
9712
- "Files staged. Now run pre-commit checks.",
9983
+ "Files staged. Now commit.",
9713
9984
  "",
9714
- 'Run any pre-commit hooks or linting, then call me with completedAction: "precommit_passed".',
9715
- 'If pre-commit fails, fix the issues, re-stage, and call me with completedAction: "files_staged" again.'
9716
- ].join("\n"),
9717
- reminders: ["Verify staged set is intact after pre-commit hooks."]
9985
+ ctx.state.ticket ? `Commit with message: "feat: <description> (${ctx.state.ticket.id})"` : "Commit with a descriptive message.",
9986
+ "",
9987
+ 'Call me with completedAction: "commit_done" and include the commitHash.'
9988
+ ].join("\n")
9718
9989
  };
9719
9990
  }
9720
9991
  const stagedResult = await gitDiffCachedNames(ctx.root);
@@ -9749,15 +10020,14 @@ var FinalizeStage = class {
9749
10020
  return { action: "retry", instruction: 'No files are staged. Stage your changes and call me again with completedAction: "files_staged".' };
9750
10021
  }
9751
10022
  const baselineUntracked = ctx.state.git.baseline?.untrackedPaths ?? [];
9752
- let overlapOverridden = false;
9753
10023
  if (baselineUntracked.length > 0) {
9754
10024
  const sessionTicketPath = ctx.state.ticket?.id ? `.story/tickets/${ctx.state.ticket.id}.json` : null;
10025
+ const sessionIssuePath = ctx.state.currentIssue?.id ? `.story/issues/${ctx.state.currentIssue.id}.json` : null;
9755
10026
  const overlap = stagedResult.data.filter(
9756
- (f) => baselineUntracked.includes(f) && f !== sessionTicketPath
10027
+ (f) => baselineUntracked.includes(f) && f !== sessionTicketPath && f !== sessionIssuePath
9757
10028
  );
9758
10029
  if (overlap.length > 0) {
9759
10030
  if (report.overrideOverlap) {
9760
- overlapOverridden = true;
9761
10031
  } else {
9762
10032
  return {
9763
10033
  action: "retry",
@@ -9787,17 +10057,17 @@ var FinalizeStage = class {
9787
10057
  }
9788
10058
  }
9789
10059
  ctx.writeState({
9790
- finalizeCheckpoint: overlapOverridden ? "staged_override" : "staged"
10060
+ finalizeCheckpoint: "precommit_passed"
9791
10061
  });
9792
10062
  return {
9793
10063
  action: "retry",
9794
10064
  instruction: [
9795
- "Files staged. Now run pre-commit checks.",
10065
+ "Files staged. Now commit.",
9796
10066
  "",
9797
- 'Run any pre-commit hooks or linting, then call me with completedAction: "precommit_passed".',
9798
- 'If pre-commit fails, fix the issues, re-stage, and call me with completedAction: "files_staged" again.'
9799
- ].join("\n"),
9800
- reminders: ["Verify staged set is intact after pre-commit hooks."]
10067
+ ctx.state.ticket ? `Commit with message: "feat: <description> (${ctx.state.ticket.id})"` : "Commit with a descriptive message.",
10068
+ "",
10069
+ 'Call me with completedAction: "commit_done" and include the commitHash.'
10070
+ ].join("\n")
9801
10071
  };
9802
10072
  }
9803
10073
  async handlePrecommit(ctx) {
@@ -9814,8 +10084,9 @@ var FinalizeStage = class {
9814
10084
  const baselineUntracked = ctx.state.git.baseline?.untrackedPaths ?? [];
9815
10085
  if (baselineUntracked.length > 0) {
9816
10086
  const sessionTicketPath = ctx.state.ticket?.id ? `.story/tickets/${ctx.state.ticket.id}.json` : null;
10087
+ const sessionIssuePath = ctx.state.currentIssue?.id ? `.story/issues/${ctx.state.currentIssue.id}.json` : null;
9817
10088
  const overlap = stagedResult.data.filter(
9818
- (f) => baselineUntracked.includes(f) && f !== sessionTicketPath
10089
+ (f) => baselineUntracked.includes(f) && f !== sessionTicketPath && f !== sessionIssuePath
9819
10090
  );
9820
10091
  if (overlap.length > 0) {
9821
10092
  ctx.writeState({ finalizeCheckpoint: null });
@@ -9886,6 +10157,7 @@ var FinalizeStage = class {
9886
10157
  finalizeCheckpoint: "committed",
9887
10158
  resolvedIssues: [...ctx.state.resolvedIssues ?? [], currentIssue.id],
9888
10159
  currentIssue: null,
10160
+ ticketStartedAt: null,
9889
10161
  git: {
9890
10162
  ...ctx.state.git,
9891
10163
  mergeBase: normalizedHash,
@@ -9895,11 +10167,20 @@ var FinalizeStage = class {
9895
10167
  ctx.appendEvent("commit", { commitHash: normalizedHash, issueId: currentIssue.id });
9896
10168
  return { action: "goto", target: "COMPLETE" };
9897
10169
  }
9898
- 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;
10170
+ const completedTicket = ctx.state.ticket ? {
10171
+ id: ctx.state.ticket.id,
10172
+ title: ctx.state.ticket.title,
10173
+ commitHash: normalizedHash,
10174
+ risk: ctx.state.ticket.risk,
10175
+ realizedRisk: ctx.state.ticket.realizedRisk,
10176
+ startedAt: ctx.state.ticketStartedAt ?? void 0,
10177
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
10178
+ } : void 0;
9899
10179
  ctx.writeState({
9900
10180
  finalizeCheckpoint: "committed",
9901
10181
  completedTickets: completedTicket ? [...ctx.state.completedTickets, completedTicket] : ctx.state.completedTickets,
9902
10182
  ticket: void 0,
10183
+ ticketStartedAt: null,
9903
10184
  git: {
9904
10185
  ...ctx.state.git,
9905
10186
  mergeBase: normalizedHash,
@@ -9933,7 +10214,7 @@ var CompleteStage = class {
9933
10214
  target: "HANDOVER",
9934
10215
  result: {
9935
10216
  instruction: [
9936
- `# Ticket Complete \u2014 ${mode} mode session ending`,
10217
+ `# Ticket Complete -- ${mode} mode session ending`,
9937
10218
  "",
9938
10219
  `Ticket **${ctx.state.ticket?.id}** completed. Write a brief session handover.`,
9939
10220
  "",
@@ -9944,36 +10225,23 @@ var CompleteStage = class {
9944
10225
  }
9945
10226
  };
9946
10227
  }
9947
- const handoverInterval = ctx.state.config.handoverInterval ?? 5;
9948
- if (handoverInterval > 0 && totalWorkDone > 0 && totalWorkDone % handoverInterval === 0) {
9949
- try {
9950
- const { handleHandoverCreate: handleHandoverCreate3 } = await Promise.resolve().then(() => (init_handover(), handover_exports));
9951
- const completedIds = ctx.state.completedTickets.map((t) => t.id).join(", ");
9952
- const resolvedIds = (ctx.state.resolvedIssues ?? []).join(", ");
9953
- const content = [
9954
- `# Checkpoint \u2014 ${totalWorkDone} items completed`,
9955
- "",
9956
- `**Session:** ${ctx.state.sessionId}`,
9957
- ...completedIds ? [`**Tickets:** ${completedIds}`] : [],
9958
- ...resolvedIds ? [`**Issues resolved:** ${resolvedIds}`] : [],
9959
- "",
9960
- "This is an automatic mid-session checkpoint. The session is still active."
9961
- ].join("\n");
9962
- await handleHandoverCreate3(content, "checkpoint", "md", ctx.root);
9963
- } catch {
9964
- }
9965
- try {
9966
- const { loadProject: loadProject2 } = await Promise.resolve().then(() => (init_project_loader(), project_loader_exports));
9967
- const { saveSnapshot: saveSnapshot2 } = await Promise.resolve().then(() => (init_snapshot(), snapshot_exports));
9968
- const loadResult = await loadProject2(ctx.root);
9969
- await saveSnapshot2(ctx.root, loadResult);
9970
- } catch {
9971
- }
9972
- ctx.appendEvent("checkpoint", { ticketsDone, issuesDone, totalWorkDone, interval: handoverInterval });
10228
+ await this.tryCheckpoint(ctx, totalWorkDone, ticketsDone, issuesDone);
10229
+ let projectState;
10230
+ try {
10231
+ ({ state: projectState } = await ctx.loadProject());
10232
+ } catch (err) {
10233
+ return {
10234
+ action: "goto",
10235
+ target: "HANDOVER",
10236
+ result: {
10237
+ instruction: `Failed to load project state: ${err instanceof Error ? err.message : String(err)}. Ending session -- write a handover noting the error.`,
10238
+ reminders: [],
10239
+ transitionedFrom: "COMPLETE"
10240
+ }
10241
+ };
9973
10242
  }
9974
- const { state: projectState } = await ctx.loadProject();
9975
- let nextTarget;
9976
10243
  const targetedRemaining = isTargetedMode(ctx.state) ? getRemainingTargets(ctx.state) : null;
10244
+ let nextTarget;
9977
10245
  if (targetedRemaining !== null) {
9978
10246
  nextTarget = targetedRemaining.length === 0 ? "HANDOVER" : "PICK_TICKET";
9979
10247
  } else if (maxTickets > 0 && totalWorkDone >= maxTickets) {
@@ -9988,66 +10256,118 @@ var CompleteStage = class {
9988
10256
  }
9989
10257
  }
9990
10258
  if (nextTarget === "HANDOVER") {
9991
- const postComplete = ctx.state.resolvedPostComplete ?? ctx.recipe.postComplete;
9992
- const postResult = findFirstPostComplete(postComplete, ctx);
9993
- if (postResult.kind === "found") {
9994
- ctx.writeState({ pipelinePhase: "postComplete" });
9995
- return { action: "goto", target: postResult.stage.id };
10259
+ return this.buildHandoverResult(ctx, targetedRemaining, ticketsDone, issuesDone);
10260
+ }
10261
+ if (targetedRemaining !== null) {
10262
+ return this.buildTargetedPickResult(ctx, targetedRemaining, projectState);
10263
+ }
10264
+ return this.buildStandardPickResult(ctx, projectState, ticketsDone, maxTickets);
10265
+ }
10266
+ async report(ctx, _report) {
10267
+ return this.enter(ctx);
10268
+ }
10269
+ // ---------------------------------------------------------------------------
10270
+ // Checkpoint -- mid-session handover + snapshot (best-effort)
10271
+ // ---------------------------------------------------------------------------
10272
+ async tryCheckpoint(ctx, totalWorkDone, ticketsDone, issuesDone) {
10273
+ const handoverInterval = ctx.state.config.handoverInterval ?? 5;
10274
+ if (handoverInterval <= 0 || totalWorkDone <= 0 || totalWorkDone % handoverInterval !== 0) return;
10275
+ try {
10276
+ const { handleHandoverCreate: handleHandoverCreate3 } = await Promise.resolve().then(() => (init_handover(), handover_exports));
10277
+ const completedIds = ctx.state.completedTickets.map((t) => t.id).join(", ");
10278
+ const resolvedIds = (ctx.state.resolvedIssues ?? []).join(", ");
10279
+ const content = [
10280
+ `# Checkpoint -- ${totalWorkDone} items completed`,
10281
+ "",
10282
+ `**Session:** ${ctx.state.sessionId}`,
10283
+ ...completedIds ? [`**Tickets:** ${completedIds}`] : [],
10284
+ ...resolvedIds ? [`**Issues resolved:** ${resolvedIds}`] : [],
10285
+ "",
10286
+ "This is an automatic mid-session checkpoint. The session is still active."
10287
+ ].join("\n");
10288
+ await handleHandoverCreate3(content, "checkpoint", "md", ctx.root);
10289
+ } catch {
10290
+ }
10291
+ try {
10292
+ const { loadProject: loadProject2 } = await Promise.resolve().then(() => (init_project_loader(), project_loader_exports));
10293
+ const { saveSnapshot: saveSnapshot2 } = await Promise.resolve().then(() => (init_snapshot(), snapshot_exports));
10294
+ const loadResult = await loadProject2(ctx.root);
10295
+ await saveSnapshot2(ctx.root, loadResult);
10296
+ } catch {
10297
+ }
10298
+ ctx.appendEvent("checkpoint", { ticketsDone, issuesDone, totalWorkDone, interval: handoverInterval });
10299
+ }
10300
+ // ---------------------------------------------------------------------------
10301
+ // HANDOVER instruction -- session ending
10302
+ // ---------------------------------------------------------------------------
10303
+ buildHandoverResult(ctx, targetedRemaining, ticketsDone, issuesDone) {
10304
+ const postComplete = ctx.state.resolvedPostComplete ?? ctx.recipe.postComplete;
10305
+ const postResult = findFirstPostComplete(postComplete, ctx);
10306
+ if (postResult.kind === "found") {
10307
+ ctx.writeState({ pipelinePhase: "postComplete" });
10308
+ return { action: "goto", target: postResult.stage.id };
10309
+ }
10310
+ 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`;
10311
+ return {
10312
+ action: "goto",
10313
+ target: "HANDOVER",
10314
+ result: {
10315
+ instruction: [
10316
+ handoverHeader,
10317
+ "",
10318
+ "Write a session handover summarizing what was accomplished, decisions made, and what's next.",
10319
+ "",
10320
+ 'Call me with completedAction: "handover_written" and include the content in handoverContent.'
10321
+ ].join("\n"),
10322
+ reminders: [],
10323
+ transitionedFrom: "COMPLETE",
10324
+ contextAdvice: "ok"
9996
10325
  }
9997
- 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`;
10326
+ };
10327
+ }
10328
+ // ---------------------------------------------------------------------------
10329
+ // Targeted PICK_TICKET instruction
10330
+ // ---------------------------------------------------------------------------
10331
+ buildTargetedPickResult(ctx, targetedRemaining, projectState) {
10332
+ const { text: candidatesText, firstReady } = buildTargetedCandidatesText(targetedRemaining, projectState);
10333
+ if (!firstReady) {
9998
10334
  return {
9999
10335
  action: "goto",
10000
10336
  target: "HANDOVER",
10001
10337
  result: {
10002
- instruction: [
10003
- handoverHeader,
10004
- "",
10005
- "Write a session handover summarizing what was accomplished, decisions made, and what's next.",
10006
- "",
10007
- 'Call me with completedAction: "handover_written" and include the content in handoverContent.'
10008
- ].join("\n"),
10338
+ instruction: buildTargetedStuckHandover(candidatesText, ctx.state.sessionId),
10009
10339
  reminders: [],
10010
- transitionedFrom: "COMPLETE",
10011
- contextAdvice: "ok"
10340
+ transitionedFrom: "COMPLETE"
10012
10341
  }
10013
10342
  };
10014
10343
  }
10015
- if (targetedRemaining !== null) {
10016
- const { text: candidatesText2, firstReady } = buildTargetedCandidatesText(targetedRemaining, projectState);
10017
- if (!firstReady) {
10018
- return {
10019
- action: "goto",
10020
- target: "HANDOVER",
10021
- result: {
10022
- instruction: buildTargetedStuckHandover(candidatesText2, ctx.state.sessionId),
10023
- reminders: [],
10024
- transitionedFrom: "COMPLETE"
10025
- }
10026
- };
10344
+ const precomputed = { text: candidatesText, firstReady };
10345
+ const targetedInstruction = buildTargetedPickInstruction(targetedRemaining, projectState, ctx.state.sessionId, precomputed);
10346
+ return {
10347
+ action: "goto",
10348
+ target: "PICK_TICKET",
10349
+ result: {
10350
+ instruction: [
10351
+ `# Item Complete -- Continuing (${ctx.state.targetWork.length - targetedRemaining.length}/${ctx.state.targetWork.length} targets done)`,
10352
+ "",
10353
+ "Do NOT stop. Do NOT ask the user. Continue immediately with the next target.",
10354
+ "",
10355
+ targetedInstruction
10356
+ ].join("\n"),
10357
+ reminders: [
10358
+ "Do NOT stop or summarize. Call autonomous_guide IMMEDIATELY to pick the next target.",
10359
+ "Do NOT ask the user for confirmation.",
10360
+ "You are in targeted auto mode -- pick ONLY from the listed items."
10361
+ ],
10362
+ transitionedFrom: "COMPLETE",
10363
+ contextAdvice: "ok"
10027
10364
  }
10028
- const precomputed = { text: candidatesText2, firstReady };
10029
- const targetedInstruction = buildTargetedPickInstruction(targetedRemaining, projectState, ctx.state.sessionId, precomputed);
10030
- return {
10031
- action: "goto",
10032
- target: "PICK_TICKET",
10033
- result: {
10034
- instruction: [
10035
- `# Item Complete -- Continuing (${ctx.state.targetWork.length - targetedRemaining.length}/${ctx.state.targetWork.length} targets done)`,
10036
- "",
10037
- "Do NOT stop. Do NOT ask the user. Continue immediately with the next target.",
10038
- "",
10039
- targetedInstruction
10040
- ].join("\n"),
10041
- reminders: [
10042
- "Do NOT stop or summarize. Call autonomous_guide IMMEDIATELY to pick the next target.",
10043
- "Do NOT ask the user for confirmation.",
10044
- "You are in targeted auto mode -- pick ONLY from the listed items."
10045
- ],
10046
- transitionedFrom: "COMPLETE",
10047
- contextAdvice: "ok"
10048
- }
10049
- };
10050
- }
10365
+ };
10366
+ }
10367
+ // ---------------------------------------------------------------------------
10368
+ // Standard auto PICK_TICKET instruction
10369
+ // ---------------------------------------------------------------------------
10370
+ buildStandardPickResult(ctx, projectState, ticketsDone, maxTickets) {
10051
10371
  const candidates = nextTickets(projectState, 5);
10052
10372
  let candidatesText = "";
10053
10373
  if (candidates.kind === "found") {
@@ -10061,7 +10381,7 @@ var CompleteStage = class {
10061
10381
  target: "PICK_TICKET",
10062
10382
  result: {
10063
10383
  instruction: [
10064
- `# Ticket Complete \u2014 Continuing (${ticketsDone}/${maxTickets})`,
10384
+ `# Ticket Complete -- Continuing (${ticketsDone}/${maxTickets})`,
10065
10385
  "",
10066
10386
  "Do NOT stop. Do NOT ask the user. Continue immediately with the next ticket.",
10067
10387
  "",
@@ -10075,16 +10395,13 @@ var CompleteStage = class {
10075
10395
  reminders: [
10076
10396
  "Do NOT stop or summarize. Call autonomous_guide IMMEDIATELY to pick the next ticket.",
10077
10397
  "Do NOT ask the user for confirmation.",
10078
- "You are in autonomous mode \u2014 continue working until all tickets are done or the session limit is reached."
10398
+ "You are in autonomous mode -- continue working until all tickets are done or the session limit is reached."
10079
10399
  ],
10080
10400
  transitionedFrom: "COMPLETE",
10081
10401
  contextAdvice: "ok"
10082
10402
  }
10083
10403
  };
10084
10404
  }
10085
- async report(ctx, _report) {
10086
- return this.enter(ctx);
10087
- }
10088
10405
  };
10089
10406
 
10090
10407
  // src/autonomous/stages/lesson-capture.ts
@@ -10200,7 +10517,32 @@ var IssueFixStage = class {
10200
10517
  if (!issue) {
10201
10518
  return { action: "goto", target: "PICK_TICKET" };
10202
10519
  }
10203
- const { state: projectState } = await ctx.loadProject();
10520
+ let projectState;
10521
+ try {
10522
+ ({ state: projectState } = await ctx.loadProject());
10523
+ } catch {
10524
+ return {
10525
+ instruction: [
10526
+ "# Fix Issue",
10527
+ "",
10528
+ `**${issue.id}**: ${issue.title} (severity: ${issue.severity})`,
10529
+ "",
10530
+ "(Warning: could not load full issue details from .story/ -- using session state.)",
10531
+ "",
10532
+ 'Fix this issue, then update its status to "resolved" in `.story/issues/`.',
10533
+ "Add a resolution description explaining the fix.",
10534
+ "",
10535
+ "When done, call `claudestory_autonomous_guide` with:",
10536
+ "```json",
10537
+ `{ "sessionId": "${ctx.state.sessionId}", "action": "report", "report": { "completedAction": "issue_fixed" } }`,
10538
+ "```"
10539
+ ].join("\n"),
10540
+ reminders: [
10541
+ 'Update the issue JSON: set status to "resolved", add resolution text, set resolvedDate.',
10542
+ "Do NOT ask the user for confirmation."
10543
+ ]
10544
+ };
10545
+ }
10204
10546
  const fullIssue = projectState.issues.find((i) => i.id === issue.id);
10205
10547
  const details = fullIssue ? [
10206
10548
  `**${fullIssue.id}**: ${fullIssue.title}`,
@@ -10235,7 +10577,12 @@ var IssueFixStage = class {
10235
10577
  if (!issue) {
10236
10578
  return { action: "goto", target: "PICK_TICKET" };
10237
10579
  }
10238
- const { state: projectState } = await ctx.loadProject();
10580
+ let projectState;
10581
+ try {
10582
+ ({ state: projectState } = await ctx.loadProject());
10583
+ } catch (err) {
10584
+ return { action: "retry", instruction: `Failed to load project state: ${err instanceof Error ? err.message : String(err)}. Check .story/ files for corruption, then report again.` };
10585
+ }
10239
10586
  const current = projectState.issues.find((i) => i.id === issue.id);
10240
10587
  if (!current || current.status !== "resolved") {
10241
10588
  return {
@@ -10244,6 +10591,10 @@ var IssueFixStage = class {
10244
10591
  reminders: ["Set status to 'resolved', add resolution text, set resolvedDate."]
10245
10592
  };
10246
10593
  }
10594
+ const enableCodeReview = !!ctx.recipe.stages.ISSUE_FIX?.enableCodeReview;
10595
+ if (enableCodeReview) {
10596
+ return { action: "goto", target: "CODE_REVIEW" };
10597
+ }
10247
10598
  return {
10248
10599
  action: "goto",
10249
10600
  target: "FINALIZE",
@@ -10253,9 +10604,10 @@ var IssueFixStage = class {
10253
10604
  "",
10254
10605
  `Issue ${issue.id} resolved. Time to commit.`,
10255
10606
  "",
10256
- `1. Ensure .story/issues/${issue.id}.json is updated with status: "resolved"`,
10257
- "2. Stage only the files you modified for this fix (code + .story/ changes). Do NOT use `git add -A` or `git add .`",
10258
- '3. Call me with completedAction: "files_staged"'
10607
+ "1. Run `git reset` to clear the staging area (ensures no stale files from prior operations)",
10608
+ `2. Ensure .story/issues/${issue.id}.json is updated with status: "resolved"`,
10609
+ "3. Stage only the files you modified for this fix (code + .story/ changes). Do NOT use `git add -A` or `git add .`",
10610
+ '4. Call me with completedAction: "files_staged"'
10259
10611
  ].join("\n"),
10260
10612
  reminders: ["Stage both code changes and .story/ issue update in the same commit. Only stage files related to this fix."],
10261
10613
  transitionedFrom: "ISSUE_FIX"
@@ -10273,7 +10625,12 @@ var IssueSweepStage = class {
10273
10625
  return !issueConfig?.enabled;
10274
10626
  }
10275
10627
  async enter(ctx) {
10276
- const { state: projectState } = await ctx.loadProject();
10628
+ let projectState;
10629
+ try {
10630
+ ({ state: projectState } = await ctx.loadProject());
10631
+ } catch {
10632
+ return { action: "goto", target: "HANDOVER" };
10633
+ }
10277
10634
  const allIssues = projectState.issues.filter((i) => i.status === "open");
10278
10635
  if (allIssues.length === 0) {
10279
10636
  return { action: "goto", target: "HANDOVER" };
@@ -10325,7 +10682,12 @@ var IssueSweepStage = class {
10325
10682
  }
10326
10683
  const current = sweep.current;
10327
10684
  if (current) {
10328
- const { state: verifyState } = await ctx.loadProject();
10685
+ let verifyState;
10686
+ try {
10687
+ ({ state: verifyState } = await ctx.loadProject());
10688
+ } catch (err) {
10689
+ return { action: "retry", instruction: `Failed to load project state: ${err instanceof Error ? err.message : String(err)}. Check .story/ files, then report again.` };
10690
+ }
10329
10691
  const currentIssue = verifyState.issues.find((i) => i.id === current);
10330
10692
  if (currentIssue && currentIssue.status === "open") {
10331
10693
  return {
@@ -10344,7 +10706,16 @@ var IssueSweepStage = class {
10344
10706
  ctx.appendEvent("issue_sweep_complete", { resolved: resolved.length });
10345
10707
  return { action: "goto", target: "HANDOVER" };
10346
10708
  }
10347
- const { state: projectState } = await ctx.loadProject();
10709
+ let projectState;
10710
+ try {
10711
+ ({ state: projectState } = await ctx.loadProject());
10712
+ } catch {
10713
+ return {
10714
+ action: "retry",
10715
+ instruction: `Issue ${next} is next. Fix it and report again. (Could not load full details from .story/.)`,
10716
+ reminders: ["Set status to 'resolved' and add a resolution description."]
10717
+ };
10718
+ }
10348
10719
  const nextIssue = projectState.issues.find((i) => i.id === next);
10349
10720
  return {
10350
10721
  action: "retry",
@@ -10368,8 +10739,292 @@ Impact: ${nextIssue.impact}` : ""}` : `Fix issue ${next}.`,
10368
10739
  // src/autonomous/stages/handover.ts
10369
10740
  init_esm_shims();
10370
10741
  init_handover();
10371
- import { writeFileSync as writeFileSync4 } from "fs";
10742
+ init_git_inspector();
10743
+ import { writeFileSync as writeFileSync5 } from "fs";
10744
+ import { join as join18 } from "path";
10745
+
10746
+ // src/autonomous/resume-marker.ts
10747
+ init_esm_shims();
10748
+ import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4, unlinkSync as unlinkSync3, existsSync as existsSync11 } from "fs";
10372
10749
  import { join as join17 } from "path";
10750
+ var MARKER_FILENAME = "autonomous-resume.md";
10751
+ function sanitize(input, maxLen = 120) {
10752
+ return input.replace(/[\r\n]+/g, " ").replace(/\s+/g, " ").trim().slice(0, maxLen);
10753
+ }
10754
+ function writeResumeMarker(root, sessionId, state) {
10755
+ try {
10756
+ const rulesDir = join17(root, ".claude", "rules");
10757
+ mkdirSync4(rulesDir, { recursive: true });
10758
+ const ticketInfo = state.ticket ? `Working on: ${sanitize(state.ticket.id, 20)} (${sanitize(state.ticket.title)})` : "Between tickets";
10759
+ const progress = `Progress: ${state.completedTickets.length} tickets completed, ${(state.resolvedIssues ?? []).length} issues resolved`;
10760
+ const lines = [
10761
+ "CRITICAL: An autonomous coding session is active and waiting to resume.",
10762
+ "",
10763
+ `Session: ${sessionId}`,
10764
+ ticketInfo,
10765
+ progress
10766
+ ];
10767
+ if (state.preCompactState) {
10768
+ lines.push(`State before compaction: ${state.preCompactState}`);
10769
+ }
10770
+ lines.push(
10771
+ "",
10772
+ "You MUST call `claudestory_autonomous_guide` with:",
10773
+ `{"sessionId": "${sessionId}", "action": "resume"}`,
10774
+ "",
10775
+ "Do NOT do any other work until you have called this.",
10776
+ "Do NOT manually create tickets, issues, or handovers.",
10777
+ "The guide manages your workflow."
10778
+ );
10779
+ const content = lines.join("\n") + "\n";
10780
+ writeFileSync4(join17(rulesDir, MARKER_FILENAME), content, "utf-8");
10781
+ } catch {
10782
+ }
10783
+ }
10784
+ function removeResumeMarker(root) {
10785
+ try {
10786
+ const markerPath = join17(root, ".claude", "rules", MARKER_FILENAME);
10787
+ if (existsSync11(markerPath)) unlinkSync3(markerPath);
10788
+ } catch {
10789
+ }
10790
+ }
10791
+
10792
+ // src/core/session-report-formatter.ts
10793
+ init_esm_shims();
10794
+ function formatSessionReport(data, format) {
10795
+ const { state, events, planContent, gitLog } = data;
10796
+ if (format === "json") {
10797
+ return JSON.stringify({
10798
+ ok: true,
10799
+ data: {
10800
+ summary: buildSummaryData(state),
10801
+ ticketProgression: state.completedTickets,
10802
+ reviewStats: state.reviews,
10803
+ events: events.events.slice(-50),
10804
+ malformedEventCount: events.malformedCount,
10805
+ contextPressure: state.contextPressure,
10806
+ git: {
10807
+ branch: state.git.branch,
10808
+ initHead: state.git.initHead,
10809
+ commits: gitLog
10810
+ },
10811
+ problems: buildProblems(state, events)
10812
+ }
10813
+ }, null, 2);
10814
+ }
10815
+ const sections = [];
10816
+ sections.push(buildSummarySection(state));
10817
+ sections.push(buildTicketSection(state));
10818
+ sections.push(buildReviewSection(state));
10819
+ sections.push(buildEventSection(events));
10820
+ sections.push(buildPressureSection(state));
10821
+ sections.push(buildGitSection(state, gitLog));
10822
+ sections.push(buildProblemsSection(state, events));
10823
+ return sections.join("\n\n---\n\n");
10824
+ }
10825
+ function buildSummaryData(state) {
10826
+ return {
10827
+ sessionId: state.sessionId,
10828
+ mode: state.mode ?? "auto",
10829
+ recipe: state.recipe,
10830
+ status: state.status,
10831
+ terminationReason: state.terminationReason,
10832
+ startedAt: state.startedAt,
10833
+ lastGuideCall: state.lastGuideCall,
10834
+ guideCallCount: state.guideCallCount,
10835
+ ticketsCompleted: state.completedTickets.length
10836
+ };
10837
+ }
10838
+ function buildSummarySection(state) {
10839
+ const duration = state.startedAt && state.lastGuideCall ? formatDuration(state.startedAt, state.lastGuideCall) : "unknown";
10840
+ return [
10841
+ "## Session Summary",
10842
+ "",
10843
+ `- **ID:** ${state.sessionId}`,
10844
+ `- **Mode:** ${state.mode ?? "auto"}`,
10845
+ `- **Recipe:** ${state.recipe}`,
10846
+ `- **Status:** ${state.status}${state.terminationReason ? ` (${state.terminationReason})` : ""}`,
10847
+ `- **Duration:** ${duration}`,
10848
+ `- **Guide calls:** ${state.guideCallCount}`,
10849
+ `- **Tickets completed:** ${state.completedTickets.length}`
10850
+ ].join("\n");
10851
+ }
10852
+ function buildTicketSection(state) {
10853
+ if (state.completedTickets.length === 0) {
10854
+ const current = state.ticket;
10855
+ if (current) {
10856
+ return [
10857
+ "## Ticket Progression",
10858
+ "",
10859
+ `In progress: **${current.id}** \u2014 ${current.title} (risk: ${current.risk ?? "unknown"})`
10860
+ ].join("\n");
10861
+ }
10862
+ return "## Ticket Progression\n\nNo tickets completed.";
10863
+ }
10864
+ const lines = ["## Ticket Progression", ""];
10865
+ for (const t of state.completedTickets) {
10866
+ const risk = t.realizedRisk ? `${t.risk ?? "?"} \u2192 ${t.realizedRisk}` : t.risk ?? "unknown";
10867
+ const duration = t.startedAt && t.completedAt ? formatDuration(t.startedAt, t.completedAt) : null;
10868
+ const durationPart = duration ? ` | duration: ${duration}` : "";
10869
+ lines.push(`- **${t.id}:** ${t.title} | risk: ${risk}${durationPart} | commit: \`${t.commitHash ?? "?"}\``);
10870
+ }
10871
+ return lines.join("\n");
10872
+ }
10873
+ function buildReviewSection(state) {
10874
+ const plan = state.reviews.plan;
10875
+ const code = state.reviews.code;
10876
+ if (plan.length === 0 && code.length === 0) {
10877
+ return "## Review Stats\n\nNo reviews recorded.";
10878
+ }
10879
+ const lines = ["## Review Stats", ""];
10880
+ if (plan.length > 0) {
10881
+ lines.push(`**Plan reviews:** ${plan.length} round(s)`);
10882
+ for (const r of plan) {
10883
+ lines.push(` - Round ${r.round}: ${r.verdict} (${r.findingCount} findings, ${r.criticalCount} critical, ${r.majorCount} major) \u2014 ${r.reviewer}`);
10884
+ }
10885
+ }
10886
+ if (code.length > 0) {
10887
+ lines.push(`**Code reviews:** ${code.length} round(s)`);
10888
+ for (const r of code) {
10889
+ lines.push(` - Round ${r.round}: ${r.verdict} (${r.findingCount} findings, ${r.criticalCount} critical, ${r.majorCount} major) \u2014 ${r.reviewer}`);
10890
+ }
10891
+ }
10892
+ const totalFindings = [...plan, ...code].reduce((sum, r) => sum + r.findingCount, 0);
10893
+ lines.push("", `**Total findings:** ${totalFindings}`);
10894
+ return lines.join("\n");
10895
+ }
10896
+ function buildEventSection(events) {
10897
+ if (events.events.length === 0 && events.malformedCount === 0) {
10898
+ return "## Event Timeline\n\nNot available.";
10899
+ }
10900
+ const capped = events.events.slice(-50);
10901
+ const omitted = events.events.length - capped.length;
10902
+ const lines = ["## Event Timeline", ""];
10903
+ if (omitted > 0) {
10904
+ lines.push(`*${omitted} earlier events omitted*`, "");
10905
+ }
10906
+ for (const e of capped) {
10907
+ const ts = e.timestamp ? e.timestamp.slice(11, 19) : "??:??:??";
10908
+ const detail = e.data ? Object.entries(e.data).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(" ") : "";
10909
+ lines.push(`- \`${ts}\` [${e.type}] ${detail}`.trimEnd());
10910
+ }
10911
+ if (events.malformedCount > 0) {
10912
+ lines.push("", `*${events.malformedCount} malformed event line(s) skipped*`);
10913
+ }
10914
+ return lines.join("\n");
10915
+ }
10916
+ function buildPressureSection(state) {
10917
+ const p = state.contextPressure;
10918
+ return [
10919
+ "## Context Pressure",
10920
+ "",
10921
+ `- **Level:** ${p.level}`,
10922
+ `- **Guide calls:** ${p.guideCallCount}`,
10923
+ `- **Tickets completed:** ${p.ticketsCompleted}`,
10924
+ `- **Compactions:** ${p.compactionCount}`,
10925
+ `- **Events log:** ${p.eventsLogBytes} bytes`
10926
+ ].join("\n");
10927
+ }
10928
+ function buildGitSection(state, gitLog) {
10929
+ const lines = [
10930
+ "## Git Summary",
10931
+ "",
10932
+ `- **Branch:** ${state.git.branch ?? "unknown"}`,
10933
+ `- **Init HEAD:** \`${state.git.initHead ?? "?"}\``,
10934
+ `- **Expected HEAD:** \`${state.git.expectedHead ?? "?"}\``
10935
+ ];
10936
+ if (gitLog && gitLog.length > 0) {
10937
+ lines.push("", "**Commits:**");
10938
+ for (const c of gitLog) {
10939
+ lines.push(`- ${c}`);
10940
+ }
10941
+ } else {
10942
+ lines.push("", "Commits: Not available.");
10943
+ }
10944
+ return lines.join("\n");
10945
+ }
10946
+ function buildProblems(state, events) {
10947
+ const problems = [];
10948
+ if (state.terminationReason && state.terminationReason !== "normal") {
10949
+ problems.push(`Abnormal termination: ${state.terminationReason}`);
10950
+ }
10951
+ if (events.malformedCount > 0) {
10952
+ problems.push(`${events.malformedCount} malformed event line(s) in events.log`);
10953
+ }
10954
+ for (const e of events.events) {
10955
+ if (e.type.includes("error") || e.type.includes("exhaustion")) {
10956
+ problems.push(`[${e.type}] ${e.timestamp ?? ""} ${JSON.stringify(e.data)}`);
10957
+ } else if (e.data?.result === "exhaustion") {
10958
+ problems.push(`[${e.type}] exhaustion at ${e.timestamp ?? ""}`);
10959
+ }
10960
+ }
10961
+ if (state.deferralsUnfiled) {
10962
+ problems.push("Session has unfiled deferrals");
10963
+ }
10964
+ return problems;
10965
+ }
10966
+ function buildProblemsSection(state, events) {
10967
+ const problems = buildProblems(state, events);
10968
+ if (problems.length === 0) {
10969
+ return "## Problems\n\nNone detected.";
10970
+ }
10971
+ return ["## Problems", "", ...problems.map((p) => `- ${p}`)].join("\n");
10972
+ }
10973
+ function formatCompactReport(data) {
10974
+ const { state, remainingWork } = data;
10975
+ const endTime = data.endedAt ?? state.lastGuideCall ?? (/* @__PURE__ */ new Date()).toISOString();
10976
+ const duration = state.startedAt ? formatDuration(state.startedAt, endTime) : "unknown";
10977
+ const ticketCount = state.completedTickets.length;
10978
+ const issueCount = (state.resolvedIssues ?? []).length;
10979
+ const reviewRounds = state.reviews.plan.length + state.reviews.code.length;
10980
+ const totalFindings = [...state.reviews.plan, ...state.reviews.code].reduce((s, r) => s + r.findingCount, 0);
10981
+ const compactions = state.contextPressure?.compactionCount ?? 0;
10982
+ const lines = [
10983
+ "## Session Report",
10984
+ "",
10985
+ `**Duration:** ${duration} | **Tickets:** ${ticketCount} | **Issues:** ${issueCount} | **Reviews:** ${reviewRounds} rounds (${totalFindings} findings) | **Compactions:** ${compactions}`
10986
+ ];
10987
+ if (ticketCount > 0) {
10988
+ lines.push("", "### Completed", "| Ticket | Title | Duration |", "|--------|-------|----------|");
10989
+ for (const t of state.completedTickets) {
10990
+ const ticketDuration = t.startedAt && t.completedAt ? formatDuration(t.startedAt, t.completedAt) : "--";
10991
+ const safeTitle = (t.title ?? "").replace(/\|/g, "\\|");
10992
+ lines.push(`| ${t.id} | ${safeTitle} | ${ticketDuration} |`);
10993
+ }
10994
+ const timings = state.completedTickets.filter((t) => t.startedAt && t.completedAt).map((t) => new Date(t.completedAt).getTime() - new Date(t.startedAt).getTime());
10995
+ if (timings.length > 0) {
10996
+ const avgMs = timings.reduce((a, b) => a + b, 0) / timings.length;
10997
+ const avgMins = Math.round(avgMs / 6e4);
10998
+ lines.push("", `**Avg time per ticket:** ${avgMins}m`);
10999
+ }
11000
+ }
11001
+ if (remainingWork && (remainingWork.tickets.length > 0 || remainingWork.issues.length > 0)) {
11002
+ lines.push("", "### What's Left");
11003
+ for (const t of remainingWork.tickets) {
11004
+ lines.push(`- ${t.id}: ${t.title} (unblocked)`);
11005
+ }
11006
+ for (const i of remainingWork.issues) {
11007
+ lines.push(`- ${i.id}: ${i.title} (${i.severity})`);
11008
+ }
11009
+ }
11010
+ return lines.join("\n");
11011
+ }
11012
+ function formatDuration(start, end) {
11013
+ try {
11014
+ const ms = new Date(end).getTime() - new Date(start).getTime();
11015
+ if (isNaN(ms) || ms < 0) return "unknown";
11016
+ const mins = Math.floor(ms / 6e4);
11017
+ if (mins < 60) return `${mins}m`;
11018
+ const hours = Math.floor(mins / 60);
11019
+ return `${hours}h ${mins % 60}m`;
11020
+ } catch {
11021
+ return "unknown";
11022
+ }
11023
+ }
11024
+
11025
+ // src/autonomous/stages/handover.ts
11026
+ init_project_loader();
11027
+ init_queries();
10373
11028
  var HandoverStage = class {
10374
11029
  id = "HANDOVER";
10375
11030
  async enter(ctx) {
@@ -10400,8 +11055,8 @@ var HandoverStage = class {
10400
11055
  } catch {
10401
11056
  handoverFailed = true;
10402
11057
  try {
10403
- const fallbackPath = join17(ctx.dir, "handover-fallback.md");
10404
- writeFileSync4(fallbackPath, content, "utf-8");
11058
+ const fallbackPath = join18(ctx.dir, "handover-fallback.md");
11059
+ writeFileSync5(fallbackPath, content, "utf-8");
10405
11060
  } catch {
10406
11061
  }
10407
11062
  }
@@ -10427,6 +11082,19 @@ var HandoverStage = class {
10427
11082
  issuesResolved: (ctx.state.resolvedIssues ?? []).length,
10428
11083
  handoverFailed
10429
11084
  });
11085
+ removeResumeMarker(ctx.root);
11086
+ let reportSection = "";
11087
+ try {
11088
+ const { state: projectState } = await loadProject(ctx.root);
11089
+ const nextResult = nextTickets(projectState, 5);
11090
+ const openIssues = projectState.issues.filter((i) => i.status === "open" || i.status === "inprogress").slice(0, 5);
11091
+ const remainingWork = {
11092
+ tickets: nextResult.kind === "found" ? nextResult.candidates.map((c) => ({ id: c.ticket.id, title: c.ticket.title })) : [],
11093
+ issues: openIssues.map((i) => ({ id: i.id, title: i.title, severity: i.severity }))
11094
+ };
11095
+ reportSection = "\n\n" + formatCompactReport({ state: ctx.state, endedAt: (/* @__PURE__ */ new Date()).toISOString(), remainingWork });
11096
+ } catch {
11097
+ }
10430
11098
  const ticketsDone = ctx.state.completedTickets.length;
10431
11099
  const issuesDone = (ctx.state.resolvedIssues ?? []).length;
10432
11100
  const resolvedList = (ctx.state.resolvedIssues ?? []).map((id) => `- ${id} (resolved)`).join("\n");
@@ -10440,7 +11108,7 @@ var HandoverStage = class {
10440
11108
  "",
10441
11109
  ctx.state.completedTickets.map((t) => `- ${t.id}${t.title ? `: ${t.title}` : ""} (${t.commitHash ?? "no commit"})`).join("\n"),
10442
11110
  ...resolvedList ? [resolvedList] : []
10443
- ].join("\n"),
11111
+ ].join("\n") + reportSection,
10444
11112
  reminders: [],
10445
11113
  transitionedFrom: "HANDOVER"
10446
11114
  }
@@ -10474,7 +11142,7 @@ init_queries();
10474
11142
  // src/autonomous/version-check.ts
10475
11143
  init_esm_shims();
10476
11144
  import { readFileSync as readFileSync9 } from "fs";
10477
- import { join as join18, dirname as dirname4 } from "path";
11145
+ import { join as join19, dirname as dirname4 } from "path";
10478
11146
  import { fileURLToPath as fileURLToPath3 } from "url";
10479
11147
  function checkVersionMismatch(runningVersion, installedVersion) {
10480
11148
  if (!installedVersion) return null;
@@ -10486,8 +11154,8 @@ function getInstalledVersion() {
10486
11154
  try {
10487
11155
  const thisFile = fileURLToPath3(import.meta.url);
10488
11156
  const candidates = [
10489
- join18(dirname4(thisFile), "..", "..", "package.json"),
10490
- join18(dirname4(thisFile), "..", "package.json")
11157
+ join19(dirname4(thisFile), "..", "..", "package.json"),
11158
+ join19(dirname4(thisFile), "..", "package.json")
10491
11159
  ];
10492
11160
  for (const candidate of candidates) {
10493
11161
  try {
@@ -10503,7 +11171,7 @@ function getInstalledVersion() {
10503
11171
  }
10504
11172
  }
10505
11173
  function getRunningVersion() {
10506
- return "0.1.61";
11174
+ return "0.1.63";
10507
11175
  }
10508
11176
 
10509
11177
  // src/autonomous/guide.ts
@@ -10522,24 +11190,25 @@ var RECOVERY_MAPPING = {
10522
11190
  CODE_REVIEW: { state: "PLAN", resetPlan: true, resetCode: true },
10523
11191
  FINALIZE: { state: "IMPLEMENT", resetPlan: false, resetCode: true },
10524
11192
  LESSON_CAPTURE: { state: "PICK_TICKET", resetPlan: false, resetCode: false },
10525
- ISSUE_FIX: { state: "PICK_TICKET", resetPlan: false, resetCode: false },
11193
+ ISSUE_FIX: { state: "ISSUE_FIX", resetPlan: false, resetCode: false },
11194
+ // T-208: self-recover to avoid dangling currentIssue
10526
11195
  ISSUE_SWEEP: { state: "PICK_TICKET", resetPlan: false, resetCode: false }
10527
11196
  };
10528
11197
  function buildGuideRecommendOptions(root) {
10529
11198
  const opts = {};
10530
11199
  try {
10531
- const handoversDir = join19(root, ".story", "handovers");
11200
+ const handoversDir = join20(root, ".story", "handovers");
10532
11201
  const files = readdirSync4(handoversDir, "utf-8").filter((f) => f.endsWith(".md")).sort();
10533
11202
  if (files.length > 0) {
10534
- opts.latestHandoverContent = readFileSync10(join19(handoversDir, files[files.length - 1]), "utf-8");
11203
+ opts.latestHandoverContent = readFileSync10(join20(handoversDir, files[files.length - 1]), "utf-8");
10535
11204
  }
10536
11205
  } catch {
10537
11206
  }
10538
11207
  try {
10539
- const snapshotsDir = join19(root, ".story", "snapshots");
11208
+ const snapshotsDir = join20(root, ".story", "snapshots");
10540
11209
  const snapFiles = readdirSync4(snapshotsDir, "utf-8").filter((f) => f.endsWith(".json")).sort();
10541
11210
  if (snapFiles.length > 0) {
10542
- const raw = readFileSync10(join19(snapshotsDir, snapFiles[snapFiles.length - 1]), "utf-8");
11211
+ const raw = readFileSync10(join20(snapshotsDir, snapFiles[snapFiles.length - 1]), "utf-8");
10543
11212
  const snap = JSON.parse(raw);
10544
11213
  if (snap.issues) {
10545
11214
  opts.previousOpenIssueCount = snap.issues.filter((i) => i.status !== "resolved").length;
@@ -10617,6 +11286,33 @@ async function recoverPendingMutation(dir, state, root) {
10617
11286
  const mutation = state.pendingProjectMutation;
10618
11287
  if (!mutation || typeof mutation !== "object") return state;
10619
11288
  const m = mutation;
11289
+ if (m.type === "issue_update") {
11290
+ const targetId2 = m.target;
11291
+ const targetValue2 = m.value;
11292
+ const expectedCurrent2 = m.expectedCurrent;
11293
+ try {
11294
+ const { loadProject: loadProject2 } = await Promise.resolve().then(() => (init_project_loader(), project_loader_exports));
11295
+ const { state: projectState } = await loadProject2(root);
11296
+ const issue = projectState.issues.find((i) => i.id === targetId2);
11297
+ if (issue) {
11298
+ if (issue.status === targetValue2) {
11299
+ } else if (expectedCurrent2 && issue.status === expectedCurrent2) {
11300
+ const { handleIssueUpdate: handleIssueUpdate2 } = await Promise.resolve().then(() => (init_issue2(), issue_exports));
11301
+ await handleIssueUpdate2(targetId2, { status: targetValue2 }, "json", root);
11302
+ } else {
11303
+ appendEvent(dir, {
11304
+ rev: state.revision,
11305
+ type: "mutation_conflict",
11306
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
11307
+ data: { targetId: targetId2, expected: expectedCurrent2, actual: issue.status, transitionId: m.transitionId }
11308
+ });
11309
+ }
11310
+ }
11311
+ } catch {
11312
+ }
11313
+ const cleared2 = { ...state, pendingProjectMutation: null };
11314
+ return writeSessionSync(dir, cleared2);
11315
+ }
10620
11316
  if (m.type !== "ticket_update") return state;
10621
11317
  const targetId = m.target;
10622
11318
  const targetValue = m.value;
@@ -10892,6 +11588,7 @@ async function handleStart(root, args) {
10892
11588
  reviewBackends: sessionConfig.reviewBackends,
10893
11589
  stages: sessionConfig.stageOverrides
10894
11590
  });
11591
+ removeResumeMarker(root);
10895
11592
  const session = createSession(root, recipe, wsId, sessionConfig);
10896
11593
  const dir = sessionDir(root, session.sessionId);
10897
11594
  try {
@@ -11014,7 +11711,7 @@ Staged: ${stagedResult.data.join(", ")}`
11014
11711
  const passMatch = combined.match(/(\d+)\s*pass/i);
11015
11712
  const failMatch = combined.match(/(\d+)\s*fail/i);
11016
11713
  const passCount = passMatch ? parseInt(passMatch[1], 10) : -1;
11017
- const failCount = failMatch ? parseInt(failMatch[1], 10) : -1;
11714
+ const failCount = failMatch ? parseInt(failMatch[1], 10) : exitCode === 0 && passCount > 0 ? 0 : -1;
11018
11715
  const output = combined.slice(-500);
11019
11716
  updated = { ...updated, testBaseline: { exitCode, passCount, failCount, summary: output } };
11020
11717
  if (writeTestsEnabled && failCount < 0) {
@@ -11054,7 +11751,7 @@ Staged: ${stagedResult.data.join(", ")}`
11054
11751
  }
11055
11752
  }
11056
11753
  const { state: projectState, warnings } = await loadProject(root);
11057
- const handoversDir = join19(root, ".story", "handovers");
11754
+ const handoversDir = join20(root, ".story", "handovers");
11058
11755
  const ctx = { state: projectState, warnings, root, handoversDir, format: "md" };
11059
11756
  let handoverText = "";
11060
11757
  try {
@@ -11065,13 +11762,13 @@ Staged: ${stagedResult.data.join(", ")}`
11065
11762
  let recapText = "";
11066
11763
  try {
11067
11764
  const snapshotInfo = await loadLatestSnapshot(root);
11068
- const recap = buildRecap(projectState, snapshotInfo);
11765
+ const recap = await buildRecap(projectState, snapshotInfo, root);
11069
11766
  if (recap.changes) {
11070
11767
  recapText = "Changes since last snapshot available.";
11071
11768
  }
11072
11769
  } catch {
11073
11770
  }
11074
- const rulesText = readFileSafe2(join19(root, "RULES.md"));
11771
+ const rulesText = readFileSafe2(join20(root, "RULES.md"));
11075
11772
  const lessonDigest = buildLessonDigest(projectState.lessons);
11076
11773
  const digestParts = [
11077
11774
  handoverText ? `## Recent Handovers
@@ -11087,7 +11784,7 @@ ${rulesText}` : "",
11087
11784
  ].filter(Boolean);
11088
11785
  const digest = digestParts.join("\n\n---\n\n");
11089
11786
  try {
11090
- writeFileSync5(join19(dir, "context-digest.md"), digest, "utf-8");
11787
+ writeFileSync6(join20(dir, "context-digest.md"), digest, "utf-8");
11091
11788
  } catch {
11092
11789
  }
11093
11790
  if (mode !== "auto" && args.ticketId) {
@@ -11485,8 +12182,18 @@ async function handleResume(root, args) {
11485
12182
  `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.`
11486
12183
  ));
11487
12184
  }
12185
+ let ownCommitDrift = false;
11488
12186
  if (expectedHead && headResult.data.hash !== expectedHead) {
11489
- const mapping = RECOVERY_MAPPING[resumeState] ?? { state: "PICK_TICKET", resetPlan: false, resetCode: false };
12187
+ const ancestorCheck = await gitIsAncestor(root, expectedHead, headResult.data.hash);
12188
+ if (ancestorCheck.ok && ancestorCheck.data) {
12189
+ ownCommitDrift = true;
12190
+ }
12191
+ }
12192
+ if (expectedHead && headResult.data.hash !== expectedHead && !ownCommitDrift) {
12193
+ let mapping = RECOVERY_MAPPING[resumeState] ?? { state: "PICK_TICKET", resetPlan: false, resetCode: false };
12194
+ if (info.state.currentIssue && resumeState === "CODE_REVIEW") {
12195
+ mapping = { state: "ISSUE_FIX", resetPlan: false, resetCode: true };
12196
+ }
11490
12197
  const recoveryReviews = {
11491
12198
  plan: mapping.resetPlan ? [] : info.state.reviews.plan,
11492
12199
  code: mapping.resetCode ? [] : info.state.reviews.code
@@ -11514,6 +12221,19 @@ async function handleResume(root, args) {
11514
12221
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
11515
12222
  data: { drift: true, previousState: resumeState, recoveryState: mapping.state, expectedHead, actualHead: headResult.data.hash, ticketId: info.state.ticket?.id }
11516
12223
  });
12224
+ appendEvent(info.dir, {
12225
+ rev: driftWritten.revision,
12226
+ type: "resumed",
12227
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
12228
+ data: {
12229
+ preCompactState: resumeState,
12230
+ compactionCount: driftWritten.contextPressure?.compactionCount ?? 0,
12231
+ ticketId: info.state.ticket?.id ?? null,
12232
+ headMatch: false,
12233
+ recoveryState: mapping.state
12234
+ }
12235
+ });
12236
+ removeResumeMarker(root);
11517
12237
  const driftPreamble = `**HEAD changed during compaction** (expected ${expectedHead.slice(0, 8)}, got ${headResult.data.hash.slice(0, 8)}). Review state invalidated.
11518
12238
 
11519
12239
  `;
@@ -11586,6 +12306,29 @@ async function handleResume(root, args) {
11586
12306
  reminders: ["Re-implement and verify before re-submitting for code review."]
11587
12307
  });
11588
12308
  }
12309
+ if (mapping.state === "ISSUE_FIX") {
12310
+ const issueFixStage = getStage("ISSUE_FIX");
12311
+ if (issueFixStage) {
12312
+ const recipe = resolveRecipeFromState(driftWritten);
12313
+ const ctx = new StageContext(root, info.dir, driftWritten, recipe);
12314
+ const enterResult = await issueFixStage.enter(ctx);
12315
+ if (isStageAdvance(enterResult)) {
12316
+ return processAdvance(ctx, issueFixStage, enterResult);
12317
+ }
12318
+ return guideResult(ctx.state, "ISSUE_FIX", {
12319
+ instruction: [
12320
+ "# Resumed After Compact \u2014 HEAD Mismatch",
12321
+ "",
12322
+ `${driftPreamble}Recovered to **ISSUE_FIX**. Re-fix the issue and mark resolved.`,
12323
+ "",
12324
+ "---",
12325
+ "",
12326
+ enterResult.instruction
12327
+ ].join("\n"),
12328
+ reminders: enterResult.reminders ?? []
12329
+ });
12330
+ }
12331
+ }
11589
12332
  return guideResult(driftWritten, mapping.state, {
11590
12333
  instruction: `# Resumed After Compact \u2014 HEAD Mismatch
11591
12334
 
@@ -11607,8 +12350,23 @@ ${driftPreamble}Recovered to state: **${mapping.state}**. Continue from here.`,
11607
12350
  compactPreparedAt: null,
11608
12351
  resumeBlocked: false,
11609
12352
  guideCallCount: 0,
11610
- contextPressure: { ...resumePressure, level: evaluatePressure({ ...info.state, guideCallCount: 0, contextPressure: resumePressure }) }
12353
+ contextPressure: { ...resumePressure, level: evaluatePressure({ ...info.state, guideCallCount: 0, contextPressure: resumePressure }) },
12354
+ // T-184: Update expectedHead on own-commit drift (mergeBase stays at branch-off point)
12355
+ ...ownCommitDrift ? { git: { ...info.state.git, expectedHead: headResult.data.hash } } : {}
12356
+ });
12357
+ appendEvent(info.dir, {
12358
+ rev: written.revision,
12359
+ type: "resumed",
12360
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
12361
+ data: {
12362
+ preCompactState: resumeState,
12363
+ compactionCount: written.contextPressure?.compactionCount ?? 0,
12364
+ ticketId: info.state.ticket?.id ?? null,
12365
+ headMatch: !ownCommitDrift,
12366
+ ownCommit: ownCommitDrift || void 0
12367
+ }
11611
12368
  });
12369
+ removeResumeMarker(root);
11612
12370
  if (resumeState === "PICK_TICKET") {
11613
12371
  if (isTargetedMode(written)) {
11614
12372
  const dispatched = await dispatchTargetedResume(root, written, info.dir, [
@@ -11722,6 +12480,12 @@ async function handlePreCompact(root, args) {
11722
12480
  await saveSnapshot2(root, loadResult);
11723
12481
  } catch {
11724
12482
  }
12483
+ writeResumeMarker(root, result.sessionId, {
12484
+ ticket: info.state.ticket,
12485
+ completedTickets: info.state.completedTickets,
12486
+ resolvedIssues: info.state.resolvedIssues,
12487
+ preCompactState: result.preCompactState
12488
+ });
11725
12489
  const reread = findSessionById(root, args.sessionId);
11726
12490
  const written = reread?.state ?? info.state;
11727
12491
  return guideResult(written, "COMPACT", {
@@ -11818,276 +12582,96 @@ async function handleCancel(root, args) {
11818
12582
  appendEvent(cancelInfo.dir, {
11819
12583
  rev: written.revision,
11820
12584
  type: "cancelled",
11821
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
11822
- data: {
11823
- previousState: cancelInfo.state.state,
11824
- ticketId: ticketId ?? null,
11825
- ticketReleased,
11826
- ticketConflict,
11827
- stashPopFailed
11828
- }
11829
- });
11830
- const stashNote = stashPopFailed ? " Auto-stash pop failed \u2014 run `git stash pop` manually." : "";
11831
- return {
11832
- content: [{ type: "text", text: `Session ${args.sessionId} cancelled. ${written.completedTickets.length} ticket(s) and ${(written.resolvedIssues ?? []).length} issue(s) were completed.${stashNote}` }]
11833
- };
11834
- }
11835
- function guideResult(state, currentState, opts) {
11836
- const summary = {
11837
- ticket: state.ticket ? `${state.ticket.id}: ${state.ticket.title}` : "none",
11838
- risk: state.ticket?.risk ?? "unknown",
11839
- completed: [...state.completedTickets.map((t) => t.id), ...state.resolvedIssues ?? []],
11840
- currentStep: currentState,
11841
- contextPressure: state.contextPressure?.level ?? "low",
11842
- branch: state.git?.branch ?? null
11843
- };
11844
- const allReminders = [...opts.reminders ?? []];
11845
- if ((state.mode === "auto" || !state.mode) && currentState !== "SESSION_END") {
11846
- allReminders.push(
11847
- "NEVER cancel this session due to context size. Compaction is automatic \u2014 Claude Story preserves all session state across compactions via hooks."
11848
- );
11849
- }
11850
- const output = {
11851
- sessionId: state.sessionId,
11852
- state: currentState,
11853
- transitionedFrom: opts.transitionedFrom,
11854
- instruction: opts.instruction,
11855
- reminders: allReminders,
11856
- contextAdvice: "ok",
11857
- sessionSummary: summary
11858
- };
11859
- const parts = [
11860
- output.instruction,
11861
- "",
11862
- "---",
11863
- `**Session:** ${output.sessionId}`,
11864
- `**State:** ${output.state}${output.transitionedFrom ? ` (from ${output.transitionedFrom})` : ""}`,
11865
- `**Ticket:** ${summary.ticket}`,
11866
- `**Risk:** ${summary.risk}`,
11867
- `**Completed:** ${summary.completed.length > 0 ? summary.completed.join(", ") : "none"}`,
11868
- `**Tickets done:** ${summary.completed.length}`,
11869
- summary.branch ? `**Branch:** ${summary.branch}` : "",
11870
- output.reminders.length > 0 ? `
11871
- **Reminders:**
11872
- ${output.reminders.map((r) => `- ${r}`).join("\n")}` : ""
11873
- ].filter(Boolean);
11874
- return { content: [{ type: "text", text: parts.join("\n") }] };
11875
- }
11876
- function guideError(err) {
11877
- const message = err instanceof Error ? err.message : String(err);
11878
- return {
11879
- content: [{ type: "text", text: `[autonomous_guide error] ${message}` }],
11880
- isError: true
11881
- };
11882
- }
11883
- function readFileSafe2(path2) {
11884
- try {
11885
- return readFileSync10(path2, "utf-8");
11886
- } catch {
11887
- return "";
11888
- }
11889
- }
11890
-
11891
- // src/cli/commands/session-report.ts
11892
- init_esm_shims();
11893
- init_session();
11894
- init_session_types();
11895
- import { readFileSync as readFileSync11, existsSync as existsSync12 } from "fs";
11896
- import { join as join20 } from "path";
11897
-
11898
- // src/core/session-report-formatter.ts
11899
- init_esm_shims();
11900
- function formatSessionReport(data, format) {
11901
- const { state, events, planContent, gitLog } = data;
11902
- if (format === "json") {
11903
- return JSON.stringify({
11904
- ok: true,
11905
- data: {
11906
- summary: buildSummaryData(state),
11907
- ticketProgression: state.completedTickets,
11908
- reviewStats: state.reviews,
11909
- events: events.events.slice(-50),
11910
- malformedEventCount: events.malformedCount,
11911
- contextPressure: state.contextPressure,
11912
- git: {
11913
- branch: state.git.branch,
11914
- initHead: state.git.initHead,
11915
- commits: gitLog
11916
- },
11917
- problems: buildProblems(state, events)
11918
- }
11919
- }, null, 2);
11920
- }
11921
- const sections = [];
11922
- sections.push(buildSummarySection(state));
11923
- sections.push(buildTicketSection(state));
11924
- sections.push(buildReviewSection(state));
11925
- sections.push(buildEventSection(events));
11926
- sections.push(buildPressureSection(state));
11927
- sections.push(buildGitSection(state, gitLog));
11928
- sections.push(buildProblemsSection(state, events));
11929
- return sections.join("\n\n---\n\n");
11930
- }
11931
- function buildSummaryData(state) {
11932
- return {
11933
- sessionId: state.sessionId,
11934
- mode: state.mode ?? "auto",
11935
- recipe: state.recipe,
11936
- status: state.status,
11937
- terminationReason: state.terminationReason,
11938
- startedAt: state.startedAt,
11939
- lastGuideCall: state.lastGuideCall,
11940
- guideCallCount: state.guideCallCount,
11941
- ticketsCompleted: state.completedTickets.length
11942
- };
11943
- }
11944
- function buildSummarySection(state) {
11945
- const duration = state.startedAt && state.lastGuideCall ? formatDuration(state.startedAt, state.lastGuideCall) : "unknown";
11946
- return [
11947
- "## Session Summary",
11948
- "",
11949
- `- **ID:** ${state.sessionId}`,
11950
- `- **Mode:** ${state.mode ?? "auto"}`,
11951
- `- **Recipe:** ${state.recipe}`,
11952
- `- **Status:** ${state.status}${state.terminationReason ? ` (${state.terminationReason})` : ""}`,
11953
- `- **Duration:** ${duration}`,
11954
- `- **Guide calls:** ${state.guideCallCount}`,
11955
- `- **Tickets completed:** ${state.completedTickets.length}`
11956
- ].join("\n");
11957
- }
11958
- function buildTicketSection(state) {
11959
- if (state.completedTickets.length === 0) {
11960
- const current = state.ticket;
11961
- if (current) {
11962
- return [
11963
- "## Ticket Progression",
11964
- "",
11965
- `In progress: **${current.id}** \u2014 ${current.title} (risk: ${current.risk ?? "unknown"})`
11966
- ].join("\n");
11967
- }
11968
- return "## Ticket Progression\n\nNo tickets completed.";
11969
- }
11970
- const lines = ["## Ticket Progression", ""];
11971
- for (const t of state.completedTickets) {
11972
- const risk = t.realizedRisk ? `${t.risk ?? "?"} \u2192 ${t.realizedRisk}` : t.risk ?? "unknown";
11973
- lines.push(`- **${t.id}:** ${t.title} | risk: ${risk} | commit: \`${t.commitHash ?? "?"}\``);
11974
- }
11975
- return lines.join("\n");
11976
- }
11977
- function buildReviewSection(state) {
11978
- const plan = state.reviews.plan;
11979
- const code = state.reviews.code;
11980
- if (plan.length === 0 && code.length === 0) {
11981
- return "## Review Stats\n\nNo reviews recorded.";
11982
- }
11983
- const lines = ["## Review Stats", ""];
11984
- if (plan.length > 0) {
11985
- lines.push(`**Plan reviews:** ${plan.length} round(s)`);
11986
- for (const r of plan) {
11987
- lines.push(` - Round ${r.round}: ${r.verdict} (${r.findingCount} findings, ${r.criticalCount} critical, ${r.majorCount} major) \u2014 ${r.reviewer}`);
11988
- }
11989
- }
11990
- if (code.length > 0) {
11991
- lines.push(`**Code reviews:** ${code.length} round(s)`);
11992
- for (const r of code) {
11993
- lines.push(` - Round ${r.round}: ${r.verdict} (${r.findingCount} findings, ${r.criticalCount} critical, ${r.majorCount} major) \u2014 ${r.reviewer}`);
11994
- }
11995
- }
11996
- const totalFindings = [...plan, ...code].reduce((sum, r) => sum + r.findingCount, 0);
11997
- lines.push("", `**Total findings:** ${totalFindings}`);
11998
- return lines.join("\n");
11999
- }
12000
- function buildEventSection(events) {
12001
- if (events.events.length === 0 && events.malformedCount === 0) {
12002
- return "## Event Timeline\n\nNot available.";
12003
- }
12004
- const capped = events.events.slice(-50);
12005
- const omitted = events.events.length - capped.length;
12006
- const lines = ["## Event Timeline", ""];
12007
- if (omitted > 0) {
12008
- lines.push(`*${omitted} earlier events omitted*`, "");
12009
- }
12010
- for (const e of capped) {
12011
- const ts = e.timestamp ? e.timestamp.slice(11, 19) : "??:??:??";
12012
- const detail = e.data ? Object.entries(e.data).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(" ") : "";
12013
- lines.push(`- \`${ts}\` [${e.type}] ${detail}`.trimEnd());
12014
- }
12015
- if (events.malformedCount > 0) {
12016
- lines.push("", `*${events.malformedCount} malformed event line(s) skipped*`);
12017
- }
12018
- return lines.join("\n");
12019
- }
12020
- function buildPressureSection(state) {
12021
- const p = state.contextPressure;
12022
- return [
12023
- "## Context Pressure",
12024
- "",
12025
- `- **Level:** ${p.level}`,
12026
- `- **Guide calls:** ${p.guideCallCount}`,
12027
- `- **Tickets completed:** ${p.ticketsCompleted}`,
12028
- `- **Compactions:** ${p.compactionCount}`,
12029
- `- **Events log:** ${p.eventsLogBytes} bytes`
12030
- ].join("\n");
12031
- }
12032
- function buildGitSection(state, gitLog) {
12033
- const lines = [
12034
- "## Git Summary",
12035
- "",
12036
- `- **Branch:** ${state.git.branch ?? "unknown"}`,
12037
- `- **Init HEAD:** \`${state.git.initHead ?? "?"}\``,
12038
- `- **Expected HEAD:** \`${state.git.expectedHead ?? "?"}\``
12039
- ];
12040
- if (gitLog && gitLog.length > 0) {
12041
- lines.push("", "**Commits:**");
12042
- for (const c of gitLog) {
12043
- lines.push(`- ${c}`);
12585
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
12586
+ data: {
12587
+ previousState: cancelInfo.state.state,
12588
+ ticketId: ticketId ?? null,
12589
+ ticketReleased,
12590
+ ticketConflict,
12591
+ stashPopFailed
12044
12592
  }
12045
- } else {
12046
- lines.push("", "Commits: Not available.");
12593
+ });
12594
+ removeResumeMarker(root);
12595
+ let reportSection = "";
12596
+ try {
12597
+ const { state: projectState } = await loadProject(root);
12598
+ const nextResult = nextTickets(projectState, 5);
12599
+ const openIssues = projectState.issues.filter((i) => i.status === "open" || i.status === "inprogress").slice(0, 5);
12600
+ const remainingWork = {
12601
+ tickets: nextResult.kind === "found" ? nextResult.candidates.map((c) => ({ id: c.ticket.id, title: c.ticket.title })) : [],
12602
+ issues: openIssues.map((i) => ({ id: i.id, title: i.title, severity: i.severity }))
12603
+ };
12604
+ reportSection = "\n\n" + formatCompactReport({ state: written, endedAt: (/* @__PURE__ */ new Date()).toISOString(), remainingWork });
12605
+ } catch {
12047
12606
  }
12048
- return lines.join("\n");
12607
+ const stashNote = stashPopFailed ? " Auto-stash pop failed \u2014 run `git stash pop` manually." : "";
12608
+ return {
12609
+ content: [{ type: "text", text: `Session ${args.sessionId} cancelled. ${written.completedTickets.length} ticket(s) and ${(written.resolvedIssues ?? []).length} issue(s) were completed.${stashNote}${reportSection}` }]
12610
+ };
12049
12611
  }
12050
- function buildProblems(state, events) {
12051
- const problems = [];
12052
- if (state.terminationReason && state.terminationReason !== "normal") {
12053
- problems.push(`Abnormal termination: ${state.terminationReason}`);
12054
- }
12055
- if (events.malformedCount > 0) {
12056
- problems.push(`${events.malformedCount} malformed event line(s) in events.log`);
12057
- }
12058
- for (const e of events.events) {
12059
- if (e.type.includes("error") || e.type.includes("exhaustion")) {
12060
- problems.push(`[${e.type}] ${e.timestamp ?? ""} ${JSON.stringify(e.data)}`);
12061
- } else if (e.data?.result === "exhaustion") {
12062
- problems.push(`[${e.type}] exhaustion at ${e.timestamp ?? ""}`);
12063
- }
12064
- }
12065
- if (state.deferralsUnfiled) {
12066
- problems.push("Session has unfiled deferrals");
12612
+ function guideResult(state, currentState, opts) {
12613
+ const summary = {
12614
+ ticket: state.ticket ? `${state.ticket.id}: ${state.ticket.title}` : "none",
12615
+ risk: state.ticket?.risk ?? "unknown",
12616
+ completed: [...state.completedTickets.map((t) => t.id), ...state.resolvedIssues ?? []],
12617
+ currentStep: currentState,
12618
+ contextPressure: state.contextPressure?.level ?? "low",
12619
+ branch: state.git?.branch ?? null
12620
+ };
12621
+ const allReminders = [...opts.reminders ?? []];
12622
+ if ((state.mode === "auto" || !state.mode) && currentState !== "SESSION_END") {
12623
+ allReminders.push(
12624
+ "NEVER cancel this session due to context size. Compaction is automatic \u2014 Claude Story preserves all session state across compactions via hooks."
12625
+ );
12067
12626
  }
12068
- return problems;
12627
+ const output = {
12628
+ sessionId: state.sessionId,
12629
+ state: currentState,
12630
+ transitionedFrom: opts.transitionedFrom,
12631
+ instruction: opts.instruction,
12632
+ reminders: allReminders,
12633
+ contextAdvice: "ok",
12634
+ sessionSummary: summary
12635
+ };
12636
+ const parts = [
12637
+ output.instruction,
12638
+ "",
12639
+ "---",
12640
+ `**Session:** ${output.sessionId}`,
12641
+ `**State:** ${output.state}${output.transitionedFrom ? ` (from ${output.transitionedFrom})` : ""}`,
12642
+ `**Ticket:** ${summary.ticket}`,
12643
+ `**Risk:** ${summary.risk}`,
12644
+ `**Completed:** ${summary.completed.length > 0 ? summary.completed.join(", ") : "none"}`,
12645
+ `**Tickets done:** ${summary.completed.length}`,
12646
+ summary.branch ? `**Branch:** ${summary.branch}` : "",
12647
+ output.reminders.length > 0 ? `
12648
+ **Reminders:**
12649
+ ${output.reminders.map((r) => `- ${r}`).join("\n")}` : ""
12650
+ ].filter(Boolean);
12651
+ return { content: [{ type: "text", text: parts.join("\n") }] };
12069
12652
  }
12070
- function buildProblemsSection(state, events) {
12071
- const problems = buildProblems(state, events);
12072
- if (problems.length === 0) {
12073
- return "## Problems\n\nNone detected.";
12074
- }
12075
- return ["## Problems", "", ...problems.map((p) => `- ${p}`)].join("\n");
12653
+ function guideError(err) {
12654
+ const message = err instanceof Error ? err.message : String(err);
12655
+ return {
12656
+ content: [{ type: "text", text: `[autonomous_guide error] ${message}` }],
12657
+ isError: true
12658
+ };
12076
12659
  }
12077
- function formatDuration(start, end) {
12660
+ function readFileSafe2(path2) {
12078
12661
  try {
12079
- const ms = new Date(end).getTime() - new Date(start).getTime();
12080
- if (isNaN(ms) || ms < 0) return "unknown";
12081
- const mins = Math.floor(ms / 6e4);
12082
- if (mins < 60) return `${mins}m`;
12083
- const hours = Math.floor(mins / 60);
12084
- return `${hours}h ${mins % 60}m`;
12662
+ return readFileSync10(path2, "utf-8");
12085
12663
  } catch {
12086
- return "unknown";
12664
+ return "";
12087
12665
  }
12088
12666
  }
12089
12667
 
12090
12668
  // src/cli/commands/session-report.ts
12669
+ init_esm_shims();
12670
+ init_session();
12671
+ init_session_types();
12672
+ init_git_inspector();
12673
+ import { readFileSync as readFileSync11, existsSync as existsSync13 } from "fs";
12674
+ import { join as join21 } from "path";
12091
12675
  var UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
12092
12676
  async function handleSessionReport(sessionId, root, format = "md") {
12093
12677
  if (!UUID_REGEX.test(sessionId)) {
@@ -12099,7 +12683,7 @@ async function handleSessionReport(sessionId, root, format = "md") {
12099
12683
  };
12100
12684
  }
12101
12685
  const dir = sessionDir(root, sessionId);
12102
- if (!existsSync12(dir)) {
12686
+ if (!existsSync13(dir)) {
12103
12687
  return {
12104
12688
  output: `Error: Session ${sessionId} not found.`,
12105
12689
  exitCode: 1,
@@ -12107,8 +12691,8 @@ async function handleSessionReport(sessionId, root, format = "md") {
12107
12691
  isError: true
12108
12692
  };
12109
12693
  }
12110
- const statePath2 = join20(dir, "state.json");
12111
- if (!existsSync12(statePath2)) {
12694
+ const statePath2 = join21(dir, "state.json");
12695
+ if (!existsSync13(statePath2)) {
12112
12696
  return {
12113
12697
  output: `Error: Session ${sessionId} corrupt \u2014 state.json missing.`,
12114
12698
  exitCode: 1,
@@ -12146,7 +12730,7 @@ async function handleSessionReport(sessionId, root, format = "md") {
12146
12730
  const events = readEvents(dir);
12147
12731
  let planContent = null;
12148
12732
  try {
12149
- planContent = readFileSync11(join20(dir, "plan.md"), "utf-8");
12733
+ planContent = readFileSync11(join21(dir, "plan.md"), "utf-8");
12150
12734
  } catch {
12151
12735
  }
12152
12736
  let gitLog = null;
@@ -12171,7 +12755,7 @@ init_issue();
12171
12755
  init_roadmap();
12172
12756
  init_output_formatter();
12173
12757
  init_helpers();
12174
- import { join as join21, resolve as resolve7 } from "path";
12758
+ import { join as join22, resolve as resolve7 } from "path";
12175
12759
  var PHASE_ID_REGEX = /^[a-z0-9]+(-[a-z0-9]+)*$/;
12176
12760
  var PHASE_ID_MAX_LENGTH = 40;
12177
12761
  function validatePhaseId(id) {
@@ -12280,7 +12864,7 @@ function formatMcpError(code, message) {
12280
12864
  async function runMcpReadTool(pinnedRoot, handler) {
12281
12865
  try {
12282
12866
  const { state, warnings } = await loadProject(pinnedRoot);
12283
- const handoversDir = join22(pinnedRoot, ".story", "handovers");
12867
+ const handoversDir = join23(pinnedRoot, ".story", "handovers");
12284
12868
  const ctx = { state, warnings, root: pinnedRoot, handoversDir, format: "md" };
12285
12869
  const result = await handler(ctx);
12286
12870
  if (result.errorCode && INFRASTRUCTURE_ERROR_CODES.includes(result.errorCode)) {
@@ -12828,6 +13412,7 @@ function registerAllTools(server, pinnedRoot) {
12828
13412
  disposition: z10.string()
12829
13413
  })).optional().describe("Review findings"),
12830
13414
  reviewerSessionId: z10.string().optional().describe("Codex session ID"),
13415
+ reviewer: z10.string().optional().describe("Actual reviewer backend used (e.g. 'agent' when codex was unavailable)"),
12831
13416
  notes: z10.string().optional().describe("Free-text notes")
12832
13417
  }).optional().describe("Report data (required for report action)")
12833
13418
  }
@@ -12852,7 +13437,7 @@ function registerAllTools(server, pinnedRoot) {
12852
13437
  }
12853
13438
  });
12854
13439
  server.registerTool("claudestory_review_lenses_synthesize", {
12855
- description: "Synthesize lens results after parallel review. Validates findings, applies blocking policy, generates merger prompt. Call after collecting all lens subagent results.",
13440
+ 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.",
12856
13441
  inputSchema: {
12857
13442
  stage: z10.enum(["CODE_REVIEW", "PLAN_REVIEW"]).optional().describe("Review stage (defaults to CODE_REVIEW)"),
12858
13443
  lensResults: z10.array(z10.object({
@@ -12863,9 +13448,13 @@ function registerAllTools(server, pinnedRoot) {
12863
13448
  activeLenses: z10.array(z10.string()).describe("Active lens names from prepare step"),
12864
13449
  skippedLenses: z10.array(z10.string()).describe("Skipped lens names from prepare step"),
12865
13450
  reviewRound: z10.number().int().min(1).optional().describe("Current review round"),
12866
- reviewId: z10.string().optional().describe("Review ID from prepare step")
13451
+ reviewId: z10.string().optional().describe("Review ID from prepare step"),
13452
+ // T-192: Origin classification inputs
13453
+ diff: z10.string().optional().describe("The diff being reviewed (for origin classification of findings into introduced vs pre-existing)"),
13454
+ changedFiles: z10.array(z10.string()).optional().describe("Changed file paths from prepare step (for origin classification)"),
13455
+ sessionId: z10.string().uuid().optional().describe("Active session ID (for dedup of auto-filed pre-existing issues across review rounds)")
12867
13456
  }
12868
- }, (args) => {
13457
+ }, async (args) => {
12869
13458
  try {
12870
13459
  const result = handleSynthesize({
12871
13460
  stage: args.stage,
@@ -12876,9 +13465,55 @@ function registerAllTools(server, pinnedRoot) {
12876
13465
  reviewRound: args.reviewRound ?? 1,
12877
13466
  reviewId: args.reviewId ?? "unknown"
12878
13467
  },
12879
- projectRoot: pinnedRoot
13468
+ projectRoot: pinnedRoot,
13469
+ diff: args.diff,
13470
+ changedFiles: args.changedFiles
12880
13471
  });
12881
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
13472
+ const filedIssues = [];
13473
+ if (result.preExistingFindings.length > 0) {
13474
+ const sessionDir2 = args.sessionId ? join23(pinnedRoot, ".story", "sessions", args.sessionId) : null;
13475
+ const alreadyFiled = sessionDir2 ? readFiledPreexisting(sessionDir2) : /* @__PURE__ */ new Set();
13476
+ const sizeBeforeLoop = alreadyFiled.size;
13477
+ for (const f of result.preExistingFindings) {
13478
+ const dedupKey = f.issueKey ?? `${f.file ?? ""}:${f.line ?? 0}:${f.category}`;
13479
+ if (alreadyFiled.has(dedupKey)) continue;
13480
+ try {
13481
+ const { handleIssueCreate: handleIssueCreate2 } = await Promise.resolve().then(() => (init_issue2(), issue_exports));
13482
+ const severityMap = { critical: "critical", major: "high", minor: "medium" };
13483
+ const severity = severityMap[f.severity] ?? "medium";
13484
+ const issueResult = await handleIssueCreate2(
13485
+ {
13486
+ title: `[pre-existing] [${f.category}] ${f.description.slice(0, 60)}`,
13487
+ severity,
13488
+ impact: f.description,
13489
+ components: ["review-lenses"],
13490
+ relatedTickets: [],
13491
+ location: f.file && f.line != null ? [`${f.file}:${f.line}`] : []
13492
+ },
13493
+ "json",
13494
+ pinnedRoot
13495
+ );
13496
+ let issueId;
13497
+ try {
13498
+ const parsed = JSON.parse(issueResult.output ?? "");
13499
+ issueId = parsed?.data?.id;
13500
+ } catch {
13501
+ const match = issueResult.output?.match(/ISS-\d+/);
13502
+ issueId = match?.[0];
13503
+ }
13504
+ if (issueId) {
13505
+ filedIssues.push({ issueKey: dedupKey, issueId });
13506
+ alreadyFiled.add(dedupKey);
13507
+ }
13508
+ } catch {
13509
+ }
13510
+ }
13511
+ if (sessionDir2 && alreadyFiled.size > sizeBeforeLoop) {
13512
+ writeFiledPreexisting(sessionDir2, alreadyFiled);
13513
+ }
13514
+ }
13515
+ const output = { ...result, filedIssues };
13516
+ return { content: [{ type: "text", text: JSON.stringify(output, null, 2) }] };
12882
13517
  } catch (err) {
12883
13518
  const msg = err instanceof Error ? err.message.replace(/\/[^\s]+/g, "<path>") : "unknown error";
12884
13519
  return { content: [{ type: "text", text: `Error synthesizing lens results: ${msg}` }], isError: true };
@@ -12919,16 +13554,33 @@ function registerAllTools(server, pinnedRoot) {
12919
13554
  }
12920
13555
  });
12921
13556
  }
13557
+ var FILED_PREEXISTING_FILE = "filed-preexisting.json";
13558
+ function readFiledPreexisting(sessionDir2) {
13559
+ try {
13560
+ const raw = readFileSync12(join23(sessionDir2, FILED_PREEXISTING_FILE), "utf-8");
13561
+ const arr = JSON.parse(raw);
13562
+ return new Set(Array.isArray(arr) ? arr : []);
13563
+ } catch {
13564
+ return /* @__PURE__ */ new Set();
13565
+ }
13566
+ }
13567
+ function writeFiledPreexisting(sessionDir2, keys) {
13568
+ try {
13569
+ mkdirSync5(sessionDir2, { recursive: true });
13570
+ writeFileSync7(join23(sessionDir2, FILED_PREEXISTING_FILE), JSON.stringify([...keys], null, 2));
13571
+ } catch {
13572
+ }
13573
+ }
12922
13574
 
12923
13575
  // src/core/init.ts
12924
13576
  init_esm_shims();
12925
13577
  init_project_loader();
12926
13578
  init_errors();
12927
13579
  import { mkdir as mkdir4, stat as stat2, readFile as readFile4, writeFile as writeFile2 } from "fs/promises";
12928
- import { join as join23, resolve as resolve8 } from "path";
13580
+ import { join as join24, resolve as resolve8 } from "path";
12929
13581
  async function initProject(root, options) {
12930
13582
  const absRoot = resolve8(root);
12931
- const wrapDir = join23(absRoot, ".story");
13583
+ const wrapDir = join24(absRoot, ".story");
12932
13584
  let exists = false;
12933
13585
  try {
12934
13586
  const s = await stat2(wrapDir);
@@ -12948,11 +13600,11 @@ async function initProject(root, options) {
12948
13600
  ".story/ already exists. Use --force to overwrite config and roadmap."
12949
13601
  );
12950
13602
  }
12951
- await mkdir4(join23(wrapDir, "tickets"), { recursive: true });
12952
- await mkdir4(join23(wrapDir, "issues"), { recursive: true });
12953
- await mkdir4(join23(wrapDir, "handovers"), { recursive: true });
12954
- await mkdir4(join23(wrapDir, "notes"), { recursive: true });
12955
- await mkdir4(join23(wrapDir, "lessons"), { recursive: true });
13603
+ await mkdir4(join24(wrapDir, "tickets"), { recursive: true });
13604
+ await mkdir4(join24(wrapDir, "issues"), { recursive: true });
13605
+ await mkdir4(join24(wrapDir, "handovers"), { recursive: true });
13606
+ await mkdir4(join24(wrapDir, "notes"), { recursive: true });
13607
+ await mkdir4(join24(wrapDir, "lessons"), { recursive: true });
12956
13608
  const created = [
12957
13609
  ".story/config.json",
12958
13610
  ".story/roadmap.json",
@@ -12992,7 +13644,7 @@ async function initProject(root, options) {
12992
13644
  };
12993
13645
  await writeConfig(config, absRoot);
12994
13646
  await writeRoadmap(roadmap, absRoot);
12995
- const gitignorePath = join23(wrapDir, ".gitignore");
13647
+ const gitignorePath = join24(wrapDir, ".gitignore");
12996
13648
  await ensureGitignoreEntries(gitignorePath, STORY_GITIGNORE_ENTRIES);
12997
13649
  const warnings = [];
12998
13650
  if (options.force && exists) {
@@ -13028,10 +13680,393 @@ async function ensureGitignoreEntries(gitignorePath, entries) {
13028
13680
  await writeFile2(gitignorePath, content, "utf-8");
13029
13681
  }
13030
13682
 
13683
+ // src/channel/inbox-watcher.ts
13684
+ init_esm_shims();
13685
+ import { watch } from "fs";
13686
+ import { readdir as readdir4, readFile as readFile5, unlink as unlink3, rename as rename2, mkdir as mkdir5 } from "fs/promises";
13687
+ import { join as join25 } from "path";
13688
+
13689
+ // src/channel/events.ts
13690
+ init_esm_shims();
13691
+ import { z as z11 } from "zod";
13692
+ var TicketRequestedPayload = z11.object({
13693
+ ticketId: z11.string().regex(/^T-\d+[a-z]?$/)
13694
+ });
13695
+ var PauseSessionPayload = z11.object({});
13696
+ var ResumeSessionPayload = z11.object({});
13697
+ var CancelSessionPayload = z11.object({
13698
+ reason: z11.string().max(1e3).optional()
13699
+ });
13700
+ var PriorityChangedPayload = z11.object({
13701
+ ticketId: z11.string().regex(/^T-\d+[a-z]?$/),
13702
+ newOrder: z11.number().int()
13703
+ });
13704
+ var PermissionResponsePayload = z11.object({
13705
+ requestId: z11.string().regex(/^[a-zA-Z0-9]{5}$/),
13706
+ behavior: z11.enum(["approve", "deny"])
13707
+ });
13708
+ var ChannelEventSchema = z11.discriminatedUnion("event", [
13709
+ z11.object({
13710
+ event: z11.literal("ticket_requested"),
13711
+ timestamp: z11.string(),
13712
+ payload: TicketRequestedPayload
13713
+ }),
13714
+ z11.object({
13715
+ event: z11.literal("pause_session"),
13716
+ timestamp: z11.string(),
13717
+ payload: PauseSessionPayload
13718
+ }),
13719
+ z11.object({
13720
+ event: z11.literal("resume_session"),
13721
+ timestamp: z11.string(),
13722
+ payload: ResumeSessionPayload
13723
+ }),
13724
+ z11.object({
13725
+ event: z11.literal("cancel_session"),
13726
+ timestamp: z11.string(),
13727
+ payload: CancelSessionPayload
13728
+ }),
13729
+ z11.object({
13730
+ event: z11.literal("priority_changed"),
13731
+ timestamp: z11.string(),
13732
+ payload: PriorityChangedPayload
13733
+ }),
13734
+ z11.object({
13735
+ event: z11.literal("permission_response"),
13736
+ timestamp: z11.string(),
13737
+ payload: PermissionResponsePayload
13738
+ })
13739
+ ]);
13740
+ var VALID_FILENAME = /^[\d]{4}-[\d]{2}-[\d]{2}T[\w.:-]+-[\w]+\.json$/;
13741
+ function isValidInboxFilename(filename) {
13742
+ if (filename.includes("/") || filename.includes("\\") || filename.includes("..")) {
13743
+ return false;
13744
+ }
13745
+ return VALID_FILENAME.test(filename);
13746
+ }
13747
+ function formatChannelContent(event) {
13748
+ switch (event.event) {
13749
+ case "ticket_requested":
13750
+ return `User requested ticket ${event.payload.ticketId} be started.`;
13751
+ case "pause_session":
13752
+ return "User requested the autonomous session be paused.";
13753
+ case "resume_session":
13754
+ return "User requested the autonomous session be resumed.";
13755
+ case "cancel_session": {
13756
+ const reason = event.payload.reason ? ` Reason: ${event.payload.reason}` : "";
13757
+ return `User requested the autonomous session be cancelled.${reason}`;
13758
+ }
13759
+ case "priority_changed":
13760
+ return `User changed priority of ticket ${event.payload.ticketId} to order ${event.payload.newOrder}.`;
13761
+ case "permission_response":
13762
+ return `Permission response for request ${event.payload.requestId}: ${event.payload.behavior}.`;
13763
+ }
13764
+ }
13765
+ function formatChannelMeta(event) {
13766
+ const meta = { event: event.event };
13767
+ switch (event.event) {
13768
+ case "ticket_requested":
13769
+ meta.ticketId = event.payload.ticketId;
13770
+ break;
13771
+ case "cancel_session":
13772
+ if (event.payload.reason) meta.reason = event.payload.reason;
13773
+ break;
13774
+ case "priority_changed":
13775
+ meta.ticketId = event.payload.ticketId;
13776
+ meta.newOrder = String(event.payload.newOrder);
13777
+ break;
13778
+ case "permission_response":
13779
+ meta.requestId = event.payload.requestId;
13780
+ meta.behavior = event.payload.behavior;
13781
+ break;
13782
+ }
13783
+ return meta;
13784
+ }
13785
+
13786
+ // src/channel/inbox-watcher.ts
13787
+ var INBOX_DIR = "channel-inbox";
13788
+ var FAILED_DIR = ".failed";
13789
+ var MAX_INBOX_DEPTH = 50;
13790
+ var MAX_FAILED_FILES = 20;
13791
+ var DEBOUNCE_MS = 100;
13792
+ var MAX_PERMISSION_RETRIES = 15;
13793
+ var MAX_EVENT_RETRIES = 30;
13794
+ var EVENT_EXPIRY_MS = 6e4;
13795
+ var watcher = null;
13796
+ var permissionRetryCount = /* @__PURE__ */ new Map();
13797
+ var eventRetryCount = /* @__PURE__ */ new Map();
13798
+ var debounceTimer = null;
13799
+ async function startInboxWatcher(root, server) {
13800
+ const inboxPath = join25(root, ".story", INBOX_DIR);
13801
+ if (watcher) {
13802
+ watcher.close();
13803
+ watcher = null;
13804
+ permissionRetryCount.clear();
13805
+ }
13806
+ await mkdir5(inboxPath, { recursive: true });
13807
+ await recoverStaleProcessingFiles(inboxPath);
13808
+ await processInbox(inboxPath, server);
13809
+ try {
13810
+ watcher = watch(inboxPath, (eventType) => {
13811
+ if (eventType === "rename") {
13812
+ scheduleDebouncedProcess(inboxPath, server);
13813
+ }
13814
+ });
13815
+ watcher.on("error", (err) => {
13816
+ process.stderr.write(`claudestory: channel inbox watcher error: ${err.message}
13817
+ `);
13818
+ startPollingFallback(inboxPath, server);
13819
+ });
13820
+ process.stderr.write(`claudestory: channel inbox watcher started at ${inboxPath}
13821
+ `);
13822
+ } catch (err) {
13823
+ const msg = err instanceof Error ? err.message : String(err);
13824
+ process.stderr.write(`claudestory: failed to start inbox watcher, using polling fallback: ${msg}
13825
+ `);
13826
+ startPollingFallback(inboxPath, server);
13827
+ }
13828
+ }
13829
+ function stopInboxWatcher() {
13830
+ if (debounceTimer) {
13831
+ clearTimeout(debounceTimer);
13832
+ debounceTimer = null;
13833
+ }
13834
+ if (watcher) {
13835
+ watcher.close();
13836
+ watcher = null;
13837
+ }
13838
+ if (pollInterval) {
13839
+ clearInterval(pollInterval);
13840
+ pollInterval = null;
13841
+ }
13842
+ permissionRetryCount.clear();
13843
+ }
13844
+ function scheduleDebouncedProcess(inboxPath, server) {
13845
+ if (debounceTimer) clearTimeout(debounceTimer);
13846
+ debounceTimer = setTimeout(() => {
13847
+ debounceTimer = null;
13848
+ processInbox(inboxPath, server).catch((err) => {
13849
+ const msg = err instanceof Error ? err.message : String(err);
13850
+ process.stderr.write(`claudestory: inbox processing error: ${msg}
13851
+ `);
13852
+ });
13853
+ }, DEBOUNCE_MS);
13854
+ }
13855
+ var pollInterval = null;
13856
+ function startPollingFallback(inboxPath, server) {
13857
+ if (pollInterval) return;
13858
+ pollInterval = setInterval(() => {
13859
+ processInbox(inboxPath, server).catch((err) => {
13860
+ const msg = err instanceof Error ? err.message : String(err);
13861
+ process.stderr.write(`claudestory: poll processing error: ${msg}
13862
+ `);
13863
+ });
13864
+ }, 2e3);
13865
+ }
13866
+ async function recoverStaleProcessingFiles(inboxPath) {
13867
+ let entries;
13868
+ try {
13869
+ entries = await readdir4(inboxPath);
13870
+ } catch {
13871
+ return;
13872
+ }
13873
+ for (const f of entries) {
13874
+ if (f.endsWith(".processing")) {
13875
+ const originalName = f.slice(0, -".processing".length);
13876
+ try {
13877
+ await rename2(join25(inboxPath, f), join25(inboxPath, originalName));
13878
+ process.stderr.write(`claudestory: recovered stale processing file: ${f}
13879
+ `);
13880
+ } catch {
13881
+ }
13882
+ }
13883
+ }
13884
+ }
13885
+ async function processInbox(inboxPath, server) {
13886
+ while (true) {
13887
+ let entries;
13888
+ try {
13889
+ entries = await readdir4(inboxPath);
13890
+ } catch {
13891
+ return;
13892
+ }
13893
+ const eventFiles = entries.filter((f) => f.endsWith(".json") && !f.startsWith(".")).sort();
13894
+ if (eventFiles.length === 0) break;
13895
+ const batch = eventFiles.slice(0, MAX_INBOX_DEPTH);
13896
+ if (eventFiles.length > MAX_INBOX_DEPTH) {
13897
+ process.stderr.write(
13898
+ `claudestory: channel inbox has ${eventFiles.length} files, processing batch of ${MAX_INBOX_DEPTH}
13899
+ `
13900
+ );
13901
+ }
13902
+ for (const filename of batch) {
13903
+ await processEventFile(inboxPath, filename, server);
13904
+ }
13905
+ if (eventFiles.length <= MAX_INBOX_DEPTH) break;
13906
+ }
13907
+ await trimFailedDirectory(inboxPath);
13908
+ }
13909
+ async function processEventFile(inboxPath, filename, server) {
13910
+ if (!isValidInboxFilename(filename)) {
13911
+ process.stderr.write(`claudestory: rejecting invalid inbox filename: ${filename}
13912
+ `);
13913
+ await moveToFailed(inboxPath, filename);
13914
+ return;
13915
+ }
13916
+ const filePath = join25(inboxPath, filename);
13917
+ const processingPath = join25(inboxPath, `${filename}.processing`);
13918
+ try {
13919
+ await rename2(filePath, processingPath);
13920
+ } catch {
13921
+ return;
13922
+ }
13923
+ let raw;
13924
+ try {
13925
+ raw = await readFile5(processingPath, "utf-8");
13926
+ } catch {
13927
+ await moveToFailed(inboxPath, `${filename}.processing`, filename);
13928
+ return;
13929
+ }
13930
+ const processingFilename = `${filename}.processing`;
13931
+ let parsed;
13932
+ try {
13933
+ parsed = JSON.parse(raw);
13934
+ } catch {
13935
+ process.stderr.write(`claudestory: invalid JSON in channel event ${filename}
13936
+ `);
13937
+ await moveToFailed(inboxPath, processingFilename, filename);
13938
+ return;
13939
+ }
13940
+ const result = ChannelEventSchema.safeParse(parsed);
13941
+ if (!result.success) {
13942
+ process.stderr.write(`claudestory: invalid channel event schema in ${filename}: ${result.error.message}
13943
+ `);
13944
+ await moveToFailed(inboxPath, processingFilename, filename);
13945
+ return;
13946
+ }
13947
+ const event = result.data;
13948
+ try {
13949
+ if (event.event === "permission_response") {
13950
+ await server.server.sendNotification({
13951
+ method: "notifications/claude/channel/permission",
13952
+ params: {
13953
+ requestId: event.payload.requestId,
13954
+ behavior: event.payload.behavior
13955
+ }
13956
+ });
13957
+ } else {
13958
+ const content = formatChannelContent(event);
13959
+ const meta = formatChannelMeta(event);
13960
+ await server.server.sendNotification({
13961
+ method: "notifications/claude/channel",
13962
+ params: { content, meta }
13963
+ });
13964
+ }
13965
+ process.stderr.write(`claudestory: sent channel event ${event.event}
13966
+ `);
13967
+ permissionRetryCount.delete(filename);
13968
+ eventRetryCount.delete(filename);
13969
+ } catch (err) {
13970
+ const msg = err instanceof Error ? err.message : String(err);
13971
+ if (event.event === "permission_response") {
13972
+ const retries2 = (permissionRetryCount.get(filename) ?? 0) + 1;
13973
+ permissionRetryCount.set(filename, retries2);
13974
+ if (retries2 >= MAX_PERMISSION_RETRIES) {
13975
+ process.stderr.write(`claudestory: permission notification failed after ${retries2} retries, quarantining: ${msg}
13976
+ `);
13977
+ permissionRetryCount.delete(filename);
13978
+ await moveToFailed(inboxPath, processingFilename, filename);
13979
+ return;
13980
+ }
13981
+ try {
13982
+ await rename2(processingPath, filePath);
13983
+ } catch (renameErr) {
13984
+ const renameMsg = renameErr instanceof Error ? renameErr.message : String(renameErr);
13985
+ process.stderr.write(`claudestory: rename-back failed for ${filename}, quarantining: ${renameMsg}
13986
+ `);
13987
+ permissionRetryCount.delete(filename);
13988
+ await moveToFailed(inboxPath, processingFilename, filename);
13989
+ return;
13990
+ }
13991
+ process.stderr.write(`claudestory: permission notification failed (attempt ${retries2}/${MAX_PERMISSION_RETRIES}), keeping for retry: ${msg}
13992
+ `);
13993
+ return;
13994
+ }
13995
+ const eventAge = Date.now() - new Date(event.timestamp).getTime();
13996
+ if (eventAge > EVENT_EXPIRY_MS) {
13997
+ process.stderr.write(`claudestory: channel event ${event.event} expired after ${Math.round(eventAge / 1e3)}s, quarantining: ${msg}
13998
+ `);
13999
+ eventRetryCount.delete(filename);
14000
+ await moveToFailed(inboxPath, processingFilename, filename);
14001
+ return;
14002
+ }
14003
+ const retries = (eventRetryCount.get(filename) ?? 0) + 1;
14004
+ eventRetryCount.set(filename, retries);
14005
+ if (retries >= MAX_EVENT_RETRIES) {
14006
+ process.stderr.write(`claudestory: channel event ${event.event} failed after ${retries} retries, quarantining: ${msg}
14007
+ `);
14008
+ eventRetryCount.delete(filename);
14009
+ await moveToFailed(inboxPath, processingFilename, filename);
14010
+ return;
14011
+ }
14012
+ try {
14013
+ await rename2(processingPath, filePath);
14014
+ } catch (renameErr) {
14015
+ const renameMsg = renameErr instanceof Error ? renameErr.message : String(renameErr);
14016
+ process.stderr.write(`claudestory: rename-back failed for ${filename}, quarantining: ${renameMsg}
14017
+ `);
14018
+ eventRetryCount.delete(filename);
14019
+ await moveToFailed(inboxPath, processingFilename, filename);
14020
+ return;
14021
+ }
14022
+ process.stderr.write(`claudestory: channel event ${event.event} failed (attempt ${retries}/${MAX_EVENT_RETRIES}), keeping for retry: ${msg}
14023
+ `);
14024
+ return;
14025
+ }
14026
+ try {
14027
+ await unlink3(processingPath);
14028
+ } catch {
14029
+ }
14030
+ }
14031
+ async function moveToFailed(inboxPath, sourceFilename, destFilename) {
14032
+ const failedDir = join25(inboxPath, FAILED_DIR);
14033
+ const targetName = destFilename ?? sourceFilename;
14034
+ try {
14035
+ await mkdir5(failedDir, { recursive: true });
14036
+ await rename2(join25(inboxPath, sourceFilename), join25(failedDir, targetName));
14037
+ } catch (err) {
14038
+ try {
14039
+ await unlink3(join25(inboxPath, sourceFilename));
14040
+ } catch {
14041
+ }
14042
+ const msg = err instanceof Error ? err.message : String(err);
14043
+ process.stderr.write(`claudestory: failed to move ${sourceFilename} to .failed/: ${msg}
14044
+ `);
14045
+ }
14046
+ }
14047
+ async function trimFailedDirectory(inboxPath) {
14048
+ const failedDir = join25(inboxPath, FAILED_DIR);
14049
+ let files;
14050
+ try {
14051
+ files = await readdir4(failedDir);
14052
+ } catch {
14053
+ return;
14054
+ }
14055
+ const sorted = files.filter((f) => f.endsWith(".json")).sort();
14056
+ if (sorted.length <= MAX_FAILED_FILES) return;
14057
+ const toDelete = sorted.slice(0, sorted.length - MAX_FAILED_FILES);
14058
+ for (const f of toDelete) {
14059
+ try {
14060
+ await unlink3(join25(failedDir, f));
14061
+ } catch {
14062
+ }
14063
+ }
14064
+ }
14065
+
13031
14066
  // src/mcp/index.ts
13032
14067
  var ENV_VAR2 = "CLAUDESTORY_PROJECT_ROOT";
13033
14068
  var CONFIG_PATH2 = ".story/config.json";
13034
- var version = "0.1.61";
14069
+ var version = "0.1.63";
13035
14070
  function tryDiscoverRoot() {
13036
14071
  const envRoot = process.env[ENV_VAR2];
13037
14072
  if (envRoot) {
@@ -13043,7 +14078,7 @@ function tryDiscoverRoot() {
13043
14078
  const resolved = resolve9(envRoot);
13044
14079
  try {
13045
14080
  const canonical = realpathSync3(resolved);
13046
- if (existsSync13(join24(canonical, CONFIG_PATH2))) {
14081
+ if (existsSync14(join26(canonical, CONFIG_PATH2))) {
13047
14082
  return canonical;
13048
14083
  }
13049
14084
  process.stderr.write(`Warning: No .story/config.json at ${canonical}
@@ -13071,9 +14106,9 @@ function registerDegradedTools(server) {
13071
14106
  const degradedInit = server.registerTool("claudestory_init", {
13072
14107
  description: "Initialize a new .story/ project in the current directory",
13073
14108
  inputSchema: {
13074
- name: z11.string().describe("Project name"),
13075
- type: z11.string().optional().describe("Project type (e.g. npm, macapp, cargo, generic)"),
13076
- language: z11.string().optional().describe("Primary language (e.g. typescript, swift, rust)")
14109
+ name: z12.string().describe("Project name"),
14110
+ type: z12.string().optional().describe("Project type (e.g. npm, macapp, cargo, generic)"),
14111
+ language: z12.string().optional().describe("Primary language (e.g. typescript, swift, rust)")
13077
14112
  }
13078
14113
  }, async (args) => {
13079
14114
  let result;
@@ -13103,6 +14138,12 @@ function registerDegradedTools(server) {
13103
14138
  return { content: [{ type: "text", text: `Initialized .story/ project "${args.name}" at ${result.root}
13104
14139
 
13105
14140
  Warning: tool registration failed. Restart the MCP server for full tool access.` }] };
14141
+ }
14142
+ try {
14143
+ await startInboxWatcher(result.root, server);
14144
+ } catch (watchErr) {
14145
+ process.stderr.write(`claudestory: inbox watcher failed after init: ${watchErr instanceof Error ? watchErr.message : String(watchErr)}
14146
+ `);
13106
14147
  }
13107
14148
  process.stderr.write(`claudestory: initialized at ${result.root}
13108
14149
  `);
@@ -13122,17 +14163,32 @@ async function main() {
13122
14163
  const server = new McpServer(
13123
14164
  { name: "claudestory", version },
13124
14165
  {
13125
- 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/."
14166
+ 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/.",
14167
+ capabilities: {
14168
+ experimental: {
14169
+ "claude/channel": {},
14170
+ "claude/channel/permission": {}
14171
+ }
14172
+ }
13126
14173
  }
13127
14174
  );
13128
14175
  if (root) {
13129
14176
  registerAllTools(server, root);
14177
+ await startInboxWatcher(root, server);
13130
14178
  process.stderr.write(`claudestory MCP server running (root: ${root})
13131
14179
  `);
13132
14180
  } else {
13133
14181
  registerDegradedTools(server);
13134
14182
  process.stderr.write("claudestory MCP server running (no project \u2014 claudestory_init available)\n");
13135
14183
  }
14184
+ process.on("SIGINT", () => {
14185
+ stopInboxWatcher();
14186
+ process.exit(0);
14187
+ });
14188
+ process.on("SIGTERM", () => {
14189
+ stopInboxWatcher();
14190
+ process.exit(0);
14191
+ });
13136
14192
  const transport = new StdioServerTransport();
13137
14193
  await server.connect(transport);
13138
14194
  }