@trycadence/cli 0.1.10-dev.0 → 0.1.14-dev.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +32 -0
  2. package/dist/cadence +1573 -164
  3. package/package.json +1 -1
package/dist/cadence CHANGED
@@ -1513,9 +1513,39 @@ function createCadenceClient(options = {}) {
1513
1513
  // src/index.ts
1514
1514
  import { spawn, spawnSync } from "child_process";
1515
1515
  import { createHash, randomUUID } from "crypto";
1516
+ import { existsSync, readdirSync, readFileSync as readFileSyncNode, statSync } from "fs";
1516
1517
  import { mkdir, readFile, rm, stat, writeFile } from "fs/promises";
1517
1518
  import { basename, dirname, isAbsolute, join, parse } from "path";
1518
1519
  import { createInterface } from "readline/promises";
1520
+ // package.json
1521
+ var package_default = {
1522
+ name: "@trycadence/cli",
1523
+ version: "0.1.14-dev.0",
1524
+ private: false,
1525
+ type: "module",
1526
+ bin: {
1527
+ cadence: "dist/cadence"
1528
+ },
1529
+ files: [
1530
+ "dist",
1531
+ "README.md"
1532
+ ],
1533
+ scripts: {
1534
+ build: "bun build src/index.ts --target=bun --outfile=dist/cadence",
1535
+ dev: "bun run src/index.ts",
1536
+ prepack: "bun run build",
1537
+ test: "bun test",
1538
+ typecheck: "tsc --noEmit -p tsconfig.json"
1539
+ },
1540
+ engines: {
1541
+ bun: ">=1.3.0"
1542
+ },
1543
+ publishConfig: {
1544
+ access: "public"
1545
+ }
1546
+ };
1547
+
1548
+ // src/index.ts
1519
1549
  var ticketPriorities = ["low", "normal", "high", "urgent"];
1520
1550
  var ticketStatuses = ["backlog", "ready", "in_progress", "blocked", "review", "done", "abandoned"];
1521
1551
  var sessionFileChangeKinds = ["added", "modified", "deleted", "renamed", "unknown"];
@@ -1524,19 +1554,19 @@ var workLogParentSelectors = ["last", "ticket-last", "session-last", "last-decis
1524
1554
  var changesetPrNoteSources = ["agent", "human", "system"];
1525
1555
  var hookScopes = ["repo", "global", "both"];
1526
1556
  var agentEventSources = ["codex", "claude-code", "opencode", "openrouter", "unknown"];
1527
- var defaultLeaseTtlSeconds = 5 * 60 * 60;
1557
+ var defaultLeaseTtlSeconds = 15 * 60;
1528
1558
  var defaultCliApiBaseUrl = "https://cadenceapi.deploy.lvl8studios.com";
1529
1559
  var defaultCliWebBaseUrl = "https://cadence.deploy.lvl8studios.com";
1530
- var defaultCheckpointThresholdMin = 3;
1531
- var defaultCheckpointThresholdMax = 5;
1560
+ var defaultCheckpointThreshold = 3;
1532
1561
  var defaultCheckpointCooldownSeconds = 10 * 60;
1533
1562
  var defaultCheckpointWorkerTimeoutMs = 10 * 60 * 1000;
1534
- var defaultHookCommand = "cadence agent-run ingest-stop --source codex --event stop";
1563
+ var defaultHookCommand = `/bin/sh -lc 'root=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0; cd "$root" || exit 0; [ -f .cadence/config.json ] || [ -f .cadence/config.local.json ] || exit 0; exec cadence agent-run ingest-stop --source codex --event stop >/dev/null'`;
1535
1564
  var agentLoopSuppressEnv = "CADENCE_AGENT_EVENT_SUPPRESS";
1536
1565
  var credentialRefreshSkewMs = 60 * 1000;
1537
1566
  var credentialRefreshLockTimeoutMs = 10 * 1000;
1538
1567
  var credentialRefreshLockStaleMs = 60 * 1000;
1539
1568
  var credentialRefreshLockPollMs = 100;
1569
+ var cliVersion = package_default.version;
1540
1570
 
1541
1571
  class CliError extends Error {
1542
1572
  code;
@@ -1569,6 +1599,7 @@ var knownCommandPaths = [
1569
1599
  ["changesets", "notes", "put"],
1570
1600
  ["changesets", "notes", "apply"],
1571
1601
  ["agent-run", "ingest-stop"],
1602
+ ["agent-run", "checkpoint"],
1572
1603
  ["agent-run", "closeout"],
1573
1604
  ["agent-run", "sweep"],
1574
1605
  ["agent-run", "doctor"],
@@ -1594,6 +1625,7 @@ var knownCommandPaths = [
1594
1625
  ["projects", "list"],
1595
1626
  ["init"],
1596
1627
  ["status"],
1628
+ ["version"],
1597
1629
  ["help"]
1598
1630
  ];
1599
1631
  function isCommandMatch(words, commandPath) {
@@ -1623,6 +1655,7 @@ function parseCliArgs(argv) {
1623
1655
  const options = {};
1624
1656
  let json = false;
1625
1657
  let help = false;
1658
+ let version = false;
1626
1659
  let server;
1627
1660
  let project;
1628
1661
  for (let index = 0;index < argv.length; index += 1) {
@@ -1638,6 +1671,10 @@ function parseCliArgs(argv) {
1638
1671
  help = true;
1639
1672
  continue;
1640
1673
  }
1674
+ if (arg === "--version" || arg === "-v") {
1675
+ version = true;
1676
+ continue;
1677
+ }
1641
1678
  if (arg === "--server") {
1642
1679
  server = readFlagValue(argv, index, arg);
1643
1680
  index += 1;
@@ -1668,6 +1705,7 @@ ${value}` : value;
1668
1705
  flags: {
1669
1706
  json,
1670
1707
  help,
1708
+ version,
1671
1709
  ...server ? { server } : {},
1672
1710
  ...project ? { project } : {}
1673
1711
  },
@@ -2207,6 +2245,8 @@ function helpText() {
2207
2245
  "Cadence CLI",
2208
2246
  "",
2209
2247
  "Usage:",
2248
+ " cadence --version",
2249
+ " cadence version [--json]",
2210
2250
  " cadence init [--project <project-id|org/project>] [--json]",
2211
2251
  " cadence auth login [--json]",
2212
2252
  " cadence auth status [--json]",
@@ -2221,7 +2261,7 @@ function helpText() {
2221
2261
  " cadence tickets attach <ticket-id> --from-intake <intake-id> --if-version <version> [--project <project-id>] [--json]",
2222
2262
  " cadence tickets create --title <text> [--from-intake <intake-id>] [--project <project-id>] [--json]",
2223
2263
  " cadence tickets update <ticket-id> --if-version <version> [--title <text>] [--description <text>] [--priority <priority>] [--status <status>] [--project <project-id>] [--json]",
2224
- " cadence tickets claim <ticket-id> --session <session-id> [--actor <actor-id>] [--project <project-id>] [--json]",
2264
+ " cadence tickets claim <ticket-id> --session <session-id> [--actor <actor-id>] [--replace-own-active true|false] [--project <project-id>] [--json]",
2225
2265
  " cadence tickets release <ticket-id> --lease <lease-id> [--project <project-id>] [--json]",
2226
2266
  " cadence tickets log <ticket-id> --kind <intent|decision|rationale|action|verification|blocker|correction|note> --body <text> [--summary <text>] [--under <entry-id|ticket-last|session-last|last-decision|last-correction|last-action>] [--session <session-id>] [--changeset <changeset-id>] [--project <project-id>] [--json]",
2227
2267
  " cadence tickets complete <ticket-id> --if-version <version> [--summary <summary>] [--project <project-id>] [--json]",
@@ -2239,16 +2279,17 @@ function helpText() {
2239
2279
  " cadence changesets notes put [--changeset <id>|--branch current|<branch>] --title <text> --body-file <path> [--head-sha <sha>] [--base-sha <sha>] [--pr-url <url>] [--pr-number <n>] [--project <project-id>] [--json]",
2240
2280
  " cadence changesets notes apply [--changeset <id>|--branch current|<branch>] --provider github --pr-number <n> --pr-url <url> [--project <project-id>] [--json]",
2241
2281
  " cadence agent-run ingest-stop --source <codex|claude-code|opencode|openrouter> [--event <event>] [--threshold <n>] [--dry-run true|false] [--project <project-id>] [--json]",
2242
- " cadence agent-run closeout --agent-session-key <key> [--reason <threshold|idle|manual>] [--event-file <path>] [--log-kind <kind>] [--update-summary true|false] [--json]",
2282
+ " cadence agent-run checkpoint --agent-session-key <key> [--reason <threshold|idle|manual>] [--event-file <path>] [--ticket <ticket-id>] [--session <session-id>] [--changeset <changeset-id>] [--checkpoint-provider codex] [--checkpoint-model <model>] [--log-kind <kind>] [--update-summary true|false] [--json]",
2243
2283
  " cadence agent-run sweep [--idle-after-seconds <n>] [--dry-run true|false] [--json]",
2244
2284
  " cadence agent-run doctor [--json]",
2245
- " cadence hooks install --provider codex --scope <repo|global|both> [--command <command>] [--json]",
2246
- " cadence hooks doctor --provider codex --scope <repo|global|both> [--json]",
2285
+ " cadence hooks install --provider codex [--scope global] [--command <command>] [--json]",
2286
+ " cadence hooks doctor --provider codex [--scope global] [--json]",
2247
2287
  " cadence events list [--project <project-id>] [--ticket <ticket-id>] [--changeset <changeset-id>] [--session <session-id>] [--json]",
2248
2288
  "",
2249
2289
  "Global flags:",
2250
2290
  " --project <id> Cadence project ID or org/project slug",
2251
2291
  " --server <url> Cadence API server override",
2292
+ " --version, -v Print the Cadence CLI version",
2252
2293
  " --json Print stable JSON envelope",
2253
2294
  "",
2254
2295
  "Work log parent selectors:",
@@ -2541,6 +2582,14 @@ function truncateText(value, maxLength) {
2541
2582
  return `${value.slice(0, maxLength - 15)}
2542
2583
  [truncated]`;
2543
2584
  }
2585
+ function estimateTokenCount(value) {
2586
+ const text = value.trim();
2587
+ if (!text) {
2588
+ return 0;
2589
+ }
2590
+ const pieces = text.match(/[A-Za-z0-9_]+|[^\sA-Za-z0-9_]/g) ?? [];
2591
+ return Math.max(pieces.length, Math.ceil(text.length / 4));
2592
+ }
2544
2593
  function stableHash(value) {
2545
2594
  return createHash("sha256").update(value).digest("hex");
2546
2595
  }
@@ -2579,13 +2628,392 @@ function firstString(record, paths) {
2579
2628
  }
2580
2629
  return;
2581
2630
  }
2631
+ function readRecentAgentTurns(input) {
2632
+ const rawTurns = input.recent_turns ?? input.recentTurns ?? input.turns;
2633
+ if (!Array.isArray(rawTurns)) {
2634
+ return;
2635
+ }
2636
+ const turns = rawTurns.map((turn) => {
2637
+ if (!turn || typeof turn !== "object" || Array.isArray(turn)) {
2638
+ return;
2639
+ }
2640
+ const record = turn;
2641
+ const user = firstString(record, [["user"], ["prompt"], ["userPrompt"], ["request"]]);
2642
+ const assistant = firstString(record, [["assistant"], ["response"], ["assistantResponse"], ["reply"]]);
2643
+ const occurredAt = firstString(record, [["occurredAt"], ["occurred_at"], ["timestamp"], ["createdAt"]]);
2644
+ if (!user && !assistant) {
2645
+ return;
2646
+ }
2647
+ return {
2648
+ ...user ? { user: truncateText(user, 6000) } : {},
2649
+ ...assistant ? { assistant: truncateText(assistant, 6000) } : {},
2650
+ ...occurredAt ? { occurredAt } : {}
2651
+ };
2652
+ }).filter((turn) => Boolean(turn));
2653
+ return turns.length ? turns.slice(-3) : undefined;
2654
+ }
2655
+ function mergeRecentAgentTurns(existing, next) {
2656
+ if (!next?.length) {
2657
+ return existing?.length ? existing.slice(-3) : undefined;
2658
+ }
2659
+ return [...existing ?? [], ...next].slice(-3);
2660
+ }
2661
+ function stringFromCodexContent(value) {
2662
+ if (typeof value === "string") {
2663
+ return value.trim() || undefined;
2664
+ }
2665
+ if (Array.isArray(value)) {
2666
+ const text = value.map((item) => {
2667
+ if (typeof item === "string") {
2668
+ return item;
2669
+ }
2670
+ if (item && typeof item === "object" && !Array.isArray(item)) {
2671
+ const record = item;
2672
+ return typeof record.text === "string" ? record.text : typeof record.content === "string" ? record.content : "";
2673
+ }
2674
+ return "";
2675
+ }).join(`
2676
+ `).trim();
2677
+ return text || undefined;
2678
+ }
2679
+ return;
2680
+ }
2681
+ function findCodexSessionFile(directory, agentSessionId, depth = 0) {
2682
+ if (depth > 6 || !existsSync(directory)) {
2683
+ return;
2684
+ }
2685
+ let entries;
2686
+ try {
2687
+ entries = readdirSync(directory, { withFileTypes: true });
2688
+ } catch {
2689
+ return;
2690
+ }
2691
+ for (const entry of entries) {
2692
+ const entryPath = join(directory, entry.name);
2693
+ if (entry.isFile() && entry.name.endsWith(".jsonl") && entry.name.includes(agentSessionId)) {
2694
+ return entryPath;
2695
+ }
2696
+ }
2697
+ for (const entry of entries) {
2698
+ if (!entry.isDirectory()) {
2699
+ continue;
2700
+ }
2701
+ const found = findCodexSessionFile(join(directory, entry.name), agentSessionId, depth + 1);
2702
+ if (found) {
2703
+ return found;
2704
+ }
2705
+ }
2706
+ return;
2707
+ }
2708
+ function codexSessionsDirectory(options) {
2709
+ const home = options.env?.HOME ?? process.env.HOME;
2710
+ const codexHome = options.env?.CODEX_HOME ?? process.env.CODEX_HOME ?? (home ? join(home, ".codex") : undefined);
2711
+ return codexHome ? join(codexHome, "sessions") : undefined;
2712
+ }
2713
+ function listCodexSessionFiles(directory, depth = 0) {
2714
+ if (depth > 6 || !existsSync(directory)) {
2715
+ return [];
2716
+ }
2717
+ let entries;
2718
+ try {
2719
+ entries = readdirSync(directory, { withFileTypes: true });
2720
+ } catch {
2721
+ return [];
2722
+ }
2723
+ return entries.flatMap((entry) => {
2724
+ const entryPath = join(directory, entry.name);
2725
+ if (entry.isDirectory()) {
2726
+ return listCodexSessionFiles(entryPath, depth + 1);
2727
+ }
2728
+ if (!entry.isFile() || !entry.name.endsWith(".jsonl")) {
2729
+ return [];
2730
+ }
2731
+ try {
2732
+ return [{ path: entryPath, mtimeMs: statSync(entryPath).mtimeMs }];
2733
+ } catch {
2734
+ return [];
2735
+ }
2736
+ });
2737
+ }
2738
+ function readCodexSessionMeta(filePath) {
2739
+ let lines;
2740
+ try {
2741
+ lines = readFileSyncNode(filePath, "utf8").split(`
2742
+ `).filter((line) => line.trim());
2743
+ } catch {
2744
+ return {};
2745
+ }
2746
+ for (const line of lines) {
2747
+ let parsed;
2748
+ try {
2749
+ parsed = JSON.parse(line);
2750
+ } catch {
2751
+ continue;
2752
+ }
2753
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
2754
+ continue;
2755
+ }
2756
+ const event = parsed;
2757
+ const payload = event.payload && typeof event.payload === "object" && !Array.isArray(event.payload) ? event.payload : {};
2758
+ if (event.type !== "session_meta") {
2759
+ continue;
2760
+ }
2761
+ return {
2762
+ ...typeof payload.id === "string" ? { id: payload.id } : {},
2763
+ ...typeof payload.cwd === "string" ? { cwd: payload.cwd } : {},
2764
+ ...typeof event.timestamp === "string" ? { timestamp: event.timestamp } : {}
2765
+ };
2766
+ }
2767
+ return {};
2768
+ }
2769
+ function findCodexSessionFileCreatedAfter(options, startedAtMs, cwd) {
2770
+ const sessionsDirectory = codexSessionsDirectory(options);
2771
+ if (!sessionsDirectory) {
2772
+ return;
2773
+ }
2774
+ const candidates = listCodexSessionFiles(sessionsDirectory).filter((candidate) => candidate.mtimeMs >= startedAtMs - 1000).map((candidate) => ({
2775
+ ...candidate,
2776
+ meta: readCodexSessionMeta(candidate.path)
2777
+ })).filter((candidate) => !candidate.meta.cwd || candidate.meta.cwd === cwd).sort((left, right) => right.mtimeMs - left.mtimeMs);
2778
+ return candidates[0]?.path;
2779
+ }
2780
+ function codexTranscriptTextFromPayload(payload) {
2781
+ return stringFromCodexContent(payload.message) ?? stringFromCodexContent(payload.text) ?? stringFromCodexContent(payload.content) ?? stringFromCodexContent(payload.output) ?? undefined;
2782
+ }
2783
+ function summarizeCodexTranscriptEvent(raw, index) {
2784
+ const payload = raw.payload && typeof raw.payload === "object" && !Array.isArray(raw.payload) ? raw.payload : {};
2785
+ const type = typeof raw.type === "string" ? raw.type : "unknown";
2786
+ const payloadType = typeof payload.type === "string" ? payload.type : undefined;
2787
+ const text = codexTranscriptTextFromPayload(payload);
2788
+ const callId = typeof payload.call_id === "string" ? payload.call_id : typeof payload.callId === "string" ? payload.callId : undefined;
2789
+ const toolArguments = typeof payload.arguments === "string" ? payload.arguments : payload.arguments && typeof payload.arguments === "object" ? JSON.stringify(payload.arguments) : undefined;
2790
+ const toolName = typeof payload.tool_name === "string" ? payload.tool_name : typeof payload.toolName === "string" ? payload.toolName : typeof payload.name === "string" ? payload.name : undefined;
2791
+ return {
2792
+ index,
2793
+ type,
2794
+ ...payloadType ? { payloadType } : {},
2795
+ ...typeof raw.timestamp === "string" ? { timestamp: raw.timestamp } : {},
2796
+ ...text ? { text: truncateText(text, 8000) } : {},
2797
+ ...callId ? { callId } : {},
2798
+ ...toolName ? { toolName } : {},
2799
+ ...toolArguments ? { toolArguments: truncateText(toolArguments, 8000) } : {},
2800
+ raw
2801
+ };
2802
+ }
2803
+ function numberFromRecord(record, key) {
2804
+ const value = record[key];
2805
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
2806
+ }
2807
+ function codexTokenUsageRecord(value) {
2808
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
2809
+ return;
2810
+ }
2811
+ const record = value;
2812
+ return {
2813
+ ...numberFromRecord(record, "input_tokens") !== undefined ? { inputTokens: numberFromRecord(record, "input_tokens") } : {},
2814
+ ...numberFromRecord(record, "cached_input_tokens") !== undefined ? { cachedInputTokens: numberFromRecord(record, "cached_input_tokens") } : {},
2815
+ ...numberFromRecord(record, "output_tokens") !== undefined ? { outputTokens: numberFromRecord(record, "output_tokens") } : {},
2816
+ ...numberFromRecord(record, "reasoning_output_tokens") !== undefined ? { reasoningOutputTokens: numberFromRecord(record, "reasoning_output_tokens") } : {},
2817
+ ...numberFromRecord(record, "total_tokens") !== undefined ? { totalTokens: numberFromRecord(record, "total_tokens") } : {}
2818
+ };
2819
+ }
2820
+ function extractCodexTokenUsage(events) {
2821
+ const tokenEvents = events.filter((event) => event.payloadType === "token_count");
2822
+ if (!tokenEvents.length) {
2823
+ return;
2824
+ }
2825
+ let total;
2826
+ let last;
2827
+ let modelContextWindow;
2828
+ let updatedAt;
2829
+ for (const event of tokenEvents) {
2830
+ const payload = event.raw.payload && typeof event.raw.payload === "object" && !Array.isArray(event.raw.payload) ? event.raw.payload : {};
2831
+ const info = payload.info && typeof payload.info === "object" && !Array.isArray(payload.info) ? payload.info : {};
2832
+ const parsedTotal = codexTokenUsageRecord(info.total_token_usage);
2833
+ const parsedLast = codexTokenUsageRecord(info.last_token_usage);
2834
+ const parsedContextWindow = numberFromRecord(info, "model_context_window");
2835
+ if (parsedTotal && Object.keys(parsedTotal).length) {
2836
+ total = parsedTotal;
2837
+ }
2838
+ if (parsedLast && Object.keys(parsedLast).length) {
2839
+ last = parsedLast;
2840
+ }
2841
+ if (parsedContextWindow !== undefined) {
2842
+ modelContextWindow = parsedContextWindow;
2843
+ }
2844
+ updatedAt = event.timestamp;
2845
+ }
2846
+ return {
2847
+ source: "codex_session_transcript",
2848
+ eventCount: tokenEvents.length,
2849
+ ...total ? { total } : {},
2850
+ ...last ? { last } : {},
2851
+ ...modelContextWindow !== undefined ? { modelContextWindow } : {},
2852
+ ...updatedAt ? { updatedAt } : {}
2853
+ };
2854
+ }
2855
+ function tokenUsageTotal(tokenUsage) {
2856
+ return tokenUsage?.total;
2857
+ }
2858
+ function roundedRatio(numerator, denominator) {
2859
+ if (denominator <= 0) {
2860
+ return;
2861
+ }
2862
+ return Math.round(numerator / denominator * 1000) / 1000;
2863
+ }
2864
+ function readCodexSessionTranscript(filePath) {
2865
+ if (!filePath) {
2866
+ return;
2867
+ }
2868
+ let lines;
2869
+ try {
2870
+ lines = readFileSyncNode(filePath, "utf8").split(`
2871
+ `).filter((line) => line.trim());
2872
+ } catch {
2873
+ return;
2874
+ }
2875
+ const events = lines.flatMap((line, index) => {
2876
+ try {
2877
+ const parsed = JSON.parse(line);
2878
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
2879
+ return [];
2880
+ }
2881
+ return [summarizeCodexTranscriptEvent(parsed, index)];
2882
+ } catch {
2883
+ return [
2884
+ {
2885
+ index,
2886
+ type: "unparsed",
2887
+ text: truncateText(line, 8000),
2888
+ raw: { line }
2889
+ }
2890
+ ];
2891
+ }
2892
+ });
2893
+ return {
2894
+ path: filePath,
2895
+ fileName: basename(filePath),
2896
+ lineCount: lines.length,
2897
+ ...extractCodexTokenUsage(events) ? { tokenUsage: extractCodexTokenUsage(events) } : {},
2898
+ events
2899
+ };
2900
+ }
2901
+ function readRecentTurnsFromCodexSessionFile(filePath) {
2902
+ let lines;
2903
+ try {
2904
+ lines = readFileSyncNode(filePath, "utf8").split(`
2905
+ `).filter((line) => line.trim());
2906
+ } catch {
2907
+ return;
2908
+ }
2909
+ const messages = [];
2910
+ const pushMessage = (message) => {
2911
+ const previous = messages[messages.length - 1];
2912
+ if (previous?.role === message.role && previous.text === message.text) {
2913
+ return;
2914
+ }
2915
+ messages.push(message);
2916
+ };
2917
+ for (const line of lines) {
2918
+ let parsed;
2919
+ try {
2920
+ parsed = JSON.parse(line);
2921
+ } catch {
2922
+ continue;
2923
+ }
2924
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
2925
+ continue;
2926
+ }
2927
+ const event = parsed;
2928
+ const payload = event.payload && typeof event.payload === "object" && !Array.isArray(event.payload) ? event.payload : undefined;
2929
+ if (event.type === "event_msg" && payload?.type === "user_message") {
2930
+ const text = stringFromCodexContent(payload.message);
2931
+ if (text) {
2932
+ pushMessage({
2933
+ role: "user",
2934
+ text,
2935
+ ...typeof event.timestamp === "string" ? { occurredAt: event.timestamp } : {}
2936
+ });
2937
+ }
2938
+ }
2939
+ if (event.type === "event_msg" && payload?.type === "agent_message") {
2940
+ const text = stringFromCodexContent(payload.message);
2941
+ if (text) {
2942
+ pushMessage({
2943
+ role: "assistant",
2944
+ text,
2945
+ ...typeof event.timestamp === "string" ? { occurredAt: event.timestamp } : {}
2946
+ });
2947
+ }
2948
+ }
2949
+ if (event.type === "response_item" && payload?.type === "message" && (payload.role === "user" || payload.role === "assistant")) {
2950
+ const text = stringFromCodexContent(payload.content) ?? stringFromCodexContent(payload.message);
2951
+ if (text) {
2952
+ pushMessage({
2953
+ role: payload.role,
2954
+ text,
2955
+ ...typeof event.timestamp === "string" ? { occurredAt: event.timestamp } : {}
2956
+ });
2957
+ }
2958
+ }
2959
+ }
2960
+ const turns = [];
2961
+ let pendingUser;
2962
+ let pendingAssistantParts = [];
2963
+ const flushPendingTurn = () => {
2964
+ if (!pendingUser) {
2965
+ return;
2966
+ }
2967
+ const assistant = pendingAssistantParts.join(`
2968
+
2969
+ `).trim();
2970
+ turns.push({
2971
+ user: truncateText(pendingUser.text, 6000),
2972
+ ...assistant ? { assistant: truncateText(assistant, 6000) } : {},
2973
+ ...pendingUser.occurredAt ? { occurredAt: pendingUser.occurredAt } : {}
2974
+ });
2975
+ pendingUser = undefined;
2976
+ pendingAssistantParts = [];
2977
+ };
2978
+ for (const message of messages) {
2979
+ if (message.role === "user") {
2980
+ flushPendingTurn();
2981
+ pendingUser = {
2982
+ text: message.text,
2983
+ ...message.occurredAt ? { occurredAt: message.occurredAt } : {}
2984
+ };
2985
+ pendingAssistantParts = [];
2986
+ continue;
2987
+ }
2988
+ if (pendingUser) {
2989
+ if (pendingAssistantParts[pendingAssistantParts.length - 1] !== message.text) {
2990
+ pendingAssistantParts.push(message.text);
2991
+ }
2992
+ } else {
2993
+ turns.push({
2994
+ assistant: truncateText(message.text, 6000),
2995
+ ...message.occurredAt ? { occurredAt: message.occurredAt } : {}
2996
+ });
2997
+ }
2998
+ }
2999
+ flushPendingTurn();
3000
+ return turns.length ? turns.slice(-3) : undefined;
3001
+ }
3002
+ function readRecentTurnsFromCodexSession(agentSessionId, options) {
3003
+ const sessionsDirectory = codexSessionsDirectory(options);
3004
+ if (!sessionsDirectory) {
3005
+ return;
3006
+ }
3007
+ const sessionFile = findCodexSessionFile(sessionsDirectory, agentSessionId);
3008
+ return sessionFile ? readRecentTurnsFromCodexSessionFile(sessionFile) : undefined;
3009
+ }
2582
3010
  function normalizeAgentEvent(input, parsed, options) {
2583
3011
  const source = parseAgentEventSource(parsed.options.source);
2584
3012
  const event = parsed.options.event ?? "stop";
2585
3013
  const base = normalizeAgentEventBase(input, source, event, options);
2586
3014
  switch (source) {
2587
3015
  case "codex":
2588
- return normalizeCodexAgentEvent(input, base);
3016
+ return normalizeCodexAgentEvent(input, base, options);
2589
3017
  case "claude-code":
2590
3018
  case "opencode":
2591
3019
  case "openrouter":
@@ -2596,6 +3024,7 @@ function normalizeAgentEvent(input, parsed, options) {
2596
3024
  function normalizeAgentEventBase(input, source, event, options) {
2597
3025
  const threadId = firstString(input, [["thread_id"], ["threadId"], ["conversation_id"], ["conversationId"]]);
2598
3026
  const turnId = firstString(input, [["turn_id"], ["turnId"], ["id"]]);
3027
+ const recentTurns = readRecentAgentTurns(input);
2599
3028
  const lastAssistantMessage = firstString(input, [
2600
3029
  ["last_assistant_message"],
2601
3030
  ["lastAssistantMessage"],
@@ -2612,10 +3041,11 @@ function normalizeAgentEventBase(input, source, event, options) {
2612
3041
  ...threadId ? { threadId } : {},
2613
3042
  ...turnId ? { turnId } : {},
2614
3043
  ...lastAssistantMessage ? { lastAssistantMessage: truncateText(lastAssistantMessage, 6000) } : {},
3044
+ ...recentTurns ? { recentTurns } : {},
2615
3045
  payloadKeys: Object.keys(input).sort()
2616
3046
  };
2617
3047
  }
2618
- function normalizeCodexAgentEvent(input, base) {
3048
+ function normalizeCodexAgentEvent(input, base, options) {
2619
3049
  const agentSessionId = firstString(input, [["session_id"], ["sessionId"], ["session", "id"]]);
2620
3050
  if (!agentSessionId) {
2621
3051
  return {
@@ -2623,10 +3053,13 @@ function normalizeCodexAgentEvent(input, base) {
2623
3053
  diagnosticReason: "missing_agent_session_id"
2624
3054
  };
2625
3055
  }
3056
+ const transcriptPath = firstString(input, [["transcript_path"], ["transcriptPath"]]);
3057
+ const recentTurns = base.recentTurns?.length ? base.recentTurns : transcriptPath ? readRecentTurnsFromCodexSessionFile(transcriptPath) : readRecentTurnsFromCodexSession(agentSessionId, options);
2626
3058
  return {
2627
3059
  ...base,
2628
3060
  agentSessionId,
2629
- agentSessionKey: agentSessionKey(base.source, agentSessionId)
3061
+ agentSessionKey: agentSessionKey(base.source, agentSessionId),
3062
+ ...recentTurns?.length ? { recentTurns } : {}
2630
3063
  };
2631
3064
  }
2632
3065
  function normalizeGenericAgentEvent(input, base) {
@@ -2652,14 +3085,23 @@ function normalizeGenericAgentEvent(input, base) {
2652
3085
  function agentSessionKey(source, agentSessionId) {
2653
3086
  return `${source}:${stableHash(agentSessionId)}`;
2654
3087
  }
3088
+ function checkpointWorkLogMetadata(settings, tokenAccounting) {
3089
+ return {
3090
+ checkpoint: {
3091
+ provider: settings.provider,
3092
+ ...settings.model ? { model: settings.model } : {},
3093
+ ...tokenAccounting ? { tokenAccounting } : {}
3094
+ }
3095
+ };
3096
+ }
2655
3097
  function defaultAgentLoopState() {
2656
3098
  return {
2657
3099
  version: 2,
2658
3100
  sessions: {}
2659
3101
  };
2660
3102
  }
2661
- function randomCheckpointThreshold() {
2662
- return defaultCheckpointThresholdMin + Math.floor(Math.random() * (defaultCheckpointThresholdMax - defaultCheckpointThresholdMin + 1));
3103
+ function defaultCheckpointThresholdValue() {
3104
+ return defaultCheckpointThreshold;
2663
3105
  }
2664
3106
  function agentLoopDirectory(parsed, options) {
2665
3107
  return parsed.options["state-dir"] ? isAbsolute(parsed.options["state-dir"]) ? parsed.options["state-dir"] : join(options.cwd ?? process.cwd(), parsed.options["state-dir"]) : join(options.cwd ?? process.cwd(), ".context", "cadence-agent-loop");
@@ -2667,8 +3109,81 @@ function agentLoopDirectory(parsed, options) {
2667
3109
  function agentLoopStatePath(parsed, options) {
2668
3110
  return join(agentLoopDirectory(parsed, options), "state.json");
2669
3111
  }
3112
+ function agentLoopSettingsPath(parsed, options) {
3113
+ if (parsed.options["state-dir"]) {
3114
+ return join(agentLoopDirectory(parsed, options), "settings.json");
3115
+ }
3116
+ return join(getConfigHome(options.env ?? process.env), "agent-run", "settings.json");
3117
+ }
3118
+ function legacyAgentLoopSettingsPath(parsed, options) {
3119
+ return join(agentLoopDirectory(parsed, options), "settings.json");
3120
+ }
2670
3121
  function agentLoopLockPath(parsed, options, agentSessionKeyValue) {
2671
- return join(agentLoopDirectory(parsed, options), agentSessionKeyValue ? `closeout-${stableHash(agentSessionKeyValue)}.lock` : "closeout.lock");
3122
+ return join(agentLoopDirectory(parsed, options), agentSessionKeyValue ? `checkpoint-${stableHash(agentSessionKeyValue)}.lock` : "checkpoint.lock");
3123
+ }
3124
+ function normalizeCheckpointModel(value) {
3125
+ const trimmed = value?.trim();
3126
+ if (!trimmed) {
3127
+ return;
3128
+ }
3129
+ if (trimmed.length > 120 || /\s/.test(trimmed)) {
3130
+ throw new CliError("CLI_USAGE", "Checkpoint model must be a non-empty model id without whitespace.");
3131
+ }
3132
+ return trimmed;
3133
+ }
3134
+ function defaultAgentLoopSettings() {
3135
+ return {
3136
+ version: 1,
3137
+ checkpoint: {
3138
+ provider: "codex"
3139
+ }
3140
+ };
3141
+ }
3142
+ async function readAgentLoopSettings(parsed, options) {
3143
+ const filePath = agentLoopSettingsPath(parsed, options);
3144
+ let file = Bun.file(filePath);
3145
+ if (!await file.exists()) {
3146
+ const legacyFilePath = legacyAgentLoopSettingsPath(parsed, options);
3147
+ if (legacyFilePath === filePath) {
3148
+ return defaultAgentLoopSettings();
3149
+ }
3150
+ file = Bun.file(legacyFilePath);
3151
+ if (!await file.exists()) {
3152
+ return defaultAgentLoopSettings();
3153
+ }
3154
+ }
3155
+ const parsedSettings = JSON.parse(await file.text());
3156
+ if (!parsedSettings || typeof parsedSettings !== "object" || Array.isArray(parsedSettings)) {
3157
+ return defaultAgentLoopSettings();
3158
+ }
3159
+ const record = parsedSettings;
3160
+ const checkpoint = record.checkpoint;
3161
+ if (!checkpoint || typeof checkpoint !== "object" || Array.isArray(checkpoint)) {
3162
+ return defaultAgentLoopSettings();
3163
+ }
3164
+ const checkpointRecord = checkpoint;
3165
+ const provider = checkpointRecord.provider === "codex" ? "codex" : "codex";
3166
+ const model = typeof checkpointRecord.model === "string" ? normalizeCheckpointModel(checkpointRecord.model) : undefined;
3167
+ return {
3168
+ version: 1,
3169
+ checkpoint: {
3170
+ provider,
3171
+ ...model ? { model } : {},
3172
+ ...typeof checkpointRecord.updatedAt === "string" ? { updatedAt: checkpointRecord.updatedAt } : {}
3173
+ }
3174
+ };
3175
+ }
3176
+ async function resolveCheckpointSettings(parsed, options) {
3177
+ const saved = await readAgentLoopSettings(parsed, options);
3178
+ const provider = parsed.options["checkpoint-provider"] ?? saved.checkpoint.provider;
3179
+ if (provider !== "codex") {
3180
+ throw new CliError("CLI_USAGE", "Only the Codex checkpoint provider is supported in this version.");
3181
+ }
3182
+ const model = normalizeCheckpointModel(parsed.options["checkpoint-model"] ?? parsed.options.model ?? saved.checkpoint.model);
3183
+ return {
3184
+ provider: "codex",
3185
+ ...model ? { model } : {}
3186
+ };
2672
3187
  }
2673
3188
  async function readAgentLoopState(parsed, options) {
2674
3189
  const filePath = agentLoopStatePath(parsed, options);
@@ -2714,24 +3229,45 @@ function readAgentLoopSessions(rawSessions) {
2714
3229
  const record = rawSession;
2715
3230
  const source = parseAgentEventSource(typeof record.source === "string" ? record.source : undefined);
2716
3231
  const stopCount = typeof record.stopCount === "number" && Number.isInteger(record.stopCount) && record.stopCount >= 0 ? record.stopCount : 0;
2717
- const threshold = typeof record.threshold === "number" && Number.isInteger(record.threshold) && record.threshold > 0 ? record.threshold : randomCheckpointThreshold();
3232
+ const threshold = typeof record.threshold === "number" && Number.isInteger(record.threshold) && record.threshold > 0 ? record.threshold : defaultCheckpointThresholdValue();
3233
+ const recentTurns = Array.isArray(record.recentTurns) ? readRecentAgentTurns({
3234
+ recentTurns: record.recentTurns
3235
+ }) : undefined;
2718
3236
  sessions[key] = {
2719
3237
  source,
2720
3238
  stopCount,
2721
3239
  threshold,
3240
+ ...readAgentSessionCadenceContext(record),
2722
3241
  ...typeof record.firstObservedAt === "string" ? { firstObservedAt: record.firstObservedAt } : {},
2723
3242
  ...typeof record.lastObservedAt === "string" ? { lastObservedAt: record.lastObservedAt } : {},
3243
+ ...typeof record.lastObservedTurnId === "string" ? { lastObservedTurnId: record.lastObservedTurnId } : {},
2724
3244
  ...typeof record.lastAction === "string" ? { lastAction: record.lastAction } : {},
2725
3245
  ...typeof record.lastReason === "string" ? { lastReason: record.lastReason } : {},
2726
3246
  ...typeof record.lastEventFile === "string" ? { lastEventFile: record.lastEventFile } : {},
2727
3247
  ...typeof record.lastAssistantMessage === "string" ? { lastAssistantMessage: record.lastAssistantMessage } : {},
3248
+ ...recentTurns ? { recentTurns } : {},
2728
3249
  ...typeof record.lastCheckpointAt === "string" ? { lastCheckpointAt: record.lastCheckpointAt } : {},
2729
3250
  ...typeof record.lastCheckpointFingerprint === "string" ? { lastCheckpointFingerprint: record.lastCheckpointFingerprint } : {},
2730
- ...typeof record.previousCheckpointSummary === "string" ? { previousCheckpointSummary: record.previousCheckpointSummary } : {}
3251
+ ...typeof record.previousCheckpointSummary === "string" ? { previousCheckpointSummary: record.previousCheckpointSummary } : {},
3252
+ ...typeof record.lastCheckpointAuditFile === "string" ? { lastCheckpointAuditFile: record.lastCheckpointAuditFile } : {}
2731
3253
  };
2732
3254
  }
2733
3255
  return sessions;
2734
3256
  }
3257
+ function readAgentSessionCadenceContext(record) {
3258
+ const value = record.cadenceContext;
3259
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
3260
+ return {};
3261
+ }
3262
+ const contextRecord = value;
3263
+ const cadenceContext = {
3264
+ ...typeof contextRecord.ticketId === "string" ? { ticketId: contextRecord.ticketId } : {},
3265
+ ...typeof contextRecord.sessionId === "string" ? { sessionId: contextRecord.sessionId } : {},
3266
+ ...typeof contextRecord.changesetId === "string" ? { changesetId: contextRecord.changesetId } : {},
3267
+ ...typeof contextRecord.capturedAt === "string" ? { capturedAt: contextRecord.capturedAt } : {}
3268
+ };
3269
+ return cadenceContext.ticketId || cadenceContext.sessionId || cadenceContext.changesetId ? { cadenceContext } : {};
3270
+ }
2735
3271
  function readAgentLoopDiagnostics(record) {
2736
3272
  return {
2737
3273
  ...typeof record.missingSessionIdCount === "number" && Number.isInteger(record.missingSessionIdCount) && record.missingSessionIdCount > 0 ? { missingSessionIdCount: record.missingSessionIdCount } : {},
@@ -2766,6 +3302,15 @@ async function writeAgentEventFile(parsed, options, event) {
2766
3302
  `);
2767
3303
  return filePath;
2768
3304
  }
3305
+ async function writeAgentCheckpointAuditFile(parsed, options, audit) {
3306
+ const checkpointsDirectory = join(agentLoopDirectory(parsed, options), "checkpoints");
3307
+ const fileName = `${new Date().toISOString().replace(/[:.]/g, "-")}-${randomUUID()}.json`;
3308
+ const filePath = join(checkpointsDirectory, fileName);
3309
+ await mkdir(checkpointsDirectory, { recursive: true });
3310
+ await writeFile(filePath, `${JSON.stringify(audit, null, 2)}
3311
+ `);
3312
+ return filePath;
3313
+ }
2769
3314
  async function removeStaleAgentLoopLock(lockPath) {
2770
3315
  try {
2771
3316
  const lockStats = await stat(lockPath);
@@ -2851,7 +3396,7 @@ function currentCliWorkerInvocation() {
2851
3396
  argsPrefix: []
2852
3397
  };
2853
3398
  }
2854
- async function spawnAgentRunCloseout(args, options) {
3399
+ async function spawnAgentRunCheckpoint(args, options) {
2855
3400
  const cwd = options.cwd ?? process.cwd();
2856
3401
  const invocation = currentCliWorkerInvocation();
2857
3402
  const env = {
@@ -2870,47 +3415,88 @@ async function spawnAgentRunCloseout(args, options) {
2870
3415
  });
2871
3416
  child.unref();
2872
3417
  }
3418
+ function currentRecordTime(value, keys) {
3419
+ for (const key of keys) {
3420
+ const candidate = value[key];
3421
+ if (typeof candidate !== "string") {
3422
+ continue;
3423
+ }
3424
+ const time = new Date(candidate).getTime();
3425
+ if (!Number.isNaN(time)) {
3426
+ return time;
3427
+ }
3428
+ }
3429
+ return 0;
3430
+ }
2873
3431
  function activeSessionFromCurrent(current) {
2874
3432
  const record = current && typeof current === "object" ? current : null;
2875
3433
  const sessions = Array.isArray(record?.sessions) ? record.sessions : [];
2876
- const activeSession = sessions.find((session) => {
2877
- if (!session || typeof session !== "object") {
2878
- return false;
2879
- }
2880
- const sessionRecord2 = session;
2881
- return sessionRecord2.status === "active" && typeof sessionRecord2.ticketId === "string";
2882
- });
2883
- if (activeSession && typeof activeSession === "object") {
2884
- return activeSession;
2885
- }
2886
3434
  const activeLeases = Array.isArray(record?.activeLeases) ? record.activeLeases : Array.isArray(record?.leases) ? record.leases.filter((lease) => lease && typeof lease === "object" && lease.status === "active") : [];
2887
- const activeLease = activeLeases.find((lease) => {
3435
+ const activeLease = activeLeases.filter((lease) => {
2888
3436
  if (!lease || typeof lease !== "object") {
2889
3437
  return false;
2890
3438
  }
2891
- const leaseRecord2 = lease;
2892
- return typeof leaseRecord2.ticketId === "string";
2893
- });
2894
- if (!activeLease || typeof activeLease !== "object") {
2895
- return null;
3439
+ const leaseRecord = lease;
3440
+ return typeof leaseRecord.ticketId === "string";
3441
+ }).map((lease) => lease).sort((left, right) => currentRecordTime(right, ["lastSeenAt", "claimedAt", "expiresAt"]) - currentRecordTime(left, ["lastSeenAt", "claimedAt", "expiresAt"]))[0];
3442
+ if (activeLease && typeof activeLease === "object") {
3443
+ const leaseRecord = activeLease;
3444
+ const sessionId = typeof leaseRecord.sessionId === "string" ? leaseRecord.sessionId : undefined;
3445
+ const matchingSession = sessions.find((session) => {
3446
+ if (!session || typeof session !== "object") {
3447
+ return false;
3448
+ }
3449
+ return sessionId && session.id === sessionId;
3450
+ });
3451
+ const sessionRecord = matchingSession && typeof matchingSession === "object" ? matchingSession : {};
3452
+ return {
3453
+ ...sessionRecord,
3454
+ ...sessionId ? { id: sessionId } : {},
3455
+ ticketId: leaseRecord.ticketId,
3456
+ ...typeof leaseRecord.changesetId === "string" ? { changesetId: leaseRecord.changesetId } : typeof sessionRecord.changesetId === "string" ? { changesetId: sessionRecord.changesetId } : {},
3457
+ status: "active"
3458
+ };
2896
3459
  }
2897
- const leaseRecord = activeLease;
2898
- const sessionId = typeof leaseRecord.sessionId === "string" ? leaseRecord.sessionId : undefined;
2899
- const matchingSession = sessions.find((session) => {
3460
+ const activeSessions = sessions.filter((session) => {
2900
3461
  if (!session || typeof session !== "object") {
2901
3462
  return false;
2902
3463
  }
2903
- return sessionId && session.id === sessionId;
3464
+ const sessionRecord = session;
3465
+ return sessionRecord.status === "active" && typeof sessionRecord.ticketId === "string";
2904
3466
  });
2905
- const sessionRecord = matchingSession && typeof matchingSession === "object" ? matchingSession : {};
3467
+ const activeSession = activeSessions.map((session) => session).sort((left, right) => currentRecordTime(right, ["lastActivityAt", "startedAt"]) - currentRecordTime(left, ["lastActivityAt", "startedAt"]))[0];
3468
+ if (activeSession && typeof activeSession === "object") {
3469
+ return activeSession;
3470
+ }
3471
+ return null;
3472
+ }
3473
+ function cadenceContextFromCurrent(current) {
3474
+ const activeSession = activeSessionFromCurrent(current);
3475
+ if (!activeSession) {
3476
+ return;
3477
+ }
3478
+ const ticketId = typeof activeSession.ticketId === "string" ? activeSession.ticketId : undefined;
3479
+ const sessionId = typeof activeSession.id === "string" ? activeSession.id : undefined;
3480
+ const changesetId = typeof activeSession.changesetId === "string" ? activeSession.changesetId : undefined;
3481
+ if (!ticketId && !sessionId && !changesetId) {
3482
+ return;
3483
+ }
2906
3484
  return {
2907
- ...sessionRecord,
2908
- ...sessionId ? { id: sessionId } : {},
2909
- ticketId: leaseRecord.ticketId,
2910
- ...typeof leaseRecord.changesetId === "string" ? { changesetId: leaseRecord.changesetId } : typeof sessionRecord.changesetId === "string" ? { changesetId: sessionRecord.changesetId } : {},
2911
- status: "active"
3485
+ ...ticketId ? { ticketId } : {},
3486
+ ...sessionId ? { sessionId } : {},
3487
+ ...changesetId ? { changesetId } : {},
3488
+ capturedAt: new Date().toISOString()
2912
3489
  };
2913
3490
  }
3491
+ async function readCurrentCadenceContext(client, projectId) {
3492
+ const current = await client.sessions.current({
3493
+ projectId,
3494
+ filters: {
3495
+ limit: 100
3496
+ }
3497
+ });
3498
+ return cadenceContextFromCurrent(current);
3499
+ }
2914
3500
  function fingerprintForCheckpoint(event, options) {
2915
3501
  const status = gitOutput(["status", "--short"], options);
2916
3502
  const changedFiles = gitOutput(["diff", "--name-only", "origin/dev..."], options);
@@ -2918,6 +3504,7 @@ function fingerprintForCheckpoint(event, options) {
2918
3504
  source: event.source,
2919
3505
  event: event.event,
2920
3506
  lastAssistantMessage: event.lastAssistantMessage ?? "",
3507
+ recentTurns: event.recentTurns ?? [],
2921
3508
  status,
2922
3509
  changedFiles
2923
3510
  }));
@@ -2932,60 +3519,486 @@ function shouldSkipForCooldown(state, cooldownSeconds) {
2932
3519
  function synthesizeAgentEventFromSession(agentSessionKeyValue, session, options) {
2933
3520
  return {
2934
3521
  source: session.source,
2935
- event: "closeout",
3522
+ event: "checkpoint",
2936
3523
  workspacePath: options.cwd ?? process.cwd(),
2937
3524
  occurredAt: new Date().toISOString(),
2938
3525
  agentSessionKey: agentSessionKeyValue,
2939
3526
  ...session.lastAssistantMessage ? { lastAssistantMessage: session.lastAssistantMessage } : {},
3527
+ ...session.recentTurns?.length ? { recentTurns: session.recentTurns } : {},
2940
3528
  payloadKeys: []
2941
3529
  };
2942
3530
  }
2943
- function parseCheckpointJson(raw) {
2944
- const trimmed = raw.trim();
2945
- try {
2946
- const parsed = JSON.parse(trimmed);
2947
- if (parsed && typeof parsed === "object") {
2948
- const record = parsed;
2949
- return {
2950
- body: typeof record.body === "string" && record.body.trim() ? record.body.trim() : trimmed,
2951
- summary: typeof record.summary === "string" && record.summary.trim() ? record.summary.trim() : undefined
2952
- };
3531
+ var checkpointRouteActions = ["current", "noop", "intake_create", "intake_attach", "switch_existing", "needs_human"];
3532
+ var checkpointConfidenceLevels = ["low", "medium", "high"];
3533
+ var checkpointSessionActions = ["keep", "handoff", "end", "complete_ticket"];
3534
+ var highAutonomyRouteActions = new Set(["intake_create", "intake_attach", "switch_existing"]);
3535
+ var checkpointServerTextLimit = 1200;
3536
+ function checkpointEntrySummary(body) {
3537
+ return truncateText(body.replace(/\s+/g, " ").trim(), 500);
3538
+ }
3539
+ function checkpointString(record, keys) {
3540
+ for (const key of keys) {
3541
+ const value = record[key];
3542
+ if (typeof value === "string" && value.trim()) {
3543
+ return value.trim();
2953
3544
  }
3545
+ }
3546
+ return;
3547
+ }
3548
+ function parseCheckpointParent(value) {
3549
+ if (typeof value !== "string" || !value.trim()) {
3550
+ return {};
3551
+ }
3552
+ try {
3553
+ return parseWorkLogParent(value.trim());
2954
3554
  } catch {
2955
- const start = trimmed.indexOf("{");
2956
- const end = trimmed.lastIndexOf("}");
2957
- if (start >= 0 && end > start) {
2958
- try {
2959
- const parsed = JSON.parse(trimmed.slice(start, end + 1));
2960
- if (parsed && typeof parsed === "object") {
2961
- const record = parsed;
2962
- return {
2963
- body: typeof record.body === "string" && record.body.trim() ? record.body.trim() : trimmed,
2964
- summary: typeof record.summary === "string" && record.summary.trim() ? record.summary.trim() : undefined
2965
- };
2966
- }
2967
- } catch {}
2968
- }
3555
+ return {};
2969
3556
  }
2970
- return {
2971
- body: trimmed,
2972
- summary: undefined
2973
- };
2974
3557
  }
2975
- function buildCheckpointPrompt(input) {
2976
- return [
2977
- "You are generating a concise Cadence checkpoint for an active coding session.",
2978
- "Do not include raw diffs, raw transcripts, terminal logs, tool streams, secrets, or model reasoning.",
2979
- 'Return JSON only with this shape: {"summary":"one short current work summary","body":"checkpoint body suitable for a Cadence ticket work log"}.',
2980
- "The body should mention what changed, decisions made, verification if known, and the next risk or next step. Keep it under 1200 characters.",
2981
- "",
2982
- `Ticket: ${input.ticketId}`,
2983
- input.sessionId ? `Session: ${input.sessionId}` : "",
2984
- input.changesetId ? `ChangeSet: ${input.changesetId}` : "",
2985
- `Agent event: ${input.event.source}/${input.event.event}`,
2986
- input.previousCheckpointSummary ? `Previous checkpoint summary: ${input.previousCheckpointSummary}` : "",
2987
- input.event.lastAssistantMessage ? `Last assistant message:
2988
- ${truncateText(input.event.lastAssistantMessage, 3000)}` : "Last assistant message: unavailable",
3558
+ function parseCheckpointEntry(value, fallbackKind) {
3559
+ if (typeof value === "string" && value.trim()) {
3560
+ const body2 = value.trim();
3561
+ return {
3562
+ kind: fallbackKind,
3563
+ body: body2,
3564
+ summary: checkpointEntrySummary(body2)
3565
+ };
3566
+ }
3567
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
3568
+ return;
3569
+ }
3570
+ const record = value;
3571
+ const body = checkpointString(record, ["body", "text", "content"]);
3572
+ if (!body) {
3573
+ return;
3574
+ }
3575
+ let kind = fallbackKind;
3576
+ const rawKind = checkpointString(record, ["kind", "entryKind", "type"]);
3577
+ if (rawKind && workLogEntryKinds.includes(rawKind)) {
3578
+ kind = rawKind;
3579
+ }
3580
+ const parent = parseCheckpointParent(record.under ?? record.parent ?? record.parentSelector ?? record.parentEntryId);
3581
+ const summary = checkpointString(record, ["summary", "title"]);
3582
+ return {
3583
+ kind,
3584
+ body,
3585
+ summary: summary ?? checkpointEntrySummary(body),
3586
+ ...parent
3587
+ };
3588
+ }
3589
+ function parseCheckpointSummaryUpdate(value) {
3590
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
3591
+ return;
3592
+ }
3593
+ const record = value;
3594
+ const update = record.update === true;
3595
+ const summary = checkpointString(record, ["value", "summary", "currentSummary"]);
3596
+ const reason = checkpointString(record, ["reason"]);
3597
+ return {
3598
+ update,
3599
+ ...summary ? { value: summary } : {},
3600
+ ...reason ? { reason } : {}
3601
+ };
3602
+ }
3603
+ function parseCheckpointRecord(record, rawText, fallbackKind) {
3604
+ const rawEntries = Array.isArray(record.entries) ? record.entries : [];
3605
+ const entries = rawEntries.map((entry) => parseCheckpointEntry(entry, fallbackKind)).filter((entry) => Boolean(entry));
3606
+ const legacyBody = checkpointString(record, ["body"]);
3607
+ const summary = checkpointString(record, ["summary"]);
3608
+ const parsedEntries = entries.length > 0 ? entries : legacyBody ? [parseCheckpointEntry({ kind: fallbackKind, body: legacyBody, summary }, fallbackKind)].filter((entry) => Boolean(entry)) : [];
3609
+ const currentSummary = parseCheckpointSummaryUpdate(record.currentSummary);
3610
+ return {
3611
+ summary: summary ?? parsedEntries[0]?.summary ?? checkpointEntrySummary(rawText),
3612
+ entries: parsedEntries,
3613
+ ...currentSummary ? { currentSummary } : {}
3614
+ };
3615
+ }
3616
+ function parseCheckpointJson(raw, fallbackKind) {
3617
+ const trimmed = raw.trim();
3618
+ try {
3619
+ const parsed = JSON.parse(trimmed);
3620
+ if (parsed && typeof parsed === "object") {
3621
+ return parseCheckpointRecord(parsed, trimmed, fallbackKind);
3622
+ }
3623
+ } catch {
3624
+ const start = trimmed.indexOf("{");
3625
+ const end = trimmed.lastIndexOf("}");
3626
+ if (start >= 0 && end > start) {
3627
+ try {
3628
+ const parsed = JSON.parse(trimmed.slice(start, end + 1));
3629
+ if (parsed && typeof parsed === "object") {
3630
+ return parseCheckpointRecord(parsed, trimmed, fallbackKind);
3631
+ }
3632
+ } catch {}
3633
+ }
3634
+ }
3635
+ return {
3636
+ summary: checkpointEntrySummary(trimmed),
3637
+ entries: trimmed ? [
3638
+ {
3639
+ kind: fallbackKind,
3640
+ body: trimmed,
3641
+ summary: checkpointEntrySummary(trimmed)
3642
+ }
3643
+ ] : []
3644
+ };
3645
+ }
3646
+ function checkpointValidationError(message, details) {
3647
+ return new CliError("AGENT_RUN_CHECKPOINT_INVALID_PLAN", message, details);
3648
+ }
3649
+ function checkpointPlanString(record, key, limit = checkpointServerTextLimit) {
3650
+ const value = record[key];
3651
+ if (typeof value !== "string") {
3652
+ return;
3653
+ }
3654
+ const trimmed = value.trim();
3655
+ if (!trimmed) {
3656
+ return;
3657
+ }
3658
+ if (trimmed.length > limit) {
3659
+ throw checkpointValidationError(`Checkpoint plan field "${key}" is too long.`, { key, limit });
3660
+ }
3661
+ return trimmed;
3662
+ }
3663
+ function parseCheckpointRoute(value) {
3664
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
3665
+ throw checkpointValidationError("Checkpoint plan requires route.");
3666
+ }
3667
+ const record = value;
3668
+ const actionValue = checkpointPlanString(record, "action", 80);
3669
+ const confidenceValue = checkpointPlanString(record, "confidence", 20) ?? "medium";
3670
+ if (!actionValue || !checkpointRouteActions.includes(actionValue)) {
3671
+ throw checkpointValidationError("Checkpoint plan route.action is invalid.", { action: actionValue });
3672
+ }
3673
+ if (!checkpointConfidenceLevels.includes(confidenceValue)) {
3674
+ throw checkpointValidationError("Checkpoint plan route.confidence is invalid.", { confidence: confidenceValue });
3675
+ }
3676
+ const reason = checkpointPlanString(record, "reason");
3677
+ const request = checkpointPlanString(record, "request");
3678
+ const targetTicketId = checkpointPlanString(record, "targetTicketId", 100);
3679
+ return {
3680
+ action: actionValue,
3681
+ confidence: confidenceValue,
3682
+ ...reason ? { reason } : {},
3683
+ ...request ? { request } : {},
3684
+ ...targetTicketId ? { targetTicketId } : {}
3685
+ };
3686
+ }
3687
+ function parseCheckpointPlanEntry(value) {
3688
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
3689
+ throw checkpointValidationError("Checkpoint plan entries must be objects.");
3690
+ }
3691
+ const record = value;
3692
+ const rawKind = checkpointPlanString(record, "kind", 40) ?? checkpointPlanString(record, "entryKind", 40) ?? checkpointPlanString(record, "type", 40);
3693
+ const body = checkpointPlanString(record, "body") ?? checkpointPlanString(record, "text") ?? checkpointPlanString(record, "content");
3694
+ if (!rawKind || !workLogEntryKinds.includes(rawKind)) {
3695
+ throw checkpointValidationError("Checkpoint plan entry kind is invalid.", { kind: rawKind });
3696
+ }
3697
+ if (!body) {
3698
+ throw checkpointValidationError("Checkpoint plan entry body is required.");
3699
+ }
3700
+ const parent = parseCheckpointParent(record.under ?? record.parent ?? record.parentSelector ?? record.parentEntryId);
3701
+ const summary = checkpointPlanString(record, "summary", 300) ?? checkpointPlanString(record, "title", 300);
3702
+ return {
3703
+ kind: rawKind,
3704
+ body,
3705
+ summary: summary ?? checkpointEntrySummary(body),
3706
+ ...parent
3707
+ };
3708
+ }
3709
+ function parseCheckpointPlanSummaryUpdate(value) {
3710
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
3711
+ return;
3712
+ }
3713
+ const record = value;
3714
+ const update = record.update === true;
3715
+ const summary = checkpointPlanString(record, "value") ?? checkpointPlanString(record, "summary") ?? checkpointPlanString(record, "currentSummary");
3716
+ const reason = checkpointPlanString(record, "reason");
3717
+ return {
3718
+ update,
3719
+ ...summary ? { value: summary } : {},
3720
+ ...reason ? { reason } : {}
3721
+ };
3722
+ }
3723
+ function parseCheckpointPlanSession(value) {
3724
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
3725
+ return;
3726
+ }
3727
+ const record = value;
3728
+ const action = checkpointPlanString(record, "action", 40) ?? "keep";
3729
+ if (!checkpointSessionActions.includes(action)) {
3730
+ throw checkpointValidationError("Checkpoint plan session.action is invalid.", { action });
3731
+ }
3732
+ const summary = checkpointPlanString(record, "summary");
3733
+ const reason = checkpointPlanString(record, "reason");
3734
+ return {
3735
+ action,
3736
+ ...summary ? { summary } : {},
3737
+ ...reason ? { reason } : {}
3738
+ };
3739
+ }
3740
+ function parseCheckpointPlanFiles(value) {
3741
+ if (!Array.isArray(value)) {
3742
+ return [];
3743
+ }
3744
+ return value.map((item) => {
3745
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
3746
+ throw checkpointValidationError("Checkpoint plan files must be objects.");
3747
+ }
3748
+ const record = item;
3749
+ const path = checkpointPlanString(record, "path", 500);
3750
+ if (!path || isAbsolute(path) || path.split("/").includes("..")) {
3751
+ throw checkpointValidationError("Checkpoint plan file path must be a safe relative path.", { path });
3752
+ }
3753
+ return {
3754
+ path,
3755
+ kind: parseSessionFileChangeKind(checkpointPlanString(record, "kind", 40) ?? checkpointPlanString(record, "changeKind", 40))
3756
+ };
3757
+ });
3758
+ }
3759
+ function routeRequiresHighConfidence(route, session) {
3760
+ return highAutonomyRouteActions.has(route.action) || session?.action === "complete_ticket";
3761
+ }
3762
+ function forceNeedsHuman(plan, reason) {
3763
+ return {
3764
+ ...plan,
3765
+ route: {
3766
+ ...plan.route,
3767
+ action: "needs_human",
3768
+ reason
3769
+ },
3770
+ validationWarnings: [...plan.validationWarnings, reason]
3771
+ };
3772
+ }
3773
+ function normalizeCheckpointPlan(plan) {
3774
+ if (routeRequiresHighConfidence(plan.route, plan.session) && plan.route.confidence !== "high") {
3775
+ return forceNeedsHuman(plan, "Lifecycle mutation requires high confidence.");
3776
+ }
3777
+ if (plan.route.action === "noop") {
3778
+ return {
3779
+ ...plan,
3780
+ entries: []
3781
+ };
3782
+ }
3783
+ return plan;
3784
+ }
3785
+ function parseCheckpointPlanRecord(record, rawText, fallbackKind) {
3786
+ if (!("route" in record)) {
3787
+ const legacy = parseCheckpointRecord(record, rawText, fallbackKind);
3788
+ const reason = legacy.currentSummary?.reason ?? legacy.summary;
3789
+ const route2 = {
3790
+ action: legacy.entries.length ? "current" : "noop",
3791
+ confidence: "high",
3792
+ ...reason ? { reason } : {}
3793
+ };
3794
+ return normalizeCheckpointPlan({
3795
+ ...legacy.summary ? { summary: legacy.summary } : {},
3796
+ route: route2,
3797
+ entries: legacy.entries,
3798
+ ...legacy.currentSummary ? { summaryUpdate: legacy.currentSummary } : {},
3799
+ files: [],
3800
+ legacy: true,
3801
+ validationWarnings: []
3802
+ });
3803
+ }
3804
+ const route = parseCheckpointRoute(record.route);
3805
+ const entries = Array.isArray(record.entries) ? record.entries.map(parseCheckpointPlanEntry) : [];
3806
+ const summary = checkpointPlanString(record, "summary");
3807
+ const summaryUpdate = parseCheckpointPlanSummaryUpdate(record.summaryUpdate ?? record.currentSummary);
3808
+ const session = parseCheckpointPlanSession(record.session);
3809
+ return normalizeCheckpointPlan({
3810
+ ...summary ? { summary } : {},
3811
+ route,
3812
+ entries,
3813
+ ...summaryUpdate ? { summaryUpdate } : {},
3814
+ ...session ? { session } : {},
3815
+ files: parseCheckpointPlanFiles(record.files),
3816
+ legacy: false,
3817
+ validationWarnings: []
3818
+ });
3819
+ }
3820
+ function parseCheckpointPlanJson(raw, fallbackKind) {
3821
+ const trimmed = raw.trim();
3822
+ try {
3823
+ const parsed = JSON.parse(trimmed);
3824
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
3825
+ return parseCheckpointPlanRecord(parsed, trimmed, fallbackKind);
3826
+ }
3827
+ } catch (error) {
3828
+ if (error instanceof CliError) {
3829
+ throw error;
3830
+ }
3831
+ const start = trimmed.indexOf("{");
3832
+ const end = trimmed.lastIndexOf("}");
3833
+ if (start >= 0 && end > start) {
3834
+ try {
3835
+ const parsed = JSON.parse(trimmed.slice(start, end + 1));
3836
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
3837
+ return parseCheckpointPlanRecord(parsed, trimmed, fallbackKind);
3838
+ }
3839
+ } catch (innerError) {
3840
+ if (innerError instanceof CliError) {
3841
+ throw innerError;
3842
+ }
3843
+ }
3844
+ }
3845
+ }
3846
+ const legacy = parseCheckpointJson(trimmed, fallbackKind);
3847
+ const reason = legacy.currentSummary?.reason ?? legacy.summary;
3848
+ return normalizeCheckpointPlan({
3849
+ ...legacy.summary ? { summary: legacy.summary } : {},
3850
+ route: {
3851
+ action: legacy.entries.length ? "current" : "noop",
3852
+ confidence: "high",
3853
+ ...reason ? { reason } : {}
3854
+ },
3855
+ entries: legacy.entries,
3856
+ ...legacy.currentSummary ? { summaryUpdate: legacy.currentSummary } : {},
3857
+ files: [],
3858
+ legacy: true,
3859
+ validationWarnings: []
3860
+ });
3861
+ }
3862
+ function checkpointRouteRequiresIntake(action) {
3863
+ return action === "intake_create" || action === "intake_attach" || action === "switch_existing";
3864
+ }
3865
+ function checkpointPlanCompletionAllowed(event, summary) {
3866
+ const text = [
3867
+ summary,
3868
+ event.lastAssistantMessage ?? "",
3869
+ ...(event.recentTurns ?? []).flatMap((turn) => [turn.user ?? "", turn.assistant ?? ""])
3870
+ ].join(`
3871
+ `).toLowerCase();
3872
+ return /\b(done|complete|completed|finish|finished|close|closed|ship|shipped|push|pushed|pr|pull request|merged|clean branch|verified)\b/.test(text);
3873
+ }
3874
+ function checkpointPlanTitle(plan) {
3875
+ const intent = plan.entries.find((entry) => entry.kind === "intent");
3876
+ return truncateText(intent?.summary ?? plan.summary ?? plan.route.request ?? "Checkpoint routed work", 120);
3877
+ }
3878
+ function checkpointPlanDescription(plan) {
3879
+ return truncateText(plan.route.request ?? plan.summary ?? plan.route.reason ?? checkpointPlanTitle(plan), 1000);
3880
+ }
3881
+ function checkpointHasConflictingIntakeResult(intake, plan) {
3882
+ if (!intake || typeof intake !== "object" || Array.isArray(intake)) {
3883
+ return false;
3884
+ }
3885
+ const record = intake;
3886
+ const classification = typeof record.classification === "string" ? record.classification : undefined;
3887
+ const candidates = Array.isArray(record.candidates) ? record.candidates.filter((candidate) => candidate && typeof candidate === "object") : [];
3888
+ if (plan.route.action === "intake_create" && classification && classification !== "new") {
3889
+ return true;
3890
+ }
3891
+ if ((plan.route.action === "intake_attach" || plan.route.action === "switch_existing") && plan.route.targetTicketId && candidates.length > 0) {
3892
+ return !candidates.some((candidate) => {
3893
+ const candidateRecord = candidate;
3894
+ return candidateRecord.id === plan.route.targetTicketId || candidateRecord.ticketId === plan.route.targetTicketId;
3895
+ });
3896
+ }
3897
+ return false;
3898
+ }
3899
+ function checkpointPlanNeedsHuman(plan, reason) {
3900
+ return forceNeedsHuman(plan, reason);
3901
+ }
3902
+ function checkpointLifecycleOperation(type, success, details = {}) {
3903
+ return {
3904
+ type,
3905
+ success,
3906
+ ...details
3907
+ };
3908
+ }
3909
+ function checkpointRecentTurns(event) {
3910
+ return event.recentTurns?.length ? event.recentTurns.slice(-3) : [];
3911
+ }
3912
+ function formatCheckpointRecentTurns(turns) {
3913
+ return turns.map((turn, index) => [
3914
+ `Turn ${index + 1}${turn.occurredAt ? ` (${turn.occurredAt})` : ""}:`,
3915
+ turn.user ? `User:
3916
+ ${truncateText(turn.user, 3000)}` : "User: unavailable",
3917
+ turn.assistant ? `Assistant:
3918
+ ${truncateText(turn.assistant, 3000)}` : "Assistant: unavailable"
3919
+ ].join(`
3920
+ `)).join(`
3921
+
3922
+ `);
3923
+ }
3924
+ function checkpointCapturedContextTokenCounts(event) {
3925
+ const turns = checkpointRecentTurns(event);
3926
+ if (!turns.length) {
3927
+ const fallbackText = event.lastAssistantMessage ? truncateText(event.lastAssistantMessage, 3000) : "";
3928
+ const fallbackTokens = fallbackText ? estimateTokenCount(fallbackText) : 0;
3929
+ return {
3930
+ mode: fallbackText ? "last_assistant_message" : "unavailable",
3931
+ turnCount: 0,
3932
+ userPromptTokens: 0,
3933
+ assistantResponseTokens: fallbackTokens,
3934
+ totalTokens: fallbackTokens
3935
+ };
3936
+ }
3937
+ const userPromptTokens = turns.reduce((total, turn) => total + (turn.user ? estimateTokenCount(truncateText(turn.user, 3000)) : 0), 0);
3938
+ const assistantResponseTokens = turns.reduce((total, turn) => total + (turn.assistant ? estimateTokenCount(truncateText(turn.assistant, 3000)) : 0), 0);
3939
+ return {
3940
+ mode: "recent_turns",
3941
+ turnCount: turns.length,
3942
+ userPromptTokens,
3943
+ assistantResponseTokens,
3944
+ totalTokens: estimateTokenCount(formatCheckpointRecentTurns(turns))
3945
+ };
3946
+ }
3947
+ function buildCheckpointTokenAccounting(event, prompt, tokenUsage) {
3948
+ const capturedContext = checkpointCapturedContextTokenCounts(event);
3949
+ const explicitCheckpointPromptTokens = estimateTokenCount(prompt);
3950
+ const explicitCadenceOverheadTokens = Math.max(0, explicitCheckpointPromptTokens - capturedContext.totalTokens);
3951
+ const reported = tokenUsageTotal(tokenUsage);
3952
+ const reportedInputTokens = reported?.inputTokens;
3953
+ const reportedCadenceOverheadTokens = typeof reportedInputTokens === "number" ? Math.max(0, reportedInputTokens - capturedContext.totalTokens) : undefined;
3954
+ return {
3955
+ source: "codex_token_count_plus_local_estimates",
3956
+ estimator: "cadence-simple-estimate-v1",
3957
+ capturedContext,
3958
+ explicitCheckpointPromptTokens,
3959
+ explicitCadenceOverheadTokens,
3960
+ explicitCadenceOverheadRatio: roundedRatio(explicitCadenceOverheadTokens, Math.max(1, capturedContext.totalTokens)),
3961
+ ...reported ? {
3962
+ reported: {
3963
+ ...reported.inputTokens !== undefined ? { inputTokens: reported.inputTokens } : {},
3964
+ ...reported.cachedInputTokens !== undefined ? { cachedInputTokens: reported.cachedInputTokens } : {},
3965
+ ...reported.outputTokens !== undefined ? { outputTokens: reported.outputTokens } : {},
3966
+ ...reported.reasoningOutputTokens !== undefined ? { reasoningOutputTokens: reported.reasoningOutputTokens } : {},
3967
+ ...reported.totalTokens !== undefined ? { totalTokens: reported.totalTokens } : {},
3968
+ ...tokenUsage?.modelContextWindow !== undefined ? { modelContextWindow: tokenUsage.modelContextWindow } : {}
3969
+ }
3970
+ } : {},
3971
+ ...reportedCadenceOverheadTokens !== undefined ? {
3972
+ reportedCadenceOverheadTokens,
3973
+ reportedCadenceOverheadRatio: roundedRatio(reportedCadenceOverheadTokens, Math.max(1, capturedContext.totalTokens))
3974
+ } : {}
3975
+ };
3976
+ }
3977
+ function buildCheckpointPrompt(input) {
3978
+ const recentTurns = checkpointRecentTurns(input.event);
3979
+ const recentTurnsText = recentTurns.length ? formatCheckpointRecentTurns(recentTurns) : "";
3980
+ return [
3981
+ "You are generating a compact Cadence dogfood operation plan for a hook-spawned checkpoint worker.",
3982
+ "The model judges; the Cadence CLI validates and executes. Return JSON only. Do not call tools to mutate Cadence yourself.",
3983
+ "Preserve durable ticket purpose. Identify user intent, changed intent, corrections, decisions, rationale, implementation actions, verification, blockers, and useful notes.",
3984
+ "Do not include raw diffs, raw transcripts, terminal logs, tool streams, secrets, file contents, or model reasoning in server-bound fields.",
3985
+ 'Return this sparse JSON shape: {"summary":"short checkpoint summary","route":{"action":"current|noop|intake_create|intake_attach|switch_existing|needs_human","confidence":"low|medium|high","reason":"short reason","request":"natural language work request when intake is needed","targetTicketId":"optional existing ticket id"},"entries":[{"kind":"intent|decision|rationale|action|verification|blocker|correction|note","summary":"short entry summary","body":"safe Cadence work-log body","under":"optional: last, ticket-last, session-last, last-decision, last-correction, last-action, or entry UUID"}],"summaryUpdate":{"update":false,"value":null,"reason":"short reason"},"session":{"action":"keep|handoff|end|complete_ticket","summary":"optional handoff or completion summary","reason":"short reason"},"files":[{"path":"relative/path.ts","kind":"added|modified|deleted|renamed|unknown"}]}',
3986
+ "Keep output sparse: omit summaryUpdate, session, and files unless needed. Use route.action noop with entries [] for filler, acknowledgements, or other turns with nothing durable to record.",
3987
+ "Use route.action current for the same ticket. Use intake_create for clearly unrelated new work. Use intake_attach or switch_existing only when a specific existing ticket is clearly the right target. Use needs_human when routing is ambiguous.",
3988
+ "Set route.confidence high only when the recent user/assistant context makes the route and lifecycle action clear. Lifecycle mutations require high confidence.",
3989
+ "Use note only as a last-resort context kind. Prefer intent, decision, rationale, action, verification, correction, or blocker when those fit.",
3990
+ "Each entry body should be concise narrative, not a transcript. Keep each entry under 800 characters and avoid duplicating the same fact across entries.",
3991
+ "Set summaryUpdate.update true only when the durable current work summary is missing or misleading; otherwise omit summaryUpdate or leave update false.",
3992
+ "Use session.action complete_ticket only when the context indicates completion, merge, push/PR finalization, or explicit user completion intent. Use handoff/end only when the current session should close.",
3993
+ "",
3994
+ `Ticket: ${input.ticketId}`,
3995
+ input.sessionId ? `Session: ${input.sessionId}` : "",
3996
+ input.changesetId ? `ChangeSet: ${input.changesetId}` : "",
3997
+ `Agent event: ${input.event.source}/${input.event.event}`,
3998
+ input.previousCheckpointSummary ? `Previous checkpoint summary: ${input.previousCheckpointSummary}` : "",
3999
+ recentTurnsText ? `Recent user/assistant turns (most recent 3, local checkpoint context only):
4000
+ ${recentTurnsText}` : input.event.lastAssistantMessage ? `Last assistant message:
4001
+ ${truncateText(input.event.lastAssistantMessage, 3000)}` : "Recent turns: unavailable",
2989
4002
  input.gitStatus ? `Git status --short:
2990
4003
  ${truncateText(input.gitStatus, 2000)}` : "Git status --short: clean or unavailable",
2991
4004
  input.gitDiffStat ? `Git diff --stat origin/dev...:
@@ -3018,6 +4031,22 @@ function codexHookEntry(command) {
3018
4031
  ]
3019
4032
  };
3020
4033
  }
4034
+ function codexHookCommands(entry) {
4035
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
4036
+ return [];
4037
+ }
4038
+ const record = entry;
4039
+ const directCommand = typeof record.command === "string" ? [record.command] : [];
4040
+ const handlers = Array.isArray(record.hooks) ? record.hooks : [];
4041
+ const handlerCommands = handlers.flatMap((handler) => {
4042
+ if (!handler || typeof handler !== "object" || Array.isArray(handler)) {
4043
+ return [];
4044
+ }
4045
+ const command = handler.command;
4046
+ return typeof command === "string" ? [command] : [];
4047
+ });
4048
+ return [...directCommand, ...handlerCommands];
4049
+ }
3021
4050
  async function readJsonObjectFile(filePath) {
3022
4051
  const file = Bun.file(filePath);
3023
4052
  if (!await file.exists()) {
@@ -3031,8 +4060,9 @@ async function installCodexHookFile(filePath, command) {
3031
4060
  const hooks = hooksValue && typeof hooksValue === "object" && !Array.isArray(hooksValue) ? hooksValue : {};
3032
4061
  const stopValue = hooks.Stop;
3033
4062
  const stopHooks = Array.isArray(stopValue) ? stopValue : [];
3034
- const alreadyInstalled = stopHooks.some((entry) => JSON.stringify(entry).includes(command));
3035
- const nextStopHooks = alreadyInstalled ? stopHooks : [...stopHooks, codexHookEntry(command)];
4063
+ const alreadyInstalled = stopHooks.some((entry) => codexHookCommands(entry).includes(command));
4064
+ const cadenceHookIndexes = stopHooks.map((entry, index) => codexHookCommands(entry).some((entryCommand) => entryCommand.includes("agent-run ingest-stop --source codex --event stop")) ? index : -1).filter((index) => index >= 0);
4065
+ const nextStopHooks = alreadyInstalled ? stopHooks : cadenceHookIndexes.length ? stopHooks.map((entry, index) => index === cadenceHookIndexes[0] ? codexHookEntry(command) : entry).filter((_, index) => !cadenceHookIndexes.slice(1).includes(index)) : [...stopHooks, codexHookEntry(command)];
3036
4066
  const nextConfig = {
3037
4067
  ...existing,
3038
4068
  hooks: {
@@ -3046,7 +4076,8 @@ async function installCodexHookFile(filePath, command) {
3046
4076
  return {
3047
4077
  path: filePath,
3048
4078
  installed: !alreadyInstalled,
3049
- alreadyInstalled
4079
+ alreadyInstalled,
4080
+ updated: !alreadyInstalled && cadenceHookIndexes.length > 0
3050
4081
  };
3051
4082
  }
3052
4083
  async function codexHookPaths(scope, options) {
@@ -3068,7 +4099,7 @@ async function codexHookInstalled(filePath, command) {
3068
4099
  const existing = await readJsonObjectFile(filePath);
3069
4100
  const hooks = existing.hooks;
3070
4101
  const stopHooks = hooks && typeof hooks === "object" && !Array.isArray(hooks) && Array.isArray(hooks.Stop) ? hooks.Stop : [];
3071
- return stopHooks.some((entry) => JSON.stringify(entry).includes(command));
4102
+ return stopHooks.some((entry) => codexHookCommands(entry).includes(command));
3072
4103
  }
3073
4104
  async function runStatus(parsed, options) {
3074
4105
  const config = await resolveCliConfig(parsed.flags, options);
@@ -3215,14 +4246,17 @@ async function runAgentRunCommand(parsed, options) {
3215
4246
  switch (parsed.command.name) {
3216
4247
  case "agent-run.ingest-stop":
3217
4248
  return await runAgentRunIngestStop(parsed, options, config, meta);
4249
+ case "agent-run.checkpoint":
3218
4250
  case "agent-run.closeout":
3219
- return await runAgentRunCloseout(parsed, options, config, meta);
4251
+ return await runAgentRunCheckpoint(parsed, options, config, meta);
3220
4252
  case "agent-run.sweep":
3221
- return await runAgentRunSweep(parsed, options, meta);
4253
+ return await runAgentRunSweep(parsed, options, config, meta);
3222
4254
  case "agent-run.doctor": {
3223
4255
  const state = await readAgentLoopState(parsed, options);
4256
+ const settings = await readAgentLoopSettings(parsed, options);
3224
4257
  const data = {
3225
4258
  action: "doctor",
4259
+ settings,
3226
4260
  state,
3227
4261
  sessionCount: Object.keys(state.sessions).length,
3228
4262
  pendingSessions: Object.entries(state.sessions).filter(([, session]) => session.stopCount > 0).map(([agentSessionKeyValue, session]) => ({
@@ -3297,8 +4331,15 @@ async function runAgentRunIngestStop(parsed, options, config, meta) {
3297
4331
  const dryRun = parseBooleanOption(parsed.options["dry-run"], false);
3298
4332
  const existingSession = state.sessions[normalized.agentSessionKey];
3299
4333
  const now = new Date().toISOString();
3300
- const threshold = forcedThreshold ?? existingSession?.threshold ?? randomCheckpointThreshold();
3301
- const nextCount = (existingSession?.stopCount ?? 0) + 1;
4334
+ const threshold = forcedThreshold ?? existingSession?.threshold ?? defaultCheckpointThresholdValue();
4335
+ const duplicateTurn = Boolean(normalized.turnId && existingSession?.lastObservedTurnId === normalized.turnId);
4336
+ const nextCount = (existingSession?.stopCount ?? 0) + (duplicateTurn ? 0 : 1);
4337
+ const recentTurns = mergeRecentAgentTurns(existingSession?.recentTurns, normalized.recentTurns);
4338
+ let cadenceContext = existingSession?.cadenceContext;
4339
+ try {
4340
+ const client = await createClient(config, options);
4341
+ cadenceContext = await readCurrentCadenceContext(client, projectId) ?? cadenceContext;
4342
+ } catch {}
3302
4343
  const observedSession = {
3303
4344
  ...clearAgentSessionReason(existingSession ?? {
3304
4345
  source: normalized.source,
@@ -3310,8 +4351,11 @@ async function runAgentRunIngestStop(parsed, options, config, meta) {
3310
4351
  threshold,
3311
4352
  firstObservedAt: existingSession?.firstObservedAt ?? now,
3312
4353
  lastObservedAt: now,
4354
+ ...normalized.turnId ? { lastObservedTurnId: normalized.turnId } : {},
3313
4355
  lastAction: "counted",
3314
- ...normalized.lastAssistantMessage ? { lastAssistantMessage: normalized.lastAssistantMessage } : {}
4356
+ ...cadenceContext ? { cadenceContext } : {},
4357
+ ...normalized.lastAssistantMessage ? { lastAssistantMessage: normalized.lastAssistantMessage } : {},
4358
+ ...recentTurns ? { recentTurns } : {}
3315
4359
  };
3316
4360
  const countedState = {
3317
4361
  ...state,
@@ -3323,7 +4367,8 @@ async function runAgentRunIngestStop(parsed, options, config, meta) {
3323
4367
  if (nextCount < threshold) {
3324
4368
  await writeAgentLoopState(parsed, options, countedState);
3325
4369
  const data2 = {
3326
- action: "counted",
4370
+ action: duplicateTurn ? "updated" : "counted",
4371
+ ...duplicateTurn ? { reason: "duplicate_turn" } : {},
3327
4372
  agentSessionKey: normalized.agentSessionKey,
3328
4373
  stopCount: nextCount,
3329
4374
  threshold
@@ -3370,7 +4415,7 @@ async function runAgentRunIngestStop(parsed, options, config, meta) {
3370
4415
  [normalized.agentSessionKey]: {
3371
4416
  ...observedSession,
3372
4417
  stopCount: 0,
3373
- threshold: randomCheckpointThreshold(),
4418
+ threshold: defaultCheckpointThresholdValue(),
3374
4419
  lastAction: "skipped",
3375
4420
  lastReason: "unchanged"
3376
4421
  }
@@ -3392,7 +4437,7 @@ async function runAgentRunIngestStop(parsed, options, config, meta) {
3392
4437
  const lockPath = agentLoopLockPath(parsed, options, normalized.agentSessionKey);
3393
4438
  const workerArgs = [
3394
4439
  "agent-run",
3395
- "closeout",
4440
+ "checkpoint",
3396
4441
  "--agent-session-key",
3397
4442
  normalized.agentSessionKey,
3398
4443
  "--reason",
@@ -3458,7 +4503,7 @@ async function runAgentRunIngestStop(parsed, options, config, meta) {
3458
4503
  };
3459
4504
  }
3460
4505
  try {
3461
- await spawnAgentRunCloseout(workerArgs, options);
4506
+ await spawnAgentRunCheckpoint(workerArgs, options);
3462
4507
  } catch (error) {
3463
4508
  await releaseAgentLoopLock(lock.lockPath);
3464
4509
  throw error;
@@ -3469,8 +4514,9 @@ async function runAgentRunIngestStop(parsed, options, config, meta) {
3469
4514
  ...state.sessions,
3470
4515
  [normalized.agentSessionKey]: {
3471
4516
  ...observedSession,
4517
+ ...cadenceContext ? { cadenceContext } : {},
3472
4518
  stopCount: 0,
3473
- threshold: randomCheckpointThreshold(),
4519
+ threshold: defaultCheckpointThresholdValue(),
3474
4520
  lastAction: "spawned",
3475
4521
  lastEventFile: eventFile,
3476
4522
  lastCheckpointAt: new Date().toISOString(),
@@ -3491,7 +4537,7 @@ async function runAgentRunIngestStop(parsed, options, config, meta) {
3491
4537
  exitCode: 0
3492
4538
  };
3493
4539
  }
3494
- async function runAgentRunCloseout(parsed, options, config, meta) {
4540
+ async function runAgentRunCheckpoint(parsed, options, config, meta) {
3495
4541
  const projectId = requireProjectId(config);
3496
4542
  const client = await createClient(config, options);
3497
4543
  const agentSessionKeyValue = requireOption(parsed, "agent-session-key");
@@ -3502,16 +4548,11 @@ async function runAgentRunCloseout(parsed, options, config, meta) {
3502
4548
  agentSessionKey: agentSessionKeyValue
3503
4549
  });
3504
4550
  }
3505
- const current = await client.sessions.current({
3506
- projectId,
3507
- filters: {
3508
- limit: 100
3509
- }
3510
- });
3511
- const currentSession = activeSessionFromCurrent(current);
3512
- const ticketId = parsed.options.ticket ?? (currentSession && typeof currentSession.ticketId === "string" ? currentSession.ticketId : undefined);
3513
- const sessionId = parsed.options.session ?? (currentSession && typeof currentSession.id === "string" ? currentSession.id : undefined);
3514
- const changesetId = parsed.options.changeset ?? (currentSession && typeof currentSession.changesetId === "string" ? currentSession.changesetId : undefined);
4551
+ const savedContext = sessionState.cadenceContext;
4552
+ const currentContext = parsed.options.ticket || savedContext?.ticketId ? undefined : await readCurrentCadenceContext(client, projectId);
4553
+ const ticketId = parsed.options.ticket ?? savedContext?.ticketId ?? currentContext?.ticketId;
4554
+ const sessionId = parsed.options.session ?? savedContext?.sessionId ?? currentContext?.sessionId;
4555
+ const changesetId = parsed.options.changeset ?? savedContext?.changesetId ?? currentContext?.changesetId;
3515
4556
  if (!ticketId) {
3516
4557
  await writeAgentLoopState(parsed, options, {
3517
4558
  ...state,
@@ -3542,6 +4583,7 @@ async function runAgentRunCloseout(parsed, options, config, meta) {
3542
4583
  const updateSummary = parseBooleanOption(parsed.options["update-summary"], false);
3543
4584
  const dryRun = parseBooleanOption(parsed.options["dry-run"], false);
3544
4585
  const lockPath = parsed.options.lock;
4586
+ const checkpointSettings = await resolveCheckpointSettings(parsed, options);
3545
4587
  const gitStatus = gitOutput(["status", "--short"], options);
3546
4588
  const gitDiffStat = gitOutput(["diff", "--stat", "origin/dev..."], options);
3547
4589
  const changedFiles = gitOutput(["diff", "--name-only", "origin/dev..."], options);
@@ -3555,10 +4597,29 @@ async function runAgentRunCloseout(parsed, options, config, meta) {
3555
4597
  changedFiles,
3556
4598
  ...sessionState.previousCheckpointSummary ? { previousCheckpointSummary: sessionState.previousCheckpointSummary } : {}
3557
4599
  });
4600
+ const promptTokenAccounting = buildCheckpointTokenAccounting(event, prompt, undefined);
3558
4601
  if (dryRun) {
4602
+ const auditFile = await writeAgentCheckpointAuditFile(parsed, options, {
4603
+ version: 1,
4604
+ action: "would_checkpoint",
4605
+ createdAt: new Date().toISOString(),
4606
+ localOnly: true,
4607
+ localOnlyReason: "Raw hook context and checkpoint prompts stay on this machine and are not uploaded to Cadence.",
4608
+ agentSessionKey: agentSessionKeyValue,
4609
+ ticketId,
4610
+ ...sessionId ? { sessionId } : {},
4611
+ ...changesetId ? { changesetId } : {},
4612
+ ...eventFile ? { eventFile } : {},
4613
+ event,
4614
+ prompt,
4615
+ checkpointSettings,
4616
+ tokenAccounting: promptTokenAccounting,
4617
+ cadenceWrites: []
4618
+ });
3559
4619
  const data = {
3560
- action: "would_closeout",
4620
+ action: "would_checkpoint",
3561
4621
  prompt,
4622
+ auditFile,
3562
4623
  ticketId,
3563
4624
  agentSessionKey: agentSessionKeyValue,
3564
4625
  ...sessionId ? { sessionId } : {},
@@ -3573,84 +4634,387 @@ async function runAgentRunCloseout(parsed, options, config, meta) {
3573
4634
  }
3574
4635
  try {
3575
4636
  const codexCommand = parsed.options["codex-command"] ?? "codex";
3576
- const codex = runLocalCommand(codexCommand, ["exec", "--disable", "hooks", "--sandbox", "read-only", "-C", options.cwd ?? process.cwd(), prompt], options, {
3577
- cwd: options.cwd ?? process.cwd(),
4637
+ const codexStartedAtMs = Date.now();
4638
+ const codexCwd = options.cwd ?? process.cwd();
4639
+ const codexArgs = [
4640
+ "exec",
4641
+ ...checkpointSettings.model ? ["-m", checkpointSettings.model] : [],
4642
+ "--disable",
4643
+ "hooks",
4644
+ "--sandbox",
4645
+ "read-only",
4646
+ "-C",
4647
+ codexCwd,
4648
+ prompt
4649
+ ];
4650
+ const codex = runLocalCommand(codexCommand, codexArgs, options, {
4651
+ cwd: codexCwd,
3578
4652
  env: {
3579
4653
  [agentLoopSuppressEnv]: "1",
3580
4654
  CADENCE_HOOK_SUPPRESS: "1"
3581
4655
  },
3582
4656
  timeoutMs: defaultCheckpointWorkerTimeoutMs
3583
4657
  });
4658
+ const codexSessionTranscript = readCodexSessionTranscript(findCodexSessionFileCreatedAfter(options, codexStartedAtMs, codexCwd));
4659
+ const tokenAccounting = buildCheckpointTokenAccounting(event, prompt, codexSessionTranscript?.tokenUsage);
3584
4660
  if (codex.status !== 0 || codex.error) {
3585
- throw new CliError("AGENT_RUN_CLOSEOUT_FAILED", "Codex closeout generation failed.", {
4661
+ throw new CliError("AGENT_RUN_CHECKPOINT_FAILED", "Codex checkpoint generation failed.", {
3586
4662
  status: codex.status,
3587
4663
  stderr: truncateText(codex.stderr, 2000),
3588
4664
  error: codex.error?.message
3589
4665
  });
3590
4666
  }
3591
- const checkpoint = parseCheckpointJson(codex.stdout);
3592
- const body = checkpoint.body.trim();
3593
- const summary = checkpoint.summary ?? truncateText(body.replace(/\s+/g, " "), 500);
3594
- if (!body) {
3595
- throw new CliError("AGENT_RUN_CLOSEOUT_FAILED", "Codex closeout generation returned an empty checkpoint.");
3596
- }
3597
- await client.tickets.log({
3598
- projectId,
3599
- ticketId,
3600
- entry: {
3601
- entryKind: logKind,
3602
- body,
4667
+ let checkpoint = parseCheckpointPlanJson(codex.stdout, logKind);
4668
+ let summary = checkpoint.summary ?? checkpoint.entries[0]?.summary ?? checkpoint.route.reason ?? "Agent run checkpoint";
4669
+ let targetTicketId = ticketId;
4670
+ let targetSessionId = sessionId;
4671
+ let targetChangesetId = changesetId;
4672
+ const cadenceWrites = [];
4673
+ const lifecycleOperations = [];
4674
+ let intakeResult;
4675
+ let selectedTicket;
4676
+ const modelAudit = {
4677
+ provider: checkpointSettings.provider,
4678
+ command: codexCommand,
4679
+ ...checkpointSettings.model ? { model: checkpointSettings.model } : {},
4680
+ status: codex.status,
4681
+ stdout: codex.stdout.trim(),
4682
+ stderr: truncateText(codex.stderr.trim(), 2000),
4683
+ tokenAccounting,
4684
+ ...codexSessionTranscript?.tokenUsage ? { tokenUsage: codexSessionTranscript.tokenUsage } : {},
4685
+ ...codexSessionTranscript ? { sessionTranscript: codexSessionTranscript } : {}
4686
+ };
4687
+ const finishCheckpoint = async (action, reason) => {
4688
+ const checkedAt = new Date().toISOString();
4689
+ const auditFile = await writeAgentCheckpointAuditFile(parsed, options, {
4690
+ version: 1,
4691
+ action,
4692
+ createdAt: checkedAt,
4693
+ localOnly: true,
4694
+ localOnlyReason: action === "noop" || action === "needs_human" ? "Raw hook context, checkpoint prompts, and model output stay on this machine; no Cadence writes were needed." : "Raw hook context, checkpoint prompts, and model output stay on this machine; Cadence receives only the structured writes listed here.",
4695
+ agentSessionKey: agentSessionKeyValue,
4696
+ ticketId: targetTicketId,
4697
+ ...targetSessionId ? { sessionId: targetSessionId } : {},
4698
+ ...targetChangesetId ? { changesetId: targetChangesetId } : {},
4699
+ ...eventFile ? { eventFile } : {},
4700
+ event,
4701
+ prompt,
4702
+ model: modelAudit,
4703
+ checkpoint,
4704
+ route: checkpoint.route,
4705
+ ...intakeResult ? { intakeResult } : {},
4706
+ ...selectedTicket ? { selectedTicket } : {},
4707
+ lifecycleOperations,
4708
+ cadenceWrites,
3603
4709
  summary,
3604
- ...sessionId ? { sessionId } : {},
3605
- ...changesetId ? { changesetId } : {},
4710
+ ...reason ? { reason } : {},
4711
+ entryCount: checkpoint.entries.length,
4712
+ needsHuman: action === "needs_human"
4713
+ });
4714
+ await writeAgentLoopState(parsed, options, {
4715
+ ...state,
4716
+ sessions: {
4717
+ ...state.sessions,
4718
+ [agentSessionKeyValue]: {
4719
+ ...sessionState,
4720
+ stopCount: 0,
4721
+ threshold: defaultCheckpointThresholdValue(),
4722
+ previousCheckpointSummary: summary,
4723
+ lastAction: action,
4724
+ ...reason ? { lastReason: reason } : {},
4725
+ ...eventFile ? { lastEventFile: eventFile } : {},
4726
+ cadenceContext: {
4727
+ ticketId: targetTicketId,
4728
+ ...targetSessionId ? { sessionId: targetSessionId } : {},
4729
+ ...targetChangesetId ? { changesetId: targetChangesetId } : {},
4730
+ capturedAt: checkedAt
4731
+ },
4732
+ lastCheckpointAt: checkedAt,
4733
+ lastCheckpointAuditFile: auditFile
4734
+ }
4735
+ }
4736
+ });
4737
+ const data = {
4738
+ action,
4739
+ route: checkpoint.route,
4740
+ ...reason ? { reason } : {},
4741
+ ticketId: targetTicketId,
4742
+ summary,
4743
+ entryCount: checkpoint.entries.length,
4744
+ auditFile,
4745
+ agentSessionKey: agentSessionKeyValue,
4746
+ ...targetSessionId ? { sessionId: targetSessionId } : {},
4747
+ ...targetChangesetId ? { changesetId: targetChangesetId } : {},
4748
+ ...action === "needs_human" ? { needsHuman: true } : {}
4749
+ };
4750
+ return {
4751
+ stdout: parsed.flags.json ? formatJson(successEnvelope(data, meta)) : `${JSON.stringify(data, null, 2)}
4752
+ `,
4753
+ stderr: "",
4754
+ exitCode: 0
4755
+ };
4756
+ };
4757
+ if (checkpoint.session?.action === "complete_ticket" && !checkpointPlanCompletionAllowed(event, summary)) {
4758
+ checkpoint = checkpointPlanNeedsHuman(checkpoint, "Completion requires explicit recent completion, push, PR, merge, clean branch, or verification intent.");
4759
+ }
4760
+ if (checkpoint.route.action === "noop") {
4761
+ return await finishCheckpoint("noop", checkpoint.route.reason ?? checkpoint.summary ?? "Nothing durable to record.");
4762
+ }
4763
+ if (checkpoint.route.action === "needs_human") {
4764
+ return await finishCheckpoint("needs_human", checkpoint.route.reason ?? "Checkpoint routing needs human review.");
4765
+ }
4766
+ if (checkpointRouteRequiresIntake(checkpoint.route.action)) {
4767
+ const intakeRequest = checkpoint.route.request ?? checkpointPlanDescription(checkpoint);
4768
+ intakeResult = await client.intake.create({
4769
+ projectId,
4770
+ intake: {
4771
+ request: intakeRequest,
4772
+ ...commandMetadata()
4773
+ }
4774
+ });
4775
+ lifecycleOperations.push(checkpointLifecycleOperation("intake.created", true, { request: intakeRequest, result: intakeResult }));
4776
+ if (checkpointHasConflictingIntakeResult(intakeResult, checkpoint)) {
4777
+ checkpoint = checkpointPlanNeedsHuman(checkpoint, "Intake returned conflicting duplicate, overlap, or completed-before candidates.");
4778
+ return await finishCheckpoint("needs_human", checkpoint.route.reason);
4779
+ }
4780
+ if (checkpoint.route.action === "intake_create") {
4781
+ selectedTicket = await client.tickets.create({
4782
+ projectId,
4783
+ ticket: {
4784
+ title: checkpointPlanTitle(checkpoint),
4785
+ description: checkpointPlanDescription(checkpoint),
4786
+ fromIntakeId: typeof intakeResult === "object" && intakeResult && "id" in intakeResult ? String(intakeResult.id) : undefined,
4787
+ ...commandMetadata()
4788
+ }
4789
+ });
4790
+ lifecycleOperations.push(checkpointLifecycleOperation("ticket.created", true, { ticket: selectedTicket }));
4791
+ if (selectedTicket && typeof selectedTicket === "object" && "id" in selectedTicket) {
4792
+ targetTicketId = String(selectedTicket.id);
4793
+ }
4794
+ } else {
4795
+ if (!checkpoint.route.targetTicketId) {
4796
+ checkpoint = checkpointPlanNeedsHuman(checkpoint, "Checkpoint route requires a target ticket id.");
4797
+ return await finishCheckpoint("needs_human", checkpoint.route.reason);
4798
+ }
4799
+ targetTicketId = checkpoint.route.targetTicketId;
4800
+ selectedTicket = await client.tickets.get({ projectId, ticketId: targetTicketId });
4801
+ lifecycleOperations.push(checkpointLifecycleOperation("ticket.selected", true, { ticketId: targetTicketId, ticket: selectedTicket }));
4802
+ const selectedVersion = selectedTicket && typeof selectedTicket === "object" && typeof selectedTicket.projectionVersion === "number" ? selectedTicket.projectionVersion : undefined;
4803
+ const intakeId = intakeResult && typeof intakeResult === "object" && "id" in intakeResult ? String(intakeResult.id) : undefined;
4804
+ if (selectedVersion !== undefined && intakeId) {
4805
+ await client.tickets.attach({
4806
+ projectId,
4807
+ ticketId: targetTicketId,
4808
+ fromIntakeId: intakeId,
4809
+ ifVersion: selectedVersion
4810
+ });
4811
+ lifecycleOperations.push(checkpointLifecycleOperation("intake.attached", true, { ticketId: targetTicketId, intakeId }));
4812
+ }
4813
+ }
4814
+ if (targetTicketId !== ticketId && targetSessionId) {
4815
+ await client.sessions.end({
4816
+ projectId,
4817
+ sessionId: targetSessionId,
4818
+ session: {
4819
+ summary: checkpoint.session?.summary ?? checkpoint.route.reason ?? summary,
4820
+ ...commandMetadata()
4821
+ }
4822
+ });
4823
+ lifecycleOperations.push(checkpointLifecycleOperation("session.ended", true, { sessionId: targetSessionId, reason: "ticket_switch" }));
4824
+ targetSessionId = undefined;
4825
+ targetChangesetId = undefined;
4826
+ }
4827
+ const startedSession = await client.sessions.start({
4828
+ projectId,
4829
+ session: {
4830
+ ticketId: targetTicketId,
4831
+ ...commandMetadata()
4832
+ }
4833
+ });
4834
+ lifecycleOperations.push(checkpointLifecycleOperation("session.started", true, { ticketId: targetTicketId, session: startedSession }));
4835
+ if (startedSession && typeof startedSession === "object" && "id" in startedSession) {
4836
+ targetSessionId = String(startedSession.id);
4837
+ }
4838
+ if (targetSessionId) {
4839
+ const lease = await client.sessions.leases.create({
4840
+ projectId,
4841
+ lease: {
4842
+ ticketId: targetTicketId,
4843
+ sessionId: targetSessionId,
4844
+ expiresAt: leaseExpiresAt(defaultLeaseTtlSeconds),
4845
+ replaceOwnActiveLease: true,
4846
+ ...commandMetadata()
4847
+ }
4848
+ });
4849
+ lifecycleOperations.push(checkpointLifecycleOperation("lease.claimed", true, { ticketId: targetTicketId, sessionId: targetSessionId, lease }));
4850
+ }
4851
+ if (targetSessionId) {
4852
+ try {
4853
+ const branchName = await resolveCurrentBranch(options);
4854
+ const changeset = await client.changesets.create({
4855
+ projectId,
4856
+ changeset: {
4857
+ ticketId: targetTicketId,
4858
+ branchName,
4859
+ baseBranch: "dev",
4860
+ sessionId: targetSessionId,
4861
+ ...commandMetadata()
4862
+ }
4863
+ });
4864
+ lifecycleOperations.push(checkpointLifecycleOperation("changeset.linked", true, { ticketId: targetTicketId, branchName, changeset }));
4865
+ if (changeset && typeof changeset === "object" && "id" in changeset) {
4866
+ targetChangesetId = String(changeset.id);
4867
+ }
4868
+ } catch (error) {
4869
+ lifecycleOperations.push(checkpointLifecycleOperation("changeset.linked", false, { error: error instanceof Error ? error.message : String(error) }));
4870
+ }
4871
+ }
4872
+ }
4873
+ const checkpointMetadata = checkpointWorkLogMetadata(checkpointSettings, tokenAccounting);
4874
+ for (const entry of checkpoint.entries) {
4875
+ const logEntry = {
4876
+ entryKind: entry.kind,
4877
+ body: entry.body,
4878
+ summary: entry.summary ?? checkpointEntrySummary(entry.body),
4879
+ ...entry.parentSelector ? { parentSelector: entry.parentSelector } : {},
4880
+ ...entry.parentEntryId ? { parentEntryId: entry.parentEntryId } : {},
4881
+ ...targetSessionId ? { sessionId: targetSessionId } : {},
4882
+ ...targetChangesetId ? { changesetId: targetChangesetId } : {},
4883
+ metadata: checkpointMetadata,
3606
4884
  ...commandMetadata()
4885
+ };
4886
+ try {
4887
+ await client.tickets.log({
4888
+ projectId,
4889
+ ticketId: targetTicketId,
4890
+ entry: logEntry
4891
+ });
4892
+ cadenceWrites.push({
4893
+ type: "ticket.work_log_appended",
4894
+ success: true,
4895
+ ticketId: targetTicketId,
4896
+ entry: logEntry
4897
+ });
4898
+ } catch (error) {
4899
+ if (!entry.parentSelector && !entry.parentEntryId) {
4900
+ cadenceWrites.push({
4901
+ type: "ticket.work_log_appended",
4902
+ success: false,
4903
+ ticketId: targetTicketId,
4904
+ entry: logEntry,
4905
+ error: error instanceof Error ? error.message : String(error)
4906
+ });
4907
+ throw error;
4908
+ }
4909
+ const { parentSelector: _parentSelector, parentEntryId: _parentEntryId, ...entryWithoutParent } = logEntry;
4910
+ cadenceWrites.push({
4911
+ type: "ticket.work_log_appended",
4912
+ success: false,
4913
+ ticketId: targetTicketId,
4914
+ entry: logEntry,
4915
+ error: error instanceof Error ? error.message : String(error)
4916
+ });
4917
+ await client.tickets.log({
4918
+ projectId,
4919
+ ticketId: targetTicketId,
4920
+ entry: entryWithoutParent
4921
+ });
4922
+ cadenceWrites.push({
4923
+ type: "ticket.work_log_appended",
4924
+ success: true,
4925
+ ticketId: targetTicketId,
4926
+ entry: entryWithoutParent,
4927
+ fallback: "removed unresolved parent reference"
4928
+ });
3607
4929
  }
3608
- });
3609
- if (updateSummary) {
3610
- const ticket = await client.tickets.get({ projectId, ticketId });
4930
+ }
4931
+ if (checkpoint.files.length && targetSessionId) {
4932
+ const filesByKind = new Map;
4933
+ for (const file of checkpoint.files) {
4934
+ filesByKind.set(file.kind, [...filesByKind.get(file.kind) ?? [], file.path]);
4935
+ }
4936
+ for (const [kind, paths] of filesByKind) {
4937
+ const filesPayload = {
4938
+ ...targetChangesetId ? { changesetId: targetChangesetId } : {},
4939
+ files: paths.map((path) => ({
4940
+ path,
4941
+ changeKind: kind
4942
+ })),
4943
+ ...commandMetadata()
4944
+ };
4945
+ await client.sessions.files({
4946
+ projectId,
4947
+ sessionId: targetSessionId,
4948
+ files: filesPayload
4949
+ });
4950
+ lifecycleOperations.push(checkpointLifecycleOperation("session.files_attached", true, { sessionId: targetSessionId, kind, files: paths }));
4951
+ }
4952
+ }
4953
+ if ((updateSummary || checkpoint.summaryUpdate?.update) && checkpoint.summaryUpdate?.value) {
4954
+ const ticket = await client.tickets.get({ projectId, ticketId: targetTicketId });
3611
4955
  if (typeof ticket.projectionVersion === "number") {
3612
4956
  await client.tickets.update({
3613
4957
  projectId,
3614
- ticketId,
4958
+ ticketId: targetTicketId,
3615
4959
  ifVersion: ticket.projectionVersion,
3616
4960
  ticket: {
3617
- currentSummary: summary,
4961
+ currentSummary: checkpoint.summaryUpdate.value,
3618
4962
  ...commandMetadata()
3619
4963
  }
3620
4964
  });
4965
+ cadenceWrites.push({
4966
+ type: "ticket.updated",
4967
+ success: true,
4968
+ ticketId: targetTicketId,
4969
+ ifVersion: ticket.projectionVersion,
4970
+ ticket: {
4971
+ currentSummary: checkpoint.summaryUpdate.value
4972
+ }
4973
+ });
3621
4974
  }
3622
4975
  }
3623
- await writeAgentLoopState(parsed, options, {
3624
- ...state,
3625
- sessions: {
3626
- ...state.sessions,
3627
- [agentSessionKeyValue]: {
3628
- ...sessionState,
3629
- previousCheckpointSummary: summary,
3630
- lastAction: "closed_out",
3631
- ...eventFile ? { lastEventFile: eventFile } : {}
3632
- }
4976
+ if (checkpoint.session?.action === "handoff" || checkpoint.session?.action === "end" || checkpoint.session?.action === "complete_ticket") {
4977
+ if (targetSessionId) {
4978
+ await client.sessions.end({
4979
+ projectId,
4980
+ sessionId: targetSessionId,
4981
+ session: {
4982
+ summary: checkpoint.session.summary ?? summary,
4983
+ ...commandMetadata()
4984
+ }
4985
+ });
4986
+ lifecycleOperations.push(checkpointLifecycleOperation("session.ended", true, { sessionId: targetSessionId, action: checkpoint.session.action }));
3633
4987
  }
3634
- });
3635
- const data = {
3636
- action: "closed_out",
3637
- ticketId,
3638
- summary,
3639
- agentSessionKey: agentSessionKeyValue,
3640
- ...sessionId ? { sessionId } : {},
3641
- ...changesetId ? { changesetId } : {}
3642
- };
3643
- return {
3644
- stdout: parsed.flags.json ? formatJson(successEnvelope(data, meta)) : `${JSON.stringify(data, null, 2)}
3645
- `,
3646
- stderr: "",
3647
- exitCode: 0
3648
- };
4988
+ }
4989
+ if (checkpoint.session?.action === "complete_ticket") {
4990
+ const latestTicket = await client.tickets.get({ projectId, ticketId: targetTicketId });
4991
+ if (typeof latestTicket.projectionVersion !== "number") {
4992
+ checkpoint = checkpointPlanNeedsHuman(checkpoint, "Ticket completion requires a latest ticket projection version.");
4993
+ return await finishCheckpoint("needs_human", checkpoint.route.reason);
4994
+ }
4995
+ await client.tickets.complete({
4996
+ projectId,
4997
+ ticketId: targetTicketId,
4998
+ ifVersion: latestTicket.projectionVersion,
4999
+ completion: {
5000
+ currentSummary: checkpoint.session.summary ?? checkpoint.summaryUpdate?.value ?? summary,
5001
+ ...commandMetadata()
5002
+ }
5003
+ });
5004
+ cadenceWrites.push({
5005
+ type: "ticket.completed",
5006
+ success: true,
5007
+ ticketId: targetTicketId,
5008
+ ifVersion: latestTicket.projectionVersion,
5009
+ summary: checkpoint.session.summary ?? checkpoint.summaryUpdate?.value ?? summary
5010
+ });
5011
+ }
5012
+ return await finishCheckpoint("checkpointed");
3649
5013
  } finally {
3650
5014
  await releaseAgentLoopLock(lockPath);
3651
5015
  }
3652
5016
  }
3653
- async function runAgentRunSweep(parsed, options, meta) {
5017
+ async function runAgentRunSweep(parsed, options, config, meta) {
3654
5018
  const state = await readAgentLoopState(parsed, options);
3655
5019
  const idleAfterSeconds = parsePositiveInteger(parsed.options["idle-after-seconds"], "--idle-after-seconds") ?? 5 * 60;
3656
5020
  const dryRun = parseBooleanOption(parsed.options["dry-run"], false);
@@ -3669,10 +5033,35 @@ async function runAgentRunSweep(parsed, options, meta) {
3669
5033
  lastObservedAt: session.lastObservedAt
3670
5034
  }));
3671
5035
  if (!dryRun) {
5036
+ const needsFallbackContext = staleSessions.some((staleSession) => !state.sessions[staleSession.agentSessionKey]?.cadenceContext);
5037
+ let fallbackContext;
5038
+ if (needsFallbackContext && config.projectId) {
5039
+ try {
5040
+ const client = await createClient(config, options);
5041
+ fallbackContext = await readCurrentCadenceContext(client, config.projectId);
5042
+ } catch {}
5043
+ }
5044
+ let nextState = state;
3672
5045
  for (const staleSession of staleSessions) {
3673
- await spawnAgentRunCloseout([
5046
+ const existingSession = nextState.sessions[staleSession.agentSessionKey];
5047
+ const cadenceContext = existingSession?.cadenceContext ?? fallbackContext;
5048
+ if (existingSession && cadenceContext) {
5049
+ nextState = {
5050
+ ...nextState,
5051
+ sessions: {
5052
+ ...nextState.sessions,
5053
+ [staleSession.agentSessionKey]: {
5054
+ ...existingSession,
5055
+ cadenceContext,
5056
+ lastAction: "sweep_spawned"
5057
+ }
5058
+ }
5059
+ };
5060
+ await writeAgentLoopState(parsed, options, nextState);
5061
+ }
5062
+ await spawnAgentRunCheckpoint([
3674
5063
  "agent-run",
3675
- "closeout",
5064
+ "checkpoint",
3676
5065
  "--agent-session-key",
3677
5066
  staleSession.agentSessionKey,
3678
5067
  "--reason",
@@ -4118,6 +5507,7 @@ async function runIntakeCommand(parsed, options) {
4118
5507
  ...parsed.options.changeset ? { changesetId: parsed.options.changeset } : {},
4119
5508
  ...parsed.options.actor ? { actorId: parsed.options.actor } : {},
4120
5509
  expiresAt: leaseExpiresAt(parsePositiveInteger(parsed.options["ttl-seconds"], "--ttl-seconds") ?? defaultLeaseTtlSeconds),
5510
+ ...parseBooleanOption(parsed.options["replace-own-active"], false) ? { replaceOwnActiveLease: true } : {},
4121
5511
  ...commandMetadata()
4122
5512
  }
4123
5513
  });
@@ -4289,8 +5679,27 @@ async function runCli(argv, options = {}) {
4289
5679
  try {
4290
5680
  const parsed = parseCliArgs(argv);
4291
5681
  const meta = {
4292
- command: parsed.command.name
5682
+ command: parsed.flags.version ? "version" : parsed.command.name
4293
5683
  };
5684
+ if (parsed.flags.version || parsed.command.name === "version") {
5685
+ const data = {
5686
+ name: package_default.name,
5687
+ version: cliVersion
5688
+ };
5689
+ if (parsed.flags.json) {
5690
+ return {
5691
+ stdout: formatJson(successEnvelope(data, meta)),
5692
+ stderr: "",
5693
+ exitCode: 0
5694
+ };
5695
+ }
5696
+ return {
5697
+ stdout: `cadence ${cliVersion}
5698
+ `,
5699
+ stderr: "",
5700
+ exitCode: 0
5701
+ };
5702
+ }
4294
5703
  if (parsed.flags.help || parsed.command.name === "help") {
4295
5704
  if (parsed.flags.json) {
4296
5705
  return {
@@ -4324,7 +5733,7 @@ async function runCli(argv, options = {}) {
4324
5733
  if (parsed.command.name === "projects.list") {
4325
5734
  return await runProjectCommand(parsed, options);
4326
5735
  }
4327
- if (parsed.command.name === "agent-run.ingest-stop" || parsed.command.name === "agent-run.closeout" || parsed.command.name === "agent-run.sweep" || parsed.command.name === "agent-run.doctor") {
5736
+ if (parsed.command.name === "agent-run.ingest-stop" || parsed.command.name === "agent-run.checkpoint" || parsed.command.name === "agent-run.closeout" || parsed.command.name === "agent-run.sweep" || parsed.command.name === "agent-run.doctor") {
4328
5737
  return await runAgentRunCommand(parsed, options);
4329
5738
  }
4330
5739
  if (parsed.command.name === "hooks.install" || parsed.command.name === "hooks.doctor") {