@anthropologies/claudestory 0.1.18 → 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);
@@ -7219,21 +7229,38 @@ var init_complete = __esm({
7219
7229
  'Call me with completedAction: "handover_written" and include the content in handoverContent.'
7220
7230
  ].join("\n"),
7221
7231
  reminders: [],
7222
- transitionedFrom: "COMPLETE",
7223
- contextAdvice: "ok"
7232
+ transitionedFrom: "COMPLETE"
7224
7233
  }
7225
7234
  };
7226
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
+ }
7227
7261
  let nextTarget;
7228
- let advice = "ok";
7229
7262
  if (maxTickets > 0 && ticketsDone >= maxTickets) {
7230
7263
  nextTarget = "HANDOVER";
7231
- } else if (pressure === "critical") {
7232
- advice = "compact-now";
7233
- nextTarget = "PICK_TICKET";
7234
- } else if (pressure === "high") {
7235
- advice = "consider-compact";
7236
- nextTarget = "PICK_TICKET";
7237
7264
  } else {
7238
7265
  nextTarget = "PICK_TICKET";
7239
7266
  }
@@ -7262,7 +7289,7 @@ var init_complete = __esm({
7262
7289
  ].join("\n"),
7263
7290
  reminders: [],
7264
7291
  transitionedFrom: "COMPLETE",
7265
- contextAdvice: advice
7292
+ contextAdvice: "ok"
7266
7293
  }
7267
7294
  };
7268
7295
  }
@@ -7730,6 +7757,7 @@ async function handleStart(root, args) {
7730
7757
  if (typeof overrides.maxTicketsPerSession === "number") sessionConfig.maxTicketsPerSession = overrides.maxTicketsPerSession;
7731
7758
  if (typeof overrides.compactThreshold === "string") sessionConfig.compactThreshold = overrides.compactThreshold;
7732
7759
  if (Array.isArray(overrides.reviewBackends)) sessionConfig.reviewBackends = overrides.reviewBackends;
7760
+ if (typeof overrides.handoverInterval === "number") sessionConfig.handoverInterval = overrides.handoverInterval;
7733
7761
  }
7734
7762
  } catch {
7735
7763
  }
@@ -8580,7 +8608,7 @@ function guideResult(state, currentState, opts) {
8580
8608
  transitionedFrom: opts.transitionedFrom,
8581
8609
  instruction: opts.instruction,
8582
8610
  reminders: opts.reminders ?? [],
8583
- contextAdvice: opts.contextAdvice ?? "ok",
8611
+ contextAdvice: "ok",
8584
8612
  sessionSummary: summary
8585
8613
  };
