@anthropologies/claudestory 0.1.11 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -261,7 +261,8 @@ var init_config = __esm({
261
261
  project: z6.string().min(1),
262
262
  type: z6.string(),
263
263
  language: z6.string(),
264
- features: FeaturesSchema
264
+ features: FeaturesSchema,
265
+ recipe: z6.string().optional()
265
266
  }).passthrough();
266
267
  }
267
268
  });
@@ -2772,9 +2773,9 @@ async function handleHandoverCreate(content, slugRaw, format, root) {
2772
2773
  const datePrefix = `${date}-`;
2773
2774
  const seqRegex = new RegExp(`^${date}-(\\d{2})-`);
2774
2775
  let maxSeq = 0;
2775
- const { readdirSync } = await import("fs");
2776
+ const { readdirSync: readdirSync2 } = await import("fs");
2776
2777
  try {
2777
- for (const f of readdirSync(handoversDir)) {
2778
+ for (const f of readdirSync2(handoversDir)) {
2778
2779
  const m = f.match(seqRegex);
2779
2780
  if (m) {
2780
2781
  const n = parseInt(m[1], 10);
@@ -3363,6 +3364,14 @@ var init_issue2 = __esm({
3363
3364
  });
3364
3365
 
3365
3366
  // src/core/snapshot.ts
3367
+ var snapshot_exports = {};
3368
+ __export(snapshot_exports, {
3369
+ SnapshotV1Schema: () => SnapshotV1Schema,
3370
+ buildRecap: () => buildRecap,
3371
+ diffStates: () => diffStates,
3372
+ loadLatestSnapshot: () => loadLatestSnapshot,
3373
+ saveSnapshot: () => saveSnapshot
3374
+ });
3366
3375
  import { readdir as readdir3, readFile as readFile3, mkdir as mkdir3, unlink as unlink2 } from "fs/promises";
3367
3376
  import { existsSync as existsSync5 } from "fs";
3368
3377
  import { join as join5, resolve as resolve5 } from "path";
@@ -4341,8 +4350,1506 @@ var init_selftest = __esm({
4341
4350
  }
4342
4351
  });
4343
4352
 
4353
+ // src/autonomous/session-types.ts
4354
+ import { realpathSync } from "fs";
4355
+ import { z as z8 } from "zod";
4356
+ function deriveClaudeStatus(state, waitingForRetry) {
4357
+ if (waitingForRetry) return "waiting";
4358
+ if (!state) return "idle";
4359
+ if (WORKING_STATES.has(state)) return "working";
4360
+ if (IDLE_STATES.has(state)) return "idle";
4361
+ return "unknown";
4362
+ }
4363
+ function deriveWorkspaceId(projectRoot) {
4364
+ return realpathSync(projectRoot);
4365
+ }
4366
+ var WORKING_STATES, IDLE_STATES, CURRENT_STATUS_SCHEMA_VERSION, WORKFLOW_STATES, WorkflowStateSchema, CURRENT_SESSION_SCHEMA_VERSION, SessionStateSchema;
4367
+ var init_session_types = __esm({
4368
+ "src/autonomous/session-types.ts"() {
4369
+ "use strict";
4370
+ init_esm_shims();
4371
+ WORKING_STATES = /* @__PURE__ */ new Set([
4372
+ "PLAN",
4373
+ "PLAN_REVIEW",
4374
+ "IMPLEMENT",
4375
+ "CODE_REVIEW",
4376
+ "FINALIZE",
4377
+ "COMPACT"
4378
+ ]);
4379
+ IDLE_STATES = /* @__PURE__ */ new Set([
4380
+ "INIT",
4381
+ "LOAD_CONTEXT",
4382
+ "PICK_TICKET",
4383
+ "HANDOVER",
4384
+ "COMPLETE",
4385
+ "SESSION_END"
4386
+ ]);
4387
+ CURRENT_STATUS_SCHEMA_VERSION = 1;
4388
+ WORKFLOW_STATES = [
4389
+ "INIT",
4390
+ "LOAD_CONTEXT",
4391
+ "PICK_TICKET",
4392
+ "PLAN",
4393
+ "PLAN_REVIEW",
4394
+ "IMPLEMENT",
4395
+ "CODE_REVIEW",
4396
+ "FINALIZE",
4397
+ "COMPACT",
4398
+ "HANDOVER",
4399
+ "COMPLETE",
4400
+ "SESSION_END"
4401
+ ];
4402
+ WorkflowStateSchema = z8.enum(WORKFLOW_STATES);
4403
+ CURRENT_SESSION_SCHEMA_VERSION = 1;
4404
+ SessionStateSchema = z8.object({
4405
+ schemaVersion: z8.literal(CURRENT_SESSION_SCHEMA_VERSION),
4406
+ sessionId: z8.string().uuid(),
4407
+ recipe: z8.string(),
4408
+ state: z8.string(),
4409
+ previousState: z8.string().optional(),
4410
+ revision: z8.number().int().min(0),
4411
+ status: z8.enum(["active", "completed", "superseded"]).default("active"),
4412
+ // Ticket in progress
4413
+ ticket: z8.object({
4414
+ id: z8.string(),
4415
+ title: z8.string(),
4416
+ risk: z8.string().optional(),
4417
+ realizedRisk: z8.string().optional(),
4418
+ claimed: z8.boolean().default(false)
4419
+ }).optional(),
4420
+ // Review tracking
4421
+ reviews: z8.object({
4422
+ plan: z8.array(z8.object({
4423
+ round: z8.number(),
4424
+ reviewer: z8.string(),
4425
+ verdict: z8.string(),
4426
+ findingCount: z8.number(),
4427
+ criticalCount: z8.number(),
4428
+ majorCount: z8.number(),
4429
+ suggestionCount: z8.number(),
4430
+ codexSessionId: z8.string().optional(),
4431
+ timestamp: z8.string()
4432
+ })).default([]),
4433
+ code: z8.array(z8.object({
4434
+ round: z8.number(),
4435
+ reviewer: z8.string(),
4436
+ verdict: z8.string(),
4437
+ findingCount: z8.number(),
4438
+ criticalCount: z8.number(),
4439
+ majorCount: z8.number(),
4440
+ suggestionCount: z8.number(),
4441
+ codexSessionId: z8.string().optional(),
4442
+ timestamp: z8.string()
4443
+ })).default([])
4444
+ }).default({ plan: [], code: [] }),
4445
+ // Completed tickets this session
4446
+ completedTickets: z8.array(z8.object({
4447
+ id: z8.string(),
4448
+ title: z8.string().optional(),
4449
+ commitHash: z8.string().optional(),
4450
+ risk: z8.string().optional()
4451
+ })).default([]),
4452
+ // FINALIZE checkpoint
4453
+ finalizeCheckpoint: z8.enum(["staged", "precommit_passed", "committed"]).nullable().default(null),
4454
+ // Git state
4455
+ git: z8.object({
4456
+ branch: z8.string().nullable().default(null),
4457
+ initHead: z8.string().optional(),
4458
+ mergeBase: z8.string().nullable().default(null),
4459
+ expectedHead: z8.string().optional(),
4460
+ baseline: z8.object({
4461
+ porcelain: z8.array(z8.string()).default([]),
4462
+ dirtyTrackedFiles: z8.record(z8.object({ blobHash: z8.string() })).default({}),
4463
+ untrackedPaths: z8.array(z8.string()).default([])
4464
+ }).optional()
4465
+ }).default({ branch: null, mergeBase: null }),
4466
+ // Lease
4467
+ lease: z8.object({
4468
+ workspaceId: z8.string().optional(),
4469
+ lastHeartbeat: z8.string(),
4470
+ expiresAt: z8.string()
4471
+ }),
4472
+ // Context pressure
4473
+ contextPressure: z8.object({
4474
+ level: z8.string().default("low"),
4475
+ guideCallCount: z8.number().default(0),
4476
+ ticketsCompleted: z8.number().default(0),
4477
+ compactionCount: z8.number().default(0),
4478
+ eventsLogBytes: z8.number().default(0)
4479
+ }).default({ level: "low", guideCallCount: 0, ticketsCompleted: 0, compactionCount: 0, eventsLogBytes: 0 }),
4480
+ // Pending project mutation (for crash recovery)
4481
+ pendingProjectMutation: z8.any().nullable().default(null),
4482
+ // COMPACT resume
4483
+ resumeFromRevision: z8.number().nullable().default(null),
4484
+ preCompactState: z8.string().nullable().default(null),
4485
+ // Session metadata
4486
+ waitingForRetry: z8.boolean().default(false),
4487
+ lastGuideCall: z8.string().optional(),
4488
+ startedAt: z8.string(),
4489
+ guideCallCount: z8.number().default(0),
4490
+ // Supersession tracking
4491
+ supersededBy: z8.string().optional(),
4492
+ supersededSession: z8.string().optional(),
4493
+ stealReason: z8.string().optional(),
4494
+ // Recipe overrides
4495
+ config: z8.object({
4496
+ maxTicketsPerSession: z8.number().default(3),
4497
+ compactThreshold: z8.string().default("high"),
4498
+ reviewBackends: z8.array(z8.string()).default(["codex", "agent"])
4499
+ }).default({ maxTicketsPerSession: 3, compactThreshold: "high", reviewBackends: ["codex", "agent"] })
4500
+ }).passthrough();
4501
+ }
4502
+ });
4503
+
4504
+ // src/autonomous/session.ts
4505
+ import { randomUUID } from "crypto";
4506
+ import {
4507
+ mkdirSync,
4508
+ readdirSync,
4509
+ readFileSync,
4510
+ writeFileSync,
4511
+ renameSync,
4512
+ unlinkSync,
4513
+ existsSync as existsSync6,
4514
+ rmSync
4515
+ } from "fs";
4516
+ import { join as join6 } from "path";
4517
+ import lockfile2 from "proper-lockfile";
4518
+ function sessionsRoot(root) {
4519
+ return join6(root, ".story", SESSIONS_DIR);
4520
+ }
4521
+ function sessionDir(root, sessionId) {
4522
+ return join6(sessionsRoot(root), sessionId);
4523
+ }
4524
+ function statePath(dir) {
4525
+ return join6(dir, "state.json");
4526
+ }
4527
+ function eventsPath(dir) {
4528
+ return join6(dir, "events.log");
4529
+ }
4530
+ function createSession(root, recipe, workspaceId) {
4531
+ const id = randomUUID();
4532
+ const dir = sessionDir(root, id);
4533
+ mkdirSync(dir, { recursive: true });
4534
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4535
+ const state = {
4536
+ schemaVersion: CURRENT_SESSION_SCHEMA_VERSION,
4537
+ sessionId: id,
4538
+ recipe,
4539
+ state: "INIT",
4540
+ revision: 0,
4541
+ status: "active",
4542
+ reviews: { plan: [], code: [] },
4543
+ completedTickets: [],
4544
+ finalizeCheckpoint: null,
4545
+ git: { branch: null, mergeBase: null },
4546
+ lease: {
4547
+ workspaceId,
4548
+ lastHeartbeat: now,
4549
+ expiresAt: new Date(Date.now() + LEASE_DURATION_MS).toISOString()
4550
+ },
4551
+ contextPressure: {
4552
+ level: "low",
4553
+ guideCallCount: 0,
4554
+ ticketsCompleted: 0,
4555
+ compactionCount: 0,
4556
+ eventsLogBytes: 0
4557
+ },
4558
+ pendingProjectMutation: null,
4559
+ resumeFromRevision: null,
4560
+ preCompactState: null,
4561
+ waitingForRetry: false,
4562
+ lastGuideCall: now,
4563
+ startedAt: now,
4564
+ guideCallCount: 0,
4565
+ config: {
4566
+ maxTicketsPerSession: 3,
4567
+ compactThreshold: "high",
4568
+ reviewBackends: ["codex", "agent"]
4569
+ }
4570
+ };
4571
+ writeSessionSync(dir, state);
4572
+ return state;
4573
+ }
4574
+ function readSession(dir) {
4575
+ const path2 = statePath(dir);
4576
+ let raw;
4577
+ try {
4578
+ raw = readFileSync(path2, "utf-8");
4579
+ } catch {
4580
+ return null;
4581
+ }
4582
+ let parsed;
4583
+ try {
4584
+ parsed = JSON.parse(raw);
4585
+ } catch {
4586
+ return null;
4587
+ }
4588
+ const result = SessionStateSchema.safeParse(parsed);
4589
+ if (!result.success) return null;
4590
+ return result.data;
4591
+ }
4592
+ function writeSessionSync(dir, state) {
4593
+ const path2 = statePath(dir);
4594
+ const updated = { ...state, revision: state.revision + 1 };
4595
+ const content = JSON.stringify(updated, null, 2) + "\n";
4596
+ const tmp = `${path2}.${process.pid}.tmp`;
4597
+ try {
4598
+ writeFileSync(tmp, content, "utf-8");
4599
+ renameSync(tmp, path2);
4600
+ } catch (err) {
4601
+ try {
4602
+ unlinkSync(tmp);
4603
+ } catch {
4604
+ }
4605
+ throw err;
4606
+ }
4607
+ return updated;
4608
+ }
4609
+ function appendEvent(dir, event) {
4610
+ try {
4611
+ const path2 = eventsPath(dir);
4612
+ const line = JSON.stringify(event) + "\n";
4613
+ writeFileSync(path2, line, { flag: "a", encoding: "utf-8" });
4614
+ } catch {
4615
+ }
4616
+ }
4617
+ function deleteSession(root, sessionId) {
4618
+ const dir = sessionDir(root, sessionId);
4619
+ try {
4620
+ rmSync(dir, { recursive: true, force: true });
4621
+ } catch {
4622
+ }
4623
+ }
4624
+ function refreshLease(state) {
4625
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4626
+ const newCallCount = state.guideCallCount + 1;
4627
+ return {
4628
+ ...state,
4629
+ lease: {
4630
+ ...state.lease,
4631
+ lastHeartbeat: now,
4632
+ expiresAt: new Date(Date.now() + LEASE_DURATION_MS).toISOString()
4633
+ },
4634
+ lastGuideCall: now,
4635
+ guideCallCount: newCallCount,
4636
+ contextPressure: {
4637
+ ...state.contextPressure,
4638
+ guideCallCount: newCallCount,
4639
+ ticketsCompleted: state.completedTickets?.length ?? 0
4640
+ }
4641
+ };
4642
+ }
4643
+ function isLeaseExpired(state) {
4644
+ if (!state.lease?.expiresAt) return true;
4645
+ const expires = new Date(state.lease.expiresAt).getTime();
4646
+ return Number.isNaN(expires) || expires <= Date.now();
4647
+ }
4648
+ function findActiveSessionFull(root) {
4649
+ const sessDir = sessionsRoot(root);
4650
+ let entries;
4651
+ try {
4652
+ entries = readdirSync(sessDir, { withFileTypes: true });
4653
+ } catch {
4654
+ return null;
4655
+ }
4656
+ let workspaceId;
4657
+ try {
4658
+ workspaceId = deriveWorkspaceId(root);
4659
+ } catch {
4660
+ return null;
4661
+ }
4662
+ let best = null;
4663
+ let bestGuideCall = 0;
4664
+ for (const entry of entries) {
4665
+ if (!entry.isDirectory()) continue;
4666
+ const dir = join6(sessDir, entry.name);
4667
+ const session = readSession(dir);
4668
+ if (!session) continue;
4669
+ if (session.status !== "active") continue;
4670
+ if (session.lease?.workspaceId && session.lease.workspaceId !== workspaceId) continue;
4671
+ if (isLeaseExpired(session)) continue;
4672
+ const guideCall = session.lastGuideCall ? new Date(session.lastGuideCall).getTime() : 0;
4673
+ const guideCallValid = Number.isNaN(guideCall) ? 0 : guideCall;
4674
+ if (!best || guideCallValid > bestGuideCall || guideCallValid === bestGuideCall && session.sessionId > best.state.sessionId) {
4675
+ best = { state: session, dir };
4676
+ bestGuideCall = guideCallValid;
4677
+ }
4678
+ }
4679
+ return best;
4680
+ }
4681
+ function findActiveSessionMinimal(root) {
4682
+ const result = findActiveSessionFull(root);
4683
+ return result?.state ?? null;
4684
+ }
4685
+ function findStaleSessions(root) {
4686
+ const sessDir = sessionsRoot(root);
4687
+ let entries;
4688
+ try {
4689
+ entries = readdirSync(sessDir, { withFileTypes: true });
4690
+ } catch {
4691
+ return [];
4692
+ }
4693
+ let workspaceId;
4694
+ try {
4695
+ workspaceId = deriveWorkspaceId(root);
4696
+ } catch {
4697
+ return [];
4698
+ }
4699
+ const results = [];
4700
+ for (const entry of entries) {
4701
+ if (!entry.isDirectory()) continue;
4702
+ const dir = join6(sessDir, entry.name);
4703
+ const session = readSession(dir);
4704
+ if (!session) continue;
4705
+ if (session.status !== "active") continue;
4706
+ if (session.lease?.workspaceId && session.lease.workspaceId !== workspaceId) continue;
4707
+ if (isLeaseExpired(session)) {
4708
+ results.push({ state: session, dir });
4709
+ }
4710
+ }
4711
+ return results;
4712
+ }
4713
+ function findSessionById(root, sessionId) {
4714
+ const dir = sessionDir(root, sessionId);
4715
+ if (!existsSync6(dir)) return null;
4716
+ const state = readSession(dir);
4717
+ if (!state) return null;
4718
+ return { state, dir };
4719
+ }
4720
+ async function withSessionLock(root, fn) {
4721
+ const sessDir = sessionsRoot(root);
4722
+ mkdirSync(sessDir, { recursive: true });
4723
+ let release;
4724
+ try {
4725
+ release = await lockfile2.lock(sessDir, {
4726
+ retries: { retries: 3, minTimeout: 100, maxTimeout: 1e3 },
4727
+ stale: 3e4,
4728
+ lockfilePath: join6(sessDir, ".lock")
4729
+ });
4730
+ return await fn();
4731
+ } finally {
4732
+ if (release) {
4733
+ try {
4734
+ await release();
4735
+ } catch {
4736
+ }
4737
+ }
4738
+ }
4739
+ }
4740
+ var LEASE_DURATION_MS, SESSIONS_DIR;
4741
+ var init_session = __esm({
4742
+ "src/autonomous/session.ts"() {
4743
+ "use strict";
4744
+ init_esm_shims();
4745
+ init_session_types();
4746
+ LEASE_DURATION_MS = 45 * 60 * 1e3;
4747
+ SESSIONS_DIR = "sessions";
4748
+ }
4749
+ });
4750
+
4751
+ // src/autonomous/state-machine.ts
4752
+ var init_state_machine = __esm({
4753
+ "src/autonomous/state-machine.ts"() {
4754
+ "use strict";
4755
+ init_esm_shims();
4756
+ }
4757
+ });
4758
+
4759
+ // src/autonomous/context-pressure.ts
4760
+ function evaluatePressure(state) {
4761
+ const calls = state.contextPressure?.guideCallCount ?? state.guideCallCount ?? 0;
4762
+ const tickets = state.contextPressure?.ticketsCompleted ?? state.completedTickets?.length ?? 0;
4763
+ const eventsBytes = state.contextPressure?.eventsLogBytes ?? 0;
4764
+ if (calls > 45 || tickets >= 3 || eventsBytes > 5e5) return "critical";
4765
+ if (calls >= 30 || tickets >= 2 || eventsBytes > 2e5) return "high";
4766
+ if (calls >= 15 || tickets >= 1 || eventsBytes > 5e4) return "medium";
4767
+ return "low";
4768
+ }
4769
+ var init_context_pressure = __esm({
4770
+ "src/autonomous/context-pressure.ts"() {
4771
+ "use strict";
4772
+ init_esm_shims();
4773
+ }
4774
+ });
4775
+
4776
+ // src/autonomous/review-depth.ts
4777
+ function assessRisk(diffStats, changedFiles) {
4778
+ let level = "low";
4779
+ if (diffStats) {
4780
+ const total = diffStats.totalLines;
4781
+ if (total > 200) level = "high";
4782
+ else if (total >= 50) level = "medium";
4783
+ }
4784
+ if (changedFiles && level !== "high") {
4785
+ const hasSensitive = changedFiles.some(
4786
+ (f) => SENSITIVE_PATTERNS.some((p) => p.test(f))
4787
+ );
4788
+ if (hasSensitive) {
4789
+ level = level === "low" ? "medium" : "high";
4790
+ }
4791
+ }
4792
+ return level;
4793
+ }
4794
+ function requiredRounds(risk) {
4795
+ switch (risk) {
4796
+ case "low":
4797
+ return 1;
4798
+ case "medium":
4799
+ return 2;
4800
+ case "high":
4801
+ return 3;
4802
+ }
4803
+ }
4804
+ function nextReviewer(previousRounds, backends) {
4805
+ if (backends.length === 0) return "agent";
4806
+ if (backends.length === 1) return backends[0];
4807
+ if (previousRounds.length === 0) return backends[0];
4808
+ const lastReviewer = previousRounds[previousRounds.length - 1].reviewer;
4809
+ const lastIndex = backends.indexOf(lastReviewer);
4810
+ if (lastIndex === -1) return backends[0];
4811
+ return backends[(lastIndex + 1) % backends.length];
4812
+ }
4813
+ var SENSITIVE_PATTERNS;
4814
+ var init_review_depth = __esm({
4815
+ "src/autonomous/review-depth.ts"() {
4816
+ "use strict";
4817
+ init_esm_shims();
4818
+ SENSITIVE_PATTERNS = [
4819
+ /\bauth\b/i,
4820
+ /\bsecurity\b/i,
4821
+ /\bmigration/i,
4822
+ /\bconfig\b/i,
4823
+ /\bmiddleware\b/i,
4824
+ /\.env/i
4825
+ ];
4826
+ }
4827
+ });
4828
+
4829
+ // src/autonomous/git-inspector.ts
4830
+ import { execFile } from "child_process";
4831
+ async function git(cwd, args, parse) {
4832
+ return new Promise((resolve9) => {
4833
+ execFile("git", args, { cwd, timeout: GIT_TIMEOUT, maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
4834
+ if (err) {
4835
+ const message = stderr?.trim() || err.message || "unknown git error";
4836
+ resolve9({ ok: false, reason: "git_error", message });
4837
+ return;
4838
+ }
4839
+ try {
4840
+ resolve9({ ok: true, data: parse(stdout) });
4841
+ } catch (parseErr) {
4842
+ resolve9({ ok: false, reason: "parse_error", message: parseErr.message });
4843
+ }
4844
+ });
4845
+ });
4846
+ }
4847
+ async function gitStatus(cwd) {
4848
+ return git(
4849
+ cwd,
4850
+ ["status", "--porcelain"],
4851
+ (out) => out.split("\n").filter((l) => l.length > 0)
4852
+ );
4853
+ }
4854
+ async function gitHead(cwd) {
4855
+ const hashResult = await git(cwd, ["rev-parse", "HEAD"], (out) => out.trim());
4856
+ if (!hashResult.ok) return hashResult;
4857
+ const branchResult = await gitBranch(cwd);
4858
+ return {
4859
+ ok: true,
4860
+ data: {
4861
+ hash: hashResult.data,
4862
+ branch: branchResult.ok ? branchResult.data : null
4863
+ }
4864
+ };
4865
+ }
4866
+ async function gitBranch(cwd) {
4867
+ return git(cwd, ["symbolic-ref", "--short", "HEAD"], (out) => out.trim());
4868
+ }
4869
+ async function gitMergeBase(cwd, base) {
4870
+ return git(cwd, ["merge-base", "HEAD", base], (out) => out.trim());
4871
+ }
4872
+ async function gitDiffStat(cwd, base) {
4873
+ return git(cwd, ["diff", "--numstat", base], parseDiffNumstat);
4874
+ }
4875
+ async function gitDiffNames(cwd, base) {
4876
+ return git(
4877
+ cwd,
4878
+ ["diff", "--name-only", base],
4879
+ (out) => out.split("\n").filter((l) => l.length > 0)
4880
+ );
4881
+ }
4882
+ async function gitBlobHash(cwd, file) {
4883
+ return git(cwd, ["hash-object", file], (out) => out.trim());
4884
+ }
4885
+ async function gitDiffCachedNames(cwd) {
4886
+ return git(
4887
+ cwd,
4888
+ ["diff", "--cached", "--name-only"],
4889
+ (out) => out.split("\n").filter((l) => l.length > 0)
4890
+ );
4891
+ }
4892
+ function parseDiffNumstat(out) {
4893
+ const lines = out.split("\n").filter((l) => l.length > 0);
4894
+ let insertions = 0;
4895
+ let deletions = 0;
4896
+ let filesChanged = 0;
4897
+ for (const line of lines) {
4898
+ const parts = line.split(" ");
4899
+ if (parts.length < 3) continue;
4900
+ const added = parseInt(parts[0], 10);
4901
+ const removed = parseInt(parts[1], 10);
4902
+ if (!Number.isNaN(added)) insertions += added;
4903
+ if (!Number.isNaN(removed)) deletions += removed;
4904
+ filesChanged++;
4905
+ }
4906
+ return { filesChanged, insertions, deletions, totalLines: insertions + deletions };
4907
+ }
4908
+ var GIT_TIMEOUT;
4909
+ var init_git_inspector = __esm({
4910
+ "src/autonomous/git-inspector.ts"() {
4911
+ "use strict";
4912
+ init_esm_shims();
4913
+ GIT_TIMEOUT = 1e4;
4914
+ }
4915
+ });
4916
+
4917
+ // src/autonomous/guide.ts
4918
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync7 } from "fs";
4919
+ import { join as join7 } from "path";
4920
+ async function handleAutonomousGuide(root, args) {
4921
+ const wsId = deriveWorkspaceId(root);
4922
+ const prev = workspaceLocks.get(wsId) ?? Promise.resolve();
4923
+ const current = prev.then(async () => {
4924
+ return withSessionLock(root, () => handleGuideInner(root, args));
4925
+ });
4926
+ workspaceLocks.set(wsId, current.then(() => {
4927
+ }, () => {
4928
+ }));
4929
+ try {
4930
+ return await current;
4931
+ } catch (err) {
4932
+ return guideError(err);
4933
+ } finally {
4934
+ const stored = workspaceLocks.get(wsId);
4935
+ if (stored) {
4936
+ stored.then(() => {
4937
+ if (workspaceLocks.get(wsId) === stored) {
4938
+ workspaceLocks.delete(wsId);
4939
+ }
4940
+ }, () => {
4941
+ if (workspaceLocks.get(wsId) === stored) {
4942
+ workspaceLocks.delete(wsId);
4943
+ }
4944
+ });
4945
+ }
4946
+ }
4947
+ }
4948
+ async function handleGuideInner(root, args) {
4949
+ switch (args.action) {
4950
+ case "start":
4951
+ return handleStart(root, args);
4952
+ case "report":
4953
+ return handleReport(root, args);
4954
+ case "resume":
4955
+ return handleResume(root, args);
4956
+ case "pre_compact":
4957
+ return handlePreCompact(root, args);
4958
+ case "cancel":
4959
+ return handleCancel(root, args);
4960
+ default:
4961
+ return guideError(new Error(`Unknown action: ${args.action}`));
4962
+ }
4963
+ }
4964
+ async function handleStart(root, args) {
4965
+ const existing = findActiveSessionFull(root);
4966
+ if (existing && !isLeaseExpired(existing.state)) {
4967
+ return guideError(new Error(
4968
+ `Active session ${existing.state.sessionId} already exists for this workspace. Use action: "resume" to continue or "cancel" to end it.`
4969
+ ));
4970
+ }
4971
+ const staleSessions = findStaleSessions(root);
4972
+ for (const stale of staleSessions) {
4973
+ writeSessionSync(stale.dir, { ...stale.state, status: "superseded" });
4974
+ }
4975
+ const wsId = deriveWorkspaceId(root);
4976
+ const recipe = "coding";
4977
+ const session = createSession(root, recipe, wsId);
4978
+ const dir = sessionDir(root, session.sessionId);
4979
+ try {
4980
+ const headResult = await gitHead(root);
4981
+ if (!headResult.ok) {
4982
+ deleteSession(root, session.sessionId);
4983
+ return guideError(new Error("This directory is not a git repository or git is not available. Autonomous mode requires git."));
4984
+ }
4985
+ const stagedResult = await gitDiffCachedNames(root);
4986
+ if (stagedResult.ok && stagedResult.data.length > 0) {
4987
+ deleteSession(root, session.sessionId);
4988
+ return guideError(new Error(
4989
+ `Cannot start: ${stagedResult.data.length} staged file(s). Unstage with \`git restore --staged .\` or commit them first, then call start again.
4990
+
4991
+ Staged: ${stagedResult.data.join(", ")}`
4992
+ ));
4993
+ }
4994
+ const statusResult = await gitStatus(root);
4995
+ let mergeBaseResult = await gitMergeBase(root, "main");
4996
+ if (!mergeBaseResult.ok) mergeBaseResult = await gitMergeBase(root, "master");
4997
+ const porcelainLines = statusResult.ok ? statusResult.data : [];
4998
+ const dirtyTracked = {};
4999
+ const untrackedPaths = [];
5000
+ for (const line of porcelainLines) {
5001
+ if (line.startsWith("??")) {
5002
+ untrackedPaths.push(line.slice(3).trim());
5003
+ } else if (line.length > 3) {
5004
+ const filePath = line.slice(3).trim();
5005
+ const hashResult = await gitBlobHash(root, filePath);
5006
+ dirtyTracked[filePath] = { blobHash: hashResult.ok ? hashResult.data : "" };
5007
+ }
5008
+ }
5009
+ if (Object.keys(dirtyTracked).length > 0) {
5010
+ deleteSession(root, session.sessionId);
5011
+ const dirtyFiles = Object.keys(dirtyTracked).join(", ");
5012
+ return guideError(new Error(
5013
+ `Cannot start: ${Object.keys(dirtyTracked).length} dirty tracked file(s): ${dirtyFiles}. Create a feature branch or stash changes first, then call start again.`
5014
+ ));
5015
+ }
5016
+ let updated = {
5017
+ ...session,
5018
+ state: "PICK_TICKET",
5019
+ previousState: "INIT",
5020
+ git: {
5021
+ branch: headResult.data.branch,
5022
+ initHead: headResult.data.hash,
5023
+ mergeBase: mergeBaseResult.ok ? mergeBaseResult.data : null,
5024
+ expectedHead: headResult.data.hash,
5025
+ baseline: {
5026
+ porcelain: porcelainLines,
5027
+ dirtyTrackedFiles: dirtyTracked,
5028
+ untrackedPaths
5029
+ }
5030
+ }
5031
+ };
5032
+ const { state: projectState, warnings } = await loadProject(root);
5033
+ const handoversDir = join7(root, ".story", "handovers");
5034
+ const ctx = { state: projectState, warnings, root, handoversDir, format: "md" };
5035
+ let handoverText = "";
5036
+ try {
5037
+ const handoverResult = await handleHandoverLatest(ctx, 3);
5038
+ handoverText = handoverResult.output;
5039
+ } catch {
5040
+ }
5041
+ let recapText = "";
5042
+ try {
5043
+ const snapshotInfo = await loadLatestSnapshot(root);
5044
+ const recap = buildRecap(projectState, snapshotInfo);
5045
+ if (recap.changes) {
5046
+ recapText = "Changes since last snapshot available.";
5047
+ }
5048
+ } catch {
5049
+ }
5050
+ const rulesText = readFileSafe(join7(root, "RULES.md"));
5051
+ const strategiesText = readFileSafe(join7(root, "WORK_STRATEGIES.md"));
5052
+ const digestParts = [
5053
+ handoverText ? `## Recent Handovers
5054
+
5055
+ ${handoverText}` : "",
5056
+ recapText ? `## Recap
5057
+
5058
+ ${recapText}` : "",
5059
+ rulesText ? `## Development Rules
5060
+
5061
+ ${rulesText}` : "",
5062
+ strategiesText ? `## Work Strategies
5063
+
5064
+ ${strategiesText}` : ""
5065
+ ].filter(Boolean);
5066
+ const digest = digestParts.join("\n\n---\n\n");
5067
+ try {
5068
+ writeFileSync2(join7(dir, "context-digest.md"), digest, "utf-8");
5069
+ } catch {
5070
+ }
5071
+ const nextResult = nextTickets(projectState, 5);
5072
+ let candidatesText = "";
5073
+ if (nextResult.kind === "found") {
5074
+ candidatesText = nextResult.candidates.map(
5075
+ (c, i) => `${i + 1}. **${c.ticket.id}: ${c.ticket.title}** (${c.ticket.type}, phase: ${c.ticket.phase ?? "unphased"})${c.unblockImpact.wouldUnblock.length > 0 ? ` \u2014 unblocks ${c.unblockImpact.wouldUnblock.map((t) => t.id).join(", ")}` : ""}`
5076
+ ).join("\n");
5077
+ } else if (nextResult.kind === "all_complete") {
5078
+ candidatesText = "All tickets are complete. No work to do.";
5079
+ } else if (nextResult.kind === "all_blocked") {
5080
+ candidatesText = "All remaining tickets are blocked.";
5081
+ } else {
5082
+ candidatesText = "No tickets found.";
5083
+ }
5084
+ const recResult = recommend(projectState, 5);
5085
+ let recsText = "";
5086
+ if (recResult.recommendations.length > 0) {
5087
+ const ticketRecs = recResult.recommendations.filter((r) => r.kind === "ticket");
5088
+ if (ticketRecs.length > 0) {
5089
+ recsText = "\n\n**Recommended:**\n" + ticketRecs.map(
5090
+ (r) => `- ${r.id}: ${r.title} (${r.reason})`
5091
+ ).join("\n");
5092
+ }
5093
+ }
5094
+ updated = refreshLease(updated);
5095
+ const pressure = evaluatePressure(updated);
5096
+ updated = { ...updated, contextPressure: { ...updated.contextPressure, level: pressure } };
5097
+ const written = writeSessionSync(dir, updated);
5098
+ appendEvent(dir, {
5099
+ rev: written.revision,
5100
+ type: "start",
5101
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
5102
+ data: { recipe, branch: written.git.branch, head: written.git.initHead }
5103
+ });
5104
+ const instruction = [
5105
+ "# Autonomous Session Started",
5106
+ "",
5107
+ "I've loaded your project context and found ticket candidates.",
5108
+ "",
5109
+ "## Ticket Candidates",
5110
+ "",
5111
+ candidatesText,
5112
+ recsText,
5113
+ "",
5114
+ "Pick a ticket by calling `claudestory_autonomous_guide` with:",
5115
+ "```json",
5116
+ `{ "sessionId": "${updated.sessionId}", "action": "report", "report": { "completedAction": "ticket_picked", "ticketId": "T-XXX" } }`,
5117
+ "```",
5118
+ "",
5119
+ "Replace T-XXX with the ticket ID you want to work on."
5120
+ ].join("\n");
5121
+ return guideResult(updated, "PICK_TICKET", {
5122
+ instruction,
5123
+ reminders: [
5124
+ "Do NOT use Claude Code's plan mode \u2014 write plans as markdown files.",
5125
+ "Do NOT ask the user for confirmation or approval.",
5126
+ "Call autonomous_guide after completing each step."
5127
+ ],
5128
+ transitionedFrom: "INIT"
5129
+ });
5130
+ } catch (err) {
5131
+ deleteSession(root, session.sessionId);
5132
+ throw err;
5133
+ }
5134
+ }
5135
+ async function handleReport(root, args) {
5136
+ if (!args.sessionId) return guideError(new Error("sessionId is required for report action"));
5137
+ if (!args.report) return guideError(new Error("report field is required for report action"));
5138
+ const info = findSessionById(root, args.sessionId);
5139
+ if (!info) return guideError(new Error(`Session ${args.sessionId} not found`));
5140
+ let state = refreshLease(info.state);
5141
+ const currentState = state.state;
5142
+ const report = args.report;
5143
+ switch (currentState) {
5144
+ case "PICK_TICKET":
5145
+ return handleReportPickTicket(root, info.dir, state, report);
5146
+ case "PLAN":
5147
+ return handleReportPlan(root, info.dir, state, report);
5148
+ case "PLAN_REVIEW":
5149
+ return handleReportPlanReview(root, info.dir, state, report);
5150
+ case "IMPLEMENT":
5151
+ return handleReportImplement(root, info.dir, state, report);
5152
+ case "CODE_REVIEW":
5153
+ return handleReportCodeReview(root, info.dir, state, report);
5154
+ case "FINALIZE":
5155
+ return handleReportFinalize(root, info.dir, state, report);
5156
+ case "COMPLETE":
5157
+ return handleReportComplete(root, info.dir, state, report);
5158
+ case "HANDOVER":
5159
+ return handleReportHandover(root, info.dir, state, report);
5160
+ default:
5161
+ return guideError(new Error(`Cannot report at state ${currentState}`));
5162
+ }
5163
+ }
5164
+ async function handleReportPickTicket(root, dir, state, report) {
5165
+ const ticketId = report.ticketId;
5166
+ if (!ticketId) return guideError(new Error("report.ticketId is required when picking a ticket"));
5167
+ const { state: projectState } = await loadProject(root);
5168
+ const ticket = projectState.ticketByID(ticketId);
5169
+ if (!ticket) return guideError(new Error(`Ticket ${ticketId} not found`));
5170
+ if (projectState.isBlocked(ticket)) return guideError(new Error(`Ticket ${ticketId} is blocked`));
5171
+ const written = writeSessionSync(dir, {
5172
+ ...state,
5173
+ state: "PLAN",
5174
+ previousState: "PICK_TICKET",
5175
+ ticket: { id: ticket.id, title: ticket.title, claimed: true }
5176
+ });
5177
+ appendEvent(dir, {
5178
+ rev: written.revision,
5179
+ type: "ticket_picked",
5180
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
5181
+ data: { ticketId: ticket.id, title: ticket.title }
5182
+ });
5183
+ return guideResult(written, "PLAN", {
5184
+ instruction: [
5185
+ `# Plan for ${ticket.id}: ${ticket.title}`,
5186
+ "",
5187
+ ticket.description ? `## Ticket Description
5188
+
5189
+ ${ticket.description}` : "",
5190
+ "",
5191
+ `Write an implementation plan for this ticket. Save it to \`.story/sessions/${state.sessionId}/plan.md\`.`,
5192
+ "",
5193
+ "When done, call `claudestory_autonomous_guide` with:",
5194
+ "```json",
5195
+ `{ "sessionId": "${state.sessionId}", "action": "report", "report": { "completedAction": "plan_written" } }`,
5196
+ "```"
5197
+ ].join("\n"),
5198
+ reminders: [
5199
+ "Write the plan as a markdown file \u2014 do NOT use Claude Code's plan mode.",
5200
+ "Do NOT ask the user for approval."
5201
+ ],
5202
+ transitionedFrom: "PICK_TICKET"
5203
+ });
5204
+ }
5205
+ async function handleReportPlan(root, dir, state, report) {
5206
+ const planPath = join7(dir, "plan.md");
5207
+ if (!existsSync7(planPath)) {
5208
+ return guideResult(state, "PLAN", {
5209
+ instruction: `Plan file not found at ${planPath}. Write your plan there and call me again.`,
5210
+ reminders: ["Save plan to .story/sessions/<id>/plan.md"]
5211
+ });
5212
+ }
5213
+ const planContent = readFileSafe(planPath);
5214
+ if (!planContent || planContent.trim().length === 0) {
5215
+ return guideResult(state, "PLAN", {
5216
+ instruction: "Plan file is empty. Write your implementation plan and call me again.",
5217
+ reminders: []
5218
+ });
5219
+ }
5220
+ const risk = assessRisk(void 0, void 0);
5221
+ const written = writeSessionSync(dir, {
5222
+ ...state,
5223
+ state: "PLAN_REVIEW",
5224
+ previousState: "PLAN",
5225
+ ticket: state.ticket ? { ...state.ticket, risk } : state.ticket
5226
+ });
5227
+ appendEvent(dir, {
5228
+ rev: written.revision,
5229
+ type: "plan_written",
5230
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
5231
+ data: { planLength: planContent.length, risk }
5232
+ });
5233
+ const backends = state.config.reviewBackends;
5234
+ const reviewer = nextReviewer([], backends);
5235
+ const rounds = requiredRounds(risk);
5236
+ return guideResult(written, "PLAN_REVIEW", {
5237
+ instruction: [
5238
+ `# Plan Review \u2014 Round 1 of ${rounds} minimum`,
5239
+ "",
5240
+ `Run a plan review using **${reviewer}**.`,
5241
+ "",
5242
+ reviewer === "codex" ? `Call \`review_plan\` MCP tool with the plan content.` : `Launch a code review agent to review the plan.`,
5243
+ "",
5244
+ "When done, call `claudestory_autonomous_guide` with:",
5245
+ "```json",
5246
+ `{ "sessionId": "${state.sessionId}", "action": "report", "report": { "completedAction": "plan_review_round", "verdict": "<approve|revise|reject>", "findings": [...] } }`,
5247
+ "```"
5248
+ ].join("\n"),
5249
+ reminders: ["Report the exact verdict and findings from the reviewer."],
5250
+ transitionedFrom: "PLAN"
5251
+ });
5252
+ }
5253
+ async function handleReportPlanReview(root, dir, state, report) {
5254
+ const verdict = report.verdict;
5255
+ if (!verdict || !["approve", "revise", "request_changes", "reject"].includes(verdict)) {
5256
+ return guideResult(state, "PLAN_REVIEW", {
5257
+ instruction: 'Invalid verdict. Re-submit with verdict: "approve", "revise", "request_changes", or "reject".',
5258
+ reminders: []
5259
+ });
5260
+ }
5261
+ const planReviews = [...state.reviews.plan];
5262
+ const roundNum = planReviews.length + 1;
5263
+ const findings = report.findings ?? [];
5264
+ const backends = state.config.reviewBackends;
5265
+ const reviewerBackend = nextReviewer(planReviews, backends);
5266
+ planReviews.push({
5267
+ round: roundNum,
5268
+ reviewer: reviewerBackend,
5269
+ verdict,
5270
+ findingCount: findings.length,
5271
+ criticalCount: findings.filter((f) => f.severity === "critical").length,
5272
+ majorCount: findings.filter((f) => f.severity === "major").length,
5273
+ suggestionCount: findings.filter((f) => f.severity === "suggestion").length,
5274
+ codexSessionId: report.reviewerSessionId,
5275
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
5276
+ });
5277
+ const risk = state.ticket?.risk ?? "low";
5278
+ const minRounds = requiredRounds(risk);
5279
+ const hasCriticalOrMajor = findings.some(
5280
+ (f) => f.severity === "critical" || f.severity === "major"
5281
+ );
5282
+ let nextState;
5283
+ if (verdict === "reject") {
5284
+ nextState = "PLAN";
5285
+ } else if (verdict === "approve" || !hasCriticalOrMajor && roundNum >= minRounds) {
5286
+ nextState = "IMPLEMENT";
5287
+ } else if (roundNum >= 5) {
5288
+ nextState = "IMPLEMENT";
5289
+ } else {
5290
+ nextState = "PLAN_REVIEW";
5291
+ }
5292
+ const written = writeSessionSync(dir, {
5293
+ ...state,
5294
+ state: nextState,
5295
+ previousState: "PLAN_REVIEW",
5296
+ reviews: { ...state.reviews, plan: planReviews }
5297
+ });
5298
+ appendEvent(dir, {
5299
+ rev: written.revision,
5300
+ type: "plan_review",
5301
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
5302
+ data: { round: roundNum, verdict, findingCount: findings.length }
5303
+ });
5304
+ if (nextState === "PLAN") {
5305
+ return guideResult(written, "PLAN", {
5306
+ instruction: 'Plan was rejected. Revise and rewrite the plan, then call me with completedAction: "plan_written".',
5307
+ reminders: [],
5308
+ transitionedFrom: "PLAN_REVIEW"
5309
+ });
5310
+ }
5311
+ if (nextState === "IMPLEMENT") {
5312
+ return guideResult(written, "IMPLEMENT", {
5313
+ instruction: [
5314
+ "# Implement",
5315
+ "",
5316
+ "Plan review passed. Implement the plan now.",
5317
+ "",
5318
+ "When done, call `claudestory_autonomous_guide` with:",
5319
+ "```json",
5320
+ `{ "sessionId": "${state.sessionId}", "action": "report", "report": { "completedAction": "implementation_done" } }`,
5321
+ "```"
5322
+ ].join("\n"),
5323
+ reminders: ["Call autonomous_guide when implementation is complete."],
5324
+ transitionedFrom: "PLAN_REVIEW"
5325
+ });
5326
+ }
5327
+ const reviewer = nextReviewer(planReviews, backends);
5328
+ return guideResult(written, "PLAN_REVIEW", {
5329
+ instruction: [
5330
+ `# Plan Review \u2014 Round ${roundNum + 1}`,
5331
+ "",
5332
+ hasCriticalOrMajor ? `Round ${roundNum} found ${findings.filter((f) => f.severity === "critical" || f.severity === "major").length} critical/major finding(s). Address them, then re-review with **${reviewer}**.` : `Round ${roundNum} complete. Run round ${roundNum + 1} with **${reviewer}**.`,
5333
+ "",
5334
+ "Report verdict and findings as before."
5335
+ ].join("\n"),
5336
+ reminders: ["Address findings before re-reviewing."]
5337
+ });
5338
+ }
5339
+ async function handleReportImplement(root, dir, state, report) {
5340
+ let realizedRisk = state.ticket?.risk ?? "low";
5341
+ const mergeBase = state.git.mergeBase;
5342
+ if (mergeBase) {
5343
+ const diffResult = await gitDiffStat(root, mergeBase);
5344
+ const namesResult = await gitDiffNames(root, mergeBase);
5345
+ if (diffResult.ok) {
5346
+ realizedRisk = assessRisk(diffResult.data, namesResult.ok ? namesResult.data : void 0);
5347
+ }
5348
+ }
5349
+ const backends = state.config.reviewBackends;
5350
+ const codeReviews = state.reviews.code;
5351
+ const reviewer = nextReviewer(codeReviews, backends);
5352
+ const rounds = requiredRounds(realizedRisk);
5353
+ const written = writeSessionSync(dir, {
5354
+ ...state,
5355
+ state: "CODE_REVIEW",
5356
+ previousState: "IMPLEMENT",
5357
+ ticket: state.ticket ? { ...state.ticket, realizedRisk } : state.ticket
5358
+ });
5359
+ appendEvent(dir, {
5360
+ rev: written.revision,
5361
+ type: "implementation_done",
5362
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
5363
+ data: { realizedRisk }
5364
+ });
5365
+ return guideResult(written, "CODE_REVIEW", {
5366
+ instruction: [
5367
+ `# Code Review \u2014 Round 1 of ${rounds} minimum`,
5368
+ "",
5369
+ `Realized risk: **${realizedRisk}**${realizedRisk !== state.ticket?.risk ? ` (was ${state.ticket?.risk})` : ""}.`,
5370
+ "",
5371
+ `Run a code review using **${reviewer}**. Capture the git diff and pass it to the reviewer.`,
5372
+ "",
5373
+ "When done, report verdict and findings."
5374
+ ].join("\n"),
5375
+ reminders: ["Capture diff with `git diff` and pass to reviewer."],
5376
+ transitionedFrom: "IMPLEMENT"
5377
+ });
5378
+ }
5379
+ async function handleReportCodeReview(root, dir, state, report) {
5380
+ const verdict = report.verdict;
5381
+ if (!verdict || !["approve", "revise", "request_changes", "reject"].includes(verdict)) {
5382
+ return guideResult(state, "CODE_REVIEW", {
5383
+ instruction: 'Invalid verdict. Re-submit with verdict: "approve", "revise", "request_changes", or "reject".',
5384
+ reminders: []
5385
+ });
5386
+ }
5387
+ const codeReviews = [...state.reviews.code];
5388
+ const roundNum = codeReviews.length + 1;
5389
+ const findings = report.findings ?? [];
5390
+ const backends = state.config.reviewBackends;
5391
+ const reviewerBackend = nextReviewer(codeReviews, backends);
5392
+ codeReviews.push({
5393
+ round: roundNum,
5394
+ reviewer: reviewerBackend,
5395
+ verdict,
5396
+ findingCount: findings.length,
5397
+ criticalCount: findings.filter((f) => f.severity === "critical").length,
5398
+ majorCount: findings.filter((f) => f.severity === "major").length,
5399
+ suggestionCount: findings.filter((f) => f.severity === "suggestion").length,
5400
+ codexSessionId: report.reviewerSessionId,
5401
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
5402
+ });
5403
+ const risk = state.ticket?.realizedRisk ?? state.ticket?.risk ?? "low";
5404
+ const minRounds = requiredRounds(risk);
5405
+ const hasCriticalOrMajor = findings.some(
5406
+ (f) => f.severity === "critical" || f.severity === "major"
5407
+ );
5408
+ const planRedirect = findings.some((f) => f.recommendedNextState === "PLAN");
5409
+ let nextState;
5410
+ if (verdict === "reject" && planRedirect) {
5411
+ nextState = "PLAN";
5412
+ } else if (verdict === "reject") {
5413
+ nextState = "IMPLEMENT";
5414
+ } else if (verdict === "approve" || !hasCriticalOrMajor && roundNum >= minRounds) {
5415
+ nextState = "FINALIZE";
5416
+ } else if (roundNum >= 5) {
5417
+ nextState = "FINALIZE";
5418
+ } else {
5419
+ nextState = "CODE_REVIEW";
5420
+ }
5421
+ const written = writeSessionSync(dir, {
5422
+ ...state,
5423
+ state: nextState,
5424
+ previousState: "CODE_REVIEW",
5425
+ reviews: { ...state.reviews, code: codeReviews }
5426
+ });
5427
+ appendEvent(dir, {
5428
+ rev: written.revision,
5429
+ type: "code_review",
5430
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
5431
+ data: { round: roundNum, verdict, findingCount: findings.length }
5432
+ });
5433
+ if (nextState === "PLAN") {
5434
+ return guideResult(written, "PLAN", {
5435
+ instruction: 'Code review recommends rethinking the approach. Revise the plan and call me with completedAction: "plan_written".',
5436
+ reminders: [],
5437
+ transitionedFrom: "CODE_REVIEW"
5438
+ });
5439
+ }
5440
+ if (nextState === "IMPLEMENT") {
5441
+ return guideResult(written, "IMPLEMENT", {
5442
+ instruction: 'Code review requested changes. Fix the issues and call me with completedAction: "implementation_done".',
5443
+ reminders: ["Address all critical/major findings before re-submitting."],
5444
+ transitionedFrom: "CODE_REVIEW"
5445
+ });
5446
+ }
5447
+ if (nextState === "FINALIZE") {
5448
+ return guideResult(written, "FINALIZE", {
5449
+ instruction: [
5450
+ "# Finalize",
5451
+ "",
5452
+ "Code review passed. Time to commit.",
5453
+ "",
5454
+ state.ticket ? `1. Update ticket ${state.ticket.id} status to "complete" in .story/` : "",
5455
+ "2. Stage all changed files (code + .story/ changes)",
5456
+ '3. Call me with completedAction: "files_staged"'
5457
+ ].filter(Boolean).join("\n"),
5458
+ reminders: ["Stage both code changes and .story/ ticket update in the same commit."],
5459
+ transitionedFrom: "CODE_REVIEW"
5460
+ });
5461
+ }
5462
+ const reviewer = nextReviewer(codeReviews, backends);
5463
+ return guideResult(written, "CODE_REVIEW", {
5464
+ instruction: `Code review round ${roundNum} found issues. Fix them and re-review with **${reviewer}**.`,
5465
+ reminders: []
5466
+ });
5467
+ }
5468
+ async function handleReportFinalize(root, dir, state, report) {
5469
+ const action = report.completedAction;
5470
+ const checkpoint = state.finalizeCheckpoint;
5471
+ if (action === "files_staged" && (!checkpoint || checkpoint === "staged")) {
5472
+ const stagedResult = await gitDiffCachedNames(root);
5473
+ if (!stagedResult.ok || stagedResult.data.length === 0) {
5474
+ return guideResult(state, "FINALIZE", {
5475
+ instruction: 'No files are staged. Stage your changes and call me again with completedAction: "files_staged".',
5476
+ reminders: []
5477
+ });
5478
+ }
5479
+ const written = writeSessionSync(dir, { ...state, finalizeCheckpoint: "staged" });
5480
+ return guideResult(written, "FINALIZE", {
5481
+ instruction: [
5482
+ "Files staged. Now run pre-commit checks.",
5483
+ "",
5484
+ 'Run any pre-commit hooks or linting, then call me with completedAction: "precommit_passed".',
5485
+ 'If pre-commit fails, fix the issues, re-stage, and call me with completedAction: "files_staged" again.'
5486
+ ].join("\n"),
5487
+ reminders: ["Verify staged set is intact after pre-commit hooks."]
5488
+ });
5489
+ }
5490
+ if (action === "precommit_passed") {
5491
+ if (!checkpoint || checkpoint === null) {
5492
+ return guideResult(state, "FINALIZE", {
5493
+ instruction: 'You must stage files first. Call me with completedAction: "files_staged" after staging.',
5494
+ reminders: []
5495
+ });
5496
+ }
5497
+ const stagedResult = await gitDiffCachedNames(root);
5498
+ if (!stagedResult.ok || stagedResult.data.length === 0) {
5499
+ const written2 = writeSessionSync(dir, { ...state, finalizeCheckpoint: null });
5500
+ return guideResult(written2, "FINALIZE", {
5501
+ instruction: 'Pre-commit hooks appear to have cleared the staging area. Re-stage your changes and call me with completedAction: "files_staged".',
5502
+ reminders: []
5503
+ });
5504
+ }
5505
+ const written = writeSessionSync(dir, { ...state, finalizeCheckpoint: "precommit_passed" });
5506
+ return guideResult(written, "FINALIZE", {
5507
+ instruction: [
5508
+ "Pre-commit passed. Now commit.",
5509
+ "",
5510
+ state.ticket ? `Commit with message: "feat: <description> (${state.ticket.id})"` : "Commit with a descriptive message.",
5511
+ "",
5512
+ 'Call me with completedAction: "commit_done" and include the commitHash.'
5513
+ ].join("\n"),
5514
+ reminders: []
5515
+ });
5516
+ }
5517
+ if (action === "commit_done") {
5518
+ if (!checkpoint || checkpoint === null) {
5519
+ return guideResult(state, "FINALIZE", {
5520
+ instruction: 'You must stage files first. Call me with completedAction: "files_staged" after staging.',
5521
+ reminders: []
5522
+ });
5523
+ }
5524
+ if (checkpoint === "staged") {
5525
+ return guideResult(state, "FINALIZE", {
5526
+ instruction: 'You must pass pre-commit checks first. Call me with completedAction: "precommit_passed".',
5527
+ reminders: []
5528
+ });
5529
+ }
5530
+ if (checkpoint === "committed") {
5531
+ return guideResult(state, "FINALIZE", {
5532
+ instruction: "Commit was already recorded. Proceeding to completion.",
5533
+ reminders: []
5534
+ });
5535
+ }
5536
+ const commitHash = report.commitHash;
5537
+ if (!commitHash) {
5538
+ return guideResult(state, "FINALIZE", {
5539
+ instruction: "Missing commitHash in report. Call me again with the commit hash.",
5540
+ reminders: []
5541
+ });
5542
+ }
5543
+ const completedTicket = state.ticket ? { id: state.ticket.id, title: state.ticket.title, commitHash, risk: state.ticket.risk } : void 0;
5544
+ const updated = {
5545
+ ...state,
5546
+ state: "COMPLETE",
5547
+ previousState: "FINALIZE",
5548
+ finalizeCheckpoint: "committed",
5549
+ completedTickets: completedTicket ? [...state.completedTickets, completedTicket] : state.completedTickets,
5550
+ ticket: void 0
5551
+ };
5552
+ const written = writeSessionSync(dir, updated);
5553
+ appendEvent(dir, {
5554
+ rev: written.revision,
5555
+ type: "commit",
5556
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
5557
+ data: { commitHash, ticketId: completedTicket?.id }
5558
+ });
5559
+ return handleReportComplete(root, dir, refreshLease(written), { completedAction: "commit_done" });
5560
+ }
5561
+ return guideResult(state, "FINALIZE", {
5562
+ instruction: 'Unexpected action at FINALIZE. Stage files and call with completedAction: "files_staged", or commit and call with completedAction: "commit_done".',
5563
+ reminders: []
5564
+ });
5565
+ }
5566
+ async function handleReportComplete(root, dir, state, report) {
5567
+ const pressure = evaluatePressure(state);
5568
+ const updated = {
5569
+ ...state,
5570
+ state: "COMPLETE",
5571
+ contextPressure: { ...state.contextPressure, level: pressure },
5572
+ finalizeCheckpoint: null
5573
+ };
5574
+ const ticketsDone = updated.completedTickets.length;
5575
+ const maxTickets = updated.config.maxTicketsPerSession;
5576
+ let nextState;
5577
+ let advice = "ok";
5578
+ if (pressure === "critical") {
5579
+ nextState = "HANDOVER";
5580
+ advice = "compact-now";
5581
+ } else if (ticketsDone >= maxTickets) {
5582
+ nextState = "HANDOVER";
5583
+ } else if (pressure === "high") {
5584
+ advice = "consider-compact";
5585
+ nextState = "PICK_TICKET";
5586
+ } else {
5587
+ nextState = "PICK_TICKET";
5588
+ }
5589
+ const { state: projectState } = await loadProject(root);
5590
+ const nextResult = nextTickets(projectState, 1);
5591
+ if (nextResult.kind !== "found") {
5592
+ nextState = "HANDOVER";
5593
+ }
5594
+ const transitioned = writeSessionSync(dir, {
5595
+ ...updated,
5596
+ state: nextState,
5597
+ previousState: "COMPLETE"
5598
+ });
5599
+ if (nextState === "HANDOVER") {
5600
+ return guideResult(transitioned, "HANDOVER", {
5601
+ instruction: [
5602
+ `# Session Complete \u2014 ${ticketsDone} ticket(s) done`,
5603
+ "",
5604
+ "Write a session handover summarizing what was accomplished, decisions made, and what's next.",
5605
+ "",
5606
+ 'Call me with completedAction: "handover_written" and include the content in handoverContent.'
5607
+ ].join("\n"),
5608
+ reminders: [],
5609
+ transitionedFrom: "COMPLETE",
5610
+ contextAdvice: advice
5611
+ });
5612
+ }
5613
+ const candidates = nextTickets(projectState, 5);
5614
+ let candidatesText = "";
5615
+ if (candidates.kind === "found") {
5616
+ candidatesText = candidates.candidates.map(
5617
+ (c, i) => `${i + 1}. **${c.ticket.id}: ${c.ticket.title}** (${c.ticket.type})`
5618
+ ).join("\n");
5619
+ }
5620
+ return guideResult(transitioned, "PICK_TICKET", {
5621
+ instruction: [
5622
+ `# Ticket Complete \u2014 Pick Next (${ticketsDone}/${maxTickets})`,
5623
+ "",
5624
+ candidatesText,
5625
+ "",
5626
+ 'Pick a ticket by calling me with completedAction: "ticket_picked" and ticketId.'
5627
+ ].join("\n"),
5628
+ reminders: [],
5629
+ transitionedFrom: "COMPLETE",
5630
+ contextAdvice: advice
5631
+ });
5632
+ }
5633
+ async function handleReportHandover(root, dir, state, report) {
5634
+ const content = report.handoverContent;
5635
+ if (!content) {
5636
+ return guideResult(state, "HANDOVER", {
5637
+ instruction: "Missing handoverContent. Write the handover and include it in the report.",
5638
+ reminders: []
5639
+ });
5640
+ }
5641
+ let handoverFailed = false;
5642
+ try {
5643
+ await handleHandoverCreate(content, "auto-session", "md", root);
5644
+ } catch (err) {
5645
+ handoverFailed = true;
5646
+ try {
5647
+ const fallbackPath = join7(dir, "handover-fallback.md");
5648
+ writeFileSync2(fallbackPath, content, "utf-8");
5649
+ } catch {
5650
+ }
5651
+ }
5652
+ const written = writeSessionSync(dir, {
5653
+ ...state,
5654
+ state: "SESSION_END",
5655
+ previousState: "HANDOVER",
5656
+ status: "completed"
5657
+ });
5658
+ appendEvent(dir, {
5659
+ rev: written.revision,
5660
+ type: "session_end",
5661
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
5662
+ data: { ticketsCompleted: written.completedTickets.length, handoverFailed }
5663
+ });
5664
+ const ticketsDone = written.completedTickets.length;
5665
+ return guideResult(written, "SESSION_END", {
5666
+ instruction: [
5667
+ "# Session Complete",
5668
+ "",
5669
+ `${ticketsDone} ticket(s) completed.${handoverFailed ? " Handover creation failed \u2014 fallback saved to session directory." : " Handover written."} Session ended.`,
5670
+ "",
5671
+ written.completedTickets.map((t) => `- ${t.id}${t.title ? `: ${t.title}` : ""} (${t.commitHash ?? "no commit"})`).join("\n")
5672
+ ].join("\n"),
5673
+ reminders: [],
5674
+ transitionedFrom: "HANDOVER"
5675
+ });
5676
+ }
5677
+ async function handleResume(root, args) {
5678
+ if (!args.sessionId) return guideError(new Error("sessionId is required for resume"));
5679
+ const info = findSessionById(root, args.sessionId);
5680
+ if (!info) return guideError(new Error(`Session ${args.sessionId} not found`));
5681
+ if (info.state.state !== "COMPACT") {
5682
+ return guideError(new Error(
5683
+ `Session ${args.sessionId} is not in COMPACT state (current: ${info.state.state}). Use action: "report" to continue.`
5684
+ ));
5685
+ }
5686
+ const resumeState = info.state.preCompactState;
5687
+ if (!resumeState || !WORKFLOW_STATES.includes(resumeState)) {
5688
+ return guideError(new Error(
5689
+ `Session ${args.sessionId} has invalid preCompactState: ${resumeState}. Cannot resume safely.`
5690
+ ));
5691
+ }
5692
+ const written = writeSessionSync(info.dir, {
5693
+ ...refreshLease(info.state),
5694
+ state: resumeState,
5695
+ preCompactState: null,
5696
+ resumeFromRevision: null,
5697
+ contextPressure: { ...info.state.contextPressure, compactionCount: (info.state.contextPressure?.compactionCount ?? 0) + 1 }
5698
+ });
5699
+ return guideResult(written, resumeState, {
5700
+ instruction: [
5701
+ "# Resumed After Compact",
5702
+ "",
5703
+ `Session restored at state: **${resumeState}**.`,
5704
+ written.ticket ? `Working on: **${written.ticket.id}: ${written.ticket.title}**` : "No ticket in progress.",
5705
+ "",
5706
+ "Continue where you left off. Call me when you complete the current step."
5707
+ ].join("\n"),
5708
+ reminders: [
5709
+ "Do NOT use plan mode.",
5710
+ "Call autonomous_guide after completing each step."
5711
+ ]
5712
+ });
5713
+ }
5714
+ async function handlePreCompact(root, args) {
5715
+ if (!args.sessionId) return guideError(new Error("sessionId is required for pre_compact"));
5716
+ const info = findSessionById(root, args.sessionId);
5717
+ if (!info) return guideError(new Error(`Session ${args.sessionId} not found`));
5718
+ if (info.state.state === "SESSION_END") {
5719
+ return guideError(new Error(`Session ${args.sessionId} is already ended and cannot be compacted.`));
5720
+ }
5721
+ if (info.state.state === "COMPACT") {
5722
+ return guideError(new Error(`Session ${args.sessionId} is already in COMPACT state. Call action: "resume" to continue.`));
5723
+ }
5724
+ const headResult = await gitHead(root);
5725
+ const written = writeSessionSync(info.dir, {
5726
+ ...refreshLease(info.state),
5727
+ state: "COMPACT",
5728
+ previousState: info.state.state,
5729
+ preCompactState: info.state.state,
5730
+ resumeFromRevision: info.state.revision,
5731
+ git: {
5732
+ ...info.state.git,
5733
+ expectedHead: headResult.ok ? headResult.data.hash : info.state.git.expectedHead
5734
+ }
5735
+ });
5736
+ try {
5737
+ const loadResult = await loadProject(root);
5738
+ const { saveSnapshot: saveSnapshot2 } = await Promise.resolve().then(() => (init_snapshot(), snapshot_exports));
5739
+ await saveSnapshot2(root, loadResult);
5740
+ } catch {
5741
+ }
5742
+ return guideResult(written, "COMPACT", {
5743
+ instruction: [
5744
+ "# Ready for Compact",
5745
+ "",
5746
+ "State flushed. Run `/compact` now.",
5747
+ "",
5748
+ "After compact, call `claudestory_autonomous_guide` with:",
5749
+ "```json",
5750
+ `{ "sessionId": "${written.sessionId}", "action": "resume" }`,
5751
+ "```"
5752
+ ].join("\n"),
5753
+ reminders: []
5754
+ });
5755
+ }
5756
+ async function handleCancel(root, args) {
5757
+ if (!args.sessionId) {
5758
+ const active = findActiveSessionFull(root);
5759
+ if (!active) return guideError(new Error("No active session to cancel"));
5760
+ args = { ...args, sessionId: active.state.sessionId };
5761
+ }
5762
+ const info = findSessionById(root, args.sessionId);
5763
+ if (!info) return guideError(new Error(`Session ${args.sessionId} not found`));
5764
+ const written = writeSessionSync(info.dir, {
5765
+ ...info.state,
5766
+ state: "SESSION_END",
5767
+ previousState: info.state.state,
5768
+ status: "completed"
5769
+ });
5770
+ appendEvent(info.dir, {
5771
+ rev: written.revision,
5772
+ type: "cancelled",
5773
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
5774
+ data: { previousState: info.state.state }
5775
+ });
5776
+ return {
5777
+ content: [{ type: "text", text: `Session ${args.sessionId} cancelled. ${written.completedTickets.length} ticket(s) were completed.` }]
5778
+ };
5779
+ }
5780
+ function guideResult(state, currentState, opts) {
5781
+ const summary = {
5782
+ ticket: state.ticket ? `${state.ticket.id}: ${state.ticket.title}` : "none",
5783
+ risk: state.ticket?.risk ?? "unknown",
5784
+ completed: state.completedTickets.map((t) => t.id),
5785
+ currentStep: currentState,
5786
+ contextPressure: state.contextPressure?.level ?? "low",
5787
+ branch: state.git?.branch ?? null
5788
+ };
5789
+ const output = {
5790
+ sessionId: state.sessionId,
5791
+ state: currentState,
5792
+ transitionedFrom: opts.transitionedFrom,
5793
+ instruction: opts.instruction,
5794
+ reminders: opts.reminders ?? [],
5795
+ contextAdvice: opts.contextAdvice ?? "ok",
5796
+ sessionSummary: summary
5797
+ };
5798
+ const parts = [
5799
+ output.instruction,
5800
+ "",
5801
+ "---",
5802
+ `**Session:** ${output.sessionId}`,
5803
+ `**State:** ${output.state}${output.transitionedFrom ? ` (from ${output.transitionedFrom})` : ""}`,
5804
+ `**Ticket:** ${summary.ticket}`,
5805
+ `**Risk:** ${summary.risk}`,
5806
+ `**Completed:** ${summary.completed.length > 0 ? summary.completed.join(", ") : "none"}`,
5807
+ `**Pressure:** ${summary.contextPressure}`,
5808
+ summary.branch ? `**Branch:** ${summary.branch}` : "",
5809
+ output.contextAdvice !== "ok" ? `**Context:** ${output.contextAdvice}` : "",
5810
+ output.reminders.length > 0 ? `
5811
+ **Reminders:**
5812
+ ${output.reminders.map((r) => `- ${r}`).join("\n")}` : ""
5813
+ ].filter(Boolean);
5814
+ return { content: [{ type: "text", text: parts.join("\n") }] };
5815
+ }
5816
+ function guideError(err) {
5817
+ const message = err instanceof Error ? err.message : String(err);
5818
+ return {
5819
+ content: [{ type: "text", text: `[autonomous_guide error] ${message}` }],
5820
+ isError: true
5821
+ };
5822
+ }
5823
+ function readFileSafe(path2) {
5824
+ try {
5825
+ return readFileSync2(path2, "utf-8");
5826
+ } catch {
5827
+ return "";
5828
+ }
5829
+ }
5830
+ var workspaceLocks;
5831
+ var init_guide = __esm({
5832
+ "src/autonomous/guide.ts"() {
5833
+ "use strict";
5834
+ init_esm_shims();
5835
+ init_session_types();
5836
+ init_session();
5837
+ init_state_machine();
5838
+ init_context_pressure();
5839
+ init_review_depth();
5840
+ init_git_inspector();
5841
+ init_project_loader();
5842
+ init_snapshot();
5843
+ init_snapshot();
5844
+ init_queries();
5845
+ init_recommend();
5846
+ init_handover();
5847
+ workspaceLocks = /* @__PURE__ */ new Map();
5848
+ }
5849
+ });
5850
+
4344
5851
  // src/cli/commands/phase.ts
4345
- import { join as join6, resolve as resolve6 } from "path";
5852
+ import { join as join8, resolve as resolve6 } from "path";
4346
5853
  function validatePhaseId(id) {
4347
5854
  if (id.length > PHASE_ID_MAX_LENGTH) {
4348
5855
  throw new CliValidationError("invalid_input", `Phase ID "${id}" exceeds ${PHASE_ID_MAX_LENGTH} characters`);
@@ -4531,21 +6038,21 @@ async function handlePhaseDelete(id, reassign, format, root) {
4531
6038
  const updated = { ...ticket, phase: reassign, order: maxOrder };
4532
6039
  const parsed = TicketSchema.parse(updated);
4533
6040
  const content = serializeJSON(parsed);
4534
- const target = join6(wrapDir, "tickets", `${parsed.id}.json`);
6041
+ const target = join8(wrapDir, "tickets", `${parsed.id}.json`);
4535
6042
  operations.push({ op: "write", target, content });
4536
6043
  }
4537
6044
  for (const issue of affectedIssues) {
4538
6045
  const updated = { ...issue, phase: reassign };
4539
6046
  const parsed = IssueSchema.parse(updated);
4540
6047
  const content = serializeJSON(parsed);
4541
- const target = join6(wrapDir, "issues", `${parsed.id}.json`);
6048
+ const target = join8(wrapDir, "issues", `${parsed.id}.json`);
4542
6049
  operations.push({ op: "write", target, content });
4543
6050
  }
4544
6051
  const newPhases = state.roadmap.phases.filter((p) => p.id !== id);
4545
6052
  const newRoadmap = { ...state.roadmap, phases: newPhases };
4546
6053
  const parsedRoadmap = RoadmapSchema.parse(newRoadmap);
4547
6054
  const roadmapContent = serializeJSON(parsedRoadmap);
4548
- const roadmapTarget = join6(wrapDir, "roadmap.json");
6055
+ const roadmapTarget = join8(wrapDir, "roadmap.json");
4549
6056
  operations.push({ op: "write", target: roadmapTarget, content: roadmapContent });
4550
6057
  await runTransactionUnlocked(root, operations);
4551
6058
  } else {
@@ -4577,15 +6084,15 @@ var init_phase = __esm({
4577
6084
  });
4578
6085
 
4579
6086
  // src/mcp/tools.ts
4580
- import { z as z8 } from "zod";
4581
- import { join as join7 } from "path";
6087
+ import { z as z9 } from "zod";
6088
+ import { join as join9 } from "path";
4582
6089
  function formatMcpError(code, message) {
4583
6090
  return `[${code}] ${message}`;
4584
6091
  }
4585
6092
  async function runMcpReadTool(pinnedRoot, handler) {
4586
6093
  try {
4587
6094
  const { state, warnings } = await loadProject(pinnedRoot);
4588
- const handoversDir = join7(pinnedRoot, ".story", "handovers");
6095
+ const handoversDir = join9(pinnedRoot, ".story", "handovers");
4589
6096
  const ctx = { state, warnings, root: pinnedRoot, handoversDir, format: "md" };
4590
6097
  const result = await handler(ctx);
4591
6098
  if (result.errorCode && INFRASTRUCTURE_ERROR_CODES.includes(result.errorCode)) {
@@ -4649,7 +6156,7 @@ function registerAllTools(server, pinnedRoot) {
4649
6156
  server.registerTool("claudestory_ticket_next", {
4650
6157
  description: "Highest-priority unblocked ticket(s) with unblock impact and umbrella progress",
4651
6158
  inputSchema: {
4652
- count: z8.number().int().min(1).max(10).optional().describe("Number of candidates to return (default: 1)")
6159
+ count: z9.number().int().min(1).max(10).optional().describe("Number of candidates to return (default: 1)")
4653
6160
  }
4654
6161
  }, (args) => runMcpReadTool(
4655
6162
  pinnedRoot,
@@ -4664,7 +6171,7 @@ function registerAllTools(server, pinnedRoot) {
4664
6171
  server.registerTool("claudestory_handover_latest", {
4665
6172
  description: "Content of the most recent handover document(s)",
4666
6173
  inputSchema: {
4667
- count: z8.number().int().min(1).max(10).optional().describe("Number of recent handovers to return (default: 1)")
6174
+ count: z9.number().int().min(1).max(10).optional().describe("Number of recent handovers to return (default: 1)")
4668
6175
  }
4669
6176
  }, (args) => runMcpReadTool(
4670
6177
  pinnedRoot,
@@ -4679,7 +6186,7 @@ function registerAllTools(server, pinnedRoot) {
4679
6186
  server.registerTool("claudestory_phase_tickets", {
4680
6187
  description: "Leaf tickets for a specific phase, sorted by order",
4681
6188
  inputSchema: {
4682
- phaseId: z8.string().describe("Phase ID (e.g. p5b, dogfood)")
6189
+ phaseId: z9.string().describe("Phase ID (e.g. p5b, dogfood)")
4683
6190
  }
4684
6191
  }, (args) => runMcpReadTool(pinnedRoot, (ctx) => {
4685
6192
  const phaseExists = ctx.state.roadmap.phases.some((p) => p.id === args.phaseId);
@@ -4695,9 +6202,9 @@ function registerAllTools(server, pinnedRoot) {
4695
6202
  server.registerTool("claudestory_ticket_list", {
4696
6203
  description: "List leaf tickets with optional filters",
4697
6204
  inputSchema: {
4698
- status: z8.enum(TICKET_STATUSES).optional().describe("Filter by status: open, inprogress, complete"),
4699
- phase: z8.string().optional().describe("Filter by phase ID"),
4700
- type: z8.enum(TICKET_TYPES).optional().describe("Filter by type: task, feature, chore")
6205
+ status: z9.enum(TICKET_STATUSES).optional().describe("Filter by status: open, inprogress, complete"),
6206
+ phase: z9.string().optional().describe("Filter by phase ID"),
6207
+ type: z9.enum(TICKET_TYPES).optional().describe("Filter by type: task, feature, chore")
4701
6208
  }
4702
6209
  }, (args) => runMcpReadTool(pinnedRoot, (ctx) => {
4703
6210
  if (args.phase) {
@@ -4718,15 +6225,15 @@ function registerAllTools(server, pinnedRoot) {
4718
6225
  server.registerTool("claudestory_ticket_get", {
4719
6226
  description: "Get a ticket by ID (includes umbrella tickets)",
4720
6227
  inputSchema: {
4721
- id: z8.string().regex(TICKET_ID_REGEX).describe("Ticket ID (e.g. T-001, T-079b)")
6228
+ id: z9.string().regex(TICKET_ID_REGEX).describe("Ticket ID (e.g. T-001, T-079b)")
4722
6229
  }
4723
6230
  }, (args) => runMcpReadTool(pinnedRoot, (ctx) => handleTicketGet(args.id, ctx)));
4724
6231
  server.registerTool("claudestory_issue_list", {
4725
6232
  description: "List issues with optional filters",
4726
6233
  inputSchema: {
4727
- status: z8.enum(ISSUE_STATUSES).optional().describe("Filter by status: open, inprogress, resolved"),
4728
- severity: z8.enum(ISSUE_SEVERITIES).optional().describe("Filter by severity: critical, high, medium, low"),
4729
- component: z8.string().optional().describe("Filter by component name")
6234
+ status: z9.enum(ISSUE_STATUSES).optional().describe("Filter by status: open, inprogress, resolved"),
6235
+ severity: z9.enum(ISSUE_SEVERITIES).optional().describe("Filter by severity: critical, high, medium, low"),
6236
+ component: z9.string().optional().describe("Filter by component name")
4730
6237
  }
4731
6238
  }, (args) => runMcpReadTool(
4732
6239
  pinnedRoot,
@@ -4735,13 +6242,13 @@ function registerAllTools(server, pinnedRoot) {
4735
6242
  server.registerTool("claudestory_issue_get", {
4736
6243
  description: "Get an issue by ID",
4737
6244
  inputSchema: {
4738
- id: z8.string().regex(ISSUE_ID_REGEX).describe("Issue ID (e.g. ISS-001)")
6245
+ id: z9.string().regex(ISSUE_ID_REGEX).describe("Issue ID (e.g. ISS-001)")
4739
6246
  }
4740
6247
  }, (args) => runMcpReadTool(pinnedRoot, (ctx) => handleIssueGet(args.id, ctx)));
4741
6248
  server.registerTool("claudestory_handover_get", {
4742
6249
  description: "Content of a specific handover document by filename",
4743
6250
  inputSchema: {
4744
- filename: z8.string().describe("Handover filename (e.g. 2026-03-20-session.md)")
6251
+ filename: z9.string().describe("Handover filename (e.g. 2026-03-20-session.md)")
4745
6252
  }
4746
6253
  }, (args) => runMcpReadTool(pinnedRoot, (ctx) => handleHandoverGet(args.filename, ctx)));
4747
6254
  server.registerTool("claudestory_recap", {
@@ -4750,7 +6257,7 @@ function registerAllTools(server, pinnedRoot) {
4750
6257
  server.registerTool("claudestory_recommend", {
4751
6258
  description: "Context-aware ranked work suggestions mixing tickets and issues",
4752
6259
  inputSchema: {
4753
- count: z8.number().int().min(1).max(10).optional().describe("Number of recommendations (default: 5)")
6260
+ count: z9.number().int().min(1).max(10).optional().describe("Number of recommendations (default: 5)")
4754
6261
  }
4755
6262
  }, (args) => runMcpReadTool(
4756
6263
  pinnedRoot,
@@ -4762,8 +6269,8 @@ function registerAllTools(server, pinnedRoot) {
4762
6269
  server.registerTool("claudestory_export", {
4763
6270
  description: "Self-contained project document for sharing",
4764
6271
  inputSchema: {
4765
- phase: z8.string().optional().describe("Export a single phase by ID"),
4766
- all: z8.boolean().optional().describe("Export entire project")
6272
+ phase: z9.string().optional().describe("Export a single phase by ID"),
6273
+ all: z9.boolean().optional().describe("Export entire project")
4767
6274
  }
4768
6275
  }, (args) => {
4769
6276
  if (!args.phase && !args.all) {
@@ -4785,8 +6292,8 @@ function registerAllTools(server, pinnedRoot) {
4785
6292
  server.registerTool("claudestory_handover_create", {
4786
6293
  description: "Create a handover document from markdown content",
4787
6294
  inputSchema: {
4788
- content: z8.string().describe("Markdown content of the handover"),
4789
- slug: z8.string().optional().describe("Slug for filename (e.g. phase5b-wrapup). Default: session")
6295
+ content: z9.string().describe("Markdown content of the handover"),
6296
+ slug: z9.string().optional().describe("Slug for filename (e.g. phase5b-wrapup). Default: session")
4790
6297
  }
4791
6298
  }, (args) => {
4792
6299
  if (!args.content?.trim()) {
@@ -4803,12 +6310,12 @@ function registerAllTools(server, pinnedRoot) {
4803
6310
  server.registerTool("claudestory_ticket_create", {
4804
6311
  description: "Create a new ticket",
4805
6312
  inputSchema: {
4806
- title: z8.string().describe("Ticket title"),
4807
- type: z8.enum(TICKET_TYPES).describe("Ticket type: task, feature, chore"),
4808
- phase: z8.string().optional().describe("Phase ID"),
4809
- description: z8.string().optional().describe("Ticket description"),
4810
- blockedBy: z8.array(z8.string().regex(TICKET_ID_REGEX)).optional().describe("IDs of blocking tickets"),
4811
- parentTicket: z8.string().regex(TICKET_ID_REGEX).optional().describe("Parent ticket ID (makes this a sub-ticket)")
6313
+ title: z9.string().describe("Ticket title"),
6314
+ type: z9.enum(TICKET_TYPES).describe("Ticket type: task, feature, chore"),
6315
+ phase: z9.string().optional().describe("Phase ID"),
6316
+ description: z9.string().optional().describe("Ticket description"),
6317
+ blockedBy: z9.array(z9.string().regex(TICKET_ID_REGEX)).optional().describe("IDs of blocking tickets"),
6318
+ parentTicket: z9.string().regex(TICKET_ID_REGEX).optional().describe("Parent ticket ID (makes this a sub-ticket)")
4812
6319
  }
4813
6320
  }, (args) => runMcpWriteTool(
4814
6321
  pinnedRoot,
@@ -4828,15 +6335,15 @@ function registerAllTools(server, pinnedRoot) {
4828
6335
  server.registerTool("claudestory_ticket_update", {
4829
6336
  description: "Update an existing ticket",
4830
6337
  inputSchema: {
4831
- id: z8.string().regex(TICKET_ID_REGEX).describe("Ticket ID (e.g. T-001)"),
4832
- status: z8.enum(TICKET_STATUSES).optional().describe("New status: open, inprogress, complete"),
4833
- title: z8.string().optional().describe("New title"),
4834
- type: z8.enum(TICKET_TYPES).optional().describe("New type: task, feature, chore"),
4835
- order: z8.number().int().optional().describe("New sort order"),
4836
- description: z8.string().optional().describe("New description"),
4837
- phase: z8.string().nullable().optional().describe("New phase ID (null to clear)"),
4838
- parentTicket: z8.string().regex(TICKET_ID_REGEX).nullable().optional().describe("Parent ticket ID (null to clear)"),
4839
- blockedBy: z8.array(z8.string().regex(TICKET_ID_REGEX)).optional().describe("IDs of blocking tickets")
6338
+ id: z9.string().regex(TICKET_ID_REGEX).describe("Ticket ID (e.g. T-001)"),
6339
+ status: z9.enum(TICKET_STATUSES).optional().describe("New status: open, inprogress, complete"),
6340
+ title: z9.string().optional().describe("New title"),
6341
+ type: z9.enum(TICKET_TYPES).optional().describe("New type: task, feature, chore"),
6342
+ order: z9.number().int().optional().describe("New sort order"),
6343
+ description: z9.string().optional().describe("New description"),
6344
+ phase: z9.string().nullable().optional().describe("New phase ID (null to clear)"),
6345
+ parentTicket: z9.string().regex(TICKET_ID_REGEX).nullable().optional().describe("Parent ticket ID (null to clear)"),
6346
+ blockedBy: z9.array(z9.string().regex(TICKET_ID_REGEX)).optional().describe("IDs of blocking tickets")
4840
6347
  }
4841
6348
  }, (args) => runMcpWriteTool(
4842
6349
  pinnedRoot,
@@ -4859,13 +6366,13 @@ function registerAllTools(server, pinnedRoot) {
4859
6366
  server.registerTool("claudestory_issue_create", {
4860
6367
  description: "Create a new issue",
4861
6368
  inputSchema: {
4862
- title: z8.string().describe("Issue title"),
4863
- severity: z8.enum(ISSUE_SEVERITIES).describe("Issue severity: critical, high, medium, low"),
4864
- impact: z8.string().describe("Impact description"),
4865
- components: z8.array(z8.string()).optional().describe("Affected components"),
4866
- relatedTickets: z8.array(z8.string().regex(TICKET_ID_REGEX)).optional().describe("Related ticket IDs"),
4867
- location: z8.array(z8.string()).optional().describe("File locations"),
4868
- phase: z8.string().optional().describe("Phase ID")
6369
+ title: z9.string().describe("Issue title"),
6370
+ severity: z9.enum(ISSUE_SEVERITIES).describe("Issue severity: critical, high, medium, low"),
6371
+ impact: z9.string().describe("Impact description"),
6372
+ components: z9.array(z9.string()).optional().describe("Affected components"),
6373
+ relatedTickets: z9.array(z9.string().regex(TICKET_ID_REGEX)).optional().describe("Related ticket IDs"),
6374
+ location: z9.array(z9.string()).optional().describe("File locations"),
6375
+ phase: z9.string().optional().describe("Phase ID")
4869
6376
  }
4870
6377
  }, (args) => runMcpWriteTool(
4871
6378
  pinnedRoot,
@@ -4886,17 +6393,17 @@ function registerAllTools(server, pinnedRoot) {
4886
6393
  server.registerTool("claudestory_issue_update", {
4887
6394
  description: "Update an existing issue",
4888
6395
  inputSchema: {
4889
- id: z8.string().regex(ISSUE_ID_REGEX).describe("Issue ID (e.g. ISS-001)"),
4890
- status: z8.enum(ISSUE_STATUSES).optional().describe("New status: open, inprogress, resolved"),
4891
- title: z8.string().optional().describe("New title"),
4892
- severity: z8.enum(ISSUE_SEVERITIES).optional().describe("New severity"),
4893
- impact: z8.string().optional().describe("New impact description"),
4894
- resolution: z8.string().nullable().optional().describe("Resolution description (null to clear)"),
4895
- components: z8.array(z8.string()).optional().describe("Affected components"),
4896
- relatedTickets: z8.array(z8.string().regex(TICKET_ID_REGEX)).optional().describe("Related ticket IDs"),
4897
- location: z8.array(z8.string()).optional().describe("File locations"),
4898
- order: z8.number().int().optional().describe("New sort order"),
4899
- phase: z8.string().nullable().optional().describe("New phase ID (null to clear)")
6396
+ id: z9.string().regex(ISSUE_ID_REGEX).describe("Issue ID (e.g. ISS-001)"),
6397
+ status: z9.enum(ISSUE_STATUSES).optional().describe("New status: open, inprogress, resolved"),
6398
+ title: z9.string().optional().describe("New title"),
6399
+ severity: z9.enum(ISSUE_SEVERITIES).optional().describe("New severity"),
6400
+ impact: z9.string().optional().describe("New impact description"),
6401
+ resolution: z9.string().nullable().optional().describe("Resolution description (null to clear)"),
6402
+ components: z9.array(z9.string()).optional().describe("Affected components"),
6403
+ relatedTickets: z9.array(z9.string().regex(TICKET_ID_REGEX)).optional().describe("Related ticket IDs"),
6404
+ location: z9.array(z9.string()).optional().describe("File locations"),
6405
+ order: z9.number().int().optional().describe("New sort order"),
6406
+ phase: z9.string().nullable().optional().describe("New phase ID (null to clear)")
4900
6407
  }
4901
6408
  }, (args) => runMcpWriteTool(
4902
6409
  pinnedRoot,
@@ -4921,8 +6428,8 @@ function registerAllTools(server, pinnedRoot) {
4921
6428
  server.registerTool("claudestory_note_list", {
4922
6429
  description: "List notes with optional status/tag filters",
4923
6430
  inputSchema: {
4924
- status: z8.enum(NOTE_STATUSES).optional().describe("Filter by status: active, archived"),
4925
- tag: z8.string().optional().describe("Filter by tag")
6431
+ status: z9.enum(NOTE_STATUSES).optional().describe("Filter by status: active, archived"),
6432
+ tag: z9.string().optional().describe("Filter by tag")
4926
6433
  }
4927
6434
  }, (args) => runMcpReadTool(
4928
6435
  pinnedRoot,
@@ -4931,15 +6438,15 @@ function registerAllTools(server, pinnedRoot) {
4931
6438
  server.registerTool("claudestory_note_get", {
4932
6439
  description: "Get a note by ID",
4933
6440
  inputSchema: {
4934
- id: z8.string().regex(NOTE_ID_REGEX).describe("Note ID (e.g. N-001)")
6441
+ id: z9.string().regex(NOTE_ID_REGEX).describe("Note ID (e.g. N-001)")
4935
6442
  }
4936
6443
  }, (args) => runMcpReadTool(pinnedRoot, (ctx) => handleNoteGet(args.id, ctx)));
4937
6444
  server.registerTool("claudestory_note_create", {
4938
6445
  description: "Create a new note",
4939
6446
  inputSchema: {
4940
- content: z8.string().describe("Note content"),
4941
- title: z8.string().optional().describe("Note title"),
4942
- tags: z8.array(z8.string()).optional().describe("Tags for the note")
6447
+ content: z9.string().describe("Note content"),
6448
+ title: z9.string().optional().describe("Note title"),
6449
+ tags: z9.array(z9.string()).optional().describe("Tags for the note")
4943
6450
  }
4944
6451
  }, (args) => runMcpWriteTool(
4945
6452
  pinnedRoot,
@@ -4956,11 +6463,11 @@ function registerAllTools(server, pinnedRoot) {
4956
6463
  server.registerTool("claudestory_note_update", {
4957
6464
  description: "Update an existing note",
4958
6465
  inputSchema: {
4959
- id: z8.string().regex(NOTE_ID_REGEX).describe("Note ID (e.g. N-001)"),
4960
- content: z8.string().optional().describe("New content"),
4961
- title: z8.string().nullable().optional().describe("New title (null to clear)"),
4962
- tags: z8.array(z8.string()).optional().describe("New tags (replaces existing)"),
4963
- status: z8.enum(NOTE_STATUSES).optional().describe("New status: active, archived")
6466
+ id: z9.string().regex(NOTE_ID_REGEX).describe("Note ID (e.g. N-001)"),
6467
+ content: z9.string().optional().describe("New content"),
6468
+ title: z9.string().nullable().optional().describe("New title (null to clear)"),
6469
+ tags: z9.array(z9.string()).optional().describe("New tags (replaces existing)"),
6470
+ status: z9.enum(NOTE_STATUSES).optional().describe("New status: active, archived")
4964
6471
  }
4965
6472
  }, (args) => runMcpWriteTool(
4966
6473
  pinnedRoot,
@@ -4980,13 +6487,13 @@ function registerAllTools(server, pinnedRoot) {
4980
6487
  server.registerTool("claudestory_phase_create", {
4981
6488
  description: "Create a new phase in the roadmap. Exactly one of after or atStart is required for positioning.",
4982
6489
  inputSchema: {
4983
- id: z8.string().describe("Phase ID \u2014 lowercase alphanumeric with hyphens (e.g. 'my-phase')"),
4984
- name: z8.string().describe("Phase display name"),
4985
- label: z8.string().describe("Phase label (e.g. 'PHASE 1')"),
4986
- description: z8.string().describe("Phase description"),
4987
- summary: z8.string().optional().describe("One-line summary for compact display"),
4988
- after: z8.string().optional().describe("Insert after this phase ID"),
4989
- atStart: z8.boolean().optional().describe("Insert at beginning of roadmap")
6490
+ id: z9.string().describe("Phase ID \u2014 lowercase alphanumeric with hyphens (e.g. 'my-phase')"),
6491
+ name: z9.string().describe("Phase display name"),
6492
+ label: z9.string().describe("Phase label (e.g. 'PHASE 1')"),
6493
+ description: z9.string().describe("Phase description"),
6494
+ summary: z9.string().optional().describe("One-line summary for compact display"),
6495
+ after: z9.string().optional().describe("Insert after this phase ID"),
6496
+ atStart: z9.boolean().optional().describe("Insert at beginning of roadmap")
4990
6497
  }
4991
6498
  }, (args) => runMcpWriteTool(
4992
6499
  pinnedRoot,
@@ -5010,6 +6517,29 @@ function registerAllTools(server, pinnedRoot) {
5010
6517
  pinnedRoot,
5011
6518
  (root, format) => handleSelftest(root, format)
5012
6519
  ));
6520
+ server.registerTool("claudestory_autonomous_guide", {
6521
+ description: "Autonomous session orchestrator. Call at every decision point during autonomous mode.",
6522
+ inputSchema: {
6523
+ sessionId: z9.string().uuid().nullable().describe("Session ID (null for start action)"),
6524
+ action: z9.enum(["start", "report", "resume", "pre_compact", "cancel"]).describe("Action to perform"),
6525
+ report: z9.object({
6526
+ completedAction: z9.string().describe("What was completed"),
6527
+ ticketId: z9.string().optional().describe("Ticket ID (for ticket_picked)"),
6528
+ commitHash: z9.string().optional().describe("Git commit hash (for commit_done)"),
6529
+ handoverContent: z9.string().optional().describe("Handover markdown content"),
6530
+ verdict: z9.string().optional().describe("Review verdict: approve|revise|request_changes|reject"),
6531
+ findings: z9.array(z9.object({
6532
+ id: z9.string(),
6533
+ severity: z9.string(),
6534
+ category: z9.string(),
6535
+ description: z9.string(),
6536
+ disposition: z9.string()
6537
+ })).optional().describe("Review findings"),
6538
+ reviewerSessionId: z9.string().optional().describe("Codex session ID"),
6539
+ notes: z9.string().optional().describe("Free-text notes")
6540
+ }).optional().describe("Report data (required for report action)")
6541
+ }
6542
+ }, (args) => handleAutonomousGuide(pinnedRoot, args));
5013
6543
  }
5014
6544
  var INFRASTRUCTURE_ERROR_CODES;
5015
6545
  var init_tools = __esm({
@@ -5033,6 +6563,7 @@ var init_tools = __esm({
5033
6563
  init_export();
5034
6564
  init_selftest();
5035
6565
  init_handover();
6566
+ init_guide();
5036
6567
  init_phase();
5037
6568
  INFRASTRUCTURE_ERROR_CODES = [
5038
6569
  "io_error",
@@ -5043,11 +6574,11 @@ var init_tools = __esm({
5043
6574
  });
5044
6575
 
5045
6576
  // src/core/init.ts
5046
- import { mkdir as mkdir4, stat as stat2 } from "fs/promises";
5047
- import { join as join8, resolve as resolve7 } from "path";
6577
+ import { mkdir as mkdir4, stat as stat2, readFile as readFile4, writeFile as writeFile2 } from "fs/promises";
6578
+ import { join as join10, resolve as resolve7 } from "path";
5048
6579
  async function initProject(root, options) {
5049
6580
  const absRoot = resolve7(root);
5050
- const wrapDir = join8(absRoot, ".story");
6581
+ const wrapDir = join10(absRoot, ".story");
5051
6582
  let exists = false;
5052
6583
  try {
5053
6584
  const s = await stat2(wrapDir);
@@ -5067,10 +6598,10 @@ async function initProject(root, options) {
5067
6598
  ".story/ already exists. Use --force to overwrite config and roadmap."
5068
6599
  );
5069
6600
  }
5070
- await mkdir4(join8(wrapDir, "tickets"), { recursive: true });
5071
- await mkdir4(join8(wrapDir, "issues"), { recursive: true });
5072
- await mkdir4(join8(wrapDir, "handovers"), { recursive: true });
5073
- await mkdir4(join8(wrapDir, "notes"), { recursive: true });
6601
+ await mkdir4(join10(wrapDir, "tickets"), { recursive: true });
6602
+ await mkdir4(join10(wrapDir, "issues"), { recursive: true });
6603
+ await mkdir4(join10(wrapDir, "handovers"), { recursive: true });
6604
+ await mkdir4(join10(wrapDir, "notes"), { recursive: true });
5074
6605
  const created = [
5075
6606
  ".story/config.json",
5076
6607
  ".story/roadmap.json",
@@ -5109,6 +6640,8 @@ async function initProject(root, options) {
5109
6640
  };
5110
6641
  await writeConfig(config, absRoot);
5111
6642
  await writeRoadmap(roadmap, absRoot);
6643
+ const gitignorePath = join10(wrapDir, ".gitignore");
6644
+ await ensureGitignoreEntries(gitignorePath, STORY_GITIGNORE_ENTRIES);
5112
6645
  const warnings = [];
5113
6646
  if (options.force && exists) {
5114
6647
  try {
@@ -5127,20 +6660,36 @@ async function initProject(root, options) {
5127
6660
  warnings
5128
6661
  };
5129
6662
  }
6663
+ async function ensureGitignoreEntries(gitignorePath, entries) {
6664
+ let existing = "";
6665
+ try {
6666
+ existing = await readFile4(gitignorePath, "utf-8");
6667
+ } catch {
6668
+ }
6669
+ const lines = existing.split("\n").map((l) => l.trim());
6670
+ const missing = entries.filter((e) => !lines.includes(e));
6671
+ if (missing.length === 0) return;
6672
+ let content = existing;
6673
+ if (content.length > 0 && !content.endsWith("\n")) content += "\n";
6674
+ content += missing.join("\n") + "\n";
6675
+ await writeFile2(gitignorePath, content, "utf-8");
6676
+ }
6677
+ var STORY_GITIGNORE_ENTRIES;
5130
6678
  var init_init = __esm({
5131
6679
  "src/core/init.ts"() {
5132
6680
  "use strict";
5133
6681
  init_esm_shims();
5134
6682
  init_project_loader();
5135
6683
  init_errors();
6684
+ STORY_GITIGNORE_ENTRIES = ["snapshots/", "status.json", "sessions/"];
5136
6685
  }
5137
6686
  });
5138
6687
 
5139
6688
  // src/mcp/index.ts
5140
6689
  var mcp_exports = {};
5141
- import { realpathSync, existsSync as existsSync6 } from "fs";
5142
- import { resolve as resolve8, join as join9, isAbsolute } from "path";
5143
- import { z as z9 } from "zod";
6690
+ import { realpathSync as realpathSync2, existsSync as existsSync8 } from "fs";
6691
+ import { resolve as resolve8, join as join11, isAbsolute } from "path";
6692
+ import { z as z10 } from "zod";
5144
6693
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5145
6694
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5146
6695
  function tryDiscoverRoot() {
@@ -5153,8 +6702,8 @@ function tryDiscoverRoot() {
5153
6702
  }
5154
6703
  const resolved = resolve8(envRoot);
5155
6704
  try {
5156
- const canonical = realpathSync(resolved);
5157
- if (existsSync6(join9(canonical, CONFIG_PATH2))) {
6705
+ const canonical = realpathSync2(resolved);
6706
+ if (existsSync8(join11(canonical, CONFIG_PATH2))) {
5158
6707
  return canonical;
5159
6708
  }
5160
6709
  process.stderr.write(`Warning: No .story/config.json at ${canonical}
@@ -5167,7 +6716,7 @@ function tryDiscoverRoot() {
5167
6716
  }
5168
6717
  try {
5169
6718
  const root = discoverProjectRoot();
5170
- return root ? realpathSync(root) : null;
6719
+ return root ? realpathSync2(root) : null;
5171
6720
  } catch {
5172
6721
  return null;
5173
6722
  }
@@ -5182,14 +6731,14 @@ function registerDegradedTools(server) {
5182
6731
  const degradedInit = server.registerTool("claudestory_init", {
5183
6732
  description: "Initialize a new .story/ project in the current directory",
5184
6733
  inputSchema: {
5185
- name: z9.string().describe("Project name"),
5186
- type: z9.string().optional().describe("Project type (e.g. npm, macapp, cargo, generic)"),
5187
- language: z9.string().optional().describe("Primary language (e.g. typescript, swift, rust)")
6734
+ name: z10.string().describe("Project name"),
6735
+ type: z10.string().optional().describe("Project type (e.g. npm, macapp, cargo, generic)"),
6736
+ language: z10.string().optional().describe("Primary language (e.g. typescript, swift, rust)")
5188
6737
  }
5189
6738
  }, async (args) => {
5190
6739
  let result;
5191
6740
  try {
5192
- const projectRoot = realpathSync(process.cwd());
6741
+ const projectRoot = realpathSync2(process.cwd());
5193
6742
  result = await initProject(projectRoot, {
5194
6743
  name: args.name,
5195
6744
  type: args.type,
@@ -5257,7 +6806,7 @@ var init_mcp = __esm({
5257
6806
  init_init();
5258
6807
  ENV_VAR2 = "CLAUDESTORY_PROJECT_ROOT";
5259
6808
  CONFIG_PATH2 = ".story/config.json";
5260
- version = "0.1.10";
6809
+ version = "0.1.12";
5261
6810
  main().catch((err) => {
5262
6811
  process.stderr.write(`Fatal: ${err instanceof Error ? err.message : String(err)}
5263
6812
  `);
@@ -5293,7 +6842,7 @@ __export(run_exports, {
5293
6842
  runReadCommand: () => runReadCommand,
5294
6843
  writeOutput: () => writeOutput
5295
6844
  });
5296
- import { join as join10 } from "path";
6845
+ import { join as join12 } from "path";
5297
6846
  function writeOutput(text) {
5298
6847
  try {
5299
6848
  process.stdout.write(text + "\n");
@@ -5321,7 +6870,7 @@ async function runReadCommand(format, handler) {
5321
6870
  return;
5322
6871
  }
5323
6872
  const { state, warnings } = await loadProject(root);
5324
- const handoversDir = join10(root, ".story", "handovers");
6873
+ const handoversDir = join12(root, ".story", "handovers");
5325
6874
  const result = await handler({ state, warnings, root, handoversDir, format });
5326
6875
  writeOutput(result.output);
5327
6876
  let exitCode = result.exitCode ?? ExitCode.OK;
@@ -5356,7 +6905,7 @@ async function runDeleteCommand(format, force, handler) {
5356
6905
  return;
5357
6906
  }
5358
6907
  const { state, warnings } = await loadProject(root);
5359
- const handoversDir = join10(root, ".story", "handovers");
6908
+ const handoversDir = join12(root, ".story", "handovers");
5360
6909
  if (!force && hasIntegrityWarnings(warnings)) {
5361
6910
  writeOutput(
5362
6911
  formatError(
@@ -5761,11 +7310,12 @@ var setup_skill_exports = {};
5761
7310
  __export(setup_skill_exports, {
5762
7311
  handleSetupSkill: () => handleSetupSkill,
5763
7312
  registerPreCompactHook: () => registerPreCompactHook,
7313
+ registerStopHook: () => registerStopHook,
5764
7314
  resolveSkillSourceDir: () => resolveSkillSourceDir
5765
7315
  });
5766
- import { mkdir as mkdir5, writeFile as writeFile2, readFile as readFile4, rm, rename as rename2, unlink as unlink3 } from "fs/promises";
5767
- import { existsSync as existsSync7 } from "fs";
5768
- import { join as join11, dirname as dirname3 } from "path";
7316
+ import { mkdir as mkdir5, writeFile as writeFile3, readFile as readFile5, rm, rename as rename2, unlink as unlink3 } from "fs/promises";
7317
+ import { existsSync as existsSync9 } from "fs";
7318
+ import { join as join13, dirname as dirname3 } from "path";
5769
7319
  import { homedir } from "os";
5770
7320
  import { execFileSync } from "child_process";
5771
7321
  import { fileURLToPath as fileURLToPath2 } from "url";
@@ -5774,27 +7324,27 @@ function log(msg) {
5774
7324
  }
5775
7325
  function resolveSkillSourceDir() {
5776
7326
  const thisDir = dirname3(fileURLToPath2(import.meta.url));
5777
- const bundledPath = join11(thisDir, "..", "src", "skill");
5778
- if (existsSync7(join11(bundledPath, "SKILL.md"))) return bundledPath;
5779
- const sourcePath = join11(thisDir, "..", "..", "skill");
5780
- if (existsSync7(join11(sourcePath, "SKILL.md"))) return sourcePath;
7327
+ const bundledPath = join13(thisDir, "..", "src", "skill");
7328
+ if (existsSync9(join13(bundledPath, "SKILL.md"))) return bundledPath;
7329
+ const sourcePath = join13(thisDir, "..", "..", "skill");
7330
+ if (existsSync9(join13(sourcePath, "SKILL.md"))) return sourcePath;
5781
7331
  throw new Error(
5782
7332
  `Cannot find bundled skill files. Checked:
5783
7333
  ${bundledPath}
5784
7334
  ${sourcePath}`
5785
7335
  );
5786
7336
  }
5787
- function isOurHook(entry) {
7337
+ function isHookWithCommand(entry, command) {
5788
7338
  if (typeof entry !== "object" || entry === null) return false;
5789
7339
  const e = entry;
5790
- return e.type === "command" && typeof e.command === "string" && e.command.trim() === HOOK_COMMAND;
7340
+ return e.type === "command" && typeof e.command === "string" && e.command.trim() === command;
5791
7341
  }
5792
- async function registerPreCompactHook(settingsPath) {
5793
- const path2 = settingsPath ?? join11(homedir(), ".claude", "settings.json");
7342
+ async function registerHook(hookType, hookEntry, settingsPath) {
7343
+ const path2 = settingsPath ?? join13(homedir(), ".claude", "settings.json");
5794
7344
  let raw = "{}";
5795
- if (existsSync7(path2)) {
7345
+ if (existsSync9(path2)) {
5796
7346
  try {
5797
- raw = await readFile4(path2, "utf-8");
7347
+ raw = await readFile5(path2, "utf-8");
5798
7348
  } catch {
5799
7349
  process.stderr.write(`Could not read ${path2} \u2014 skipping hook registration.
5800
7350
  `);
@@ -5825,43 +7375,45 @@ async function registerPreCompactHook(settingsPath) {
5825
7375
  settings.hooks = {};
5826
7376
  }
5827
7377
  const hooks = settings.hooks;
5828
- if ("PreCompact" in hooks) {
5829
- if (!Array.isArray(hooks.PreCompact)) {
5830
- process.stderr.write(`${path2} has unexpected hooks.PreCompact format \u2014 skipping hook registration.
7378
+ if (hookType in hooks) {
7379
+ if (!Array.isArray(hooks[hookType])) {
7380
+ process.stderr.write(`${path2} has unexpected hooks.${hookType} format \u2014 skipping hook registration.
5831
7381
  `);
5832
7382
  return "skipped";
5833
7383
  }
5834
7384
  } else {
5835
- hooks.PreCompact = [];
7385
+ hooks[hookType] = [];
5836
7386
  }
5837
- const preCompact = hooks.PreCompact;
5838
- for (const group of preCompact) {
5839
- if (typeof group !== "object" || group === null) continue;
5840
- const g = group;
5841
- if (!Array.isArray(g.hooks)) continue;
5842
- for (const entry of g.hooks) {
5843
- if (isOurHook(entry)) return "exists";
7387
+ const hookArray = hooks[hookType];
7388
+ const hookCommand = hookEntry.command;
7389
+ if (hookCommand) {
7390
+ for (const group of hookArray) {
7391
+ if (typeof group !== "object" || group === null) continue;
7392
+ const g = group;
7393
+ if (!Array.isArray(g.hooks)) continue;
7394
+ for (const entry of g.hooks) {
7395
+ if (isHookWithCommand(entry, hookCommand)) return "exists";
7396
+ }
5844
7397
  }
5845
7398
  }
5846
- const ourEntry = { type: "command", command: HOOK_COMMAND };
5847
7399
  let appended = false;
5848
- for (const group of preCompact) {
7400
+ for (const group of hookArray) {
5849
7401
  if (typeof group !== "object" || group === null) continue;
5850
7402
  const g = group;
5851
7403
  if (g.matcher === "" && Array.isArray(g.hooks)) {
5852
- g.hooks.push(ourEntry);
7404
+ g.hooks.push(hookEntry);
5853
7405
  appended = true;
5854
7406
  break;
5855
7407
  }
5856
7408
  }
5857
7409
  if (!appended) {
5858
- preCompact.push({ matcher: "", hooks: [ourEntry] });
7410
+ hookArray.push({ matcher: "", hooks: [hookEntry] });
5859
7411
  }
5860
7412
  const tmpPath = `${path2}.${process.pid}.tmp`;
5861
7413
  try {
5862
7414
  const dir = dirname3(path2);
5863
7415
  await mkdir5(dir, { recursive: true });
5864
- await writeFile2(tmpPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
7416
+ await writeFile3(tmpPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
5865
7417
  await rename2(tmpPath, path2);
5866
7418
  } catch (err) {
5867
7419
  try {
@@ -5875,9 +7427,15 @@ async function registerPreCompactHook(settingsPath) {
5875
7427
  }
5876
7428
  return "registered";
5877
7429
  }
7430
+ async function registerPreCompactHook(settingsPath) {
7431
+ return registerHook("PreCompact", { type: "command", command: PRECOMPACT_HOOK_COMMAND }, settingsPath);
7432
+ }
7433
+ async function registerStopHook(settingsPath) {
7434
+ return registerHook("Stop", { type: "command", command: STOP_HOOK_COMMAND, async: true }, settingsPath);
7435
+ }
5878
7436
  async function handleSetupSkill(options = {}) {
5879
7437
  const { skipHooks = false } = options;
5880
- const skillDir = join11(homedir(), ".claude", "skills", "story");
7438
+ const skillDir = join13(homedir(), ".claude", "skills", "story");
5881
7439
  await mkdir5(skillDir, { recursive: true });
5882
7440
  let srcSkillDir;
5883
7441
  try {
@@ -5890,19 +7448,19 @@ async function handleSetupSkill(options = {}) {
5890
7448
  process.exitCode = 1;
5891
7449
  return;
5892
7450
  }
5893
- const oldPrimeDir = join11(homedir(), ".claude", "skills", "prime");
5894
- if (existsSync7(oldPrimeDir)) {
7451
+ const oldPrimeDir = join13(homedir(), ".claude", "skills", "prime");
7452
+ if (existsSync9(oldPrimeDir)) {
5895
7453
  await rm(oldPrimeDir, { recursive: true, force: true });
5896
7454
  log("Removed old /prime skill (migrated to /story)");
5897
7455
  }
5898
- const existed = existsSync7(join11(skillDir, "SKILL.md"));
5899
- const skillContent = await readFile4(join11(srcSkillDir, "SKILL.md"), "utf-8");
5900
- await writeFile2(join11(skillDir, "SKILL.md"), skillContent, "utf-8");
7456
+ const existed = existsSync9(join13(skillDir, "SKILL.md"));
7457
+ const skillContent = await readFile5(join13(srcSkillDir, "SKILL.md"), "utf-8");
7458
+ await writeFile3(join13(skillDir, "SKILL.md"), skillContent, "utf-8");
5901
7459
  let referenceWritten = false;
5902
- const refSrcPath = join11(srcSkillDir, "reference.md");
5903
- if (existsSync7(refSrcPath)) {
5904
- const refContent = await readFile4(refSrcPath, "utf-8");
5905
- await writeFile2(join11(skillDir, "reference.md"), refContent, "utf-8");
7460
+ const refSrcPath = join13(srcSkillDir, "reference.md");
7461
+ if (existsSync9(refSrcPath)) {
7462
+ const refContent = await readFile5(refSrcPath, "utf-8");
7463
+ await writeFile3(join13(skillDir, "reference.md"), refContent, "utf-8");
5906
7464
  referenceWritten = true;
5907
7465
  }
5908
7466
  log(`${existed ? "Updated" : "Installed"} /story skill at ${skillDir}/`);
@@ -5966,6 +7524,21 @@ async function handleSetupSkill(options = {}) {
5966
7524
  } else if (skipHooks) {
5967
7525
  log(" Hook registration skipped (--skip-hooks)");
5968
7526
  }
7527
+ if (cliInPath && !skipHooks) {
7528
+ const stopResult = await registerStopHook();
7529
+ switch (stopResult) {
7530
+ case "registered":
7531
+ log(" Stop hook registered \u2014 status.json updated after every Claude response");
7532
+ break;
7533
+ case "exists":
7534
+ log(" Stop hook already configured");
7535
+ break;
7536
+ case "skipped":
7537
+ break;
7538
+ }
7539
+ } else if (!cliInPath) {
7540
+ } else if (skipHooks) {
7541
+ }
5969
7542
  log("");
5970
7543
  if (mcpRegistered) {
5971
7544
  log("Done! Restart Claude Code, then type /story in any project.");
@@ -5973,12 +7546,142 @@ async function handleSetupSkill(options = {}) {
5973
7546
  log("Skill installed. After registering MCP, restart Claude Code and type /story.");
5974
7547
  }
5975
7548
  }
5976
- var HOOK_COMMAND;
7549
+ var PRECOMPACT_HOOK_COMMAND, STOP_HOOK_COMMAND;
5977
7550
  var init_setup_skill = __esm({
5978
7551
  "src/cli/commands/setup-skill.ts"() {
5979
7552
  "use strict";
5980
7553
  init_esm_shims();
5981
- HOOK_COMMAND = "claudestory snapshot --quiet";
7554
+ PRECOMPACT_HOOK_COMMAND = "claudestory snapshot --quiet";
7555
+ STOP_HOOK_COMMAND = "claudestory hook-status";
7556
+ }
7557
+ });
7558
+
7559
+ // src/cli/commands/hook-status.ts
7560
+ var hook_status_exports = {};
7561
+ __export(hook_status_exports, {
7562
+ handleHookStatus: () => handleHookStatus
7563
+ });
7564
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, renameSync as renameSync2, unlinkSync as unlinkSync2 } from "fs";
7565
+ import { join as join14 } from "path";
7566
+ async function readStdinSilent() {
7567
+ try {
7568
+ const chunks = [];
7569
+ for await (const chunk of process.stdin) {
7570
+ chunks.push(chunk);
7571
+ }
7572
+ return Buffer.concat(
7573
+ chunks.map((c) => Buffer.isBuffer(c) ? c : Buffer.from(c))
7574
+ ).toString("utf-8");
7575
+ } catch {
7576
+ return null;
7577
+ }
7578
+ }
7579
+ function atomicWriteSync(targetPath, content) {
7580
+ const tmp = `${targetPath}.${process.pid}.tmp`;
7581
+ try {
7582
+ writeFileSync3(tmp, content, "utf-8");
7583
+ renameSync2(tmp, targetPath);
7584
+ return true;
7585
+ } catch {
7586
+ try {
7587
+ unlinkSync2(tmp);
7588
+ } catch {
7589
+ }
7590
+ return false;
7591
+ }
7592
+ }
7593
+ function inactivePayload() {
7594
+ return { schemaVersion: CURRENT_STATUS_SCHEMA_VERSION, sessionActive: false, source: "hook" };
7595
+ }
7596
+ function activePayload(session) {
7597
+ return {
7598
+ schemaVersion: CURRENT_STATUS_SCHEMA_VERSION,
7599
+ sessionActive: true,
7600
+ sessionId: session.sessionId,
7601
+ state: session.state,
7602
+ ticket: session.ticket?.id ?? null,
7603
+ ticketTitle: session.ticket?.title ?? null,
7604
+ risk: session.ticket?.risk ?? null,
7605
+ claudeStatus: deriveClaudeStatus(session.state, session.waitingForRetry),
7606
+ observedAt: (/* @__PURE__ */ new Date()).toISOString(),
7607
+ lastGuideCall: session.lastGuideCall ?? null,
7608
+ completedThisSession: session.completedTickets?.map((t) => t.id) ?? [],
7609
+ contextPressure: session.contextPressure?.level ?? "unknown",
7610
+ branch: session.git?.branch ?? null,
7611
+ source: "hook"
7612
+ };
7613
+ }
7614
+ function ensureGitignore(root) {
7615
+ const gitignorePath = join14(root, ".story", ".gitignore");
7616
+ let existing = "";
7617
+ try {
7618
+ existing = readFileSync3(gitignorePath, "utf-8");
7619
+ } catch {
7620
+ }
7621
+ const lines = existing.split("\n").map((l) => l.trim());
7622
+ const missing = STORY_GITIGNORE_ENTRIES.filter((e) => !lines.includes(e));
7623
+ if (missing.length === 0) return;
7624
+ let content = existing;
7625
+ if (content.length > 0 && !content.endsWith("\n")) content += "\n";
7626
+ content += missing.join("\n") + "\n";
7627
+ try {
7628
+ writeFileSync3(gitignorePath, content, "utf-8");
7629
+ } catch {
7630
+ }
7631
+ }
7632
+ function writeStatus(root, payload) {
7633
+ ensureGitignore(root);
7634
+ const statusPath = join14(root, ".story", "status.json");
7635
+ const content = JSON.stringify(payload, null, 2) + "\n";
7636
+ atomicWriteSync(statusPath, content);
7637
+ }
7638
+ async function handleHookStatus() {
7639
+ try {
7640
+ if (process.stdin.isTTY) {
7641
+ const root2 = discoverProjectRoot();
7642
+ if (root2) {
7643
+ const session2 = findActiveSessionMinimal(root2);
7644
+ const payload2 = session2 ? activePayload(session2) : inactivePayload();
7645
+ writeStatus(root2, payload2);
7646
+ }
7647
+ process.exit(0);
7648
+ }
7649
+ const raw = await readStdinSilent();
7650
+ if (raw === null || raw === "") {
7651
+ process.exit(0);
7652
+ }
7653
+ let input;
7654
+ try {
7655
+ input = JSON.parse(raw);
7656
+ } catch {
7657
+ process.exit(0);
7658
+ }
7659
+ if (input.stop_hook_active === true) {
7660
+ process.exit(0);
7661
+ }
7662
+ const cwd = input.cwd;
7663
+ if (typeof cwd !== "string" || !cwd) {
7664
+ process.exit(0);
7665
+ }
7666
+ const root = discoverProjectRoot(cwd);
7667
+ if (!root) {
7668
+ process.exit(0);
7669
+ }
7670
+ const session = findActiveSessionMinimal(root);
7671
+ const payload = session ? activePayload(session) : inactivePayload();
7672
+ writeStatus(root, payload);
7673
+ } catch {
7674
+ }
7675
+ process.exit(0);
7676
+ }
7677
+ var init_hook_status = __esm({
7678
+ "src/cli/commands/hook-status.ts"() {
7679
+ "use strict";
7680
+ init_esm_shims();
7681
+ init_project_root_discovery();
7682
+ init_init();
7683
+ init_session_types();
7684
+ init_session();
5982
7685
  }
5983
7686
  });
5984
7687
 
@@ -5988,6 +7691,7 @@ __export(register_exports, {
5988
7691
  registerBlockerCommand: () => registerBlockerCommand,
5989
7692
  registerExportCommand: () => registerExportCommand,
5990
7693
  registerHandoverCommand: () => registerHandoverCommand,
7694
+ registerHookStatusCommand: () => registerHookStatusCommand,
5991
7695
  registerInitCommand: () => registerInitCommand,
5992
7696
  registerIssueCommand: () => registerIssueCommand,
5993
7697
  registerNoteCommand: () => registerNoteCommand,
@@ -7530,7 +9234,7 @@ function registerSetupSkillCommand(yargs) {
7530
9234
  (y) => y.option("skip-hooks", {
7531
9235
  type: "boolean",
7532
9236
  default: false,
7533
- description: "Skip PreCompact hook registration"
9237
+ description: "Skip hook registration (PreCompact + Stop)"
7534
9238
  }),
7535
9239
  async (argv) => {
7536
9240
  const { handleSetupSkill: handleSetupSkill2 } = await Promise.resolve().then(() => (init_setup_skill(), setup_skill_exports));
@@ -7538,6 +9242,18 @@ function registerSetupSkillCommand(yargs) {
7538
9242
  }
7539
9243
  );
7540
9244
  }
9245
+ function registerHookStatusCommand(yargs) {
9246
+ return yargs.command(
9247
+ "hook-status",
9248
+ false,
9249
+ // hidden — machine-facing, not shown in --help
9250
+ (y) => y,
9251
+ async () => {
9252
+ const { handleHookStatus: handleHookStatus2 } = await Promise.resolve().then(() => (init_hook_status(), hook_status_exports));
9253
+ await handleHookStatus2();
9254
+ }
9255
+ );
9256
+ }
7541
9257
  var init_register = __esm({
7542
9258
  "src/cli/register.ts"() {
7543
9259
  "use strict";
@@ -7591,9 +9307,10 @@ async function runCli() {
7591
9307
  registerRecommendCommand: registerRecommendCommand2,
7592
9308
  registerReferenceCommand: registerReferenceCommand2,
7593
9309
  registerSelftestCommand: registerSelftestCommand2,
7594
- registerSetupSkillCommand: registerSetupSkillCommand2
9310
+ registerSetupSkillCommand: registerSetupSkillCommand2,
9311
+ registerHookStatusCommand: registerHookStatusCommand2
7595
9312
  } = await Promise.resolve().then(() => (init_register(), register_exports));
7596
- const version2 = "0.1.10";
9313
+ const version2 = "0.1.12";
7597
9314
  class HandledError extends Error {
7598
9315
  constructor() {
7599
9316
  super("HANDLED_ERROR");
@@ -7631,6 +9348,7 @@ async function runCli() {
7631
9348
  cli = registerReferenceCommand2(cli);
7632
9349
  cli = registerSelftestCommand2(cli);
7633
9350
  cli = registerSetupSkillCommand2(cli);
9351
+ cli = registerHookStatusCommand2(cli);
7634
9352
  function handleUnexpectedError(err) {
7635
9353
  if (err instanceof HandledError) return;
7636
9354
  const message = err instanceof Error ? err.message : String(err);