@anthropologies/claudestory 0.1.19 → 0.1.20

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
@@ -2993,6 +2993,14 @@ var init_validate = __esm({
2993
2993
  });
2994
2994
 
2995
2995
  // src/cli/commands/handover.ts
2996
+ var handover_exports = {};
2997
+ __export(handover_exports, {
2998
+ handleHandoverCreate: () => handleHandoverCreate,
2999
+ handleHandoverGet: () => handleHandoverGet,
3000
+ handleHandoverLatest: () => handleHandoverLatest,
3001
+ handleHandoverList: () => handleHandoverList,
3002
+ normalizeSlug: () => normalizeSlug
3003
+ });
2996
3004
  import { existsSync as existsSync4 } from "fs";
2997
3005
  import { mkdir as mkdir2 } from "fs/promises";
2998
3006
  import { join as join4, resolve as resolve4 } from "path";
@@ -5144,9 +5152,10 @@ var init_session_types = __esm({
5144
5152
  // Recipe overrides (maxTicketsPerSession: 0 = no limit)
5145
5153
  config: z9.object({
5146
5154
  maxTicketsPerSession: z9.number().min(0).default(3),
5155
+ handoverInterval: z9.number().min(0).default(5),
5147
5156
  compactThreshold: z9.string().default("high"),
5148
5157
  reviewBackends: z9.array(z9.string()).default(["codex", "agent"])
5149
- }).default({ maxTicketsPerSession: 3, compactThreshold: "high", reviewBackends: ["codex", "agent"] }),
5158
+ }).default({ maxTicketsPerSession: 3, compactThreshold: "high", reviewBackends: ["codex", "agent"], handoverInterval: 5 }),
5150
5159
  // T-123: Issue sweep tracking
