@trycadence/cli 0.1.11-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 +1491 -141
  3. package/package.json +1 -1
package/dist/cadence CHANGED
@@ -1513,13 +1513,14 @@ 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";
1519
1520
  // package.json
1520
1521
  var package_default = {
1521
1522
  name: "@trycadence/cli",
1522
- version: "0.1.11-dev.0",
1523
+ version: "0.1.14-dev.0",
1523
1524
  private: false,
1524
1525
  type: "module",
1525
1526
  bin: {
@@ -1553,14 +1554,13 @@ var workLogParentSelectors = ["last", "ticket-last", "session-last", "last-decis
1553
1554
  var changesetPrNoteSources = ["agent", "human", "system"];
1554
1555
  var hookScopes = ["repo", "global", "both"];
1555
1556
  var agentEventSources = ["codex", "claude-code", "opencode", "openrouter", "unknown"];
1556
- var defaultLeaseTtlSeconds = 5 * 60 * 60;
1557
+ var defaultLeaseTtlSeconds = 15 * 60;
1557
1558
  var defaultCliApiBaseUrl = "https://cadenceapi.deploy.lvl8studios.com";
1558
1559
  var defaultCliWebBaseUrl = "https://cadence.deploy.lvl8studios.com";
1559
- var defaultCheckpointThresholdMin = 3;
1560
- var defaultCheckpointThresholdMax = 5;
1560
+ var defaultCheckpointThreshold = 3;
1561
1561
  var defaultCheckpointCooldownSeconds = 10 * 60;
1562
1562
  var defaultCheckpointWorkerTimeoutMs = 10 * 60 * 1000;
1563
- 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'`;
1564
1564
  var agentLoopSuppressEnv = "CADENCE_AGENT_EVENT_SUPPRESS";
1565
1565
  var credentialRefreshSkewMs = 60 * 1000;
1566
1566
  var credentialRefreshLockTimeoutMs = 10 * 1000;
@@ -1599,6 +1599,7 @@ var knownCommandPaths = [
1599
1599
  ["changesets", "notes", "put"],
1600
1600
  ["changesets", "notes", "apply"],
1601
1601
  ["agent-run", "ingest-stop"],
1602
+ ["agent-run", "checkpoint"],
1602
1603
  ["agent-run", "closeout"],
1603
1604
  ["agent-run", "sweep"],
1604
1605
  ["agent-run", "doctor"],
@@ -2260,7 +2261,7 @@ function helpText() {
2260
2261
  " cadence tickets attach <ticket-id> --from-intake <intake-id> --if-version <version> [--project <project-id>] [--json]",
2261
2262
  " cadence tickets create --title <text> [--from-intake <intake-id>] [--project <project-id>] [--json]",
2262
2263
  " cadence tickets update <ticket-id> --if-version <version> [--title <text>] [--description <text>] [--priority <priority>] [--status <status>] [--project <project-id>] [--json]",
2263
- " 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]",
2264
2265
  " cadence tickets release <ticket-id> --lease <lease-id> [--project <project-id>] [--json]",
2265
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]",
2266
2267
  " cadence tickets complete <ticket-id> --if-version <version> [--summary <summary>] [--project <project-id>] [--json]",
@@ -2278,11 +2279,11 @@ function helpText() {
2278
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]",
2279
2280
  " cadence changesets notes apply [--changeset <id>|--branch current|<branch>] --provider github --pr-number <n> --pr-url <url> [--project <project-id>] [--json]",
2280
2281
  " cadence agent-run ingest-stop --source <codex|claude-code|opencode|openrouter> [--event <event>] [--threshold <n>] [--dry-run true|false] [--project <project-id>] [--json]",
2281
- " 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]",
2282
2283
  " cadence agent-run sweep [--idle-after-seconds <n>] [--dry-run true|false] [--json]",
2283
2284
  " cadence agent-run doctor [--json]",
2284
- " cadence hooks install --provider codex --scope <repo|global|both> [--command <command>] [--json]",
2285
- " 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]",
2286
2287
  " cadence events list [--project <project-id>] [--ticket <ticket-id>] [--changeset <changeset-id>] [--session <session-id>] [--json]",
2287
2288
  "",
2288
2289
  "Global flags:",
@@ -2581,6 +2582,14 @@ function truncateText(value, maxLength) {
2581
2582
  return `${value.slice(0, maxLength - 15)}
2582
2583
  [truncated]`;
2583
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
+ }
2584
2593
  function stableHash(value) {
2585
2594
  return createHash("sha256").update(value).digest("hex");
2586
2595
  }
@@ -2619,13 +2628,392 @@ function firstString(record, paths) {
2619
2628
  }
2620
2629
  return;
2621
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
+ }
2622
3010
  function normalizeAgentEvent(input, parsed, options) {
2623
3011
  const source = parseAgentEventSource(parsed.options.source);
2624
3012
  const event = parsed.options.event ?? "stop";
2625
3013
  const base = normalizeAgentEventBase(input, source, event, options);
2626
3014
  switch (source) {
2627
3015
  case "codex":
2628
- return normalizeCodexAgentEvent(input, base);
3016
+ return normalizeCodexAgentEvent(input, base, options);
2629
3017
  case "claude-code":
2630
3018
  case "opencode":
2631
3019
  case "openrouter":
@@ -2636,6 +3024,7 @@ function normalizeAgentEvent(input, parsed, options) {
2636
3024
  function normalizeAgentEventBase(input, source, event, options) {
2637
3025
  const threadId = firstString(input, [["thread_id"], ["threadId"], ["conversation_id"], ["conversationId"]]);
2638
3026
  const turnId = firstString(input, [["turn_id"], ["turnId"], ["id"]]);
3027
+ const recentTurns = readRecentAgentTurns(input);
2639
3028
  const lastAssistantMessage = firstString(input, [
2640
3029
  ["last_assistant_message"],
2641
3030
  ["lastAssistantMessage"],
@@ -2652,10 +3041,11 @@ function normalizeAgentEventBase(input, source, event, options) {
2652
3041
  ...threadId ? { threadId } : {},
2653
3042
  ...turnId ? { turnId } : {},
2654
3043
  ...lastAssistantMessage ? { lastAssistantMessage: truncateText(lastAssistantMessage, 6000) } : {},
3044
+ ...recentTurns ? { recentTurns } : {},
2655
3045
  payloadKeys: Object.keys(input).sort()
2656
3046
  };
2657
3047
  }
2658
- function normalizeCodexAgentEvent(input, base) {
3048
+ function normalizeCodexAgentEvent(input, base, options) {
2659
3049
  const agentSessionId = firstString(input, [["session_id"], ["sessionId"], ["session", "id"]]);
2660
3050
  if (!agentSessionId) {
2661
3051
  return {
@@ -2663,10 +3053,13 @@ function normalizeCodexAgentEvent(input, base) {
2663
3053
  diagnosticReason: "missing_agent_session_id"
2664
3054
  };
2665
3055
  }
3056
+ const transcriptPath = firstString(input, [["transcript_path"], ["transcriptPath"]]);
3057
+ const recentTurns = base.recentTurns?.length ? base.recentTurns : transcriptPath ? readRecentTurnsFromCodexSessionFile(transcriptPath) : readRecentTurnsFromCodexSession(agentSessionId, options);
2666
3058
  return {
2667
3059
  ...base,
2668
3060
  agentSessionId,
2669
- agentSessionKey: agentSessionKey(base.source, agentSessionId)
3061
+ agentSessionKey: agentSessionKey(base.source, agentSessionId),
3062
+ ...recentTurns?.length ? { recentTurns } : {}
2670
3063
  };
2671
3064
  }
2672
3065
  function normalizeGenericAgentEvent(input, base) {
@@ -2692,14 +3085,23 @@ function normalizeGenericAgentEvent(input, base) {
2692
3085
  function agentSessionKey(source, agentSessionId) {
2693
3086
  return `${source}:${stableHash(agentSessionId)}`;
2694
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
+ }
2695
3097
  function defaultAgentLoopState() {
2696
3098
  return {
2697
3099
  version: 2,
2698
3100
  sessions: {}
2699
3101
  };
2700
3102
  }
2701
- function randomCheckpointThreshold() {
2702
- return defaultCheckpointThresholdMin + Math.floor(Math.random() * (defaultCheckpointThresholdMax - defaultCheckpointThresholdMin + 1));
3103
+ function defaultCheckpointThresholdValue() {
3104
+ return defaultCheckpointThreshold;
2703
3105
  }
2704
3106
  function agentLoopDirectory(parsed, options) {
2705
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");
@@ -2707,8 +3109,81 @@ function agentLoopDirectory(parsed, options) {
2707
3109
  function agentLoopStatePath(parsed, options) {
2708
3110
  return join(agentLoopDirectory(parsed, options), "state.json");
2709
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
+ }
2710
3121
  function agentLoopLockPath(parsed, options, agentSessionKeyValue) {
2711
- 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
+ };
2712
3187
  }
2713
3188
  async function readAgentLoopState(parsed, options) {
2714
3189
  const filePath = agentLoopStatePath(parsed, options);
@@ -2754,24 +3229,45 @@ function readAgentLoopSessions(rawSessions) {
2754
3229
  const record = rawSession;
2755
3230
  const source = parseAgentEventSource(typeof record.source === "string" ? record.source : undefined);
2756
3231
  const stopCount = typeof record.stopCount === "number" && Number.isInteger(record.stopCount) && record.stopCount >= 0 ? record.stopCount : 0;
2757
- 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;
2758
3236
  sessions[key] = {
2759
3237
  source,
2760
3238
  stopCount,
2761
3239
  threshold,
3240
+ ...readAgentSessionCadenceContext(record),
2762
3241
  ...typeof record.firstObservedAt === "string" ? { firstObservedAt: record.firstObservedAt } : {},
2763
3242
  ...typeof record.lastObservedAt === "string" ? { lastObservedAt: record.lastObservedAt } : {},
3243
+ ...typeof record.lastObservedTurnId === "string" ? { lastObservedTurnId: record.lastObservedTurnId } : {},
2764
3244
  ...typeof record.lastAction === "string" ? { lastAction: record.lastAction } : {},
2765
3245
  ...typeof record.lastReason === "string" ? { lastReason: record.lastReason } : {},
2766
3246
  ...typeof record.lastEventFile === "string" ? { lastEventFile: record.lastEventFile } : {},
2767
3247
  ...typeof record.lastAssistantMessage === "string" ? { lastAssistantMessage: record.lastAssistantMessage } : {},
3248
+ ...recentTurns ? { recentTurns } : {},
2768
3249
  ...typeof record.lastCheckpointAt === "string" ? { lastCheckpointAt: record.lastCheckpointAt } : {},
2769
3250
  ...typeof record.lastCheckpointFingerprint === "string" ? { lastCheckpointFingerprint: record.lastCheckpointFingerprint } : {},
2770
- ...typeof record.previousCheckpointSummary === "string" ? { previousCheckpointSummary: record.previousCheckpointSummary } : {}
3251
+ ...typeof record.previousCheckpointSummary === "string" ? { previousCheckpointSummary: record.previousCheckpointSummary } : {},
3252
+ ...typeof record.lastCheckpointAuditFile === "string" ? { lastCheckpointAuditFile: record.lastCheckpointAuditFile } : {}
2771
3253
  };
2772
3254
  }
2773
3255
  return sessions;
2774
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
+ }
2775
3271
  function readAgentLoopDiagnostics(record) {
2776
3272
  return {
2777
3273
  ...typeof record.missingSessionIdCount === "number" && Number.isInteger(record.missingSessionIdCount) && record.missingSessionIdCount > 0 ? { missingSessionIdCount: record.missingSessionIdCount } : {},
@@ -2806,6 +3302,15 @@ async function writeAgentEventFile(parsed, options, event) {
2806
3302
  `);
2807
3303
  return filePath;
2808
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
+ }
2809
3314
  async function removeStaleAgentLoopLock(lockPath) {
2810
3315
  try {
2811
3316
  const lockStats = await stat(lockPath);
@@ -2891,7 +3396,7 @@ function currentCliWorkerInvocation() {
2891
3396
  argsPrefix: []
2892
3397
  };
2893
3398
  }
2894
- async function spawnAgentRunCloseout(args, options) {
3399
+ async function spawnAgentRunCheckpoint(args, options) {
2895
3400
  const cwd = options.cwd ?? process.cwd();
2896
3401
  const invocation = currentCliWorkerInvocation();
2897
3402
  const env = {
@@ -2910,47 +3415,88 @@ async function spawnAgentRunCloseout(args, options) {
2910
3415
  });
2911
3416
  child.unref();
2912
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
+ }
2913
3431
  function activeSessionFromCurrent(current) {
2914
3432
  const record = current && typeof current === "object" ? current : null;
2915
3433
  const sessions = Array.isArray(record?.sessions) ? record.sessions : [];
2916
- const activeSession = sessions.find((session) => {
2917
- if (!session || typeof session !== "object") {
2918
- return false;
2919
- }
2920
- const sessionRecord2 = session;
2921
- return sessionRecord2.status === "active" && typeof sessionRecord2.ticketId === "string";
2922
- });
2923
- if (activeSession && typeof activeSession === "object") {
2924
- return activeSession;
2925
- }
2926
3434
  const activeLeases = Array.isArray(record?.activeLeases) ? record.activeLeases : Array.isArray(record?.leases) ? record.leases.filter((lease) => lease && typeof lease === "object" && lease.status === "active") : [];
2927
- const activeLease = activeLeases.find((lease) => {
3435
+ const activeLease = activeLeases.filter((lease) => {
2928
3436
  if (!lease || typeof lease !== "object") {
2929
3437
  return false;
2930
3438
  }
2931
- const leaseRecord2 = lease;
2932
- return typeof leaseRecord2.ticketId === "string";
2933
- });
2934
- if (!activeLease || typeof activeLease !== "object") {
2935
- 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
+ };
2936
3459
  }
2937
- const leaseRecord = activeLease;
2938
- const sessionId = typeof leaseRecord.sessionId === "string" ? leaseRecord.sessionId : undefined;
2939
- const matchingSession = sessions.find((session) => {
3460
+ const activeSessions = sessions.filter((session) => {
2940
3461
  if (!session || typeof session !== "object") {
2941
3462
  return false;
2942
3463
  }
2943
- return sessionId && session.id === sessionId;
3464
+ const sessionRecord = session;
3465
+ return sessionRecord.status === "active" && typeof sessionRecord.ticketId === "string";
2944
3466
  });
2945
- 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
+ }
2946
3484
  return {
2947
- ...sessionRecord,
2948
- ...sessionId ? { id: sessionId } : {},
2949
- ticketId: leaseRecord.ticketId,
2950
- ...typeof leaseRecord.changesetId === "string" ? { changesetId: leaseRecord.changesetId } : typeof sessionRecord.changesetId === "string" ? { changesetId: sessionRecord.changesetId } : {},
2951
- status: "active"
3485
+ ...ticketId ? { ticketId } : {},
3486
+ ...sessionId ? { sessionId } : {},
3487
+ ...changesetId ? { changesetId } : {},
3488
+ capturedAt: new Date().toISOString()
2952
3489
  };
2953
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
+ }
2954
3500
  function fingerprintForCheckpoint(event, options) {
2955
3501
  const status = gitOutput(["status", "--short"], options);
2956
3502
  const changedFiles = gitOutput(["diff", "--name-only", "origin/dev..."], options);
@@ -2958,6 +3504,7 @@ function fingerprintForCheckpoint(event, options) {
2958
3504
  source: event.source,
2959
3505
  event: event.event,
2960
3506
  lastAssistantMessage: event.lastAssistantMessage ?? "",
3507
+ recentTurns: event.recentTurns ?? [],
2961
3508
  status,
2962
3509
  changedFiles
2963
3510
  }));
@@ -2972,24 +3519,106 @@ function shouldSkipForCooldown(state, cooldownSeconds) {
2972
3519
  function synthesizeAgentEventFromSession(agentSessionKeyValue, session, options) {
2973
3520
  return {
2974
3521
  source: session.source,
2975
- event: "closeout",
3522
+ event: "checkpoint",
2976
3523
  workspacePath: options.cwd ?? process.cwd(),
2977
3524
  occurredAt: new Date().toISOString(),
2978
3525
  agentSessionKey: agentSessionKeyValue,
2979
3526
  ...session.lastAssistantMessage ? { lastAssistantMessage: session.lastAssistantMessage } : {},
3527
+ ...session.recentTurns?.length ? { recentTurns: session.recentTurns } : {},
2980
3528
  payloadKeys: []
2981
3529
  };
2982
3530
  }
2983
- function parseCheckpointJson(raw) {
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();
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());
3554
+ } catch {
3555
+ return {};
3556
+ }
3557
+ }
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) {
2984
3617
  const trimmed = raw.trim();
2985
3618
  try {
2986
3619
  const parsed = JSON.parse(trimmed);
2987
3620
  if (parsed && typeof parsed === "object") {
2988
- const record = parsed;
2989
- return {
2990
- body: typeof record.body === "string" && record.body.trim() ? record.body.trim() : trimmed,
2991
- summary: typeof record.summary === "string" && record.summary.trim() ? record.summary.trim() : undefined
2992
- };
3621
+ return parseCheckpointRecord(parsed, trimmed, fallbackKind);
2993
3622
  }
2994
3623
  } catch {
2995
3624
  const start = trimmed.indexOf("{");
@@ -2998,34 +3627,378 @@ function parseCheckpointJson(raw) {
2998
3627
  try {
2999
3628
  const parsed = JSON.parse(trimmed.slice(start, end + 1));
3000
3629
  if (parsed && typeof parsed === "object") {
3001
- const record = parsed;
3002
- return {
3003
- body: typeof record.body === "string" && record.body.trim() ? record.body.trim() : trimmed,
3004
- summary: typeof record.summary === "string" && record.summary.trim() ? record.summary.trim() : undefined
3005
- };
3630
+ return parseCheckpointRecord(parsed, trimmed, fallbackKind);
3006
3631
  }
3007
3632
  } catch {}
3008
3633
  }
3009
3634
  }
3010
3635
  return {
3011
- body: trimmed,
3012
- summary: undefined
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
+ } : {}
3013
3975
  };
3014
3976
  }
3015
3977
  function buildCheckpointPrompt(input) {
3978
+ const recentTurns = checkpointRecentTurns(input.event);
3979
+ const recentTurnsText = recentTurns.length ? formatCheckpointRecentTurns(recentTurns) : "";
3016
3980
  return [
3017
- "You are generating a concise Cadence checkpoint for an active coding session.",
3018
- "Do not include raw diffs, raw transcripts, terminal logs, tool streams, secrets, or model reasoning.",
3019
- 'Return JSON only with this shape: {"summary":"one short current work summary","body":"checkpoint body suitable for a Cadence ticket work log"}.',
3020
- "The body should mention what changed, decisions made, verification if known, and the next risk or next step. Keep it under 1200 characters.",
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.",
3021
3993
  "",
3022
3994
  `Ticket: ${input.ticketId}`,
3023
3995
  input.sessionId ? `Session: ${input.sessionId}` : "",
3024
3996
  input.changesetId ? `ChangeSet: ${input.changesetId}` : "",
3025
3997
  `Agent event: ${input.event.source}/${input.event.event}`,
3026
3998
  input.previousCheckpointSummary ? `Previous checkpoint summary: ${input.previousCheckpointSummary}` : "",
3027
- input.event.lastAssistantMessage ? `Last assistant message:
3028
- ${truncateText(input.event.lastAssistantMessage, 3000)}` : "Last assistant message: unavailable",
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",
3029
4002
  input.gitStatus ? `Git status --short:
3030
4003
  ${truncateText(input.gitStatus, 2000)}` : "Git status --short: clean or unavailable",
3031
4004
  input.gitDiffStat ? `Git diff --stat origin/dev...:
@@ -3058,6 +4031,22 @@ function codexHookEntry(command) {
3058
4031
  ]
3059
4032
  };
3060
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
+ }
3061
4050
  async function readJsonObjectFile(filePath) {
3062
4051
  const file = Bun.file(filePath);
3063
4052
  if (!await file.exists()) {
@@ -3071,8 +4060,9 @@ async function installCodexHookFile(filePath, command) {
3071
4060
  const hooks = hooksValue && typeof hooksValue === "object" && !Array.isArray(hooksValue) ? hooksValue : {};
3072
4061
  const stopValue = hooks.Stop;
3073
4062
  const stopHooks = Array.isArray(stopValue) ? stopValue : [];
3074
- const alreadyInstalled = stopHooks.some((entry) => JSON.stringify(entry).includes(command));
3075
- 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)];
3076
4066
  const nextConfig = {
3077
4067
  ...existing,
3078
4068
  hooks: {
@@ -3086,7 +4076,8 @@ async function installCodexHookFile(filePath, command) {
3086
4076
  return {
3087
4077
  path: filePath,
3088
4078
  installed: !alreadyInstalled,
3089
- alreadyInstalled
4079
+ alreadyInstalled,
4080
+ updated: !alreadyInstalled && cadenceHookIndexes.length > 0
3090
4081
  };
3091
4082
  }
3092
4083
  async function codexHookPaths(scope, options) {
@@ -3108,7 +4099,7 @@ async function codexHookInstalled(filePath, command) {
3108
4099
  const existing = await readJsonObjectFile(filePath);
3109
4100
  const hooks = existing.hooks;
3110
4101
  const stopHooks = hooks && typeof hooks === "object" && !Array.isArray(hooks) && Array.isArray(hooks.Stop) ? hooks.Stop : [];
3111
- return stopHooks.some((entry) => JSON.stringify(entry).includes(command));
4102
+ return stopHooks.some((entry) => codexHookCommands(entry).includes(command));
3112
4103
  }
3113
4104
  async function runStatus(parsed, options) {
3114
4105
  const config = await resolveCliConfig(parsed.flags, options);
@@ -3255,14 +4246,17 @@ async function runAgentRunCommand(parsed, options) {
3255
4246
  switch (parsed.command.name) {
3256
4247
  case "agent-run.ingest-stop":
3257
4248
  return await runAgentRunIngestStop(parsed, options, config, meta);
4249
+ case "agent-run.checkpoint":
3258
4250
  case "agent-run.closeout":
3259
- return await runAgentRunCloseout(parsed, options, config, meta);
4251
+ return await runAgentRunCheckpoint(parsed, options, config, meta);
3260
4252
  case "agent-run.sweep":
3261
- return await runAgentRunSweep(parsed, options, meta);
4253
+ return await runAgentRunSweep(parsed, options, config, meta);
3262
4254
  case "agent-run.doctor": {
3263
4255
  const state = await readAgentLoopState(parsed, options);
4256
+ const settings = await readAgentLoopSettings(parsed, options);
3264
4257
  const data = {
3265
4258
  action: "doctor",
4259
+ settings,
3266
4260
  state,
3267
4261
  sessionCount: Object.keys(state.sessions).length,
3268
4262
  pendingSessions: Object.entries(state.sessions).filter(([, session]) => session.stopCount > 0).map(([agentSessionKeyValue, session]) => ({
@@ -3337,8 +4331,15 @@ async function runAgentRunIngestStop(parsed, options, config, meta) {
3337
4331
  const dryRun = parseBooleanOption(parsed.options["dry-run"], false);
3338
4332
  const existingSession = state.sessions[normalized.agentSessionKey];
3339
4333
  const now = new Date().toISOString();
3340
- const threshold = forcedThreshold ?? existingSession?.threshold ?? randomCheckpointThreshold();
3341
- 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 {}
3342
4343
  const observedSession = {
3343
4344
  ...clearAgentSessionReason(existingSession ?? {
3344
4345
  source: normalized.source,
@@ -3350,8 +4351,11 @@ async function runAgentRunIngestStop(parsed, options, config, meta) {
3350
4351
  threshold,
3351
4352
  firstObservedAt: existingSession?.firstObservedAt ?? now,
3352
4353
  lastObservedAt: now,
4354
+ ...normalized.turnId ? { lastObservedTurnId: normalized.turnId } : {},
3353
4355
  lastAction: "counted",
3354
- ...normalized.lastAssistantMessage ? { lastAssistantMessage: normalized.lastAssistantMessage } : {}
4356
+ ...cadenceContext ? { cadenceContext } : {},
4357
+ ...normalized.lastAssistantMessage ? { lastAssistantMessage: normalized.lastAssistantMessage } : {},
4358
+ ...recentTurns ? { recentTurns } : {}
3355
4359
  };
3356
4360
  const countedState = {
3357
4361
  ...state,
@@ -3363,7 +4367,8 @@ async function runAgentRunIngestStop(parsed, options, config, meta) {
3363
4367
  if (nextCount < threshold) {
3364
4368
  await writeAgentLoopState(parsed, options, countedState);
3365
4369
  const data2 = {
3366
- action: "counted",
4370
+ action: duplicateTurn ? "updated" : "counted",
4371
+ ...duplicateTurn ? { reason: "duplicate_turn" } : {},
3367
4372
  agentSessionKey: normalized.agentSessionKey,
3368
4373
  stopCount: nextCount,
3369
4374
  threshold
@@ -3410,7 +4415,7 @@ async function runAgentRunIngestStop(parsed, options, config, meta) {
3410
4415
  [normalized.agentSessionKey]: {
3411
4416
  ...observedSession,
3412
4417
  stopCount: 0,
3413
- threshold: randomCheckpointThreshold(),
4418
+ threshold: defaultCheckpointThresholdValue(),
3414
4419
  lastAction: "skipped",
3415
4420
  lastReason: "unchanged"
3416
4421
  }
@@ -3432,7 +4437,7 @@ async function runAgentRunIngestStop(parsed, options, config, meta) {
3432
4437
  const lockPath = agentLoopLockPath(parsed, options, normalized.agentSessionKey);
3433
4438
  const workerArgs = [
3434
4439
  "agent-run",
3435
- "closeout",
4440
+ "checkpoint",
3436
4441
  "--agent-session-key",
3437
4442
  normalized.agentSessionKey,
3438
4443
  "--reason",
@@ -3498,7 +4503,7 @@ async function runAgentRunIngestStop(parsed, options, config, meta) {
3498
4503
  };
3499
4504
  }
3500
4505
  try {
3501
- await spawnAgentRunCloseout(workerArgs, options);
4506
+ await spawnAgentRunCheckpoint(workerArgs, options);
3502
4507
  } catch (error) {
3503
4508
  await releaseAgentLoopLock(lock.lockPath);
3504
4509
  throw error;
@@ -3509,8 +4514,9 @@ async function runAgentRunIngestStop(parsed, options, config, meta) {
3509
4514
  ...state.sessions,
3510
4515
  [normalized.agentSessionKey]: {
3511
4516
  ...observedSession,
4517
+ ...cadenceContext ? { cadenceContext } : {},
3512
4518
  stopCount: 0,
3513
- threshold: randomCheckpointThreshold(),
4519
+ threshold: defaultCheckpointThresholdValue(),
3514
4520
  lastAction: "spawned",
3515
4521
  lastEventFile: eventFile,
3516
4522
  lastCheckpointAt: new Date().toISOString(),
@@ -3531,7 +4537,7 @@ async function runAgentRunIngestStop(parsed, options, config, meta) {
3531
4537
  exitCode: 0
3532
4538
  };
3533
4539
  }
3534
- async function runAgentRunCloseout(parsed, options, config, meta) {
4540
+ async function runAgentRunCheckpoint(parsed, options, config, meta) {
3535
4541
  const projectId = requireProjectId(config);
3536
4542
  const client = await createClient(config, options);
3537
4543
  const agentSessionKeyValue = requireOption(parsed, "agent-session-key");
@@ -3542,16 +4548,11 @@ async function runAgentRunCloseout(parsed, options, config, meta) {
3542
4548
  agentSessionKey: agentSessionKeyValue
3543
4549
  });
3544
4550
  }
3545
- const current = await client.sessions.current({
3546
- projectId,
3547
- filters: {
3548
- limit: 100
3549
- }
3550
- });
3551
- const currentSession = activeSessionFromCurrent(current);
3552
- const ticketId = parsed.options.ticket ?? (currentSession && typeof currentSession.ticketId === "string" ? currentSession.ticketId : undefined);
3553
- const sessionId = parsed.options.session ?? (currentSession && typeof currentSession.id === "string" ? currentSession.id : undefined);
3554
- 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;
3555
4556
  if (!ticketId) {
3556
4557
  await writeAgentLoopState(parsed, options, {
3557
4558
  ...state,
@@ -3582,6 +4583,7 @@ async function runAgentRunCloseout(parsed, options, config, meta) {
3582
4583
  const updateSummary = parseBooleanOption(parsed.options["update-summary"], false);
3583
4584
  const dryRun = parseBooleanOption(parsed.options["dry-run"], false);
3584
4585
  const lockPath = parsed.options.lock;
4586
+ const checkpointSettings = await resolveCheckpointSettings(parsed, options);
3585
4587
  const gitStatus = gitOutput(["status", "--short"], options);
3586
4588
  const gitDiffStat = gitOutput(["diff", "--stat", "origin/dev..."], options);
3587
4589
  const changedFiles = gitOutput(["diff", "--name-only", "origin/dev..."], options);
@@ -3595,10 +4597,29 @@ async function runAgentRunCloseout(parsed, options, config, meta) {
3595
4597
  changedFiles,
3596
4598
  ...sessionState.previousCheckpointSummary ? { previousCheckpointSummary: sessionState.previousCheckpointSummary } : {}
3597
4599
  });
4600
+ const promptTokenAccounting = buildCheckpointTokenAccounting(event, prompt, undefined);
3598
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
+ });
3599
4619
  const data = {
3600
- action: "would_closeout",
4620
+ action: "would_checkpoint",
3601
4621
  prompt,
4622
+ auditFile,
3602
4623
  ticketId,
3603
4624
  agentSessionKey: agentSessionKeyValue,
3604
4625
  ...sessionId ? { sessionId } : {},
@@ -3613,84 +4634,387 @@ async function runAgentRunCloseout(parsed, options, config, meta) {
3613
4634
  }
3614
4635
  try {
3615
4636
  const codexCommand = parsed.options["codex-command"] ?? "codex";
3616
- const codex = runLocalCommand(codexCommand, ["exec", "--disable", "hooks", "--sandbox", "read-only", "-C", options.cwd ?? process.cwd(), prompt], options, {
3617
- 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,
3618
4652
  env: {
3619
4653
  [agentLoopSuppressEnv]: "1",
3620
4654
  CADENCE_HOOK_SUPPRESS: "1"
3621
4655
  },
3622
4656
  timeoutMs: defaultCheckpointWorkerTimeoutMs
3623
4657
  });
4658
+ const codexSessionTranscript = readCodexSessionTranscript(findCodexSessionFileCreatedAfter(options, codexStartedAtMs, codexCwd));
4659
+ const tokenAccounting = buildCheckpointTokenAccounting(event, prompt, codexSessionTranscript?.tokenUsage);
3624
4660
  if (codex.status !== 0 || codex.error) {
3625
- throw new CliError("AGENT_RUN_CLOSEOUT_FAILED", "Codex closeout generation failed.", {
4661
+ throw new CliError("AGENT_RUN_CHECKPOINT_FAILED", "Codex checkpoint generation failed.", {
3626
4662
  status: codex.status,
3627
4663
  stderr: truncateText(codex.stderr, 2000),
3628
4664
  error: codex.error?.message
3629
4665
  });
3630
4666
  }
3631
- const checkpoint = parseCheckpointJson(codex.stdout);
3632
- const body = checkpoint.body.trim();
3633
- const summary = checkpoint.summary ?? truncateText(body.replace(/\s+/g, " "), 500);
3634
- if (!body) {
3635
- throw new CliError("AGENT_RUN_CLOSEOUT_FAILED", "Codex closeout generation returned an empty checkpoint.");
3636
- }
3637
- await client.tickets.log({
3638
- projectId,
3639
- ticketId,
3640
- entry: {
3641
- entryKind: logKind,
3642
- 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,
4709
+ summary,
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,
3643
4742
  summary,
3644
- ...sessionId ? { sessionId } : {},
3645
- ...changesetId ? { changesetId } : {},
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,
3646
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
+ });
3647
4929
  }
3648
- });
3649
- if (updateSummary) {
3650
- 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 });
3651
4955
  if (typeof ticket.projectionVersion === "number") {
3652
4956
  await client.tickets.update({
3653
4957
  projectId,
3654
- ticketId,
4958
+ ticketId: targetTicketId,
3655
4959
  ifVersion: ticket.projectionVersion,
3656
4960
  ticket: {
3657
- currentSummary: summary,
4961
+ currentSummary: checkpoint.summaryUpdate.value,
3658
4962
  ...commandMetadata()
3659
4963
  }
3660
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
+ });
3661
4974
  }
3662
4975
  }
3663
- await writeAgentLoopState(parsed, options, {
3664
- ...state,
3665
- sessions: {
3666
- ...state.sessions,
3667
- [agentSessionKeyValue]: {
3668
- ...sessionState,
3669
- previousCheckpointSummary: summary,
3670
- lastAction: "closed_out",
3671
- ...eventFile ? { lastEventFile: eventFile } : {}
3672
- }
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 }));
3673
4987
  }
3674
- });
3675
- const data = {
3676
- action: "closed_out",
3677
- ticketId,
3678
- summary,
3679
- agentSessionKey: agentSessionKeyValue,
3680
- ...sessionId ? { sessionId } : {},
3681
- ...changesetId ? { changesetId } : {}
3682
- };
3683
- return {
3684
- stdout: parsed.flags.json ? formatJson(successEnvelope(data, meta)) : `${JSON.stringify(data, null, 2)}
3685
- `,
3686
- stderr: "",
3687
- exitCode: 0
3688
- };
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");
3689
5013
  } finally {
3690
5014
  await releaseAgentLoopLock(lockPath);
3691
5015
  }
3692
5016
  }
3693
- async function runAgentRunSweep(parsed, options, meta) {
5017
+ async function runAgentRunSweep(parsed, options, config, meta) {
3694
5018
  const state = await readAgentLoopState(parsed, options);
3695
5019
  const idleAfterSeconds = parsePositiveInteger(parsed.options["idle-after-seconds"], "--idle-after-seconds") ?? 5 * 60;
3696
5020
  const dryRun = parseBooleanOption(parsed.options["dry-run"], false);
@@ -3709,10 +5033,35 @@ async function runAgentRunSweep(parsed, options, meta) {
3709
5033
  lastObservedAt: session.lastObservedAt
3710
5034
  }));
3711
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;
3712
5045
  for (const staleSession of staleSessions) {
3713
- 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([
3714
5063
  "agent-run",
3715
- "closeout",
5064
+ "checkpoint",
3716
5065
  "--agent-session-key",
3717
5066
  staleSession.agentSessionKey,
3718
5067
  "--reason",
@@ -4158,6 +5507,7 @@ async function runIntakeCommand(parsed, options) {
4158
5507
  ...parsed.options.changeset ? { changesetId: parsed.options.changeset } : {},
4159
5508
  ...parsed.options.actor ? { actorId: parsed.options.actor } : {},
4160
5509
  expiresAt: leaseExpiresAt(parsePositiveInteger(parsed.options["ttl-seconds"], "--ttl-seconds") ?? defaultLeaseTtlSeconds),
5510
+ ...parseBooleanOption(parsed.options["replace-own-active"], false) ? { replaceOwnActiveLease: true } : {},
4161
5511
  ...commandMetadata()
4162
5512
  }
4163
5513
  });
@@ -4383,7 +5733,7 @@ async function runCli(argv, options = {}) {
4383
5733
  if (parsed.command.name === "projects.list") {
4384
5734
  return await runProjectCommand(parsed, options);
4385
5735
  }
4386
- 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") {
4387
5737
  return await runAgentRunCommand(parsed, options);
4388
5738
  }
4389
5739
  if (parsed.command.name === "hooks.install" || parsed.command.name === "hooks.doctor") {