@anthropologies/claudestory 0.1.33 → 0.1.35

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
@@ -298,7 +298,9 @@ var init_config = __esm({
298
298
  recipeOverrides: z7.object({
299
299
  maxTicketsPerSession: z7.number().min(0).optional(),
300
300
  compactThreshold: z7.string().optional(),
301
- reviewBackends: z7.array(z7.string()).optional()
301
+ reviewBackends: z7.array(z7.string()).optional(),
302
+ handoverInterval: z7.number().min(0).optional(),
303
+ stages: z7.record(z7.record(z7.unknown())).optional()
302
304
  }).optional()
303
305
  }).passthrough();
304
306
  }
@@ -3380,7 +3382,20 @@ function validateParentTicket(parentId, ticketId, state) {
3380
3382
  throw new CliValidationError("invalid_input", `Parent ticket ${parentId} not found`);
3381
3383
  }
3382
3384
  }
3385
+ function buildErrorMultiset(findings) {
3386
+ const counts = /* @__PURE__ */ new Map();
3387
+ const messages = /* @__PURE__ */ new Map();
3388
+ for (const f of findings) {
3389
+ if (f.level !== "error") continue;
3390
+ const key = `${f.code}|${f.entity ?? ""}|${f.message}`;
3391
+ counts.set(key, (counts.get(key) ?? 0) + 1);
3392
+ messages.set(key, f.message);
3393
+ }
3394
+ return { counts, messages };
3395
+ }
3383
3396
  function validatePostWriteState(candidate, state, isCreate) {
3397
+ const preResult = validateProject(state);
3398
+ const { counts: preErrors } = buildErrorMultiset(preResult.findings);
3384
3399
  const existingTickets = [...state.tickets];
3385
3400
  if (isCreate) {
3386
3401
  existingTickets.push(candidate);
@@ -3397,11 +3412,17 @@ function validatePostWriteState(candidate, state, isCreate) {
3397
3412
  config: state.config,
3398
3413
  handoverFilenames: [...state.handoverFilenames]
3399
3414
  });
3400
- const result = validateProject(postState);
3401
- if (!result.valid) {
3402
- const errors = result.findings.filter((f) => f.level === "error");
3403
- const msg = errors.map((f) => f.message).join("; ");
3404
- throw new CliValidationError("validation_failed", `Write would create invalid state: ${msg}`);
3415
+ const postResult = validateProject(postState);
3416
+ const { counts: postErrors, messages: postMessages } = buildErrorMultiset(postResult.findings);
3417
+ const newErrors = [];
3418
+ for (const [key, postCount] of postErrors) {
3419
+ const preCount = preErrors.get(key) ?? 0;
3420
+ if (postCount > preCount) {
3421
+ newErrors.push(postMessages.get(key) ?? key);
3422
+ }
3423
+ }
3424
+ if (newErrors.length > 0) {
3425
+ throw new CliValidationError("validation_failed", `Write would create invalid state: ${newErrors.join("; ")}`);
3405
3426
  }
3406
3427
  }
3407
3428
  async function handleTicketCreate(args, format, root) {
@@ -3585,7 +3606,20 @@ function validateRelatedTickets(ids, state) {
3585
3606
  }
3586
3607
  }
3587
3608
  }
