@anthropologies/claudestory 0.1.22 → 0.1.24

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
@@ -5798,6 +5798,13 @@ async function gitStashPop(cwd, commitHash) {
5798
5798
  }
5799
5799
  return git(cwd, ["stash", "pop", match.ref], () => void 0);
5800
5800
  }
5801
+ async function gitDiffTreeNames(cwd, commitHash) {
5802
+ return git(
5803
+ cwd,
5804
+ ["diff-tree", "--name-only", "--no-commit-id", "-r", commitHash],
5805
+ (out) => out.split("\n").filter((l) => l.trim().length > 0)
5806
+ );
5807
+ }
5801
5808
  async function gitLogRange(cwd, from, to, limit = 20) {
5802
5809
  if (from && !SAFE_REF.test(from)) {
5803
5810
  return { ok: false, reason: "invalid_ref", message: `Invalid git ref: ${from}` };
@@ -6372,8 +6379,10 @@ var init_plan_review = __esm({
6372
6379
  const isRevise = verdict === "revise" || verdict === "request_changes";
6373
6380
  const isReject = verdict === "reject";
6374
6381
  let nextAction;
6375
- if (isReject || isRevise) {
6382
+ if (isReject) {
6376
6383
  nextAction = "PLAN";
6384
+ } else if (isRevise) {
6385
+ nextAction = "PLAN_REVIEW";
6377
6386
  } else if (verdict === "approve" || !hasCriticalOrMajor && roundNum >= minRounds) {
6378
6387
  nextAction = "IMPLEMENT";
6379
6388
  } else if (roundNum >= 5) {
@@ -6395,7 +6404,21 @@ var init_plan_review = __esm({
6395
6404
  return {
6396
6405
  action: "back",
6397
6406
  target: "PLAN",
6398
- reason: isRevise ? "revise" : "reject"
6407
+ reason: "reject"
6408
+ };
6409
+ }
6410
+ if (isRevise) {
6411
+ const findingSummary = findings.length > 0 ? findings.slice(0, 5).map((f) => `- [${f.severity}] ${f.description}`).join("\n") : "Address the reviewer's concerns.";
6412
+ return {
6413
+ action: "retry",
6414
+ instruction: [
6415
+ `# Plan Review \u2014 Round ${roundNum} requested changes`,
6416
+ "",
6417
+ 'Update the plan to address these findings, then call me with completedAction: "plan_review_round" and the new review verdict.',
6418
+ "",
6419
+ findingSummary
6420
+ ].join("\n"),
6421
+ reminders: ["Update the plan file, then re-review. Do NOT rewrite from scratch."]
6399
6422
  };
6400
6423
  }
6401
6424
  if (nextAction === "IMPLEMENT") {
@@ -6501,7 +6524,7 @@ function exhaustionAction(ctx) {
6501
6524
  return {
6502
6525
  action: "back",
6503
6526
  target: "PLAN",
6504
- reason: "no_failing_tests"
6527
+ reason: "TDD exhausted: could not verify new failing tests after 3 attempts. Revise the plan to make the test expectations clearer or simpler."
6505
6528
  };
6506
6529
  }
6507
6530
  var MAX_WRITE_TESTS_RETRIES, EXIT_CODE_REGEX, FAIL_COUNT_REGEX, WriteTestsStage;
@@ -6664,6 +6687,13 @@ var init_test = __esm({
6664
6687
  const retryCount = ctx.state.testRetryCount ?? 0;
6665
6688
  const exitCodeMatch = notes.match(/exit\s*(?:code[:\s]*)?\s*(\d+)/i);
6666
6689
  if (!exitCodeMatch) {
6690
+ const nextRetry = retryCount + 1;
6691
+ if (nextRetry >= MAX_TEST_RETRIES) {
6692
+ ctx.writeState({ testRetryCount: 0 });
6693
+ ctx.appendEvent("tests_parse_exhausted", { retryCount: nextRetry });
6694
+ return { action: "advance" };
6695
+ }
6696
+ ctx.writeState({ testRetryCount: nextRetry });
6667
6697
  return { action: "retry", instruction: 'Could not parse exit code from notes. Include "exit code: 0" (or non-zero) in your notes.' };
6668
6698
  }
6669
6699
  const exitCode = parseInt(exitCodeMatch[1], 10);
@@ -7092,6 +7122,22 @@ var init_finalize = __esm({
7092
7122
  async handleStage(ctx, report) {
7093
7123
  const stagedResult = await gitDiffCachedNames(ctx.root);
7094
7124
  if (!stagedResult.ok || stagedResult.data.length === 0) {
7125
+ const headResult = await gitHead(ctx.root);
7126
+ const previousHead = ctx.state.git.expectedHead ?? ctx.state.git.initHead;
7127
+ if (headResult.ok && previousHead && headResult.data.hash !== previousHead) {
7128
+ const ticketId2 = ctx.state.ticket?.id;
7129
+ if (ticketId2) {
7130
+ const treeResult = await gitDiffTreeNames(ctx.root, headResult.data.hash);
7131
+ const ticketPath = `.story/tickets/${ticketId2}.json`;
7132
+ if (treeResult.ok && !treeResult.data.includes(ticketPath)) {
7133
+ return {
7134
+ action: "retry",
7135
+ instruction: `Commit detected (${headResult.data.hash.slice(0, 7)}) but ticket file ${ticketPath} is not in the commit. Amend the commit to include it: \`git add ${ticketPath} && git commit --amend --no-edit\`, then report completedAction: "commit_done" with the new hash.`
7136
+ };
7137
+ }
7138
+ }
7139
+ return this.handleCommit(ctx, { ...report, commitHash: headResult.data.hash });
7140
+ }
7095
7141
  return { action: "retry", instruction: 'No files are staged. Stage your changes and call me again with completedAction: "files_staged".' };
7096
7142
  }
7097
7143
  const baselineUntracked = ctx.state.git.baseline?.untrackedPaths ?? [];
@@ -7109,6 +7155,16 @@ var init_finalize = __esm({
7109
7155
  }
7110
7156
  }
7111
7157
  }
7158
+ const ticketId = ctx.state.ticket?.id;
7159
+ if (ticketId) {
7160
+ const ticketPath = `.story/tickets/${ticketId}.json`;
7161
+ if (!stagedResult.data.includes(ticketPath)) {
7162
+ return {
7163
+ action: "retry",
7164
+ instruction: `Ticket file ${ticketPath} is not staged. Run \`git add ${ticketPath}\` and call me again with completedAction: "files_staged".`
7165
+ };
7166
+ }
7167
+ }
7112
7168
  ctx.writeState({
7113
7169
  finalizeCheckpoint: overlapOverridden ? "staged_override" : "staged"
7114
7170
  });
@@ -7169,27 +7225,29 @@ var init_finalize = __esm({
7169
7225
  }
7170
7226
  const headResult = await gitHead(ctx.root);
7171
7227
  const previousHead = ctx.state.git.expectedHead ?? ctx.state.git.initHead;
7172
- if (!headResult.ok || headResult.data.hash !== commitHash) {
7228
+ const fullHead = headResult.ok ? headResult.data.hash : null;
7229
+ if (!fullHead || !fullHead.startsWith(commitHash) && commitHash !== fullHead) {
7173
7230
  return {
7174
7231
  action: "retry",
7175
- instruction: `Commit hash mismatch: reported ${commitHash} but HEAD is ${headResult.ok ? headResult.data.hash : "unknown"}. Verify the commit succeeded and report the correct hash.`
7232
+ instruction: `Commit hash mismatch: reported ${commitHash} but HEAD is ${fullHead ?? "unknown"}. Verify the commit succeeded and report the correct hash.`
7176
7233
  };
7177
7234
  }
7178
- if (previousHead && commitHash === previousHead) {
7179
- return { action: "retry", instruction: `No new commit detected: HEAD (${commitHash}) has not changed. Create a commit first, then report the new hash.` };
7235
+ const normalizedHash = fullHead;
7236
+ if (previousHead && normalizedHash === previousHead) {
7237
+ return { action: "retry", instruction: `No new commit detected: HEAD (${normalizedHash}) has not changed. Create a commit first, then report the new hash.` };
7180
7238
  }
7181
- const completedTicket = ctx.state.ticket ? { id: ctx.state.ticket.id, title: ctx.state.ticket.title, commitHash, risk: ctx.state.ticket.risk } : void 0;
7239
+ const completedTicket = ctx.state.ticket ? { id: ctx.state.ticket.id, title: ctx.state.ticket.title, commitHash: normalizedHash, risk: ctx.state.ticket.risk, realizedRisk: ctx.state.ticket.realizedRisk } : void 0;
7182
7240
  ctx.writeState({
7183
7241
  finalizeCheckpoint: "committed",
7184
7242
  completedTickets: completedTicket ? [...ctx.state.completedTickets, completedTicket] : ctx.state.completedTickets,
7185
7243
  ticket: void 0,
7186
7244
  git: {
7187
7245
  ...ctx.state.git,
7188
- mergeBase: commitHash,
7189
- expectedHead: commitHash
7246
+ mergeBase: normalizedHash,
7247
+ expectedHead: normalizedHash
7190
7248
  }
7191
7249
  });
7192
- ctx.appendEvent("commit", { commitHash, ticketId: completedTicket?.id });
7250
+ ctx.appendEvent("commit", { commitHash: normalizedHash, ticketId: completedTicket?.id });
7193
7251
  return { action: "advance" };
7194
7252
  }
7195
7253
  };
@@ -7323,7 +7381,7 @@ var init_complete = __esm({
7323
7381
  "You are in autonomous mode \u2014 continue working until all tickets are done or the session limit is reached."
7324
7382
  ],
7325
7383
  transitionedFrom: "COMPLETE",
7326
- contextAdvice: advice
7384
+ contextAdvice: "ok"
7327
7385
  }
7328
7386
  };
7329
7387
  }
@@ -8097,10 +8155,14 @@ ${ticket.description}` : "",
8097
8155
  data: { recipe, branch: written.git.branch, head: written.git.initHead, mode: "auto" }
8098
8156
  });
8099
8157
  const topCandidate = nextResult.kind === "found" ? nextResult.candidates[0] : null;
8158
+ const maxTickets = updated.config.maxTicketsPerSession;
8159
+ const interval = updated.config.handoverInterval ?? 5;
8160
+ const sessionDesc = maxTickets > 0 ? `Work continuously until all tickets are done or you reach ${maxTickets} tickets.` : "Work continuously until all tickets are done.";
8161
+ const checkpointDesc = interval > 0 ? ` A checkpoint handover will be saved every ${interval} tickets.` : "";
8100
8162
  const instruction = [
8101
8163
  "# Autonomous Session Started",
8102
8164
  "",
8103
- "You are now in autonomous mode. Work continuously until all tickets are done or the session limit is reached.",
8165
+ `You are now in autonomous mode. ${sessionDesc}${checkpointDesc}`,
8104
8166
  "Do NOT stop to summarize. Do NOT ask the user. Pick a ticket and start working immediately.",
8105
8167
  "",
8106
8168
  "## Ticket Candidates",
@@ -8530,11 +8592,15 @@ async function handleCancel(root, args) {
8530
8592
  }
8531
8593
  const info = findSessionById(root, args.sessionId);
8532
8594
  if (!info) return guideError(new Error(`Session ${args.sessionId} not found`));
8533
- if (info.state.recipe === "coding") {
8595
+ if (info.state.state === "SESSION_END" || info.state.status === "completed") {
8596
+ return guideError(new Error("Session already ended."));
8597
+ }
8598
+ const CANCELLABLE_STATES = /* @__PURE__ */ new Set(["PICK_TICKET", "COMPLETE", "HANDOVER"]);
8599
+ if (info.state.recipe === "coding" && !CANCELLABLE_STATES.has(info.state.state)) {
8534
8600
  const sessionMode = info.state.mode ?? "auto";
8535
8601
  const modeGuidance = sessionMode === "plan" ? "Plan mode sessions end after plan review approval \u2014 continue to that step." : sessionMode === "review" ? "Review mode sessions end after code review approval \u2014 continue to that step." : sessionMode === "guided" ? "Guided mode sessions end after ticket completion \u2014 continue to FINALIZE." : "Complete the current ticket and write a handover to end the session.";
8536
8602
  return guideError(new Error(
8537
- `Cannot cancel a coding session. ${modeGuidance}`
8603
+ `Cannot cancel a coding session from ${info.state.state}. ${modeGuidance}`
8538
8604
  ));
8539
8605
  }
8540
8606
  await recoverPendingMutation(info.dir, info.state, root);
@@ -10024,7 +10090,7 @@ var init_mcp = __esm({
10024
10090
  init_init();
10025
10091
  ENV_VAR2 = "CLAUDESTORY_PROJECT_ROOT";
10026
10092
  CONFIG_PATH2 = ".story/config.json";
10027
- version = "0.1.22";
10093
+ version = "0.1.24";
10028
10094
  main().catch((err) => {
10029
10095
  process.stderr.write(`Fatal: ${err instanceof Error ? err.message : String(err)}
10030
10096
  `);
@@ -13302,7 +13368,7 @@ async function runCli() {
13302
13368
  registerConfigCommand: registerConfigCommand2,
13303
13369
  registerSessionCommand: registerSessionCommand2
13304
13370
  } = await Promise.resolve().then(() => (init_register(), register_exports));
13305
- const version2 = "0.1.22";
13371
+ const version2 = "0.1.24";
13306
13372
  class HandledError extends Error {
13307
13373
  constructor() {
13308
13374
  super("HANDLED_ERROR");
package/dist/index.d.ts CHANGED
@@ -1331,40 +1331,15 @@ declare const SnapshotV1Schema: z.ZodObject<{
1331
1331
  file: z.ZodString;
1332
1332
  message: z.ZodString;
1333
1333
  }, "strip", z.ZodTypeAny, {
1334
- type: string;
1335
1334
  message: string;
1335
+ type: string;
1336
1336
  file: string;
1337
1337
  }, {
1338
- type: string;
1339
1338
  message: string;
1339
+ type: string;
1340
1340
  file: string;
1341
1341
  }>, "many">>;
1342
1342
  }, "strip", z.ZodTypeAny, {
1343
- version: 1;
1344
- config: {
1345
- version: number;
1346
- type: string;
1347
- language: string;
1348
- project: string;
1349
- features: {
1350
- issues: boolean;
1351
- tickets: boolean;
1352
- handovers: boolean;
1353
- roadmap: boolean;
1354
- reviews: boolean;
1355
- } & {
1356
- [k: string]: unknown;
1357
- };
1358
- schemaVersion?: number | undefined;
1359
- recipe?: string | undefined;
1360
- recipeOverrides?: {
1361
- maxTicketsPerSession?: number | undefined;
1362
- compactThreshold?: string | undefined;
1363
- reviewBackends?: string[] | undefined;
1364
- } | undefined;
1365
- } & {
1366
- [k: string]: unknown;
1367
- };
1368
1343
  issues: z.objectOutputType<{
1369
1344
  id: z.ZodString;
1370
1345
  title: z.ZodString;
@@ -1401,8 +1376,8 @@ declare const SnapshotV1Schema: z.ZodObject<{
1401
1376
  claimedBySession: z.ZodOptional<z.ZodNullable<z.ZodString>>;
1402
1377
  }, z.ZodTypeAny, "passthrough">[];
1403
1378
  roadmap: {
1404
- date: string;
1405
1379
  title: string;
1380
+ date: string;
1406
1381
  phases: z.objectOutputType<{
1407
1382
  id: z.ZodString;
1408
1383
  label: z.ZodString;
@@ -1420,6 +1395,7 @@ declare const SnapshotV1Schema: z.ZodObject<{
1420
1395
  } & {
1421
1396
  [k: string]: unknown;
1422
1397
  };
1398
+ version: 1;
1423
1399
  project: string;
1424
1400
  notes: z.objectOutputType<{
1425
1401
  id: z.ZodString;
@@ -1445,19 +1421,11 @@ declare const SnapshotV1Schema: z.ZodObject<{
1445
1421
  status: z.ZodEnum<["active", "deprecated", "superseded"]>;
1446
1422
  }, z.ZodTypeAny, "passthrough">[];
1447
1423
  createdAt: string;
1448
- handoverFilenames: string[];
1449
- warnings?: {
1450
- type: string;
1451
- message: string;
1452
- file: string;
1453
- }[] | undefined;
1454
- }, {
1455
- version: 1;
1456
1424
  config: {
1457
- version: number;
1458
1425
  type: string;
1459
- language: string;
1426
+ version: number;
1460
1427
  project: string;
1428
+ language: string;
1461
1429
  features: {
1462
1430
  issues: boolean;
1463
1431
  tickets: boolean;
@@ -1477,6 +1445,13 @@ declare const SnapshotV1Schema: z.ZodObject<{
1477
1445
  } & {
1478
1446
  [k: string]: unknown;
1479
1447
  };
1448
+ handoverFilenames: string[];
1449
+ warnings?: {
1450
+ message: string;
1451
+ type: string;
1452
+ file: string;
1453
+ }[] | undefined;
1454
+ }, {
1480
1455
  issues: z.objectInputType<{
1481
1456
  id: z.ZodString;
1482
1457
  title: z.ZodString;
@@ -1513,8 +1488,8 @@ declare const SnapshotV1Schema: z.ZodObject<{
1513
1488
  claimedBySession: z.ZodOptional<z.ZodNullable<z.ZodString>>;
1514
1489
  }, z.ZodTypeAny, "passthrough">[];
1515
1490
  roadmap: {
1516
- date: string;
1517
1491
  title: string;
1492
+ date: string;
1518
1493
  phases: z.objectInputType<{
1519
1494
  id: z.ZodString;
1520
1495
  label: z.ZodString;
@@ -1532,8 +1507,33 @@ declare const SnapshotV1Schema: z.ZodObject<{
1532
1507
  } & {
1533
1508
  [k: string]: unknown;
1534
1509
  };
1510
+ version: 1;
1535
1511
  project: string;
1536
1512
  createdAt: string;
1513
+ config: {
1514
+ type: string;
1515
+ version: number;
1516
+ project: string;
1517
+ language: string;
1518
+ features: {
1519
+ issues: boolean;
1520
+ tickets: boolean;
1521
+ handovers: boolean;
1522
+ roadmap: boolean;
1523
+ reviews: boolean;
1524
+ } & {
1525
+ [k: string]: unknown;
1526
+ };
1527
+ schemaVersion?: number | undefined;
1528
+ recipe?: string | undefined;
1529
+ recipeOverrides?: {
1530
+ maxTicketsPerSession?: number | undefined;
1531
+ compactThreshold?: string | undefined;
1532
+ reviewBackends?: string[] | undefined;
1533
+ } | undefined;
1534
+ } & {
1535
+ [k: string]: unknown;
1536
+ };
1537
1537
  notes?: z.objectInputType<{
1538
1538
  id: z.ZodString;
1539
1539
  title: z.ZodNullable<z.ZodString>;
@@ -1558,8 +1558,8 @@ declare const SnapshotV1Schema: z.ZodObject<{
1558
1558
  status: z.ZodEnum<["active", "deprecated", "superseded"]>;
1559
1559
  }, z.ZodTypeAny, "passthrough">[] | undefined;
1560
1560
  warnings?: {
1561
- type: string;
1562
1561
  message: string;
1562
+ type: string;
1563
1563
  file: string;
1564
1564
  }[] | undefined;
1565
1565
  handoverFilenames?: string[] | undefined;
package/dist/mcp.js CHANGED
@@ -5402,6 +5402,13 @@ async function gitStashPop(cwd, commitHash) {
5402
5402
  }
5403
5403
  return git(cwd, ["stash", "pop", match.ref], () => void 0);
5404
5404
  }
5405
+ async function gitDiffTreeNames(cwd, commitHash) {
5406
+ return git(
5407
+ cwd,
5408
+ ["diff-tree", "--name-only", "--no-commit-id", "-r", commitHash],
5409
+ (out) => out.split("\n").filter((l) => l.trim().length > 0)
5410
+ );
5411
+ }
5405
5412
  var SAFE_REF = /^[0-9a-f]{4,40}$/i;
5406
5413
  async function gitLogRange(cwd, from, to, limit = 20) {
5407
5414
  if (from && !SAFE_REF.test(from)) {
@@ -5935,8 +5942,10 @@ var PlanReviewStage = class {
5935
5942
  const isRevise = verdict === "revise" || verdict === "request_changes";
5936
5943
  const isReject = verdict === "reject";
5937
5944
  let nextAction;
5938
- if (isReject || isRevise) {
5945
+ if (isReject) {
5939
5946
  nextAction = "PLAN";
5947
+ } else if (isRevise) {
5948
+ nextAction = "PLAN_REVIEW";
5940
5949
  } else if (verdict === "approve" || !hasCriticalOrMajor && roundNum >= minRounds) {
5941
5950
  nextAction = "IMPLEMENT";
5942
5951
  } else if (roundNum >= 5) {
@@ -5958,7 +5967,21 @@ var PlanReviewStage = class {
5958
5967
  return {
5959
5968
  action: "back",
5960
5969
  target: "PLAN",
5961
- reason: isRevise ? "revise" : "reject"
5970
+ reason: "reject"
5971
+ };
5972
+ }
5973
+ if (isRevise) {
5974
+ const findingSummary = findings.length > 0 ? findings.slice(0, 5).map((f) => `- [${f.severity}] ${f.description}`).join("\n") : "Address the reviewer's concerns.";
5975
+ return {
5976
+ action: "retry",
5977
+ instruction: [
5978
+ `# Plan Review \u2014 Round ${roundNum} requested changes`,
5979
+ "",
5980
+ 'Update the plan to address these findings, then call me with completedAction: "plan_review_round" and the new review verdict.',
5981
+ "",
5982
+ findingSummary
5983
+ ].join("\n"),
5984
+ reminders: ["Update the plan file, then re-review. Do NOT rewrite from scratch."]
5962
5985
  };
5963
5986
  }
5964
5987
  if (nextAction === "IMPLEMENT") {
@@ -6168,7 +6191,7 @@ function exhaustionAction(ctx) {
6168
6191
  return {
6169
6192
  action: "back",
6170
6193
  target: "PLAN",
6171
- reason: "no_failing_tests"
6194
+ reason: "TDD exhausted: could not verify new failing tests after 3 attempts. Revise the plan to make the test expectations clearer or simpler."
6172
6195
  };
6173
6196
  }
6174
6197
 
@@ -6207,6 +6230,13 @@ var TestStage = class {
6207
6230
  const retryCount = ctx.state.testRetryCount ?? 0;
6208
6231
  const exitCodeMatch = notes.match(/exit\s*(?:code[:\s]*)?\s*(\d+)/i);
6209
6232
  if (!exitCodeMatch) {
6233
+ const nextRetry = retryCount + 1;
6234
+ if (nextRetry >= MAX_TEST_RETRIES) {
6235
+ ctx.writeState({ testRetryCount: 0 });
6236
+ ctx.appendEvent("tests_parse_exhausted", { retryCount: nextRetry });
6237
+ return { action: "advance" };
6238
+ }
6239
+ ctx.writeState({ testRetryCount: nextRetry });
6210
6240
  return { action: "retry", instruction: 'Could not parse exit code from notes. Include "exit code: 0" (or non-zero) in your notes.' };
6211
6241
  }
6212
6242
  const exitCode = parseInt(exitCodeMatch[1], 10);
@@ -6614,6 +6644,22 @@ var FinalizeStage = class {
6614
6644
  async handleStage(ctx, report) {
6615
6645
  const stagedResult = await gitDiffCachedNames(ctx.root);
6616
6646
  if (!stagedResult.ok || stagedResult.data.length === 0) {
6647
+ const headResult = await gitHead(ctx.root);
6648
+ const previousHead = ctx.state.git.expectedHead ?? ctx.state.git.initHead;
6649
+ if (headResult.ok && previousHead && headResult.data.hash !== previousHead) {
6650
+ const ticketId2 = ctx.state.ticket?.id;
6651
+ if (ticketId2) {
6652
+ const treeResult = await gitDiffTreeNames(ctx.root, headResult.data.hash);
6653
+ const ticketPath = `.story/tickets/${ticketId2}.json`;
6654
+ if (treeResult.ok && !treeResult.data.includes(ticketPath)) {
6655
+ return {
6656
+ action: "retry",
6657
+ instruction: `Commit detected (${headResult.data.hash.slice(0, 7)}) but ticket file ${ticketPath} is not in the commit. Amend the commit to include it: \`git add ${ticketPath} && git commit --amend --no-edit\`, then report completedAction: "commit_done" with the new hash.`
6658
+ };
6659
+ }
6660
+ }
6661
+ return this.handleCommit(ctx, { ...report, commitHash: headResult.data.hash });
6662
+ }
6617
6663
  return { action: "retry", instruction: 'No files are staged. Stage your changes and call me again with completedAction: "files_staged".' };
6618
6664
  }
6619
6665
  const baselineUntracked = ctx.state.git.baseline?.untrackedPaths ?? [];
@@ -6631,6 +6677,16 @@ var FinalizeStage = class {
6631
6677
  }
6632
6678
  }
6633
6679
  }
6680
+ const ticketId = ctx.state.ticket?.id;
6681
+ if (ticketId) {
6682
+ const ticketPath = `.story/tickets/${ticketId}.json`;
6683
+ if (!stagedResult.data.includes(ticketPath)) {
6684
+ return {
6685
+ action: "retry",
6686
+ instruction: `Ticket file ${ticketPath} is not staged. Run \`git add ${ticketPath}\` and call me again with completedAction: "files_staged".`
6687
+ };
6688
+ }
6689
+ }
6634
6690
  ctx.writeState({
6635
6691
  finalizeCheckpoint: overlapOverridden ? "staged_override" : "staged"
6636
6692
  });
@@ -6691,27 +6747,29 @@ var FinalizeStage = class {
6691
6747
  }
6692
6748
  const headResult = await gitHead(ctx.root);
6693
6749
  const previousHead = ctx.state.git.expectedHead ?? ctx.state.git.initHead;
6694
- if (!headResult.ok || headResult.data.hash !== commitHash) {
6750
+ const fullHead = headResult.ok ? headResult.data.hash : null;
6751
+ if (!fullHead || !fullHead.startsWith(commitHash) && commitHash !== fullHead) {
6695
6752
  return {
6696
6753
  action: "retry",
6697
- instruction: `Commit hash mismatch: reported ${commitHash} but HEAD is ${headResult.ok ? headResult.data.hash : "unknown"}. Verify the commit succeeded and report the correct hash.`
6754
+ instruction: `Commit hash mismatch: reported ${commitHash} but HEAD is ${fullHead ?? "unknown"}. Verify the commit succeeded and report the correct hash.`
6698
6755
  };
6699
6756
  }
6700
- if (previousHead && commitHash === previousHead) {
6701
- return { action: "retry", instruction: `No new commit detected: HEAD (${commitHash}) has not changed. Create a commit first, then report the new hash.` };
6757
+ const normalizedHash = fullHead;
6758
+ if (previousHead && normalizedHash === previousHead) {
6759
+ return { action: "retry", instruction: `No new commit detected: HEAD (${normalizedHash}) has not changed. Create a commit first, then report the new hash.` };
6702
6760
  }
6703
- const completedTicket = ctx.state.ticket ? { id: ctx.state.ticket.id, title: ctx.state.ticket.title, commitHash, risk: ctx.state.ticket.risk } : void 0;
6761
+ const completedTicket = ctx.state.ticket ? { id: ctx.state.ticket.id, title: ctx.state.ticket.title, commitHash: normalizedHash, risk: ctx.state.ticket.risk, realizedRisk: ctx.state.ticket.realizedRisk } : void 0;
6704
6762
  ctx.writeState({
6705
6763
  finalizeCheckpoint: "committed",
6706
6764
  completedTickets: completedTicket ? [...ctx.state.completedTickets, completedTicket] : ctx.state.completedTickets,
6707
6765
  ticket: void 0,
6708
6766
  git: {
6709
6767
  ...ctx.state.git,
6710
- mergeBase: commitHash,
6711
- expectedHead: commitHash
6768
+ mergeBase: normalizedHash,
6769
+ expectedHead: normalizedHash
6712
6770
  }
6713
6771
  });
6714
- ctx.appendEvent("commit", { commitHash, ticketId: completedTicket?.id });
6772
+ ctx.appendEvent("commit", { commitHash: normalizedHash, ticketId: completedTicket?.id });
6715
6773
  return { action: "advance" };
6716
6774
  }
6717
6775
  };
@@ -6837,7 +6895,7 @@ var CompleteStage = class {
6837
6895
  "You are in autonomous mode \u2014 continue working until all tickets are done or the session limit is reached."
6838
6896
  ],
6839
6897
  transitionedFrom: "COMPLETE",
6840
- contextAdvice: advice
6898
+ contextAdvice: "ok"
6841
6899
  }
6842
6900
  };
6843
6901
  }
@@ -7586,10 +7644,14 @@ ${ticket.description}` : "",
7586
7644
  data: { recipe, branch: written.git.branch, head: written.git.initHead, mode: "auto" }
7587
7645
  });
7588
7646
  const topCandidate = nextResult.kind === "found" ? nextResult.candidates[0] : null;
7647
+ const maxTickets = updated.config.maxTicketsPerSession;
7648
+ const interval = updated.config.handoverInterval ?? 5;
7649
+ const sessionDesc = maxTickets > 0 ? `Work continuously until all tickets are done or you reach ${maxTickets} tickets.` : "Work continuously until all tickets are done.";
7650
+ const checkpointDesc = interval > 0 ? ` A checkpoint handover will be saved every ${interval} tickets.` : "";
7589
7651
  const instruction = [
7590
7652
  "# Autonomous Session Started",
7591
7653
  "",
7592
- "You are now in autonomous mode. Work continuously until all tickets are done or the session limit is reached.",
7654
+ `You are now in autonomous mode. ${sessionDesc}${checkpointDesc}`,
7593
7655
  "Do NOT stop to summarize. Do NOT ask the user. Pick a ticket and start working immediately.",
7594
7656
  "",
7595
7657
  "## Ticket Candidates",
@@ -8020,11 +8082,15 @@ async function handleCancel(root, args) {
8020
8082
  }
8021
8083
  const info = findSessionById(root, args.sessionId);
8022
8084
  if (!info) return guideError(new Error(`Session ${args.sessionId} not found`));
8023
- if (info.state.recipe === "coding") {
8085
+ if (info.state.state === "SESSION_END" || info.state.status === "completed") {
8086
+ return guideError(new Error("Session already ended."));
8087
+ }
8088
+ const CANCELLABLE_STATES = /* @__PURE__ */ new Set(["PICK_TICKET", "COMPLETE", "HANDOVER"]);
8089
+ if (info.state.recipe === "coding" && !CANCELLABLE_STATES.has(info.state.state)) {
8024
8090
  const sessionMode = info.state.mode ?? "auto";
8025
8091
  const modeGuidance = sessionMode === "plan" ? "Plan mode sessions end after plan review approval \u2014 continue to that step." : sessionMode === "review" ? "Review mode sessions end after code review approval \u2014 continue to that step." : sessionMode === "guided" ? "Guided mode sessions end after ticket completion \u2014 continue to FINALIZE." : "Complete the current ticket and write a handover to end the session.";
8026
8092
  return guideError(new Error(
8027
- `Cannot cancel a coding session. ${modeGuidance}`
8093
+ `Cannot cancel a coding session from ${info.state.state}. ${modeGuidance}`
8028
8094
  ));
8029
8095
  }
8030
8096
  await recoverPendingMutation(info.dir, info.state, root);
@@ -9189,7 +9255,7 @@ async function ensureGitignoreEntries(gitignorePath, entries) {
9189
9255
  // src/mcp/index.ts
9190
9256
  var ENV_VAR2 = "CLAUDESTORY_PROJECT_ROOT";
9191
9257
  var CONFIG_PATH2 = ".story/config.json";
9192
- var version = "0.1.22";
9258
+ var version = "0.1.24";
9193
9259
  function tryDiscoverRoot() {
9194
9260
  const envRoot = process.env[ENV_VAR2];
9195
9261
  if (envRoot) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anthropologies/claudestory",
3
- "version": "0.1.22",
3
+ "version": "0.1.24",
4
4
  "license": "UNLICENSED",
5
5
  "description": "Cross-session context persistence for AI coding projects. Tracks tickets, issues, roadmap, and handovers so every session builds on the last.",
6
6
  "keywords": [