8586
8614
  const parts = [
@@ -8594,7 +8622,6 @@ function guideResult(state, currentState, opts) {
8594
8622
  `**Completed:** ${summary.completed.length > 0 ? summary.completed.join(", ") : "none"}`,
8595
8623
  `**Tickets done:** ${summary.completed.length}`,
8596
8624
  summary.branch ? `**Branch:** ${summary.branch}` : "",
8597
- output.contextAdvice !== "ok" ? `**Context:** ${output.contextAdvice}` : "",
8598
8625
  output.reminders.length > 0 ? `
8599
8626
  **Reminders:**
8600
8627
  ${output.reminders.map((r) => `- ${r}`).join("\n")}` : ""
@@ -9996,7 +10023,7 @@ var init_mcp = __esm({
9996
10023
  init_init();
9997
10024
  ENV_VAR2 = "CLAUDESTORY_PROJECT_ROOT";
9998
10025
  CONFIG_PATH2 = ".story/config.json";
9999
- version = "0.1.18";
10026
+ version = "0.1.20";
10000
10027
  main().catch((err) => {
10001
10028
  process.stderr.write(`Fatal: ${err instanceof Error ? err.message : String(err)}
10002
10029
  `);
@@ -13274,7 +13301,7 @@ async function runCli() {
13274
13301
  registerConfigCommand: registerConfigCommand2,
13275
13302
  registerSessionCommand: registerSessionCommand2
13276
13303
  } = await Promise.resolve().then(() => (init_register(), register_exports));
13277
- const version2 = "0.1.18";
13304
+ const version2 = "0.1.20";
13278
13305
  class HandledError extends Error {
13279
13306
  constructor() {
13280
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();
@@ -6722,21 +6743,38 @@ var CompleteStage = class {
6722
6743
  'Call me with completedAction: "handover_written" and include the content in handoverContent.'
6723
6744
  ].join("\n"),
6724
6745
  reminders: [],
6725
- transitionedFrom: "COMPLETE",
6726
- contextAdvice: "ok"
6746
+ transitionedFrom: "COMPLETE"
6727
6747
  }
6728
6748
  };
6729
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
+ }
6730
6775
  let nextTarget;
6731
- let advice = "ok";
6732
6776
  if (maxTickets > 0 && ticketsDone >= maxTickets) {
6733
6777
  nextTarget = "HANDOVER";
6734
- } else if (pressure === "critical") {
6735
- advice = "compact-now";
6736
- nextTarget = "PICK_TICKET";
6737
- } else if (pressure === "high") {
6738
- advice = "consider-compact";
6739
- nextTarget = "PICK_TICKET";
6740
6778
  } else {
6741
6779
  nextTarget = "PICK_TICKET";
6742
6780
  }
@@ -6765,7 +6803,7 @@ var CompleteStage = class {
6765
6803
  ].join("\n"),
6766
6804
  reminders: [],
6767
6805
  transitionedFrom: "COMPLETE",
6768
- contextAdvice: advice
6806
+ contextAdvice: "ok"
6769
6807
  }
6770
6808
  };
6771
6809
  }
@@ -6911,6 +6949,7 @@ Impact: ${nextIssue.impact}` : ""}` : `Fix issue ${next}.`,
6911
6949
 
6912
6950
  // src/autonomous/stages/handover.ts
6913
6951
  init_esm_shims();
6952
+ init_handover();
6914
6953
  import { writeFileSync as writeFileSync2 } from "fs";
6915
6954
  import { join as join10 } from "path";
6916
6955
  var HandoverStage = class {
@@ -7005,6 +7044,7 @@ init_project_loader();
7005
7044
  init_snapshot();
7006
7045
  init_snapshot();
7007
7046
  init_queries();
7047
+ init_handover();
7008
7048
  async function recoverPendingMutation(dir, state, root) {
7009
7049
  const mutation = state.pendingProjectMutation;
7010
7050
  if (!mutation || typeof mutation !== "object") return state;
@@ -7206,6 +7246,7 @@ async function handleStart(root, args) {
7206
7246
  if (typeof overrides.maxTicketsPerSession === "number") sessionConfig.maxTicketsPerSession = overrides.maxTicketsPerSession;
7207
7247
  if (typeof overrides.compactThreshold === "string") sessionConfig.compactThreshold = overrides.compactThreshold;
7208
7248
  if (Array.isArray(overrides.reviewBackends)) sessionConfig.reviewBackends = overrides.reviewBackends;
7249
+ if (typeof overrides.handoverInterval === "number") sessionConfig.handoverInterval = overrides.handoverInterval;
7209
7250
  }
7210
7251
  } catch {
7211
7252
  }
@@ -8057,7 +8098,7 @@ function guideResult(state, currentState, opts) {
8057
8098
  transitionedFrom: opts.transitionedFrom,
8058
8099
  instruction: opts.instruction,
8059
8100
  reminders: opts.reminders ?? [],
8060
- contextAdvice: opts.contextAdvice ?? "ok",
8101
+ contextAdvice: "ok",
8061
8102
  sessionSummary: summary
8062
8103
  };
8063
8104
  const parts = [
@@ -8071,7 +8112,6 @@ function guideResult(state, currentState, opts) {
8071
8112
  `**Completed:** ${summary.completed.length > 0 ? summary.completed.join(", ") : "none"}`,
8072
8113
  `**Tickets done:** ${summary.completed.length}`,
8073
8114
  summary.branch ? `**Branch:** ${summary.branch}` : "",
8074
- output.contextAdvice !== "ok" ? `**Context:** ${output.contextAdvice}` : "",
8075
8115
  output.reminders.length > 0 ? `
8076
8116
  **Reminders:**
8077
8117
  ${output.reminders.map((r) => `- ${r}`).join("\n")}` : ""
@@ -9148,7 +9188,7 @@ async function ensureGitignoreEntries(gitignorePath, entries) {
9148
9188
  // src/mcp/index.ts
9149
9189
  var ENV_VAR2 = "CLAUDESTORY_PROJECT_ROOT";
9150
9190
  var CONFIG_PATH2 = ".story/config.json";
9151
- var version = "0.1.18";
9191
+ var version = "0.1.20";
9152
9192
  function tryDiscoverRoot() {
9153
9193
  const envRoot = process.env[ENV_VAR2];
9154
9194
  if (envRoot) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anthropologies/claudestory",
3
- "version": "0.1.18",
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": [