@anthropologies/claudestory 0.1.14 → 0.1.16

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
@@ -562,6 +562,29 @@ var init_handover_parser = __esm({
562
562
  });
563
563
 
564
564
  // src/core/project-loader.ts
565
+ var project_loader_exports = {};
566
+ __export(project_loader_exports, {
567
+ atomicWrite: () => atomicWrite,
568
+ deleteIssue: () => deleteIssue,
569
+ deleteNote: () => deleteNote,
570
+ deleteTicket: () => deleteTicket,
571
+ guardPath: () => guardPath,
572
+ loadProject: () => loadProject,
573
+ runTransaction: () => runTransaction,
574
+ runTransactionUnlocked: () => runTransactionUnlocked,
575
+ serializeJSON: () => serializeJSON,
576
+ sortKeysDeep: () => sortKeysDeep,
577
+ withProjectLock: () => withProjectLock,
578
+ writeConfig: () => writeConfig,
579
+ writeIssue: () => writeIssue,
580
+ writeIssueUnlocked: () => writeIssueUnlocked,
581
+ writeNote: () => writeNote,
582
+ writeNoteUnlocked: () => writeNoteUnlocked,
583
+ writeRoadmap: () => writeRoadmap,
584
+ writeRoadmapUnlocked: () => writeRoadmapUnlocked,
585
+ writeTicket: () => writeTicket,
586
+ writeTicketUnlocked: () => writeTicketUnlocked
587
+ });
565
588
  import {
566
589
  readdir as readdir2,
567
590
  readFile as readFile2,
@@ -920,6 +943,12 @@ async function runTransactionUnlocked(root, operations) {
920
943
  throw new ProjectLoaderError("io_error", "Transaction failed", err);
921
944
  }
922
945
  }
946
+ async function runTransaction(root, operations) {
947
+ const wrapDir = resolve2(root, ".story");
948
+ await withLock(wrapDir, async () => {
949
+ await runTransactionUnlocked(root, operations);
950
+ });
951
+ }
923
952
  async function doRecoverTransaction(wrapDir) {
924
953
  const journalPath = join3(wrapDir, ".txn.json");
925
954
  let entries;
@@ -5237,6 +5266,19 @@ async function handleReportPlan(root, dir, state, report) {
5237
5266
  });
5238
5267
  }
5239
5268
  const risk = assessRisk(void 0, void 0);
