@anthropologies/claudestory 0.1.23 → 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.
Files changed (3) hide show
  1. package/dist/cli.js +78 -16
  2. package/dist/mcp.js +77 -15
  3. package/package.json +1 -1
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
  }
@@ -8534,11 +8592,15 @@ async function handleCancel(root, args) {
8534
8592
  }
8535
8593
  const info = findSessionById(root, args.sessionId);
8536
8594
  if (!info) return guideError(new Error(`Session ${args.sessionId} not found`));
8537
- 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)) {
8538
8600
  const sessionMode = info.state.mode ?? "auto";
8539
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.";
8540
8602
  return guideError(new Error(
8541
- `Cannot cancel a coding session. ${modeGuidance}`
8603
+ `Cannot cancel a coding session from ${info.state.state}. ${modeGuidance}`
8542
8604
  ));
8543
8605
  }
8544
8606
  await recoverPendingMutation(info.dir, info.state, root);
@@ -10028,7 +10090,7 @@ var init_mcp = __esm({
10028
10090
  init_init();
10029
10091
  ENV_VAR2 = "CLAUDESTORY_PROJECT_ROOT";
10030
10092
  CONFIG_PATH2 = ".story/config.json";
10031
- version = "0.1.23";
10093
+ version = "0.1.24";
10032
10094
  main().catch((err) => {
10033
10095
  process.stderr.write(`Fatal: ${err instanceof Error ? err.message : String(err)}
10034
10096
  `);
@@ -13306,7 +13368,7 @@ async function runCli() {
13306
13368
  registerConfigCommand: registerConfigCommand2,
13307
13369
  registerSessionCommand: registerSessionCommand2
13308
13370
  } = await Promise.resolve().then(() => (init_register(), register_exports));
13309
- const version2 = "0.1.23";
13371
+ const version2 = "0.1.24";
13310
13372
  class HandledError extends Error {
13311
13373
  constructor() {
13312
13374
  super("HANDLED_ERROR");
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
  }
@@ -8024,11 +8082,15 @@ async function handleCancel(root, args) {
8024
8082
  }
8025
8083
  const info = findSessionById(root, args.sessionId);
8026
8084
  if (!info) return guideError(new Error(`Session ${args.sessionId} not found`));
8027
- 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)) {
8028
8090
  const sessionMode = info.state.mode ?? "auto";
8029
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.";
8030
8092
  return guideError(new Error(
8031
- `Cannot cancel a coding session. ${modeGuidance}`
8093
+ `Cannot cancel a coding session from ${info.state.state}. ${modeGuidance}`
8032
8094
  ));
8033
8095
  }
8034
8096
  await recoverPendingMutation(info.dir, info.state, root);
@@ -9193,7 +9255,7 @@ async function ensureGitignoreEntries(gitignorePath, entries) {
9193
9255
  // src/mcp/index.ts
9194
9256
  var ENV_VAR2 = "CLAUDESTORY_PROJECT_ROOT";
9195
9257
  var CONFIG_PATH2 = ".story/config.json";
9196
- var version = "0.1.23";
9258
+ var version = "0.1.24";
9197
9259
  function tryDiscoverRoot() {
9198
9260
  const envRoot = process.env[ENV_VAR2];
9199
9261
  if (envRoot) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anthropologies/claudestory",
3
- "version": "0.1.23",
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": [