3609
+ function buildErrorMultiset2(findings) {
3610
+ const counts = /* @__PURE__ */ new Map();
3611
+ const messages = /* @__PURE__ */ new Map();
3612
+ for (const f of findings) {
3613
+ if (f.level !== "error") continue;
3614
+ const key = `${f.code}|${f.entity ?? ""}|${f.message}`;
3615
+ counts.set(key, (counts.get(key) ?? 0) + 1);
3616
+ messages.set(key, f.message);
3617
+ }
3618
+ return { counts, messages };
3619
+ }
3588
3620
  function validatePostWriteIssueState(candidate, state, isCreate) {
3621
+ const preResult = validateProject(state);
3622
+ const { counts: preErrors } = buildErrorMultiset2(preResult.findings);
3589
3623
  const existingIssues = [...state.issues];
3590
3624
  if (isCreate) {
3591
3625
  existingIssues.push(candidate);
@@ -3602,11 +3636,17 @@ function validatePostWriteIssueState(candidate, state, isCreate) {
3602
3636
  config: state.config,
3603
3637
  handoverFilenames: [...state.handoverFilenames]
3604
3638
  });
3605
- const result = validateProject(postState);
3606
- if (!result.valid) {
3607
- const errors = result.findings.filter((f) => f.level === "error");
3608
- const msg = errors.map((f) => f.message).join("; ");
3609
- throw new CliValidationError("validation_failed", `Write would create invalid state: ${msg}`);
3639
+ const postResult = validateProject(postState);
3640
+ const { counts: postErrors, messages: postMessages } = buildErrorMultiset2(postResult.findings);
3641
+ const newErrors = [];
3642
+ for (const [key, postCount] of postErrors) {
3643
+ const preCount = preErrors.get(key) ?? 0;
3644
+ if (postCount > preCount) {
3645
+ newErrors.push(postMessages.get(key) ?? key);
3646
+ }
3647
+ }
3648
+ if (newErrors.length > 0) {
3649
+ throw new CliValidationError("validation_failed", `Write would create invalid state: ${newErrors.join("; ")}`);
3610
3650
  }
3611
3651
  }
3612
3652
  async function handleIssueCreate(args, format, root) {
@@ -5904,7 +5944,19 @@ function resolveRecipe(recipeName, projectOverrides) {
5904
5944
  }
5905
5945
  }
5906
5946
  let pipeline = raw.pipeline ? [...raw.pipeline] : [...DEFAULT_PIPELINE];
5907
- const stages2 = raw.stages ?? {};
5947
+ const recipeStages = raw.stages ?? {};
5948
+ const stageOverrides = projectOverrides?.stages ?? {};
5949
+ const stages2 = {};
5950
+ for (const [key, value] of Object.entries(recipeStages)) {
5951
+ const override = stageOverrides[key];
5952
+ const safeOverride = override && typeof override === "object" && !Array.isArray(override) ? override : {};
5953
+ stages2[key] = { ...value, ...safeOverride };
5954
+ }
5955
+ for (const [key, value] of Object.entries(stageOverrides)) {
5956
+ if (!stages2[key] && value && typeof value === "object" && !Array.isArray(value)) {
5957
+ stages2[key] = { ...value };
5958
+ }
5959
+ }
5908
5960
  if (stages2.WRITE_TESTS?.enabled) {
5909
5961
  const implementIdx = pipeline.indexOf("IMPLEMENT");
5910
5962
  if (implementIdx !== -1 && !pipeline.includes("WRITE_TESTS")) {
@@ -8059,6 +8111,9 @@ async function handleStart(root, args) {
8059
8111
  if (typeof overrides.compactThreshold === "string") sessionConfig.compactThreshold = overrides.compactThreshold;
8060
8112
  if (Array.isArray(overrides.reviewBackends)) sessionConfig.reviewBackends = overrides.reviewBackends;
8061
8113
  if (typeof overrides.handoverInterval === "number") sessionConfig.handoverInterval = overrides.handoverInterval;
8114
+ if (overrides.stages && typeof overrides.stages === "object") {
8115
+ sessionConfig.stageOverrides = overrides.stages;
8116
+ }
8062
8117
  }
8063
8118
  } catch {
8064
8119
  }
@@ -8068,7 +8123,8 @@ async function handleStart(root, args) {
8068
8123
  const resolvedRecipe = resolveRecipe(recipe, {
8069
8124
  maxTicketsPerSession: sessionConfig.maxTicketsPerSession,
8070
8125
  compactThreshold: sessionConfig.compactThreshold,
8071
- reviewBackends: sessionConfig.reviewBackends
8126
+ reviewBackends: sessionConfig.reviewBackends,
8127
+ stages: sessionConfig.stageOverrides
8072
8128
  });
8073
8129
  const session = createSession(root, recipe, wsId, sessionConfig);
8074
8130
  const dir = sessionDir(root, session.sessionId);
@@ -8868,14 +8924,6 @@ async function handleCancel(root, args) {
8868
8924
  if (info.state.state === "SESSION_END" || info.state.status === "completed") {
8869
8925
  return guideError(new Error("Session already ended."));
8870
8926
  }
8871
- const CANCELLABLE_STATES = /* @__PURE__ */ new Set(["PICK_TICKET", "COMPLETE", "HANDOVER"]);
8872
- if (info.state.recipe === "coding" && !CANCELLABLE_STATES.has(info.state.state)) {
8873
- const sessionMode = info.state.mode ?? "auto";
8874
- 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.";
8875
- return guideError(new Error(
8876
- `Cannot cancel a coding session from ${info.state.state}. ${modeGuidance}`
8877
- ));
8878
- }
8879
8927
  await recoverPendingMutation(info.dir, info.state, root);
8880
8928
  const cancelInfo = findSessionById(root, args.sessionId) ?? info;
8881
8929
  let ticketReleased = false;
@@ -10363,7 +10411,7 @@ var init_mcp = __esm({
10363
10411
  init_init();
10364
10412
  ENV_VAR2 = "CLAUDESTORY_PROJECT_ROOT";
10365
10413
  CONFIG_PATH2 = ".story/config.json";
10366
- version = "0.1.33";
10414
+ version = "0.1.35";
10367
10415
  main().catch((err) => {
10368
10416
  process.stderr.write(`Fatal: ${err instanceof Error ? err.message : String(err)}
10369
10417
  `);
@@ -10511,6 +10559,102 @@ var init_run = __esm({
10511
10559
  }
10512
10560
  });
10513
10561
 
10562
+ // src/cli/commands/repair.ts
10563
+ function computeRepairs(state, warnings) {
10564
+ const integrityWarning = warnings.find(
10565
+ (w) => INTEGRITY_WARNING_TYPES.includes(w.type)
10566
+ );
10567
+ if (integrityWarning) {
10568
+ return {
10569
+ fixes: [],
10570
+ error: `Cannot repair: data integrity issue in ${integrityWarning.file}: ${integrityWarning.message}. Fix the corrupt file first, then retry.`,
10571
+ tickets: [],
10572
+ issues: []
10573
+ };
10574
+ }
10575
+ const fixes = [];
10576
+ const modifiedTickets = [];
10577
+ const modifiedIssues = [];
10578
+ const ticketIDs = new Set(state.tickets.map((t) => t.id));
10579
+ const phaseIDs = new Set(state.roadmap.phases.map((p) => {
10580
+ const id = p.id;
10581
+ return typeof id === "object" && id !== null ? id.rawValue ?? String(id) : String(id);
10582
+ }));
10583
+ for (const ticket of state.tickets) {
10584
+ let modified = false;
10585
+ let blockedBy = [...ticket.blockedBy];
10586
+ let parentTicket = ticket.parentTicket;
10587
+ let phase = ticket.phase;
10588
+ const validBlockedBy = blockedBy.filter((ref) => ticketIDs.has(ref));
10589
+ if (validBlockedBy.length < blockedBy.length) {
10590
+ const removed = blockedBy.filter((ref) => !ticketIDs.has(ref));
10591
+ blockedBy = validBlockedBy;
10592
+ modified = true;
10593
+ fixes.push({ entity: ticket.id, field: "blockedBy", description: `Removed stale refs: ${removed.join(", ")}` });
10594
+ }
10595
+ if (parentTicket && !ticketIDs.has(parentTicket)) {
10596
+ fixes.push({ entity: ticket.id, field: "parentTicket", description: `Cleared stale ref: ${parentTicket}` });
10597
+ parentTicket = null;
10598
+ modified = true;
10599
+ }
10600
+ const phaseRaw = typeof phase === "object" && phase !== null ? phase.rawValue ?? String(phase) : phase != null ? String(phase) : null;
10601
+ if (phaseRaw && !phaseIDs.has(phaseRaw)) {
10602
+ fixes.push({ entity: ticket.id, field: "phase", description: `Cleared stale phase: ${phaseRaw}` });
10603
+ phase = null;
10604
+ modified = true;
10605
+ }
10606
+ if (modified) {
10607
+ modifiedTickets.push({ ...ticket, blockedBy, parentTicket, phase });
10608
+ }
10609
+ }
10610
+ for (const issue of state.issues) {
10611
+ let modified = false;
10612
+ let relatedTickets = [...issue.relatedTickets];
10613
+ let phase = issue.phase;
10614
+ const validRelated = relatedTickets.filter((ref) => ticketIDs.has(ref));
10615
+ if (validRelated.length < relatedTickets.length) {
10616
+ const removed = relatedTickets.filter((ref) => !ticketIDs.has(ref));
10617
+ relatedTickets = validRelated;
10618
+ modified = true;
10619
+ fixes.push({ entity: issue.id, field: "relatedTickets", description: `Removed stale refs: ${removed.join(", ")}` });
10620
+ }
10621
+ const issuePhaseRaw = typeof phase === "object" && phase !== null ? phase.rawValue ?? String(phase) : phase != null ? String(phase) : null;
10622
+ if (issuePhaseRaw && !phaseIDs.has(issuePhaseRaw)) {
10623
+ fixes.push({ entity: issue.id, field: "phase", description: `Cleared stale phase: ${issuePhaseRaw}` });
10624
+ phase = null;
10625
+ modified = true;
10626
+ }
10627
+ if (modified) {
10628
+ modifiedIssues.push({ ...issue, relatedTickets, phase });
10629
+ }
10630
+ }
10631
+ return { fixes, tickets: modifiedTickets, issues: modifiedIssues };
10632
+ }
10633
+ function handleRepair(ctx, dryRun) {
10634
+ const { fixes, error } = computeRepairs(ctx.state, ctx.warnings);
10635
+ if (error) {
10636
+ return { output: error, errorCode: "project_corrupt" };
10637
+ }
10638
+ if (fixes.length === 0) {
10639
+ return { output: "No stale references found. Project is clean." };
10640
+ }
10641
+ const lines = [`Found ${fixes.length} stale reference(s)${dryRun ? " (dry run)" : ""}:`, ""];
10642
+ for (const fix of fixes) {
10643
+ lines.push(`- ${fix.entity}.${fix.field}: ${fix.description}`);
10644
+ }
10645
+ if (dryRun) {
10646
+ lines.push("", "Run without --dry-run to apply fixes.");
10647
+ }
10648
+ return { output: lines.join("\n") };
10649
+ }
10650
+ var init_repair = __esm({
10651
+ "src/cli/commands/repair.ts"() {
10652
+ "use strict";
10653
+ init_esm_shims();
10654
+ init_errors();
10655
+ }
10656
+ });
10657
+
10514
10658
  // src/cli/commands/init.ts
10515
10659
  import { basename as basename2 } from "path";
10516
10660
  function registerInitCommand(yargs) {
@@ -11595,6 +11739,7 @@ __export(register_exports, {
11595
11739
  registerRecapCommand: () => registerRecapCommand,
11596
11740
  registerRecommendCommand: () => registerRecommendCommand,
11597
11741
  registerReferenceCommand: () => registerReferenceCommand,
11742
+ registerRepairCommand: () => registerRepairCommand,
11598
11743
  registerSelftestCommand: () => registerSelftestCommand,
11599
11744
  registerSessionCommand: () => registerSessionCommand,
11600
11745
  registerSetupSkillCommand: () => registerSetupSkillCommand,
@@ -11625,6 +11770,47 @@ function registerValidateCommand(yargs) {
11625
11770
  }
11626
11771
  );
11627
11772
  }
11773
+ function registerRepairCommand(yargs) {
11774
+ return yargs.command(
11775
+ "repair",
11776
+ "Fix stale references in .story/ data",
11777
+ (y) => y.option("dry-run", { type: "boolean", default: false, describe: "Show what would be fixed without writing" }),
11778
+ async (argv) => {
11779
+ const dryRun = argv["dry-run"];
11780
+ if (dryRun) {
11781
+ await runReadCommand("md", (ctx) => handleRepair(ctx, true));
11782
+ } else {
11783
+ const { withProjectLock: withProjectLock2, writeTicketUnlocked: writeTicketUnlocked2, writeIssueUnlocked: writeIssueUnlocked2, runTransactionUnlocked: runTransactionUnlocked2 } = await Promise.resolve().then(() => (init_project_loader(), project_loader_exports));
11784
+ const root = (await Promise.resolve().then(() => (init_project_root_discovery(), project_root_discovery_exports))).discoverProjectRoot();
11785
+ await withProjectLock2(root, { strict: false }, async ({ state, warnings }) => {
11786
+ const result = computeRepairs(state, warnings);
11787
+ if (result.error) {
11788
+ writeOutput(result.error);
11789
+ process.exitCode = ExitCode.USER_ERROR;
11790
+ return;
11791
+ }
11792
+ if (result.fixes.length === 0) {
11793
+ writeOutput("No stale references found. Project is clean.");
11794
+ return;
11795
+ }
11796
+ await runTransactionUnlocked2(root, async () => {
11797
+ for (const ticket of result.tickets) {
11798
+ await writeTicketUnlocked2(ticket, root);
11799
+ }
11800
+ for (const issue of result.issues) {
11801
+ await writeIssueUnlocked2(issue, root);
11802
+ }
11803
+ });
11804
+ const lines = [`Fixed ${result.fixes.length} stale reference(s):`, ""];
11805
+ for (const fix of result.fixes) {
11806
+ lines.push(`- ${fix.entity}.${fix.field}: ${fix.description}`);
11807
+ }
11808
+ writeOutput(lines.join("\n"));
11809
+ });
11810
+ }
11811
+ }
11812
+ );
11813
+ }
11628
11814
  function registerHandoverCommand(yargs) {
11629
11815
  return yargs.command(
11630
11816
  "handover",
@@ -13596,6 +13782,7 @@ var init_register = __esm({
13596
13782
  init_output_formatter();
13597
13783
  init_status();
13598
13784
  init_validate();
13785
+ init_repair();
13599
13786
  init_handover();
13600
13787
  init_blocker();
13601
13788
  init_ticket2();
@@ -13645,9 +13832,10 @@ async function runCli() {
13645
13832
  registerSetupSkillCommand: registerSetupSkillCommand2,
13646
13833
  registerHookStatusCommand: registerHookStatusCommand2,
13647
13834
  registerConfigCommand: registerConfigCommand2,
13648
- registerSessionCommand: registerSessionCommand2
13835
+ registerSessionCommand: registerSessionCommand2,
13836
+ registerRepairCommand: registerRepairCommand2
13649
13837
  } = await Promise.resolve().then(() => (init_register(), register_exports));
13650
- const version2 = "0.1.33";
13838
+ const version2 = "0.1.35";
13651
13839
  class HandledError extends Error {
13652
13840
  constructor() {
13653
13841
  super("HANDLED_ERROR");
@@ -13679,6 +13867,7 @@ async function runCli() {
13679
13867
  cli = registerHandoverCommand2(cli);
13680
13868
  cli = registerBlockerCommand2(cli);
13681
13869
  cli = registerValidateCommand2(cli);
13870
+ cli = registerRepairCommand2(cli);
13682
13871
  cli = registerSnapshotCommand2(cli);
13683
13872
  cli = registerRecapCommand2(cli);
13684
13873
  cli = registerExportCommand2(cli);
package/dist/index.d.ts CHANGED
@@ -348,14 +348,20 @@ declare const ConfigSchema: z.ZodObject<{
348
348
  maxTicketsPerSession: z.ZodOptional<z.ZodNumber>;
349
349
  compactThreshold: z.ZodOptional<z.ZodString>;
350
350
  reviewBackends: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
351
+ handoverInterval: z.ZodOptional<z.ZodNumber>;
352
+ stages: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
351
353
  }, "strip", z.ZodTypeAny, {
352
354
  maxTicketsPerSession?: number | undefined;
353
355
  compactThreshold?: string | undefined;
354
356
  reviewBackends?: string[] | undefined;
357
+ handoverInterval?: number | undefined;
358
+ stages?: Record<string, Record<string, unknown>> | undefined;
355
359
  }, {
356
360
  maxTicketsPerSession?: number | undefined;
357
361
  compactThreshold?: string | undefined;
358
362
  reviewBackends?: string[] | undefined;
363
+ handoverInterval?: number | undefined;
364
+ stages?: Record<string, Record<string, unknown>> | undefined;
359
365
  }>>;
360
366
  }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
361
367
  version: z.ZodNumber;
@@ -387,14 +393,20 @@ declare const ConfigSchema: z.ZodObject<{
387
393
  maxTicketsPerSession: z.ZodOptional<z.ZodNumber>;
388
394
  compactThreshold: z.ZodOptional<z.ZodString>;
389
395
  reviewBackends: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
396
+ handoverInterval: z.ZodOptional<z.ZodNumber>;
397
+ stages: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
390
398
  }, "strip", z.ZodTypeAny, {
391
399
  maxTicketsPerSession?: number | undefined;
392
400
  compactThreshold?: string | undefined;
393
401
  reviewBackends?: string[] | undefined;
402
+ handoverInterval?: number | undefined;
403
+ stages?: Record<string, Record<string, unknown>> | undefined;
394
404
  }, {
395
405
  maxTicketsPerSession?: number | undefined;
396
406
  compactThreshold?: string | undefined;
397
407
  reviewBackends?: string[] | undefined;
408
+ handoverInterval?: number | undefined;
409
+ stages?: Record<string, Record<string, unknown>> | undefined;
398
410
  }>>;
399
411
  }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
400
412
  version: z.ZodNumber;
@@ -426,14 +438,20 @@ declare const ConfigSchema: z.ZodObject<{
426
438
  maxTicketsPerSession: z.ZodOptional<z.ZodNumber>;
427
439
  compactThreshold: z.ZodOptional<z.ZodString>;
428
440
  reviewBackends: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
441
+ handoverInterval: z.ZodOptional<z.ZodNumber>;
442
+ stages: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
429
443
  }, "strip", z.ZodTypeAny, {
430
444
  maxTicketsPerSession?: number | undefined;
431
445
  compactThreshold?: string | undefined;
432
446
  reviewBackends?: string[] | undefined;
447
+ handoverInterval?: number | undefined;
448
+ stages?: Record<string, Record<string, unknown>> | undefined;
433
449
  }, {
434
450
  maxTicketsPerSession?: number | undefined;
435
451
  compactThreshold?: string | undefined;
436
452
  reviewBackends?: string[] | undefined;
453
+ handoverInterval?: number | undefined;
454
+ stages?: Record<string, Record<string, unknown>> | undefined;
437
455
  }>>;
438
456
  }, z.ZodTypeAny, "passthrough">>;
439
457
  type Config = z.infer<typeof ConfigSchema>;
@@ -950,14 +968,20 @@ declare const SnapshotV1Schema: z.ZodObject<{
950
968
  maxTicketsPerSession: z.ZodOptional<z.ZodNumber>;
951
969
  compactThreshold: z.ZodOptional<z.ZodString>;
952
970
  reviewBackends: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
971
+ handoverInterval: z.ZodOptional<z.ZodNumber>;
972
+ stages: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
953
973
  }, "strip", z.ZodTypeAny, {
954
974
  maxTicketsPerSession?: number | undefined;
955
975
  compactThreshold?: string | undefined;
956
976
  reviewBackends?: string[] | undefined;
977
+ handoverInterval?: number | undefined;
978
+ stages?: Record<string, Record<string, unknown>> | undefined;
957
979
  }, {
958
980
  maxTicketsPerSession?: number | undefined;
959
981
  compactThreshold?: string | undefined;
960
982
  reviewBackends?: string[] | undefined;
983
+ handoverInterval?: number | undefined;
984
+ stages?: Record<string, Record<string, unknown>> | undefined;
961
985
  }>>;
962
986
  }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
963
987
  version: z.ZodNumber;
@@ -989,14 +1013,20 @@ declare const SnapshotV1Schema: z.ZodObject<{
989
1013
  maxTicketsPerSession: z.ZodOptional<z.ZodNumber>;
990
1014
  compactThreshold: z.ZodOptional<z.ZodString>;
991
1015
  reviewBackends: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
1016
+ handoverInterval: z.ZodOptional<z.ZodNumber>;
1017
+ stages: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
992
1018
  }, "strip", z.ZodTypeAny, {
993
1019
  maxTicketsPerSession?: number | undefined;
994
1020
  compactThreshold?: string | undefined;
995
1021
  reviewBackends?: string[] | undefined;
1022
+ handoverInterval?: number | undefined;
1023
+ stages?: Record<string, Record<string, unknown>> | undefined;
996
1024
  }, {
997
1025
  maxTicketsPerSession?: number | undefined;
998
1026
  compactThreshold?: string | undefined;
999
1027
  reviewBackends?: string[] | undefined;
1028
+ handoverInterval?: number | undefined;
1029
+ stages?: Record<string, Record<string, unknown>> | undefined;
1000
1030
  }>>;
1001
1031
  }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
1002
1032
  version: z.ZodNumber;
@@ -1028,14 +1058,20 @@ declare const SnapshotV1Schema: z.ZodObject<{
1028
1058
  maxTicketsPerSession: z.ZodOptional<z.ZodNumber>;
1029
1059
  compactThreshold: z.ZodOptional<z.ZodString>;
1030
1060
  reviewBackends: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
1061
+ handoverInterval: z.ZodOptional<z.ZodNumber>;
1062
+ stages: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
1031
1063
  }, "strip", z.ZodTypeAny, {
1032
1064
  maxTicketsPerSession?: number | undefined;
1033
1065
  compactThreshold?: string | undefined;
1034
1066
  reviewBackends?: string[] | undefined;
1067
+ handoverInterval?: number | undefined;
1068
+ stages?: Record<string, Record<string, unknown>> | undefined;
1035
1069
  }, {
1036
1070
  maxTicketsPerSession?: number | undefined;
1037
1071
  compactThreshold?: string | undefined;
1038
1072
  reviewBackends?: string[] | undefined;
1073
+ handoverInterval?: number | undefined;
1074
+ stages?: Record<string, Record<string, unknown>> | undefined;
1039
1075
  }>>;
1040
1076
  }, z.ZodTypeAny, "passthrough">>;
1041
1077
  roadmap: z.ZodObject<{
@@ -1334,40 +1370,15 @@ declare const SnapshotV1Schema: z.ZodObject<{
1334
1370
  file: z.ZodString;
1335
1371
  message: z.ZodString;
1336
1372
  }, "strip", z.ZodTypeAny, {
1337
- type: string;
1338
1373
  message: string;
1374
+ type: string;
1339
1375
  file: string;
1340
1376
  }, {
1341
- type: string;
1342
1377
  message: string;
1378
+ type: string;
1343
1379
  file: string;
1344
1380
  }>, "many">>;
1345
1381
  }, "strip", z.ZodTypeAny, {
1346
- version: 1;
1347
- config: {
1348
- version: number;
1349
- type: string;
1350
- language: string;
1351
- project: string;
1352
- features: {
1353
- issues: boolean;
1354
- tickets: boolean;
1355
- handovers: boolean;
1356
- roadmap: boolean;
1357
- reviews: boolean;
1358
- } & {
1359
- [k: string]: unknown;
1360
- };
1361
- schemaVersion?: number | undefined;
1362
- recipe?: string | undefined;
1363
- recipeOverrides?: {
1364
- maxTicketsPerSession?: number | undefined;
1365
- compactThreshold?: string | undefined;
1366
- reviewBackends?: string[] | undefined;
1367
- } | undefined;
1368
- } & {
1369
- [k: string]: unknown;
1370
- };
1371
1382
  issues: z.objectOutputType<{
1372
1383
  id: z.ZodString;
1373
1384
  title: z.ZodString;
@@ -1404,8 +1415,8 @@ declare const SnapshotV1Schema: z.ZodObject<{
1404
1415
  claimedBySession: z.ZodOptional<z.ZodNullable<z.ZodString>>;
1405
1416
  }, z.ZodTypeAny, "passthrough">[];
1406
1417
  roadmap: {
1407
- date: string;
1408
1418
  title: string;
1419
+ date: string;
1409
1420
  phases: z.objectOutputType<{
1410
1421
  id: z.ZodString;
1411
1422
  label: z.ZodString;
@@ -1423,6 +1434,7 @@ declare const SnapshotV1Schema: z.ZodObject<{
1423
1434
  } & {
1424
1435
  [k: string]: unknown;
1425
1436
  };
1437
+ version: 1;
1426
1438
  project: string;
1427
1439
  notes: z.objectOutputType<{
1428
1440
  id: z.ZodString;
@@ -1448,19 +1460,11 @@ declare const SnapshotV1Schema: z.ZodObject<{
1448
1460
  status: z.ZodEnum<["active", "deprecated", "superseded"]>;
1449
1461
  }, z.ZodTypeAny, "passthrough">[];
1450
1462
  createdAt: string;
1451
- handoverFilenames: string[];
1452
- warnings?: {
1453
- type: string;
1454
- message: string;
1455
- file: string;
1456
- }[] | undefined;
1457
- }, {
1458
- version: 1;
1459
1463
  config: {
1460
- version: number;
1461
1464
  type: string;
1462
- language: string;
1465
+ version: number;
1463
1466
  project: string;
1467
+ language: string;
1464
1468
  features: {
1465
1469
  issues: boolean;
1466
1470
  tickets: boolean;
@@ -1476,10 +1480,19 @@ declare const SnapshotV1Schema: z.ZodObject<{
1476
1480
  maxTicketsPerSession?: number | undefined;
1477
1481
  compactThreshold?: string | undefined;
1478
1482
  reviewBackends?: string[] | undefined;
1483
+ handoverInterval?: number | undefined;
1484
+ stages?: Record<string, Record<string, unknown>> | undefined;
1479
1485
  } | undefined;
1480
1486
  } & {
1481
1487
  [k: string]: unknown;
1482
1488
  };
1489
+ handoverFilenames: string[];
1490
+ warnings?: {
1491
+ message: string;
1492
+ type: string;
1493
+ file: string;
1494
+ }[] | undefined;
1495
+ }, {
1483
1496
  issues: z.objectInputType<{
1484
1497
  id: z.ZodString;
1485
1498
  title: z.ZodString;
@@ -1516,8 +1529,8 @@ declare const SnapshotV1Schema: z.ZodObject<{
1516
1529
  claimedBySession: z.ZodOptional<z.ZodNullable<z.ZodString>>;
1517
1530
  }, z.ZodTypeAny, "passthrough">[];
1518
1531
  roadmap: {
1519
- date: string;
1520
1532
  title: string;
1533
+ date: string;
1521
1534
  phases: z.objectInputType<{
1522
1535
  id: z.ZodString;
1523
1536
  label: z.ZodString;
@@ -1535,8 +1548,35 @@ declare const SnapshotV1Schema: z.ZodObject<{
1535
1548
  } & {
1536
1549
  [k: string]: unknown;
1537
1550
  };
1551
+ version: 1;
1538
1552
  project: string;
1539
1553
  createdAt: string;
1554
+ config: {
1555
+ type: string;
1556
+ version: number;
1557
+ project: string;
1558
+ language: string;
1559
+ features: {
1560
+ issues: boolean;
1561
+ tickets: boolean;
1562
+ handovers: boolean;
1563
+ roadmap: boolean;
1564
+ reviews: boolean;
1565
+ } & {
1566
+ [k: string]: unknown;
1567
+ };
1568
+ schemaVersion?: number | undefined;
1569
+ recipe?: string | undefined;
1570
+ recipeOverrides?: {
1571
+ maxTicketsPerSession?: number | undefined;
1572
+ compactThreshold?: string | undefined;
1573
+ reviewBackends?: string[] | undefined;
1574
+ handoverInterval?: number | undefined;
1575
+ stages?: Record<string, Record<string, unknown>> | undefined;
1576
+ } | undefined;
1577
+ } & {
1578
+ [k: string]: unknown;
1579
+ };
1540
1580
  notes?: z.objectInputType<{
1541
1581
  id: z.ZodString;
1542
1582
  title: z.ZodNullable<z.ZodString>;
@@ -1561,8 +1601,8 @@ declare const SnapshotV1Schema: z.ZodObject<{
1561
1601
  status: z.ZodEnum<["active", "deprecated", "superseded"]>;
1562
1602
  }, z.ZodTypeAny, "passthrough">[] | undefined;
1563
1603
  warnings?: {
1564
- type: string;
1565
1604
  message: string;
1605
+ type: string;
1566
1606
  file: string;
1567
1607
  }[] | undefined;
1568
1608
  handoverFilenames?: string[] | undefined;
package/dist/index.js CHANGED
@@ -140,7 +140,9 @@ var ConfigSchema = z6.object({
140
140
  recipeOverrides: z6.object({
141
141
  maxTicketsPerSession: z6.number().min(0).optional(),
142
142
  compactThreshold: z6.string().optional(),
143
- reviewBackends: z6.array(z6.string()).optional()
143
+ reviewBackends: z6.array(z6.string()).optional(),
144
+ handoverInterval: z6.number().min(0).optional(),
145
+ stages: z6.record(z6.record(z6.unknown())).optional()
144
146
  }).optional()
145
147
  }).passthrough();
146
148
 
package/dist/mcp.js CHANGED
@@ -240,7 +240,9 @@ var init_config = __esm({
240
240
  recipeOverrides: z7.object({
241
241
  maxTicketsPerSession: z7.number().min(0).optional(),
242
242
  compactThreshold: z7.string().optional(),
243
- reviewBackends: z7.array(z7.string()).optional()
243
+ reviewBackends: z7.array(z7.string()).optional(),
244
+ handoverInterval: z7.number().min(0).optional(),
245
+ stages: z7.record(z7.record(z7.unknown())).optional()
244
246
  }).optional()
245
247
  }).passthrough();
246
248
  }
@@ -2982,7 +2984,20 @@ function validateRelatedTickets(ids, state) {
2982
2984
  }
2983
2985
  }
2984
2986
  }
2987
+ function buildErrorMultiset2(findings) {
2988
+ const counts = /* @__PURE__ */ new Map();
2989
+ const messages = /* @__PURE__ */ new Map();
2990
+ for (const f of findings) {
2991
+ if (f.level !== "error") continue;
2992
+ const key = `${f.code}|${f.entity ?? ""}|${f.message}`;
2993
+ counts.set(key, (counts.get(key) ?? 0) + 1);
2994
+ messages.set(key, f.message);
2995
+ }
2996
+ return { counts, messages };
2997
+ }
2985
2998
  function validatePostWriteIssueState(candidate, state, isCreate) {
2999
+ const preResult = validateProject(state);
3000
+ const { counts: preErrors } = buildErrorMultiset2(preResult.findings);
2986
3001
  const existingIssues = [...state.issues];
2987
3002
  if (isCreate) {
2988
3003
  existingIssues.push(candidate);
@@ -2999,11 +3014,17 @@ function validatePostWriteIssueState(candidate, state, isCreate) {
2999
3014
  config: state.config,
3000
3015
  handoverFilenames: [...state.handoverFilenames]
3001
3016
  });
3002
- const result = validateProject(postState);
3003
- if (!result.valid) {
3004
- const errors = result.findings.filter((f) => f.level === "error");
3005
- const msg = errors.map((f) => f.message).join("; ");
3006
- throw new CliValidationError("validation_failed", `Write would create invalid state: ${msg}`);
3017
+ const postResult = validateProject(postState);
3018
+ const { counts: postErrors, messages: postMessages } = buildErrorMultiset2(postResult.findings);
3019
+ const newErrors = [];
3020
+ for (const [key, postCount] of postErrors) {
3021
+ const preCount = preErrors.get(key) ?? 0;
3022
+ if (postCount > preCount) {
3023
+ newErrors.push(postMessages.get(key) ?? key);
3024
+ }
3025
+ }
3026
+ if (newErrors.length > 0) {
3027
+ throw new CliValidationError("validation_failed", `Write would create invalid state: ${newErrors.join("; ")}`);
3007
3028
  }
3008
3029
  }
3009
3030
  async function handleIssueCreate(args, format, root) {
@@ -4203,7 +4224,20 @@ function validateParentTicket(parentId, ticketId, state) {
4203
4224
  throw new CliValidationError("invalid_input", `Parent ticket ${parentId} not found`);
4204
4225
  }
4205
4226
  }
4227
+ function buildErrorMultiset(findings) {
4228
+ const counts = /* @__PURE__ */ new Map();
4229
+ const messages = /* @__PURE__ */ new Map();
4230
+ for (const f of findings) {
4231
+ if (f.level !== "error") continue;
4232
+ const key = `${f.code}|${f.entity ?? ""}|${f.message}`;
4233
+ counts.set(key, (counts.get(key) ?? 0) + 1);
4234
+ messages.set(key, f.message);
4235
+ }
4236
+ return { counts, messages };
4237
+ }
4206
4238
  function validatePostWriteState(candidate, state, isCreate) {
4239
+ const preResult = validateProject(state);
4240
+ const { counts: preErrors } = buildErrorMultiset(preResult.findings);
4207
4241
  const existingTickets = [...state.tickets];
4208
4242
  if (isCreate) {
4209
4243
  existingTickets.push(candidate);
@@ -4220,11 +4254,17 @@ function validatePostWriteState(candidate, state, isCreate) {
4220
4254
  config: state.config,
4221
4255
  handoverFilenames: [...state.handoverFilenames]
4222
4256
  });
4223
- const result = validateProject(postState);
4224
- if (!result.valid) {
4225
- const errors = result.findings.filter((f) => f.level === "error");
4226
- const msg = errors.map((f) => f.message).join("; ");
4227
- throw new CliValidationError("validation_failed", `Write would create invalid state: ${msg}`);
4257
+ const postResult = validateProject(postState);
4258
+ const { counts: postErrors, messages: postMessages } = buildErrorMultiset(postResult.findings);
4259
+ const newErrors = [];
4260
+ for (const [key, postCount] of postErrors) {
4261
+ const preCount = preErrors.get(key) ?? 0;
4262
+ if (postCount > preCount) {
4263
+ newErrors.push(postMessages.get(key) ?? key);
4264
+ }
4265
+ }
4266
+ if (newErrors.length > 0) {
4267
+ throw new CliValidationError("validation_failed", `Write would create invalid state: ${newErrors.join("; ")}`);
4228
4268
  }
4229
4269
  }
4230
4270
  async function handleTicketCreate(args, format, root) {
@@ -5512,7 +5552,19 @@ function resolveRecipe(recipeName, projectOverrides) {
5512
5552
  }
5513
5553
  }
5514
5554
  let pipeline = raw.pipeline ? [...raw.pipeline] : [...DEFAULT_PIPELINE];
5515
- const stages2 = raw.stages ?? {};
5555
+ const recipeStages = raw.stages ?? {};
5556
+ const stageOverrides = projectOverrides?.stages ?? {};
5557
+ const stages2 = {};
5558
+ for (const [key, value] of Object.entries(recipeStages)) {
5559
+ const override = stageOverrides[key];
5560
+ const safeOverride = override && typeof override === "object" && !Array.isArray(override) ? override : {};
5561
+ stages2[key] = { ...value, ...safeOverride };
5562
+ }
5563
+ for (const [key, value] of Object.entries(stageOverrides)) {
5564
+ if (!stages2[key] && value && typeof value === "object" && !Array.isArray(value)) {
5565
+ stages2[key] = { ...value };
5566
+ }
5567
+ }
5516
5568
  if (stages2.WRITE_TESTS?.enabled) {
5517
5569
  const implementIdx = pipeline.indexOf("IMPLEMENT");
5518
5570
  if (implementIdx !== -1 && !pipeline.includes("WRITE_TESTS")) {
@@ -7531,6 +7583,9 @@ async function handleStart(root, args) {
7531
7583
  if (typeof overrides.compactThreshold === "string") sessionConfig.compactThreshold = overrides.compactThreshold;
7532
7584
  if (Array.isArray(overrides.reviewBackends)) sessionConfig.reviewBackends = overrides.reviewBackends;
7533
7585
  if (typeof overrides.handoverInterval === "number") sessionConfig.handoverInterval = overrides.handoverInterval;
7586
+ if (overrides.stages && typeof overrides.stages === "object") {
7587
+ sessionConfig.stageOverrides = overrides.stages;
7588
+ }
7534
7589
  }
7535
7590
  } catch {
7536
7591
  }
@@ -7540,7 +7595,8 @@ async function handleStart(root, args) {
7540
7595
  const resolvedRecipe = resolveRecipe(recipe, {
7541
7596
  maxTicketsPerSession: sessionConfig.maxTicketsPerSession,
7542
7597
  compactThreshold: sessionConfig.compactThreshold,
7543
- reviewBackends: sessionConfig.reviewBackends
7598
+ reviewBackends: sessionConfig.reviewBackends,
7599
+ stages: sessionConfig.stageOverrides
7544
7600
  });
7545
7601
  const session = createSession(root, recipe, wsId, sessionConfig);
7546
7602
  const dir = sessionDir(root, session.sessionId);
@@ -8341,14 +8397,6 @@ async function handleCancel(root, args) {
8341
8397
  if (info.state.state === "SESSION_END" || info.state.status === "completed") {
8342
8398
  return guideError(new Error("Session already ended."));
8343
8399
  }
8344
- const CANCELLABLE_STATES = /* @__PURE__ */ new Set(["PICK_TICKET", "COMPLETE", "HANDOVER"]);
8345
- if (info.state.recipe === "coding" && !CANCELLABLE_STATES.has(info.state.state)) {
8346
- const sessionMode = info.state.mode ?? "auto";
8347
- 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.";
8348
- return guideError(new Error(
8349
- `Cannot cancel a coding session from ${info.state.state}. ${modeGuidance}`
8350
- ));
8351
- }
8352
8400
  await recoverPendingMutation(info.dir, info.state, root);
8353
8401
  const cancelInfo = findSessionById(root, args.sessionId) ?? info;
8354
8402
  let ticketReleased = false;
@@ -9511,7 +9559,7 @@ async function ensureGitignoreEntries(gitignorePath, entries) {
9511
9559
  // src/mcp/index.ts
9512
9560
  var ENV_VAR2 = "CLAUDESTORY_PROJECT_ROOT";
9513
9561
  var CONFIG_PATH2 = ".story/config.json";
9514
- var version = "0.1.33";
9562
+ var version = "0.1.35";
9515
9563
  function tryDiscoverRoot() {
9516
9564
  const envRoot = process.env[ENV_VAR2];
9517
9565
  if (envRoot) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anthropologies/claudestory",
3
- "version": "0.1.33",
3
+ "version": "0.1.35",
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": [