5269
+ if (state.ticket) {
5270
+ try {
5271
+ const { withProjectLock: withProjectLock2, writeTicketUnlocked: writeTicketUnlocked2 } = await Promise.resolve().then(() => (init_project_loader(), project_loader_exports));
5272
+ await withProjectLock2(root, { strict: false }, async ({ state: projectState }) => {
5273
+ const ticket = projectState.ticketByID(state.ticket.id);
5274
+ if (ticket && ticket.status !== "inprogress") {
5275
+ const updated = { ...ticket, status: "inprogress" };
5276
+ await writeTicketUnlocked2(updated, root);
5277
+ }
5278
+ });
5279
+ } catch {
5280
+ }
5281
+ }
5240
5282
  const written = writeSessionSync(dir, {
5241
5283
  ...state,
5242
5284
  state: "PLAN_REVIEW",
@@ -5594,11 +5636,11 @@ async function handleReportComplete(root, dir, state, report) {
5594
5636
  const maxTickets = updated.config.maxTicketsPerSession;
5595
5637
  let nextState;
5596
5638
  let advice = "ok";
5597
- if (pressure === "critical") {
5639
+ if (maxTickets > 0 && ticketsDone >= maxTickets) {
5598
5640
  nextState = "HANDOVER";
5599
- advice = "compact-now";
5600
- } else if (maxTickets > 0 && ticketsDone >= maxTickets) {
5641
+ } else if (pressure === "critical") {
5601
5642
  nextState = "HANDOVER";
5643
+ advice = "compact-now";
5602
5644
  } else if (pressure === "high") {
5603
5645
  advice = "consider-compact";
5604
5646
  nextState = "PICK_TICKET";
@@ -5678,6 +5720,36 @@ async function handleReportHandover(root, dir, state, report) {
5678
5720
  } catch {
5679
5721
  }
5680
5722
  }
5723
+ const pressureLevel = state.contextPressure?.level ?? "low";
5724
+ const maxTickets = state.config.maxTicketsPerSession;
5725
+ const capReached = maxTickets > 0 && state.completedTickets.length >= maxTickets;
5726
+ let hasMoreTickets = false;
5727
+ try {
5728
+ const { state: ps } = await loadProject(root);
5729
+ hasMoreTickets = nextTickets(ps, 1).kind === "found";
5730
+ } catch {
5731
+ }
5732
+ if (pressureLevel === "critical" && hasMoreTickets && !capReached) {
5733
+ return guideResult(state, "HANDOVER", {
5734
+ instruction: [
5735
+ "# Context Compaction Needed",
5736
+ "",
5737
+ `${state.completedTickets.length} ticket(s) completed so far. Handover written. Context is large \u2014 time to compact and continue.`,
5738
+ "",
5739
+ 'Call `claudestory_autonomous_guide` with `action: "pre_compact"` now:',
5740
+ "```json",
5741
+ `{ "sessionId": "${state.sessionId}", "action": "pre_compact" }`,
5742
+ "```",
5743
+ "",
5744
+ 'After pre_compact responds, run `/compact`, then call with `action: "resume"` to continue working on more tickets.'
5745
+ ].join("\n"),
5746
+ reminders: [
5747
+ "Do NOT stop. This is a context compaction, not a session end.",
5748
+ "Call pre_compact \u2192 /compact \u2192 resume to continue autonomous work."
5749
+ ],
5750
+ contextAdvice: "compact-now"
5751
+ });
5752
+ }
5681
5753
  const written = writeSessionSync(dir, {
5682
5754
  ...state,
5683
5755
  state: "SESSION_END",
@@ -5725,6 +5797,40 @@ async function handleResume(root, args) {
5725
5797
  resumeFromRevision: null,
5726
5798
  contextPressure: { ...info.state.contextPressure, compactionCount: (info.state.contextPressure?.compactionCount ?? 0) + 1 }
5727
5799
  });
5800
+ if (resumeState === "PICK_TICKET") {
5801
+ let candidatesText = "No ticket candidates available.";
5802
+ let topCandidate = null;
5803
+ try {
5804
+ const { state: ps } = await loadProject(root);
5805
+ const result = nextTickets(ps, 5);
5806
+ if (result.kind === "found") {
5807
+ topCandidate = result.candidates[0] ?? null;
5808
+ candidatesText = result.candidates.map(
5809
+ (c, i) => `${i + 1}. **${c.ticket.id}: ${c.ticket.title}** (${c.ticket.type})`
5810
+ ).join("\n");
5811
+ }
5812
+ } catch {
5813
+ }
5814
+ return guideResult(written, "PICK_TICKET", {
5815
+ instruction: [
5816
+ "# Resumed After Compact \u2014 Continue Working",
5817
+ "",
5818
+ `${written.completedTickets.length} ticket(s) done so far. Context compacted. Pick the next ticket immediately.`,
5819
+ "",
5820
+ candidatesText,
5821
+ "",
5822
+ topCandidate ? `Pick **${topCandidate.ticket.id}** by calling \`claudestory_autonomous_guide\` now:` : "Pick a ticket now:",
5823
+ "```json",
5824
+ topCandidate ? `{ "sessionId": "${written.sessionId}", "action": "report", "report": { "completedAction": "ticket_picked", "ticketId": "${topCandidate.ticket.id}" } }` : `{ "sessionId": "${written.sessionId}", "action": "report", "report": { "completedAction": "ticket_picked", "ticketId": "T-XXX" } }`,
5825
+ "```"
5826
+ ].join("\n"),
5827
+ reminders: [
5828
+ "Do NOT stop or summarize. Pick the next ticket IMMEDIATELY.",
5829
+ "Do NOT ask the user for confirmation.",
5830
+ "You are in autonomous mode \u2014 continue working."
5831
+ ]
5832
+ });
5833
+ }
5728
5834
  return guideResult(written, resumeState, {
5729
5835
  instruction: [
5730
5836
  "# Resumed After Compact",
@@ -5736,6 +5842,7 @@ async function handleResume(root, args) {
5736
5842
  ].join("\n"),
5737
5843
  reminders: [
5738
5844
  "Do NOT use plan mode.",
5845
+ "Do NOT stop or summarize.",
5739
5846
  "Call autonomous_guide after completing each step."
5740
5847
  ]
5741
5848
  });
@@ -5751,11 +5858,12 @@ async function handlePreCompact(root, args) {
5751
5858
  return guideError(new Error(`Session ${args.sessionId} is already in COMPACT state. Call action: "resume" to continue.`));
5752
5859
  }
5753
5860
  const headResult = await gitHead(root);
5861
+ const resumeTarget = info.state.state === "HANDOVER" ? "PICK_TICKET" : info.state.state;
5754
5862
  const written = writeSessionSync(info.dir, {
5755
5863
  ...refreshLease(info.state),
5756
5864
  state: "COMPACT",
5757
5865
  previousState: info.state.state,
5758
- preCompactState: info.state.state,
5866
+ preCompactState: resumeTarget,
5759
5867
  resumeFromRevision: info.state.revision,
5760
5868
  git: {
5761
5869
  ...info.state.git,
@@ -6835,7 +6943,7 @@ var init_mcp = __esm({
6835
6943
  init_init();
6836
6944
  ENV_VAR2 = "CLAUDESTORY_PROJECT_ROOT";
6837
6945
  CONFIG_PATH2 = ".story/config.json";
6838
- version = "0.1.14";
6946
+ version = "0.1.16";
6839
6947
  main().catch((err) => {
6840
6948
  process.stderr.write(`Fatal: ${err instanceof Error ? err.message : String(err)}
6841
6949
  `);
@@ -7714,10 +7822,95 @@ var init_hook_status = __esm({
7714
7822
  }
7715
7823
  });
7716
7824
 
7825
+ // src/cli/commands/config-update.ts
7826
+ var config_update_exports = {};
7827
+ __export(config_update_exports, {
7828
+ handleConfigSetOverrides: () => handleConfigSetOverrides
7829
+ });
7830
+ import { readFileSync as readFileSync4 } from "fs";
7831
+ import { join as join15 } from "path";
7832
+ async function handleConfigSetOverrides(root, format, options) {
7833
+ const { json: jsonArg, clear } = options;
7834
+ if (!clear && !jsonArg) {
7835
+ return {
7836
+ output: format === "json" ? JSON.stringify({ version: 1, error: "Provide --json or --clear" }) : "Error: Provide --json or --clear",
7837
+ errorCode: "invalid_input"
7838
+ };
7839
+ }
7840
+ let parsedOverrides = {};
7841
+ if (jsonArg) {
7842
+ try {
7843
+ parsedOverrides = JSON.parse(jsonArg);
7844
+ if (typeof parsedOverrides !== "object" || parsedOverrides === null || Array.isArray(parsedOverrides)) {
7845
+ return {
7846
+ output: format === "json" ? JSON.stringify({ version: 1, error: "Invalid JSON: expected an object" }) : "Error: Invalid JSON: expected an object",
7847
+ errorCode: "invalid_input"
7848
+ };
7849
+ }
7850
+ } catch {
7851
+ return {
7852
+ output: format === "json" ? JSON.stringify({ version: 1, error: "Invalid JSON syntax" }) : "Error: Invalid JSON syntax",
7853
+ errorCode: "invalid_input"
7854
+ };
7855
+ }
7856
+ }
7857
+ let resultOverrides = null;
7858
+ await withProjectLock(root, { strict: false }, async () => {
7859
+ const configPath = join15(root, ".story", "config.json");
7860
+ const rawContent = readFileSync4(configPath, "utf-8");
7861
+ const raw = JSON.parse(rawContent);
7862
+ if (clear) {
7863
+ delete raw.recipeOverrides;
7864
+ } else {
7865
+ const existing = raw.recipeOverrides ?? {};
7866
+ const merged = { ...existing, ...parsedOverrides };
7867
+ for (const [k, v] of Object.entries(merged)) {
7868
+ if (v === null) delete merged[k];
7869
+ }
7870
+ if (Object.keys(merged).length === 0) {
7871
+ delete raw.recipeOverrides;
7872
+ } else {
7873
+ raw.recipeOverrides = merged;
7874
+ }
7875
+ }
7876
+ const validated = ConfigSchema.safeParse(raw);
7877
+ if (!validated.success) {
7878
+ const message = validated.error.issues.map((i) => i.message).join("; ");
7879
+ throw new ProjectLoaderError(
7880
+ "invalid_input",
7881
+ `Invalid config after merge: ${message}`
7882
+ );
7883
+ }
7884
+ await guardPath(configPath, root);
7885
+ await atomicWrite(configPath, JSON.stringify(raw, null, 2) + "\n");
7886
+ resultOverrides = raw.recipeOverrides ?? null;
7887
+ });
7888
+ const data = { recipeOverrides: resultOverrides };
7889
+ if (format === "json") {
7890
+ return { output: JSON.stringify({ version: 1, data }, null, 2) };
7891
+ }
7892
+ if (resultOverrides === null) {
7893
+ return { output: "Recipe overrides cleared (using recipe defaults)." };
7894
+ }
7895
+ const lines = Object.entries(resultOverrides).map(([k, v]) => ` ${k}: ${JSON.stringify(v)}`);
7896
+ return { output: `Recipe overrides updated:
7897
+ ${lines.join("\n")}` };
7898
+ }
7899
+ var init_config_update = __esm({
7900
+ "src/cli/commands/config-update.ts"() {
7901
+ "use strict";
7902
+ init_esm_shims();
7903
+ init_project_loader();
7904
+ init_config();
7905
+ init_errors();
7906
+ }
7907
+ });
7908
+
7717
7909
  // src/cli/register.ts
7718
7910
  var register_exports = {};
7719
7911
  __export(register_exports, {
7720
7912
  registerBlockerCommand: () => registerBlockerCommand,
7913
+ registerConfigCommand: () => registerConfigCommand,
7721
7914
  registerExportCommand: () => registerExportCommand,
7722
7915
  registerHandoverCommand: () => registerHandoverCommand,
7723
7916
  registerHookStatusCommand: () => registerHookStatusCommand,
@@ -9283,6 +9476,51 @@ function registerHookStatusCommand(yargs) {
9283
9476
  }
9284
9477
  );
9285
9478
  }
9479
+ function registerConfigCommand(yargs) {
9480
+ return yargs.command(
9481
+ "config",
9482
+ "Manage project configuration",
9483
+ (y) => y.command(
9484
+ "set-overrides",
9485
+ "Set or clear recipe overrides in config.json",
9486
+ (y2) => y2.option("json", {
9487
+ type: "string",
9488
+ describe: "JSON object to merge into recipeOverrides"
9489
+ }).option("clear", {
9490
+ type: "boolean",
9491
+ describe: "Remove recipeOverrides entirely (reset to defaults)"
9492
+ }).option("format", {
9493
+ choices: ["json", "md"],
9494
+ default: "md",
9495
+ describe: "Output format"
9496
+ }),
9497
+ async (argv) => {
9498
+ const { handleConfigSetOverrides: handleConfigSetOverrides2 } = await Promise.resolve().then(() => (init_config_update(), config_update_exports));
9499
+ const { writeOutput: writeOutput2 } = await Promise.resolve().then(() => (init_run(), run_exports));
9500
+ const format = argv.format;
9501
+ try {
9502
+ const result = await handleConfigSetOverrides2(
9503
+ process.cwd(),
9504
+ format,
9505
+ { json: argv.json, clear: argv.clear === true }
9506
+ );
9507
+ writeOutput2(result.output);
9508
+ if (result.errorCode) process.exitCode = 1;
9509
+ } catch (err) {
9510
+ const { formatError: formatError2, ExitCode: ExitCode2 } = await Promise.resolve().then(() => (init_output_formatter(), output_formatter_exports));
9511
+ const { ProjectLoaderError: ProjectLoaderError2 } = await Promise.resolve().then(() => (init_errors(), errors_exports));
9512
+ if (err instanceof ProjectLoaderError2) {
9513
+ writeOutput2(formatError2(err.code, err.message, format));
9514
+ } else {
9515
+ const message = err instanceof Error ? err.message : String(err);
9516
+ writeOutput2(formatError2("io_error", message, format));
9517
+ }
9518
+ process.exitCode = ExitCode2.USER_ERROR;
9519
+ }
9520
+ }
9521
+ ).demandCommand(1, "Specify a config subcommand. Available: set-overrides")
9522
+ );
9523
+ }
9286
9524
  var init_register = __esm({
9287
9525
  "src/cli/register.ts"() {
9288
9526
  "use strict";
@@ -9337,9 +9575,10 @@ async function runCli() {
9337
9575
  registerReferenceCommand: registerReferenceCommand2,
9338
9576
  registerSelftestCommand: registerSelftestCommand2,
9339
9577
  registerSetupSkillCommand: registerSetupSkillCommand2,
9340
- registerHookStatusCommand: registerHookStatusCommand2
9578
+ registerHookStatusCommand: registerHookStatusCommand2,
9579
+ registerConfigCommand: registerConfigCommand2
9341
9580
  } = await Promise.resolve().then(() => (init_register(), register_exports));
9342
- const version2 = "0.1.14";
9581
+ const version2 = "0.1.16";
9343
9582
  class HandledError extends Error {
9344
9583
  constructor() {
9345
9584
  super("HANDLED_ERROR");
@@ -9378,6 +9617,7 @@ async function runCli() {
9378
9617
  cli = registerSelftestCommand2(cli);
9379
9618
  cli = registerSetupSkillCommand2(cli);
9380
9619
  cli = registerHookStatusCommand2(cli);
9620
+ cli = registerConfigCommand2(cli);
9381
9621
  function handleUnexpectedError(err) {
9382
9622
  if (err instanceof HandledError) return;
9383
9623
  const message = err instanceof Error ? err.message : String(err);
package/dist/index.d.ts CHANGED
@@ -1237,15 +1237,40 @@ declare const SnapshotV1Schema: z.ZodObject<{
1237
1237
  file: z.ZodString;
1238
1238
  message: z.ZodString;
1239
1239
  }, "strip", z.ZodTypeAny, {
1240
- message: string;
1241
1240
  type: string;
1241
+ message: string;
1242
1242
  file: string;
1243
1243
  }, {
1244
- message: string;
1245
1244
  type: string;
1245
+ message: string;
1246
1246
  file: string;
1247
1247
  }>, "many">>;
1248
1248
  }, "strip", z.ZodTypeAny, {
1249
+ version: 1;
1250
+ config: {
1251
+ version: number;
1252
+ type: string;
1253
+ language: string;
1254
+ project: string;
1255
+ features: {
1256
+ issues: boolean;
1257
+ tickets: boolean;
1258
+ handovers: boolean;
1259
+ roadmap: boolean;
1260
+ reviews: boolean;
1261
+ } & {
1262
+ [k: string]: unknown;
1263
+ };
1264
+ schemaVersion?: number | undefined;
1265
+ recipe?: string | undefined;
1266
+ recipeOverrides?: {
1267
+ maxTicketsPerSession?: number | undefined;
1268
+ compactThreshold?: string | undefined;
1269
+ reviewBackends?: string[] | undefined;
1270
+ } | undefined;
1271
+ } & {
1272
+ [k: string]: unknown;
1273
+ };
1249
1274
  issues: z.objectOutputType<{
1250
1275
  id: z.ZodString;
1251
1276
  title: z.ZodString;
@@ -1281,8 +1306,8 @@ declare const SnapshotV1Schema: z.ZodObject<{
1281
1306
  lastModifiedBy: z.ZodOptional<z.ZodNullable<z.ZodString>>;
1282
1307
  }, z.ZodTypeAny, "passthrough">[];
1283
1308
  roadmap: {
1284
- title: string;
1285
1309
  date: string;
1310
+ title: string;
1286
1311
  phases: z.objectOutputType<{
1287
1312
  id: z.ZodString;
1288
1313
  label: z.ZodString;
@@ -1300,7 +1325,6 @@ declare const SnapshotV1Schema: z.ZodObject<{
1300
1325
  } & {
1301
1326
  [k: string]: unknown;
1302
1327
  };
1303
- version: 1;
1304
1328
  project: string;
1305
1329
  notes: z.objectOutputType<{
1306
1330
  id: z.ZodString;
@@ -1312,11 +1336,19 @@ declare const SnapshotV1Schema: z.ZodObject<{
1312
1336
  updatedDate: z.ZodEffects<z.ZodString, string, string>;
1313
1337
  }, z.ZodTypeAny, "passthrough">[];
1314
1338
  createdAt: string;
1315
- config: {
1339
+ handoverFilenames: string[];
1340
+ warnings?: {
1316
1341
  type: string;
1342
+ message: string;
1343
+ file: string;
1344
+ }[] | undefined;
1345
+ }, {
1346
+ version: 1;
1347
+ config: {
1317
1348
  version: number;
1318
- project: string;
1349
+ type: string;
1319
1350
  language: string;
1351
+ project: string;
1320
1352
  features: {
1321
1353
  issues: boolean;
1322
1354
  tickets: boolean;
@@ -1336,13 +1368,6 @@ declare const SnapshotV1Schema: z.ZodObject<{
1336
1368
  } & {
1337
1369
  [k: string]: unknown;
1338
1370
  };
1339
- handoverFilenames: string[];
1340
- warnings?: {
1341
- message: string;
1342
- type: string;
1343
- file: string;
1344
- }[] | undefined;
1345
- }, {
1346
1371
  issues: z.objectInputType<{
1347
1372
  id: z.ZodString;
1348
1373
  title: z.ZodString;
@@ -1378,8 +1403,8 @@ declare const SnapshotV1Schema: z.ZodObject<{
1378
1403
  lastModifiedBy: z.ZodOptional<z.ZodNullable<z.ZodString>>;
1379
1404
  }, z.ZodTypeAny, "passthrough">[];
1380
1405
  roadmap: {
1381
- title: string;
1382
1406
  date: string;
1407
+ title: string;
1383
1408
  phases: z.objectInputType<{
1384
1409
  id: z.ZodString;
1385
1410
  label: z.ZodString;
@@ -1397,33 +1422,8 @@ declare const SnapshotV1Schema: z.ZodObject<{
1397
1422
  } & {
1398
1423
  [k: string]: unknown;
1399
1424
  };
1400
- version: 1;
1401
1425
  project: string;
1402
1426
  createdAt: string;
1403
- config: {
1404
- type: string;
1405
- version: number;
1406
- project: string;
1407
- language: string;
1408
- features: {
1409
- issues: boolean;
1410
- tickets: boolean;
1411
- handovers: boolean;
1412
- roadmap: boolean;
1413
- reviews: boolean;
1414
- } & {
1415
- [k: string]: unknown;
1416
- };
1417
- schemaVersion?: number | undefined;
1418
- recipe?: string | undefined;
1419
- recipeOverrides?: {
1420
- maxTicketsPerSession?: number | undefined;
1421
- compactThreshold?: string | undefined;
1422
- reviewBackends?: string[] | undefined;
1423
- } | undefined;
1424
- } & {
1425
- [k: string]: unknown;
1426
- };
1427
1427
  notes?: z.objectInputType<{
1428
1428
  id: z.ZodString;
1429
1429
  title: z.ZodNullable<z.ZodString>;
@@ -1434,8 +1434,8 @@ declare const SnapshotV1Schema: z.ZodObject<{
1434
1434
  updatedDate: z.ZodEffects<z.ZodString, string, string>;
1435
1435
  }, z.ZodTypeAny, "passthrough">[] | undefined;
1436
1436
  warnings?: {
1437
- message: string;
1438
1437
  type: string;
1438
+ message: string;
1439
1439
  file: string;
1440
1440
  }[] | undefined;
1441
1441
  handoverFilenames?: string[] | undefined;
package/dist/mcp.js CHANGED
@@ -504,6 +504,29 @@ var init_handover_parser = __esm({
504
504
  });
505
505
 
506
506
  // src/core/project-loader.ts
507
+ var project_loader_exports = {};
508
+ __export(project_loader_exports, {
509
+ atomicWrite: () => atomicWrite,
510
+ deleteIssue: () => deleteIssue,
511
+ deleteNote: () => deleteNote,
512
+ deleteTicket: () => deleteTicket,
513
+ guardPath: () => guardPath,
514
+ loadProject: () => loadProject,
515
+ runTransaction: () => runTransaction,
516
+ runTransactionUnlocked: () => runTransactionUnlocked,
517
+ serializeJSON: () => serializeJSON,
518
+ sortKeysDeep: () => sortKeysDeep,
519
+ withProjectLock: () => withProjectLock,
520
+ writeConfig: () => writeConfig,
521
+ writeIssue: () => writeIssue,
522
+ writeIssueUnlocked: () => writeIssueUnlocked,
523
+ writeNote: () => writeNote,
524
+ writeNoteUnlocked: () => writeNoteUnlocked,
525
+ writeRoadmap: () => writeRoadmap,
526
+ writeRoadmapUnlocked: () => writeRoadmapUnlocked,
527
+ writeTicket: () => writeTicket,
528
+ writeTicketUnlocked: () => writeTicketUnlocked
529
+ });
507
530
  import {
508
531
  readdir as readdir2,
509
532
  readFile as readFile2,
@@ -807,6 +830,67 @@ async function withProjectLock(root, options, handler) {
807
830
  await handler(result);
808
831
  });
809
832
  }
833
+ async function runTransactionUnlocked(root, operations) {
834
+ const wrapDir = resolve2(root, ".story");
835
+ const journalPath = join3(wrapDir, ".txn.json");
836
+ const entries = [];
837
+ let commitStarted = false;
838
+ try {
839
+ for (const op of operations) {
840
+ if (op.op === "write") {
841
+ const tempPath = `${op.target}.${process.pid}.tmp`;
842
+ entries.push({ op: "write", target: op.target, tempPath });
843
+ } else {
844
+ entries.push({ op: "delete", target: op.target });
845
+ }
846
+ }
847
+ const journal = { entries, commitStarted: false };
848
+ await fsyncWrite(journalPath, JSON.stringify(journal, null, 2));
849
+ for (const op of operations) {
850
+ if (op.op === "write") {
851
+ const tempPath = `${op.target}.${process.pid}.tmp`;
852
+ await fsyncWrite(tempPath, op.content);
853
+ }
854
+ }
855
+ journal.commitStarted = true;
856
+ await fsyncWrite(journalPath, JSON.stringify(journal, null, 2));
857
+ commitStarted = true;
858
+ for (const entry of entries) {
859
+ if (entry.op === "write" && entry.tempPath) {
860
+ await rename(entry.tempPath, entry.target);
861
+ } else if (entry.op === "delete") {
862
+ try {
863
+ await unlink(entry.target);
864
+ } catch {
865
+ }
866
+ }
867
+ }
868
+ await unlink(journalPath);
869
+ } catch (err) {
870
+ if (!commitStarted) {
871
+ for (const entry of entries) {
872
+ if (entry.tempPath) {
873
+ try {
874
+ await unlink(entry.tempPath);
875
+ } catch {
876
+ }
877
+ }
878
+ }
879
+ try {
880
+ await unlink(journalPath);
881
+ } catch {
882
+ }
883
+ }
884
+ if (err instanceof ProjectLoaderError) throw err;
885
+ throw new ProjectLoaderError("io_error", "Transaction failed", err);
886
+ }
887
+ }
888
+ async function runTransaction(root, operations) {
889
+ const wrapDir = resolve2(root, ".story");
890
+ await withLock(wrapDir, async () => {
891
+ await runTransactionUnlocked(root, operations);
892
+ });
893
+ }
810
894
  async function doRecoverTransaction(wrapDir) {
811
895
  const journalPath = join3(wrapDir, ".txn.json");
812
896
  let entries;
@@ -996,6 +1080,15 @@ async function atomicWrite(targetPath, content) {
996
1080
  );
997
1081
  }
998
1082
  }
1083
+ async function fsyncWrite(filePath, content) {
1084
+ const fh = await open(filePath, "w");
1085
+ try {
1086
+ await fh.writeFile(content, "utf-8");
1087
+ await fh.sync();
1088
+ } finally {
1089
+ await fh.close();
1090
+ }
1091
+ }
999
1092
  async function guardPath(target, root) {
1000
1093
  let resolvedRoot;
1001
1094
  try {
@@ -4739,6 +4832,19 @@ async function handleReportPlan(root, dir, state, report) {
4739
4832
  });
4740
4833
  }
4741
4834
  const risk = assessRisk(void 0, void 0);
4835
+ if (state.ticket) {
4836
+ try {
4837
+ const { withProjectLock: withProjectLock2, writeTicketUnlocked: writeTicketUnlocked2 } = await Promise.resolve().then(() => (init_project_loader(), project_loader_exports));
4838
+ await withProjectLock2(root, { strict: false }, async ({ state: projectState }) => {
4839
+ const ticket = projectState.ticketByID(state.ticket.id);
4840
+ if (ticket && ticket.status !== "inprogress") {
4841
+ const updated = { ...ticket, status: "inprogress" };
4842
+ await writeTicketUnlocked2(updated, root);
4843
+ }
4844
+ });
4845
+ } catch {
4846
+ }
4847
+ }
4742
4848
  const written = writeSessionSync(dir, {
4743
4849
  ...state,
4744
4850
  state: "PLAN_REVIEW",
@@ -5096,11 +5202,11 @@ async function handleReportComplete(root, dir, state, report) {
5096
5202
  const maxTickets = updated.config.maxTicketsPerSession;
5097
5203
  let nextState;
5098
5204
  let advice = "ok";
5099
- if (pressure === "critical") {
5205
+ if (maxTickets > 0 && ticketsDone >= maxTickets) {
5100
5206
  nextState = "HANDOVER";
5101
- advice = "compact-now";
5102
- } else if (maxTickets > 0 && ticketsDone >= maxTickets) {
5207
+ } else if (pressure === "critical") {
5103
5208
  nextState = "HANDOVER";
5209
+ advice = "compact-now";
5104
5210
  } else if (pressure === "high") {
5105
5211
  advice = "consider-compact";
5106
5212
  nextState = "PICK_TICKET";
@@ -5180,6 +5286,36 @@ async function handleReportHandover(root, dir, state, report) {
5180
5286
  } catch {
5181
5287
  }
5182
5288
  }
5289
+ const pressureLevel = state.contextPressure?.level ?? "low";
5290
+ const maxTickets = state.config.maxTicketsPerSession;
5291
+ const capReached = maxTickets > 0 && state.completedTickets.length >= maxTickets;
5292
+ let hasMoreTickets = false;
5293
+ try {
5294
+ const { state: ps } = await loadProject(root);
5295
+ hasMoreTickets = nextTickets(ps, 1).kind === "found";
5296
+ } catch {
5297
+ }
5298
+ if (pressureLevel === "critical" && hasMoreTickets && !capReached) {
5299
+ return guideResult(state, "HANDOVER", {
5300
+ instruction: [
5301
+ "# Context Compaction Needed",
5302
+ "",
5303
+ `${state.completedTickets.length} ticket(s) completed so far. Handover written. Context is large \u2014 time to compact and continue.`,
5304
+ "",
5305
+ 'Call `claudestory_autonomous_guide` with `action: "pre_compact"` now:',
5306
+ "```json",
5307
+ `{ "sessionId": "${state.sessionId}", "action": "pre_compact" }`,
5308
+ "```",
5309
+ "",
5310
+ 'After pre_compact responds, run `/compact`, then call with `action: "resume"` to continue working on more tickets.'
5311
+ ].join("\n"),
5312
+ reminders: [
5313
+ "Do NOT stop. This is a context compaction, not a session end.",
5314
+ "Call pre_compact \u2192 /compact \u2192 resume to continue autonomous work."
5315
+ ],
5316
+ contextAdvice: "compact-now"
5317
+ });
5318
+ }
5183
5319
  const written = writeSessionSync(dir, {
5184
5320
  ...state,
5185
5321
  state: "SESSION_END",
@@ -5227,6 +5363,40 @@ async function handleResume(root, args) {
5227
5363
  resumeFromRevision: null,
5228
5364
  contextPressure: { ...info.state.contextPressure, compactionCount: (info.state.contextPressure?.compactionCount ?? 0) + 1 }
5229
5365
  });
5366
+ if (resumeState === "PICK_TICKET") {
5367
+ let candidatesText = "No ticket candidates available.";
5368
+ let topCandidate = null;
5369
+ try {
5370
+ const { state: ps } = await loadProject(root);
5371
+ const result = nextTickets(ps, 5);
5372
+ if (result.kind === "found") {
5373
+ topCandidate = result.candidates[0] ?? null;
5374
+ candidatesText = result.candidates.map(
5375
+ (c, i) => `${i + 1}. **${c.ticket.id}: ${c.ticket.title}** (${c.ticket.type})`
5376
+ ).join("\n");
5377
+ }
5378
+ } catch {
5379
+ }
5380
+ return guideResult(written, "PICK_TICKET", {
5381
+ instruction: [
5382
+ "# Resumed After Compact \u2014 Continue Working",
5383
+ "",
5384
+ `${written.completedTickets.length} ticket(s) done so far. Context compacted. Pick the next ticket immediately.`,
5385
+ "",
5386
+ candidatesText,
5387
+ "",
5388
+ topCandidate ? `Pick **${topCandidate.ticket.id}** by calling \`claudestory_autonomous_guide\` now:` : "Pick a ticket now:",
5389
+ "```json",
5390
+ topCandidate ? `{ "sessionId": "${written.sessionId}", "action": "report", "report": { "completedAction": "ticket_picked", "ticketId": "${topCandidate.ticket.id}" } }` : `{ "sessionId": "${written.sessionId}", "action": "report", "report": { "completedAction": "ticket_picked", "ticketId": "T-XXX" } }`,
5391
+ "```"
5392
+ ].join("\n"),
5393
+ reminders: [
5394
+ "Do NOT stop or summarize. Pick the next ticket IMMEDIATELY.",
5395
+ "Do NOT ask the user for confirmation.",
5396
+ "You are in autonomous mode \u2014 continue working."
5397
+ ]
5398
+ });
5399
+ }
5230
5400
  return guideResult(written, resumeState, {
5231
5401
  instruction: [
5232
5402
  "# Resumed After Compact",
@@ -5238,6 +5408,7 @@ async function handleResume(root, args) {
5238
5408
  ].join("\n"),
5239
5409
  reminders: [
5240
5410
  "Do NOT use plan mode.",
5411
+ "Do NOT stop or summarize.",
5241
5412
  "Call autonomous_guide after completing each step."
5242
5413
  ]
5243
5414
  });
@@ -5253,11 +5424,12 @@ async function handlePreCompact(root, args) {
5253
5424
  return guideError(new Error(`Session ${args.sessionId} is already in COMPACT state. Call action: "resume" to continue.`));
5254
5425
  }
5255
5426
  const headResult = await gitHead(root);
5427
+ const resumeTarget = info.state.state === "HANDOVER" ? "PICK_TICKET" : info.state.state;
5256
5428
  const written = writeSessionSync(info.dir, {
5257
5429
  ...refreshLease(info.state),
5258
5430
  state: "COMPACT",
5259
5431
  previousState: info.state.state,
5260
- preCompactState: info.state.state,
5432
+ preCompactState: resumeTarget,
5261
5433
  resumeFromRevision: info.state.revision,
5262
5434
  git: {
5263
5435
  ...info.state.git,
@@ -6034,7 +6206,7 @@ async function ensureGitignoreEntries(gitignorePath, entries) {
6034
6206
  // src/mcp/index.ts
6035
6207
  var ENV_VAR2 = "CLAUDESTORY_PROJECT_ROOT";
6036
6208
  var CONFIG_PATH2 = ".story/config.json";
6037
- var version = "0.1.14";
6209
+ var version = "0.1.16";
6038
6210
  function tryDiscoverRoot() {
6039
6211
  const envRoot = process.env[ENV_VAR2];
6040
6212
  if (envRoot) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anthropologies/claudestory",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
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": [