5151
5160
  issueSweepState: z9.object({
5152
5161
  remaining: z9.array(z9.string()),
@@ -5269,7 +5278,8 @@ function createSession(root, recipe, workspaceId, configOverrides) {
5269
5278
  config: {
5270
5279
  maxTicketsPerSession: configOverrides?.maxTicketsPerSession ?? 3,
5271
5280
  compactThreshold: configOverrides?.compactThreshold ?? "high",
5272
- reviewBackends: configOverrides?.reviewBackends ?? ["codex", "agent"]
5281
+ reviewBackends: configOverrides?.reviewBackends ?? ["codex", "agent"],
5282
+ handoverInterval: configOverrides?.handoverInterval ?? 5
5273
5283
  }
5274
5284
  };
5275
5285
  writeSessionSync(dir, state);
@@ -7223,6 +7233,31 @@ var init_complete = __esm({
7223
7233
  }
7224
7234
  };
7225
7235
  }
7236
+ const handoverInterval = ctx.state.config.handoverInterval ?? 5;
7237
+ if (handoverInterval > 0 && ticketsDone > 0 && ticketsDone % handoverInterval === 0) {
7238
+ try {
7239
+ const { handleHandoverCreate: handleHandoverCreate3 } = await Promise.resolve().then(() => (init_handover(), handover_exports));
7240
+ const completedIds = ctx.state.completedTickets.map((t) => t.id).join(", ");
7241
+ const content = [
7242
+ `# Checkpoint \u2014 ${ticketsDone} tickets completed`,
7243
+ "",
7244
+ `**Session:** ${ctx.state.sessionId}`,
7245
+ `**Tickets:** ${completedIds}`,
7246
+ "",
7247
+ "This is an automatic mid-session checkpoint. The session is still active."
7248
+ ].join("\n");
7249
+ await handleHandoverCreate3(content, "checkpoint", "md", ctx.root);
7250
+ } catch {
7251
+ }
7252
+ try {
7253
+ const { loadProject: loadProject2 } = await Promise.resolve().then(() => (init_project_loader(), project_loader_exports));
7254
+ const { saveSnapshot: saveSnapshot2 } = await Promise.resolve().then(() => (init_snapshot(), snapshot_exports));
7255
+ const loadResult = await loadProject2(ctx.root);
7256
+ await saveSnapshot2(ctx.root, loadResult);
7257
+ } catch {
7258
+ }
7259
+ ctx.appendEvent("checkpoint", { ticketsDone, interval: handoverInterval });
7260
+ }
7226
7261
  let nextTarget;
7227
7262
  if (maxTickets > 0 && ticketsDone >= maxTickets) {
7228
7263
  nextTarget = "HANDOVER";
@@ -7722,6 +7757,7 @@ async function handleStart(root, args) {
7722
7757
  if (typeof overrides.maxTicketsPerSession === "number") sessionConfig.maxTicketsPerSession = overrides.maxTicketsPerSession;
7723
7758
  if (typeof overrides.compactThreshold === "string") sessionConfig.compactThreshold = overrides.compactThreshold;
7724
7759
  if (Array.isArray(overrides.reviewBackends)) sessionConfig.reviewBackends = overrides.reviewBackends;
7760
+ if (typeof overrides.handoverInterval === "number") sessionConfig.handoverInterval = overrides.handoverInterval;
7725
7761
  }
7726
7762
  } catch {
7727
7763
  }
@@ -9987,7 +10023,7 @@ var init_mcp = __esm({
9987
10023
  init_init();
9988
10024
  ENV_VAR2 = "CLAUDESTORY_PROJECT_ROOT";
9989
10025
  CONFIG_PATH2 = ".story/config.json";
9990
- version = "0.1.19";
10026
+ version = "0.1.20";
9991
10027
  main().catch((err) => {
9992
10028
  process.stderr.write(`Fatal: ${err instanceof Error ? err.message : String(err)}
9993
10029
  `);
@@ -13265,7 +13301,7 @@ async function runCli() {
13265
13301
  registerConfigCommand: registerConfigCommand2,
13266
13302
  registerSessionCommand: registerSessionCommand2
13267
13303
  } = await Promise.resolve().then(() => (init_register(), register_exports));
13268
- const version2 = "0.1.19";
13304
+ const version2 = "0.1.20";
13269
13305
  class HandledError extends Error {
13270
13306
  constructor() {
13271
13307
  super("HANDLED_ERROR");
package/dist/index.d.ts CHANGED
@@ -1331,15 +1331,40 @@ declare const SnapshotV1Schema: z.ZodObject<{
1331
1331
  file: z.ZodString;
1332
1332
  message: z.ZodString;
1333
1333
  }, "strip", z.ZodTypeAny, {
1334
- message: string;
1335
1334
  type: string;
1335
+ message: string;
1336
1336
  file: string;
1337
1337
  }, {
1338
- message: string;
1339
1338
  type: string;
1339
+ message: 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
+ };
1343
1368
  issues: z.objectOutputType<{
1344
1369
  id: z.ZodString;
1345
1370
  title: z.ZodString;
@@ -1376,8 +1401,8 @@ declare const SnapshotV1Schema: z.ZodObject<{
1376
1401
  claimedBySession: z.ZodOptional<z.ZodNullable<z.ZodString>>;
1377
1402
  }, z.ZodTypeAny, "passthrough">[];
1378
1403
  roadmap: {
1379
- title: string;
1380
1404
  date: string;
1405
+ title: string;
1381
1406
  phases: z.objectOutputType<{
1382
1407
  id: z.ZodString;
1383
1408
  label: z.ZodString;
@@ -1395,7 +1420,6 @@ declare const SnapshotV1Schema: z.ZodObject<{
1395
1420
  } & {
1396
1421
  [k: string]: unknown;
1397
1422
  };
1398
- version: 1;
1399
1423
  project: string;
1400
1424
  notes: z.objectOutputType<{
1401
1425
  id: z.ZodString;
@@ -1421,11 +1445,19 @@ declare const SnapshotV1Schema: z.ZodObject<{
1421
1445
  status: z.ZodEnum<["active", "deprecated", "superseded"]>;
1422
1446
  }, z.ZodTypeAny, "passthrough">[];
1423
1447
  createdAt: string;
1424
- config: {
1448
+ handoverFilenames: string[];
1449
+ warnings?: {
1425
1450
  type: string;
1451
+ message: string;
1452
+ file: string;
1453
+ }[] | undefined;
1454
+ }, {
1455
+ version: 1;
1456
+ config: {
1426
1457
  version: number;
1427
- project: string;
1458
+ type: string;
1428
1459
  language: string;
1460
+ project: string;
1429
1461
  features: {
1430
1462
  issues: boolean;
1431
1463
  tickets: boolean;
@@ -1445,13 +1477,6 @@ declare const SnapshotV1Schema: z.ZodObject<{
1445
1477
  } & {
1446
1478
  [k: string]: unknown;
1447
1479
  };
1448
- handoverFilenames: string[];
1449
- warnings?: {
1450
- message: string;
1451
- type: string;
1452
- file: string;
1453
- }[] | undefined;
1454
- }, {
1455
1480
  issues: z.objectInputType<{
1456
1481
  id: z.ZodString;
1457
1482
  title: z.ZodString;
@@ -1488,8 +1513,8 @@ declare const SnapshotV1Schema: z.ZodObject<{
1488
1513
  claimedBySession: z.ZodOptional<z.ZodNullable<z.ZodString>>;
1489
1514
  }, z.ZodTypeAny, "passthrough">[];
1490
1515
  roadmap: {
1491
- title: string;
1492
1516
  date: string;
1517
+ title: string;
1493
1518
  phases: z.objectInputType<{
1494
1519
  id: z.ZodString;
1495
1520
  label: z.ZodString;
@@ -1507,33 +1532,8 @@ declare const SnapshotV1Schema: z.ZodObject<{
1507
1532
  } & {
1508
1533
  [k: string]: unknown;
1509
1534
  };
1510
- version: 1;
1511
1535
  project: string;
1512
1536
  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
- message: string;
1562
1561
  type: string;
1562
+ message: string;
1563
1563
  file: string;
1564
1564
  }[] | undefined;
1565
1565
  handoverFilenames?: string[] | undefined;
package/dist/mcp.js CHANGED
@@ -2689,6 +2689,157 @@ var init_validation = __esm({
2689
2689
  }
2690
2690
  });
2691
2691
 
2692
+ // src/cli/commands/handover.ts
2693
+ var handover_exports = {};
2694
+ __export(handover_exports, {
2695
+ handleHandoverCreate: () => handleHandoverCreate,
2696
+ handleHandoverGet: () => handleHandoverGet,
2697
+ handleHandoverLatest: () => handleHandoverLatest,
2698
+ handleHandoverList: () => handleHandoverList,
2699
+ normalizeSlug: () => normalizeSlug
2700
+ });
2701
+ import { existsSync as existsSync4 } from "fs";
2702
+ import { mkdir as mkdir2 } from "fs/promises";
2703
+ import { join as join4, resolve as resolve4 } from "path";
2704
+ function handleHandoverList(ctx) {
2705
+ return { output: formatHandoverList(ctx.state.handoverFilenames, ctx.format) };
2706
+ }
2707
+ async function handleHandoverLatest(ctx, count = 1) {
2708
+ if (ctx.state.handoverFilenames.length === 0) {
2709
+ return {
2710
+ output: formatError("not_found", "No handovers found", ctx.format),
2711
+ exitCode: ExitCode.USER_ERROR,
2712
+ errorCode: "not_found"
2713
+ };
2714
+ }
2715
+ const filenames = ctx.state.handoverFilenames.slice(0, count);
2716
+ const parts = [];
2717
+ for (const filename of filenames) {
2718
+ await parseHandoverFilename(filename, ctx.handoversDir);
2719
+ try {
2720
+ const content = await readHandover(ctx.handoversDir, filename);
2721
+ parts.push(formatHandoverContent(filename, content, ctx.format));
2722
+ } catch (err) {
2723
+ if (err.code === "ENOENT") {
2724
+ if (count > 1) continue;
2725
+ return {
2726
+ output: formatError("not_found", `Handover file not found: ${filename}`, ctx.format),
2727
+ exitCode: ExitCode.USER_ERROR,
2728
+ errorCode: "not_found"
2729
+ };
2730
+ }
2731
+ return {
2732
+ output: formatError("io_error", `Cannot read handover: ${err.message}`, ctx.format),
2733
+ exitCode: ExitCode.USER_ERROR,
2734
+ errorCode: "io_error"
2735
+ };
2736
+ }
2737
+ }
2738
+ if (parts.length === 0) {
2739
+ return {
2740
+ output: formatError("not_found", "No handovers found", ctx.format),
2741
+ exitCode: ExitCode.USER_ERROR,
2742
+ errorCode: "not_found"
2743
+ };
2744
+ }
2745
+ const separator = ctx.format === "json" ? "\n" : "\n\n---\n\n";
2746
+ return { output: parts.join(separator) };
2747
+ }
2748
+ async function handleHandoverGet(filename, ctx) {
2749
+ await parseHandoverFilename(filename, ctx.handoversDir);
2750
+ try {
2751
+ const content = await readHandover(ctx.handoversDir, filename);
2752
+ return { output: formatHandoverContent(filename, content, ctx.format) };
2753
+ } catch (err) {
2754
+ if (err.code === "ENOENT") {
2755
+ return {
2756
+ output: formatError("not_found", `Handover not found: ${filename}`, ctx.format),
2757
+ exitCode: ExitCode.USER_ERROR,
2758
+ errorCode: "not_found"
2759
+ };
2760
+ }
2761
+ return {
2762
+ output: formatError("io_error", `Cannot read handover: ${err.message}`, ctx.format),
2763
+ exitCode: ExitCode.USER_ERROR,
2764
+ errorCode: "io_error"
2765
+ };
2766
+ }
2767
+ }
2768
+ function normalizeSlug(raw) {
2769
+ let slug = raw.trim().toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
2770
+ if (slug.length > 60) slug = slug.slice(0, 60).replace(/-$/, "");
2771
+ if (!slug) {
2772
+ throw new CliValidationError(
2773
+ "invalid_input",
2774
+ `Slug is empty after normalization: "${raw}"`
2775
+ );
2776
+ }
2777
+ return slug;
2778
+ }
2779
+ async function handleHandoverCreate(content, slugRaw, format, root) {
2780
+ if (!content.trim()) {
2781
+ throw new CliValidationError("invalid_input", "Handover content is empty");
2782
+ }
2783
+ const slug = normalizeSlug(slugRaw);
2784
+ const date = todayISO();
2785
+ let filename;
2786
+ await withProjectLock(root, { strict: false }, async () => {
2787
+ const absRoot = resolve4(root);
2788
+ const handoversDir = join4(absRoot, ".story", "handovers");
2789
+ await mkdir2(handoversDir, { recursive: true });
2790
+ const wrapDir = join4(absRoot, ".story");
2791
+ const datePrefix = `${date}-`;
2792
+ const seqRegex = new RegExp(`^${date}-(\\d{2})-`);
2793
+ let maxSeq = 0;
2794
+ const { readdirSync: readdirSync2 } = await import("fs");
2795
+ try {
2796
+ for (const f of readdirSync2(handoversDir)) {
2797
+ const m = f.match(seqRegex);
2798
+ if (m) {
2799
+ const n = parseInt(m[1], 10);
2800
+ if (n > maxSeq) maxSeq = n;
2801
+ }
2802
+ }
2803
+ } catch {
2804
+ }
2805
+ let nextSeq = maxSeq + 1;
2806
+ if (nextSeq > 99) {
2807
+ throw new CliValidationError(
2808
+ "conflict",
2809
+ `Too many handovers for ${date}; limit is 99 per day`
2810
+ );
2811
+ }
2812
+ let candidate = `${date}-${String(nextSeq).padStart(2, "0")}-${slug}.md`;
2813
+ let candidatePath = join4(handoversDir, candidate);
2814
+ while (existsSync4(candidatePath)) {
2815
+ nextSeq++;
2816
+ if (nextSeq > 99) {
2817
+ throw new CliValidationError(
2818
+ "conflict",
2819
+ `Too many handovers for ${date}; limit is 99 per day`
2820
+ );
2821
+ }
2822
+ candidate = `${date}-${String(nextSeq).padStart(2, "0")}-${slug}.md`;
2823
+ candidatePath = join4(handoversDir, candidate);
2824
+ }
2825
+ await parseHandoverFilename(candidate, handoversDir);
2826
+ await guardPath(candidatePath, wrapDir);
2827
+ await atomicWrite(candidatePath, content);
2828
+ filename = candidate;
2829
+ });
2830
+ return { output: formatHandoverCreateResult(filename, format) };
2831
+ }
2832
+ var init_handover = __esm({
2833
+ "src/cli/commands/handover.ts"() {
2834
+ "use strict";
2835
+ init_esm_shims();
2836
+ init_handover_parser();
2837
+ init_output_formatter();
2838
+ init_project_loader();
2839
+ init_helpers();
2840
+ }
2841
+ });
2842
+
2692
2843
  // src/core/id-allocation.ts
2693
2844
  function nextTicketID(tickets) {
2694
2845
  let max = 0;
@@ -3445,9 +3596,10 @@ var init_session_types = __esm({
3445
3596
  // Recipe overrides (maxTicketsPerSession: 0 = no limit)
3446
3597
  config: z9.object({
3447
3598
  maxTicketsPerSession: z9.number().min(0).default(3),
3599
+ handoverInterval: z9.number().min(0).default(5),
3448
3600
  compactThreshold: z9.string().default("high"),
3449
3601
  reviewBackends: z9.array(z9.string()).default(["codex", "agent"])
3450
- }).default({ maxTicketsPerSession: 3, compactThreshold: "high", reviewBackends: ["codex", "agent"] }),
3602
+ }).default({ maxTicketsPerSession: 3, compactThreshold: "high", reviewBackends: ["codex", "agent"], handoverInterval: 5 }),
3451
3603
  // T-123: Issue sweep tracking
3452
3604
  issueSweepState: z9.object({
3453
3605
  remaining: z9.array(z9.string()),
@@ -3570,7 +3722,8 @@ function createSession(root, recipe, workspaceId, configOverrides) {
3570
3722
  config: {
3571
3723
  maxTicketsPerSession: configOverrides?.maxTicketsPerSession ?? 3,
3572
3724
  compactThreshold: configOverrides?.compactThreshold ?? "high",
3573
- reviewBackends: configOverrides?.reviewBackends ?? ["codex", "agent"]
3725
+ reviewBackends: configOverrides?.reviewBackends ?? ["codex", "agent"],
3726
+ handoverInterval: configOverrides?.handoverInterval ?? 5
3574
3727
  }
3575
3728
  };
3576
3729
  writeSessionSync(dir, state);
@@ -3926,143 +4079,8 @@ function handleValidate(ctx) {
3926
4079
  };
3927
4080
  }
3928
4081
 
3929
- // src/cli/commands/handover.ts
3930
- init_esm_shims();
3931
- init_handover_parser();
3932
- init_output_formatter();
3933
- init_project_loader();
3934
- init_helpers();
3935
- import { existsSync as existsSync4 } from "fs";
3936
- import { mkdir as mkdir2 } from "fs/promises";
3937
- import { join as join4, resolve as resolve4 } from "path";
3938
- function handleHandoverList(ctx) {
3939
- return { output: formatHandoverList(ctx.state.handoverFilenames, ctx.format) };
3940
- }
3941
- async function handleHandoverLatest(ctx, count = 1) {
3942
- if (ctx.state.handoverFilenames.length === 0) {
3943
- return {
3944
- output: formatError("not_found", "No handovers found", ctx.format),
3945
- exitCode: ExitCode.USER_ERROR,
3946
- errorCode: "not_found"
3947
- };
3948
- }
3949
- const filenames = ctx.state.handoverFilenames.slice(0, count);
3950
- const parts = [];
3951
- for (const filename of filenames) {
3952
- await parseHandoverFilename(filename, ctx.handoversDir);
3953
- try {
3954
- const content = await readHandover(ctx.handoversDir, filename);
3955
- parts.push(formatHandoverContent(filename, content, ctx.format));
3956
- } catch (err) {
3957
- if (err.code === "ENOENT") {
3958
- if (count > 1) continue;
3959
- return {
3960
- output: formatError("not_found", `Handover file not found: ${filename}`, ctx.format),
3961
- exitCode: ExitCode.USER_ERROR,
3962
- errorCode: "not_found"
3963
- };
3964
- }
3965
- return {
3966
- output: formatError("io_error", `Cannot read handover: ${err.message}`, ctx.format),
3967
- exitCode: ExitCode.USER_ERROR,
3968
- errorCode: "io_error"
3969
- };
3970
- }
3971
- }
3972
- if (parts.length === 0) {
3973
- return {
3974
- output: formatError("not_found", "No handovers found", ctx.format),
3975
- exitCode: ExitCode.USER_ERROR,
3976
- errorCode: "not_found"
3977
- };
3978
- }
3979
- const separator = ctx.format === "json" ? "\n" : "\n\n---\n\n";
3980
- return { output: parts.join(separator) };
3981
- }
3982
- async function handleHandoverGet(filename, ctx) {
3983
- await parseHandoverFilename(filename, ctx.handoversDir);
3984
- try {
3985
- const content = await readHandover(ctx.handoversDir, filename);
3986
- return { output: formatHandoverContent(filename, content, ctx.format) };
3987
- } catch (err) {
3988
- if (err.code === "ENOENT") {
3989
- return {
3990
- output: formatError("not_found", `Handover not found: ${filename}`, ctx.format),
3991
- exitCode: ExitCode.USER_ERROR,
3992
- errorCode: "not_found"
3993
- };
3994
- }
3995
- return {
3996
- output: formatError("io_error", `Cannot read handover: ${err.message}`, ctx.format),
3997
- exitCode: ExitCode.USER_ERROR,
3998
- errorCode: "io_error"
3999
- };
4000
- }
4001
- }
4002
- function normalizeSlug(raw) {
4003
- let slug = raw.trim().toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
4004
- if (slug.length > 60) slug = slug.slice(0, 60).replace(/-$/, "");
4005
- if (!slug) {
4006
- throw new CliValidationError(
4007
- "invalid_input",
4008
- `Slug is empty after normalization: "${raw}"`
4009
- );
4010
- }
4011
- return slug;
4012
- }
4013
- async function handleHandoverCreate(content, slugRaw, format, root) {
4014
- if (!content.trim()) {
4015
- throw new CliValidationError("invalid_input", "Handover content is empty");
4016
- }
4017
- const slug = normalizeSlug(slugRaw);
4018
- const date = todayISO();
4019
- let filename;
4020
- await withProjectLock(root, { strict: false }, async () => {
4021
- const absRoot = resolve4(root);
4022
- const handoversDir = join4(absRoot, ".story", "handovers");
4023
- await mkdir2(handoversDir, { recursive: true });
4024
- const wrapDir = join4(absRoot, ".story");
4025
- const datePrefix = `${date}-`;
4026
- const seqRegex = new RegExp(`^${date}-(\\d{2})-`);
4027
- let maxSeq = 0;
4028
- const { readdirSync: readdirSync2 } = await import("fs");
4029
- try {
4030
- for (const f of readdirSync2(handoversDir)) {
4031
- const m = f.match(seqRegex);
4032
- if (m) {
4033
- const n = parseInt(m[1], 10);
4034
- if (n > maxSeq) maxSeq = n;
4035
- }
4036
- }
4037
- } catch {
4038
- }
4039
- let nextSeq = maxSeq + 1;
4040
- if (nextSeq > 99) {
4041
- throw new CliValidationError(
4042
- "conflict",
4043
- `Too many handovers for ${date}; limit is 99 per day`
4044
- );
4045
- }
4046
- let candidate = `${date}-${String(nextSeq).padStart(2, "0")}-${slug}.md`;
4047
- let candidatePath = join4(handoversDir, candidate);
4048
- while (existsSync4(candidatePath)) {
4049
- nextSeq++;
4050
- if (nextSeq > 99) {
4051
- throw new CliValidationError(
4052
- "conflict",
4053
- `Too many handovers for ${date}; limit is 99 per day`
4054
- );
4055
- }
4056
- candidate = `${date}-${String(nextSeq).padStart(2, "0")}-${slug}.md`;
4057
- candidatePath = join4(handoversDir, candidate);
4058
- }
4059
- await parseHandoverFilename(candidate, handoversDir);
4060
- await guardPath(candidatePath, wrapDir);
4061
- await atomicWrite(candidatePath, content);
4062
- filename = candidate;
4063
- });
4064
- return { output: formatHandoverCreateResult(filename, format) };
4065
- }
4082
+ // src/mcp/tools.ts
4083
+ init_handover();
4066
4084
 
4067
4085
  // src/cli/commands/blocker.ts
4068
4086
  init_esm_shims();
@@ -5149,6 +5167,9 @@ function errMsg(err) {
5149
5167
  return err instanceof Error ? err.message : String(err);
5150
5168
  }
5151
5169
 
5170
+ // src/mcp/tools.ts
5171
+ init_handover();
5172
+
5152
5173
  // src/autonomous/guide.ts
5153
5174
  init_esm_shims();
5154
5175
  init_session_types();
@@ -6726,6 +6747,31 @@ var CompleteStage = class {
6726
6747
  }
6727
6748
  };
6728
6749
  }
6750
+ const handoverInterval = ctx.state.config.handoverInterval ?? 5;
6751
+ if (handoverInterval > 0 && ticketsDone > 0 && ticketsDone % handoverInterval === 0) {
6752
+ try {
6753
+ const { handleHandoverCreate: handleHandoverCreate3 } = await Promise.resolve().then(() => (init_handover(), handover_exports));
6754
+ const completedIds = ctx.state.completedTickets.map((t) => t.id).join(", ");
6755
+ const content = [
6756
+ `# Checkpoint \u2014 ${ticketsDone} tickets completed`,
6757
+ "",
6758
+ `**Session:** ${ctx.state.sessionId}`,
6759
+ `**Tickets:** ${completedIds}`,
6760
+ "",
6761
+ "This is an automatic mid-session checkpoint. The session is still active."
6762
+ ].join("\n");
6763
+ await handleHandoverCreate3(content, "checkpoint", "md", ctx.root);
6764
+ } catch {
6765
+ }
6766
+ try {
6767
+ const { loadProject: loadProject2 } = await Promise.resolve().then(() => (init_project_loader(), project_loader_exports));
6768
+ const { saveSnapshot: saveSnapshot2 } = await Promise.resolve().then(() => (init_snapshot(), snapshot_exports));
6769
+ const loadResult = await loadProject2(ctx.root);
6770
+ await saveSnapshot2(ctx.root, loadResult);
6771
+ } catch {
6772
+ }
6773
+ ctx.appendEvent("checkpoint", { ticketsDone, interval: handoverInterval });
6774
+ }
6729
6775
  let nextTarget;
6730
6776
  if (maxTickets > 0 && ticketsDone >= maxTickets) {
6731
6777
  nextTarget = "HANDOVER";
@@ -6903,6 +6949,7 @@ Impact: ${nextIssue.impact}` : ""}` : `Fix issue ${next}.`,
6903
6949
 
6904
6950
  // src/autonomous/stages/handover.ts
6905
6951
  init_esm_shims();
6952
+ init_handover();
6906
6953
  import { writeFileSync as writeFileSync2 } from "fs";
6907
6954
  import { join as join10 } from "path";
6908
6955
  var HandoverStage = class {
@@ -6997,6 +7044,7 @@ init_project_loader();
6997
7044
  init_snapshot();
6998
7045
  init_snapshot();
6999
7046
  init_queries();
7047
+ init_handover();
7000
7048
  async function recoverPendingMutation(dir, state, root) {
7001
7049
  const mutation = state.pendingProjectMutation;
7002
7050
  if (!mutation || typeof mutation !== "object") return state;
@@ -7198,6 +7246,7 @@ async function handleStart(root, args) {
7198
7246
  if (typeof overrides.maxTicketsPerSession === "number") sessionConfig.maxTicketsPerSession = overrides.maxTicketsPerSession;
7199
7247
  if (typeof overrides.compactThreshold === "string") sessionConfig.compactThreshold = overrides.compactThreshold;
7200
7248
  if (Array.isArray(overrides.reviewBackends)) sessionConfig.reviewBackends = overrides.reviewBackends;
7249
+ if (typeof overrides.handoverInterval === "number") sessionConfig.handoverInterval = overrides.handoverInterval;
7201
7250
  }
7202
7251
  } catch {
7203
7252
  }
@@ -9139,7 +9188,7 @@ async function ensureGitignoreEntries(gitignorePath, entries) {
9139
9188
  // src/mcp/index.ts
9140
9189
  var ENV_VAR2 = "CLAUDESTORY_PROJECT_ROOT";
9141
9190
  var CONFIG_PATH2 = ".story/config.json";
9142
- var version = "0.1.19";
9191
+ var version = "0.1.20";
9143
9192
  function tryDiscoverRoot() {
9144
9193
  const envRoot = process.env[ENV_VAR2];
9145
9194
  if (envRoot) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anthropologies/claudestory",
3
- "version": "0.1.19",
3
+ "version": "0.1.20",
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": [