@trycadence/cli 0.1.11-dev.0 → 0.1.16-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 +2395 -165
  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.16-dev.0",
1523
1524
  private: false,
1524
1525
  type: "module",
1525
1526
  bin: {
@@ -1553,14 +1554,15 @@ 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 agentRunMemoryModes = ["checkpoint", "closeout"];
1558
+ var closeoutSessionActions = ["handoff", "end", "keep"];
1559
+ var defaultLeaseTtlSeconds = 15 * 60;
1557
1560
  var defaultCliApiBaseUrl = "https://cadenceapi.deploy.lvl8studios.com";
1558
1561
  var defaultCliWebBaseUrl = "https://cadence.deploy.lvl8studios.com";
1559
- var defaultCheckpointThresholdMin = 3;
1560
- var defaultCheckpointThresholdMax = 5;
1562
+ var defaultCheckpointThreshold = 3;
1561
1563
  var defaultCheckpointCooldownSeconds = 10 * 60;
1562
1564
  var defaultCheckpointWorkerTimeoutMs = 10 * 60 * 1000;
1563
- var defaultHookCommand = "cadence agent-run ingest-stop --source codex --event stop";
1565
+ 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
1566
  var agentLoopSuppressEnv = "CADENCE_AGENT_EVENT_SUPPRESS";
1565
1567
  var credentialRefreshSkewMs = 60 * 1000;
1566
1568
  var credentialRefreshLockTimeoutMs = 10 * 1000;
@@ -1599,6 +1601,8 @@ var knownCommandPaths = [
1599
1601
  ["changesets", "notes", "put"],
1600
1602
  ["changesets", "notes", "apply"],
1601
1603
  ["agent-run", "ingest-stop"],
1604
+ ["agent-run", "route"],
1605
+ ["agent-run", "checkpoint"],
1602
1606
  ["agent-run", "closeout"],
1603
1607
  ["agent-run", "sweep"],
1604
1608
  ["agent-run", "doctor"],
@@ -2260,7 +2264,7 @@ function helpText() {
2260
2264
  " cadence tickets attach <ticket-id> --from-intake <intake-id> --if-version <version> [--project <project-id>] [--json]",
2261
2265
  " cadence tickets create --title <text> [--from-intake <intake-id>] [--project <project-id>] [--json]",
2262
2266
  " 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]",
2267
+ " cadence tickets claim <ticket-id> --session <session-id> [--actor <actor-id>] [--replace-own-active true|false] [--project <project-id>] [--json]",
2264
2268
  " cadence tickets release <ticket-id> --lease <lease-id> [--project <project-id>] [--json]",
2265
2269
  " 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
2270
  " cadence tickets complete <ticket-id> --if-version <version> [--summary <summary>] [--project <project-id>] [--json]",
@@ -2278,11 +2282,13 @@ function helpText() {
2278
2282
  " 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
2283
  " cadence changesets notes apply [--changeset <id>|--branch current|<branch>] --provider github --pr-number <n> --pr-url <url> [--project <project-id>] [--json]",
2280
2284
  " 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]",
2285
+ " cadence agent-run route --agent-session-key <key> --reason <missing_context|checkpoint_reroute|manual> [--event-file <path>] [--checkpoint-provider codex] [--checkpoint-model <model>] [--json]",
2286
+ " 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]",
2287
+ " cadence agent-run closeout --agent-session-key <key> [--session-action handoff|end|keep] [--complete-ticket true|false] [--event-file <path>] [--ticket <ticket-id>] [--session <session-id>] [--changeset <changeset-id>] [--checkpoint-provider codex] [--checkpoint-model <model>] [--update-summary true|false] [--json]",
2282
2288
  " cadence agent-run sweep [--idle-after-seconds <n>] [--dry-run true|false] [--json]",
2283
2289
  " 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]",
2290
+ " cadence hooks install --provider codex [--scope global] [--command <command>] [--json]",
2291
+ " cadence hooks doctor --provider codex [--scope global] [--json]",
2286
2292
  " cadence events list [--project <project-id>] [--ticket <ticket-id>] [--changeset <changeset-id>] [--session <session-id>] [--json]",
2287
2293
  "",
2288
2294
  "Global flags:",
@@ -2581,6 +2587,14 @@ function truncateText(value, maxLength) {
2581
2587
  return `${value.slice(0, maxLength - 15)}
2582
2588
  [truncated]`;
2583
2589
  }
2590
+ function estimateTokenCount(value) {
2591
+ const text = value.trim();
2592
+ if (!text) {
2593
+ return 0;
2594
+ }
2595
+ const pieces = text.match(/[A-Za-z0-9_]+|[^\sA-Za-z0-9_]/g) ?? [];
2596
+ return Math.max(pieces.length, Math.ceil(text.length / 4));
2597
+ }
2584
2598
  function stableHash(value) {
2585
2599
  return createHash("sha256").update(value).digest("hex");
2586
2600
  }
@@ -2619,13 +2633,392 @@ function firstString(record, paths) {
2619
2633
  }
2620
2634
  return;
2621
2635
  }
2636
+ function readRecentAgentTurns(input) {
2637
+ const rawTurns = input.recent_turns ?? input.recentTurns ?? input.turns;
2638
+ if (!Array.isArray(rawTurns)) {
2639
+ return;
2640
+ }
2641
+ const turns = rawTurns.map((turn) => {
2642
+ if (!turn || typeof turn !== "object" || Array.isArray(turn)) {
2643
+ return;
2644
+ }
2645
+ const record = turn;
2646
+ const user = firstString(record, [["user"], ["prompt"], ["userPrompt"], ["request"]]);
2647
+ const assistant = firstString(record, [["assistant"], ["response"], ["assistantResponse"], ["reply"]]);
2648
+ const occurredAt = firstString(record, [["occurredAt"], ["occurred_at"], ["timestamp"], ["createdAt"]]);
2649
+ if (!user && !assistant) {
2650
+ return;
2651
+ }
2652
+ return {
2653
+ ...user ? { user: truncateText(user, 6000) } : {},
2654
+ ...assistant ? { assistant: truncateText(assistant, 6000) } : {},
2655
+ ...occurredAt ? { occurredAt } : {}
2656
+ };
2657
+ }).filter((turn) => Boolean(turn));
2658
+ return turns.length ? turns.slice(-3) : undefined;
2659
+ }
2660
+ function mergeRecentAgentTurns(existing, next) {
2661
+ if (!next?.length) {
2662
+ return existing?.length ? existing.slice(-3) : undefined;
2663
+ }
2664
+ return [...existing ?? [], ...next].slice(-3);
2665
+ }
2666
+ function stringFromCodexContent(value) {
2667
+ if (typeof value === "string") {
2668
+ return value.trim() || undefined;
2669
+ }
2670
+ if (Array.isArray(value)) {
2671
+ const text = value.map((item) => {
2672
+ if (typeof item === "string") {
2673
+ return item;
2674
+ }
2675
+ if (item && typeof item === "object" && !Array.isArray(item)) {
2676
+ const record = item;
2677
+ return typeof record.text === "string" ? record.text : typeof record.content === "string" ? record.content : "";
2678
+ }
2679
+ return "";
2680
+ }).join(`
2681
+ `).trim();
2682
+ return text || undefined;
2683
+ }
2684
+ return;
2685
+ }
2686
+ function findCodexSessionFile(directory, agentSessionId, depth = 0) {
2687
+ if (depth > 6 || !existsSync(directory)) {
2688
+ return;
2689
+ }
2690
+ let entries;
2691
+ try {
2692
+ entries = readdirSync(directory, { withFileTypes: true });
2693
+ } catch {
2694
+ return;
2695
+ }
2696
+ for (const entry of entries) {
2697
+ const entryPath = join(directory, entry.name);
2698
+ if (entry.isFile() && entry.name.endsWith(".jsonl") && entry.name.includes(agentSessionId)) {
2699
+ return entryPath;
2700
+ }
2701
+ }
2702
+ for (const entry of entries) {
2703
+ if (!entry.isDirectory()) {
2704
+ continue;
2705
+ }
2706
+ const found = findCodexSessionFile(join(directory, entry.name), agentSessionId, depth + 1);
2707
+ if (found) {
2708
+ return found;
2709
+ }
2710
+ }
2711
+ return;
2712
+ }
2713
+ function codexSessionsDirectory(options) {
2714
+ const home = options.env?.HOME ?? process.env.HOME;
2715
+ const codexHome = options.env?.CODEX_HOME ?? process.env.CODEX_HOME ?? (home ? join(home, ".codex") : undefined);
2716
+ return codexHome ? join(codexHome, "sessions") : undefined;
2717
+ }
2718
+ function listCodexSessionFiles(directory, depth = 0) {
2719
+ if (depth > 6 || !existsSync(directory)) {
2720
+ return [];
2721
+ }
2722
+ let entries;
2723
+ try {
2724
+ entries = readdirSync(directory, { withFileTypes: true });
2725
+ } catch {
2726
+ return [];
2727
+ }
2728
+ return entries.flatMap((entry) => {
2729
+ const entryPath = join(directory, entry.name);
2730
+ if (entry.isDirectory()) {
2731
+ return listCodexSessionFiles(entryPath, depth + 1);
2732
+ }
2733
+ if (!entry.isFile() || !entry.name.endsWith(".jsonl")) {
2734
+ return [];
2735
+ }
2736
+ try {
2737
+ return [{ path: entryPath, mtimeMs: statSync(entryPath).mtimeMs }];
2738
+ } catch {
2739
+ return [];
2740
+ }
2741
+ });
2742
+ }
2743
+ function readCodexSessionMeta(filePath) {
2744
+ let lines;
2745
+ try {
2746
+ lines = readFileSyncNode(filePath, "utf8").split(`
2747
+ `).filter((line) => line.trim());
2748
+ } catch {
2749
+ return {};
2750
+ }
2751
+ for (const line of lines) {
2752
+ let parsed;
2753
+ try {
2754
+ parsed = JSON.parse(line);
2755
+ } catch {
2756
+ continue;
2757
+ }
2758
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
2759
+ continue;
2760
+ }
2761
+ const event = parsed;
2762
+ const payload = event.payload && typeof event.payload === "object" && !Array.isArray(event.payload) ? event.payload : {};
2763
+ if (event.type !== "session_meta") {
2764
+ continue;
2765
+ }
2766
+ return {
2767
+ ...typeof payload.id === "string" ? { id: payload.id } : {},
2768
+ ...typeof payload.cwd === "string" ? { cwd: payload.cwd } : {},
2769
+ ...typeof event.timestamp === "string" ? { timestamp: event.timestamp } : {}
2770
+ };
2771
+ }
2772
+ return {};
2773
+ }
2774
+ function findCodexSessionFileCreatedAfter(options, startedAtMs, cwd) {
2775
+ const sessionsDirectory = codexSessionsDirectory(options);
2776
+ if (!sessionsDirectory) {
2777
+ return;
2778
+ }
2779
+ const candidates = listCodexSessionFiles(sessionsDirectory).filter((candidate) => candidate.mtimeMs >= startedAtMs - 1000).map((candidate) => ({
2780
+ ...candidate,
2781
+ meta: readCodexSessionMeta(candidate.path)
2782
+ })).filter((candidate) => !candidate.meta.cwd || candidate.meta.cwd === cwd).sort((left, right) => right.mtimeMs - left.mtimeMs);
2783
+ return candidates[0]?.path;
2784
+ }
2785
+ function codexTranscriptTextFromPayload(payload) {
2786
+ return stringFromCodexContent(payload.message) ?? stringFromCodexContent(payload.text) ?? stringFromCodexContent(payload.content) ?? stringFromCodexContent(payload.output) ?? undefined;
2787
+ }
2788
+ function summarizeCodexTranscriptEvent(raw, index) {
2789
+ const payload = raw.payload && typeof raw.payload === "object" && !Array.isArray(raw.payload) ? raw.payload : {};
2790
+ const type = typeof raw.type === "string" ? raw.type : "unknown";
2791
+ const payloadType = typeof payload.type === "string" ? payload.type : undefined;
2792
+ const text = codexTranscriptTextFromPayload(payload);
2793
+ const callId = typeof payload.call_id === "string" ? payload.call_id : typeof payload.callId === "string" ? payload.callId : undefined;
2794
+ const toolArguments = typeof payload.arguments === "string" ? payload.arguments : payload.arguments && typeof payload.arguments === "object" ? JSON.stringify(payload.arguments) : undefined;
2795
+ const toolName = typeof payload.tool_name === "string" ? payload.tool_name : typeof payload.toolName === "string" ? payload.toolName : typeof payload.name === "string" ? payload.name : undefined;
2796
+ return {
2797
+ index,
2798
+ type,
2799
+ ...payloadType ? { payloadType } : {},
2800
+ ...typeof raw.timestamp === "string" ? { timestamp: raw.timestamp } : {},
2801
+ ...text ? { text: truncateText(text, 8000) } : {},
2802
+ ...callId ? { callId } : {},
2803
+ ...toolName ? { toolName } : {},
2804
+ ...toolArguments ? { toolArguments: truncateText(toolArguments, 8000) } : {},
2805
+ raw
2806
+ };
2807
+ }
2808
+ function numberFromRecord(record, key) {
2809
+ const value = record[key];
2810
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
2811
+ }
2812
+ function codexTokenUsageRecord(value) {
2813
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
2814
+ return;
2815
+ }
2816
+ const record = value;
2817
+ return {
2818
+ ...numberFromRecord(record, "input_tokens") !== undefined ? { inputTokens: numberFromRecord(record, "input_tokens") } : {},
2819
+ ...numberFromRecord(record, "cached_input_tokens") !== undefined ? { cachedInputTokens: numberFromRecord(record, "cached_input_tokens") } : {},
2820
+ ...numberFromRecord(record, "output_tokens") !== undefined ? { outputTokens: numberFromRecord(record, "output_tokens") } : {},
2821
+ ...numberFromRecord(record, "reasoning_output_tokens") !== undefined ? { reasoningOutputTokens: numberFromRecord(record, "reasoning_output_tokens") } : {},
2822
+ ...numberFromRecord(record, "total_tokens") !== undefined ? { totalTokens: numberFromRecord(record, "total_tokens") } : {}
2823
+ };
2824
+ }
2825
+ function extractCodexTokenUsage(events) {
2826
+ const tokenEvents = events.filter((event) => event.payloadType === "token_count");
2827
+ if (!tokenEvents.length) {
2828
+ return;
2829
+ }
2830
+ let total;
2831
+ let last;
2832
+ let modelContextWindow;
2833
+ let updatedAt;
2834
+ for (const event of tokenEvents) {
2835
+ const payload = event.raw.payload && typeof event.raw.payload === "object" && !Array.isArray(event.raw.payload) ? event.raw.payload : {};
2836
+ const info = payload.info && typeof payload.info === "object" && !Array.isArray(payload.info) ? payload.info : {};
2837
+ const parsedTotal = codexTokenUsageRecord(info.total_token_usage);
2838
+ const parsedLast = codexTokenUsageRecord(info.last_token_usage);
2839
+ const parsedContextWindow = numberFromRecord(info, "model_context_window");
2840
+ if (parsedTotal && Object.keys(parsedTotal).length) {
2841
+ total = parsedTotal;
2842
+ }
2843
+ if (parsedLast && Object.keys(parsedLast).length) {
2844
+ last = parsedLast;
2845
+ }
2846
+ if (parsedContextWindow !== undefined) {
2847
+ modelContextWindow = parsedContextWindow;
2848
+ }
2849
+ updatedAt = event.timestamp;
2850
+ }
2851
+ return {
2852
+ source: "codex_session_transcript",
2853
+ eventCount: tokenEvents.length,
2854
+ ...total ? { total } : {},
2855
+ ...last ? { last } : {},
2856
+ ...modelContextWindow !== undefined ? { modelContextWindow } : {},
2857
+ ...updatedAt ? { updatedAt } : {}
2858
+ };
2859
+ }
2860
+ function tokenUsageTotal(tokenUsage) {
2861
+ return tokenUsage?.total;
2862
+ }
2863
+ function roundedRatio(numerator, denominator) {
2864
+ if (denominator <= 0) {
2865
+ return;
2866
+ }
2867
+ return Math.round(numerator / denominator * 1000) / 1000;
2868
+ }
2869
+ function readCodexSessionTranscript(filePath) {
2870
+ if (!filePath) {
2871
+ return;
2872
+ }
2873
+ let lines;
2874
+ try {
2875
+ lines = readFileSyncNode(filePath, "utf8").split(`
2876
+ `).filter((line) => line.trim());
2877
+ } catch {
2878
+ return;
2879
+ }
2880
+ const events = lines.flatMap((line, index) => {
2881
+ try {
2882
+ const parsed = JSON.parse(line);
2883
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
2884
+ return [];
2885
+ }
2886
+ return [summarizeCodexTranscriptEvent(parsed, index)];
2887
+ } catch {
2888
+ return [
2889
+ {
2890
+ index,
2891
+ type: "unparsed",
2892
+ text: truncateText(line, 8000),
2893
+ raw: { line }
2894
+ }
2895
+ ];
2896
+ }
2897
+ });
2898
+ return {
2899
+ path: filePath,
2900
+ fileName: basename(filePath),
2901
+ lineCount: lines.length,
2902
+ ...extractCodexTokenUsage(events) ? { tokenUsage: extractCodexTokenUsage(events) } : {},
2903
+ events
2904
+ };
2905
+ }
2906
+ function readRecentTurnsFromCodexSessionFile(filePath) {
2907
+ let lines;
2908
+ try {
2909
+ lines = readFileSyncNode(filePath, "utf8").split(`
2910
+ `).filter((line) => line.trim());
2911
+ } catch {
2912
+ return;
2913
+ }
2914
+ const messages = [];
2915
+ const pushMessage = (message) => {
2916
+ const previous = messages[messages.length - 1];
2917
+ if (previous?.role === message.role && previous.text === message.text) {
2918
+ return;
2919
+ }
2920
+ messages.push(message);
2921
+ };
2922
+ for (const line of lines) {
2923
+ let parsed;
2924
+ try {
2925
+ parsed = JSON.parse(line);
2926
+ } catch {
2927
+ continue;
2928
+ }
2929
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
2930
+ continue;
2931
+ }
2932
+ const event = parsed;
2933
+ const payload = event.payload && typeof event.payload === "object" && !Array.isArray(event.payload) ? event.payload : undefined;
2934
+ if (event.type === "event_msg" && payload?.type === "user_message") {
2935
+ const text = stringFromCodexContent(payload.message);
2936
+ if (text) {
2937
+ pushMessage({
2938
+ role: "user",
2939
+ text,
2940
+ ...typeof event.timestamp === "string" ? { occurredAt: event.timestamp } : {}
2941
+ });
2942
+ }
2943
+ }
2944
+ if (event.type === "event_msg" && payload?.type === "agent_message") {
2945
+ const text = stringFromCodexContent(payload.message);
2946
+ if (text) {
2947
+ pushMessage({
2948
+ role: "assistant",
2949
+ text,
2950
+ ...typeof event.timestamp === "string" ? { occurredAt: event.timestamp } : {}
2951
+ });
2952
+ }
2953
+ }
2954
+ if (event.type === "response_item" && payload?.type === "message" && (payload.role === "user" || payload.role === "assistant")) {
2955
+ const text = stringFromCodexContent(payload.content) ?? stringFromCodexContent(payload.message);
2956
+ if (text) {
2957
+ pushMessage({
2958
+ role: payload.role,
2959
+ text,
2960
+ ...typeof event.timestamp === "string" ? { occurredAt: event.timestamp } : {}
2961
+ });
2962
+ }
2963
+ }
2964
+ }
2965
+ const turns = [];
2966
+ let pendingUser;
2967
+ let pendingAssistantParts = [];
2968
+ const flushPendingTurn = () => {
2969
+ if (!pendingUser) {
2970
+ return;
2971
+ }
2972
+ const assistant = pendingAssistantParts.join(`
2973
+
2974
+ `).trim();
2975
+ turns.push({
2976
+ user: truncateText(pendingUser.text, 6000),
2977
+ ...assistant ? { assistant: truncateText(assistant, 6000) } : {},
2978
+ ...pendingUser.occurredAt ? { occurredAt: pendingUser.occurredAt } : {}
2979
+ });
2980
+ pendingUser = undefined;
2981
+ pendingAssistantParts = [];
2982
+ };
2983
+ for (const message of messages) {
2984
+ if (message.role === "user") {
2985
+ flushPendingTurn();
2986
+ pendingUser = {
2987
+ text: message.text,
2988
+ ...message.occurredAt ? { occurredAt: message.occurredAt } : {}
2989
+ };
2990
+ pendingAssistantParts = [];
2991
+ continue;
2992
+ }
2993
+ if (pendingUser) {
2994
+ if (pendingAssistantParts[pendingAssistantParts.length - 1] !== message.text) {
2995
+ pendingAssistantParts.push(message.text);
2996
+ }
2997
+ } else {
2998
+ turns.push({
2999
+ assistant: truncateText(message.text, 6000),
3000
+ ...message.occurredAt ? { occurredAt: message.occurredAt } : {}
3001
+ });
3002
+ }
3003
+ }
3004
+ flushPendingTurn();
3005
+ return turns.length ? turns.slice(-3) : undefined;
3006
+ }
3007
+ function readRecentTurnsFromCodexSession(agentSessionId, options) {
3008
+ const sessionsDirectory = codexSessionsDirectory(options);
3009
+ if (!sessionsDirectory) {
3010
+ return;
3011
+ }
3012
+ const sessionFile = findCodexSessionFile(sessionsDirectory, agentSessionId);
3013
+ return sessionFile ? readRecentTurnsFromCodexSessionFile(sessionFile) : undefined;
3014
+ }
2622
3015
  function normalizeAgentEvent(input, parsed, options) {
2623
3016
  const source = parseAgentEventSource(parsed.options.source);
2624
3017
  const event = parsed.options.event ?? "stop";
2625
3018
  const base = normalizeAgentEventBase(input, source, event, options);
2626
3019
  switch (source) {
2627
3020
  case "codex":
2628
- return normalizeCodexAgentEvent(input, base);
3021
+ return normalizeCodexAgentEvent(input, base, options);
2629
3022
  case "claude-code":
2630
3023
  case "opencode":
2631
3024
  case "openrouter":
@@ -2636,6 +3029,7 @@ function normalizeAgentEvent(input, parsed, options) {
2636
3029
  function normalizeAgentEventBase(input, source, event, options) {
2637
3030
  const threadId = firstString(input, [["thread_id"], ["threadId"], ["conversation_id"], ["conversationId"]]);
2638
3031
  const turnId = firstString(input, [["turn_id"], ["turnId"], ["id"]]);
3032
+ const recentTurns = readRecentAgentTurns(input);
2639
3033
  const lastAssistantMessage = firstString(input, [
2640
3034
  ["last_assistant_message"],
2641
3035
  ["lastAssistantMessage"],
@@ -2652,10 +3046,11 @@ function normalizeAgentEventBase(input, source, event, options) {
2652
3046
  ...threadId ? { threadId } : {},
2653
3047
  ...turnId ? { turnId } : {},
2654
3048
  ...lastAssistantMessage ? { lastAssistantMessage: truncateText(lastAssistantMessage, 6000) } : {},
3049
+ ...recentTurns ? { recentTurns } : {},
2655
3050
  payloadKeys: Object.keys(input).sort()
2656
3051
  };
2657
3052
  }
2658
- function normalizeCodexAgentEvent(input, base) {
3053
+ function normalizeCodexAgentEvent(input, base, options) {
2659
3054
  const agentSessionId = firstString(input, [["session_id"], ["sessionId"], ["session", "id"]]);
2660
3055
  if (!agentSessionId) {
2661
3056
  return {
@@ -2663,10 +3058,13 @@ function normalizeCodexAgentEvent(input, base) {
2663
3058
  diagnosticReason: "missing_agent_session_id"
2664
3059
  };
2665
3060
  }
3061
+ const transcriptPath = firstString(input, [["transcript_path"], ["transcriptPath"]]);
3062
+ const recentTurns = base.recentTurns?.length ? base.recentTurns : transcriptPath ? readRecentTurnsFromCodexSessionFile(transcriptPath) : readRecentTurnsFromCodexSession(agentSessionId, options);
2666
3063
  return {
2667
3064
  ...base,
2668
3065
  agentSessionId,
2669
- agentSessionKey: agentSessionKey(base.source, agentSessionId)
3066
+ agentSessionKey: agentSessionKey(base.source, agentSessionId),
3067
+ ...recentTurns?.length ? { recentTurns } : {}
2670
3068
  };
2671
3069
  }
2672
3070
  function normalizeGenericAgentEvent(input, base) {
@@ -2692,14 +3090,24 @@ function normalizeGenericAgentEvent(input, base) {
2692
3090
  function agentSessionKey(source, agentSessionId) {
2693
3091
  return `${source}:${stableHash(agentSessionId)}`;
2694
3092
  }
3093
+ function checkpointWorkLogMetadata(settings, mode, tokenAccounting) {
3094
+ return {
3095
+ checkpoint: {
3096
+ ...mode ? { mode } : {},
3097
+ provider: settings.provider,
3098
+ ...settings.model ? { model: settings.model } : {},
3099
+ ...tokenAccounting ? { tokenAccounting } : {}
3100
+ }
3101
+ };
3102
+ }
2695
3103
  function defaultAgentLoopState() {
2696
3104
  return {
2697
3105
  version: 2,
2698
3106
  sessions: {}
2699
3107
  };
2700
3108
  }
2701
- function randomCheckpointThreshold() {
2702
- return defaultCheckpointThresholdMin + Math.floor(Math.random() * (defaultCheckpointThresholdMax - defaultCheckpointThresholdMin + 1));
3109
+ function defaultCheckpointThresholdValue() {
3110
+ return defaultCheckpointThreshold;
2703
3111
  }
2704
3112
  function agentLoopDirectory(parsed, options) {
2705
3113
  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 +3115,81 @@ function agentLoopDirectory(parsed, options) {
2707
3115
  function agentLoopStatePath(parsed, options) {
2708
3116
  return join(agentLoopDirectory(parsed, options), "state.json");
2709
3117
  }
3118
+ function agentLoopSettingsPath(parsed, options) {
3119
+ if (parsed.options["state-dir"]) {
3120
+ return join(agentLoopDirectory(parsed, options), "settings.json");
3121
+ }
3122
+ return join(getConfigHome(options.env ?? process.env), "agent-run", "settings.json");
3123
+ }
3124
+ function legacyAgentLoopSettingsPath(parsed, options) {
3125
+ return join(agentLoopDirectory(parsed, options), "settings.json");
3126
+ }
2710
3127
  function agentLoopLockPath(parsed, options, agentSessionKeyValue) {
2711
- return join(agentLoopDirectory(parsed, options), agentSessionKeyValue ? `closeout-${stableHash(agentSessionKeyValue)}.lock` : "closeout.lock");
3128
+ return join(agentLoopDirectory(parsed, options), agentSessionKeyValue ? `checkpoint-${stableHash(agentSessionKeyValue)}.lock` : "checkpoint.lock");
3129
+ }
3130
+ function normalizeCheckpointModel(value) {
3131
+ const trimmed = value?.trim();
3132
+ if (!trimmed) {
3133
+ return;
3134
+ }
3135
+ if (trimmed.length > 120 || /\s/.test(trimmed)) {
3136
+ throw new CliError("CLI_USAGE", "Checkpoint model must be a non-empty model id without whitespace.");
3137
+ }
3138
+ return trimmed;
3139
+ }
3140
+ function defaultAgentLoopSettings() {
3141
+ return {
3142
+ version: 1,
3143
+ checkpoint: {
3144
+ provider: "codex"
3145
+ }
3146
+ };
3147
+ }
3148
+ async function readAgentLoopSettings(parsed, options) {
3149
+ const filePath = agentLoopSettingsPath(parsed, options);
3150
+ let file = Bun.file(filePath);
3151
+ if (!await file.exists()) {
3152
+ const legacyFilePath = legacyAgentLoopSettingsPath(parsed, options);
3153
+ if (legacyFilePath === filePath) {
3154
+ return defaultAgentLoopSettings();
3155
+ }
3156
+ file = Bun.file(legacyFilePath);
3157
+ if (!await file.exists()) {
3158
+ return defaultAgentLoopSettings();
3159
+ }
3160
+ }
3161
+ const parsedSettings = JSON.parse(await file.text());
3162
+ if (!parsedSettings || typeof parsedSettings !== "object" || Array.isArray(parsedSettings)) {
3163
+ return defaultAgentLoopSettings();
3164
+ }
3165
+ const record = parsedSettings;
3166
+ const checkpoint = record.checkpoint;
3167
+ if (!checkpoint || typeof checkpoint !== "object" || Array.isArray(checkpoint)) {
3168
+ return defaultAgentLoopSettings();
3169
+ }
3170
+ const checkpointRecord = checkpoint;
3171
+ const provider = checkpointRecord.provider === "codex" ? "codex" : "codex";
3172
+ const model = typeof checkpointRecord.model === "string" ? normalizeCheckpointModel(checkpointRecord.model) : undefined;
3173
+ return {
3174
+ version: 1,
3175
+ checkpoint: {
3176
+ provider,
3177
+ ...model ? { model } : {},
3178
+ ...typeof checkpointRecord.updatedAt === "string" ? { updatedAt: checkpointRecord.updatedAt } : {}
3179
+ }
3180
+ };
3181
+ }
3182
+ async function resolveCheckpointSettings(parsed, options) {
3183
+ const saved = await readAgentLoopSettings(parsed, options);
3184
+ const provider = parsed.options["checkpoint-provider"] ?? saved.checkpoint.provider;
3185
+ if (provider !== "codex") {
3186
+ throw new CliError("CLI_USAGE", "Only the Codex checkpoint provider is supported in this version.");
3187
+ }
3188
+ const model = normalizeCheckpointModel(parsed.options["checkpoint-model"] ?? parsed.options.model ?? saved.checkpoint.model);
3189
+ return {
3190
+ provider: "codex",
3191
+ ...model ? { model } : {}
3192
+ };
2712
3193
  }
2713
3194
  async function readAgentLoopState(parsed, options) {
2714
3195
  const filePath = agentLoopStatePath(parsed, options);
@@ -2754,24 +3235,87 @@ function readAgentLoopSessions(rawSessions) {
2754
3235
  const record = rawSession;
2755
3236
  const source = parseAgentEventSource(typeof record.source === "string" ? record.source : undefined);
2756
3237
  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();
3238
+ const threshold = typeof record.threshold === "number" && Number.isInteger(record.threshold) && record.threshold > 0 ? record.threshold : defaultCheckpointThresholdValue();
3239
+ const recentTurns = Array.isArray(record.recentTurns) ? readRecentAgentTurns({
3240
+ recentTurns: record.recentTurns
3241
+ }) : undefined;
2758
3242
  sessions[key] = {
2759
3243
  source,
2760
3244
  stopCount,
2761
3245
  threshold,
3246
+ ...readAgentSessionCadenceContext(record),
2762
3247
  ...typeof record.firstObservedAt === "string" ? { firstObservedAt: record.firstObservedAt } : {},
2763
3248
  ...typeof record.lastObservedAt === "string" ? { lastObservedAt: record.lastObservedAt } : {},
3249
+ ...typeof record.lastObservedTurnId === "string" ? { lastObservedTurnId: record.lastObservedTurnId } : {},
2764
3250
  ...typeof record.lastAction === "string" ? { lastAction: record.lastAction } : {},
2765
3251
  ...typeof record.lastReason === "string" ? { lastReason: record.lastReason } : {},
2766
3252
  ...typeof record.lastEventFile === "string" ? { lastEventFile: record.lastEventFile } : {},
2767
3253
  ...typeof record.lastAssistantMessage === "string" ? { lastAssistantMessage: record.lastAssistantMessage } : {},
3254
+ ...recentTurns ? { recentTurns } : {},
2768
3255
  ...typeof record.lastCheckpointAt === "string" ? { lastCheckpointAt: record.lastCheckpointAt } : {},
2769
3256
  ...typeof record.lastCheckpointFingerprint === "string" ? { lastCheckpointFingerprint: record.lastCheckpointFingerprint } : {},
2770
- ...typeof record.previousCheckpointSummary === "string" ? { previousCheckpointSummary: record.previousCheckpointSummary } : {}
3257
+ ...readAgentRunFingerprints(record.lastCheckpointFingerprints),
3258
+ ...typeof record.lastCheckpointMode === "string" && agentRunMemoryModes.includes(record.lastCheckpointMode) ? { lastCheckpointMode: record.lastCheckpointMode } : {},
3259
+ ...readAgentRunCoverage(record.lastCheckpointCoverage),
3260
+ ...typeof record.previousCheckpointSummary === "string" ? { previousCheckpointSummary: record.previousCheckpointSummary } : {},
3261
+ ...typeof record.lastCheckpointAuditFile === "string" ? { lastCheckpointAuditFile: record.lastCheckpointAuditFile } : {}
2771
3262
  };
2772
3263
  }
2773
3264
  return sessions;
2774
3265
  }
3266
+ function readAgentRunFingerprints(value) {
3267
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
3268
+ return {};
3269
+ }
3270
+ const record = value;
3271
+ const event = typeof record.event === "string" ? record.event : undefined;
3272
+ const git = typeof record.git === "string" ? record.git : undefined;
3273
+ const cadenceContext = typeof record.cadenceContext === "string" ? record.cadenceContext : undefined;
3274
+ if (!event || !git || !cadenceContext) {
3275
+ return {};
3276
+ }
3277
+ return {
3278
+ lastCheckpointFingerprints: {
3279
+ event,
3280
+ git,
3281
+ cadenceContext,
3282
+ ...typeof record.verification === "string" ? { verification: record.verification } : {},
3283
+ ...typeof record.coverageDebt === "string" ? { coverageDebt: record.coverageDebt } : {}
3284
+ }
3285
+ };
3286
+ }
3287
+ function readAgentRunCoverage(value) {
3288
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
3289
+ return {};
3290
+ }
3291
+ const record = value;
3292
+ const coverage = {
3293
+ outcome: record.outcome === true,
3294
+ implementation: record.implementation === true,
3295
+ decisions: record.decisions === true,
3296
+ corrections: record.corrections === true,
3297
+ verification: record.verification === true,
3298
+ blockers: record.blockers === true,
3299
+ scope: record.scope === true,
3300
+ handoff: record.handoff === true,
3301
+ hasDebt: record.hasDebt === true
3302
+ };
3303
+ return { lastCheckpointCoverage: coverage };
3304
+ }
3305
+ function readAgentSessionCadenceContext(record) {
3306
+ const value = record.cadenceContext;
3307
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
3308
+ return {};
3309
+ }
3310
+ const contextRecord = value;
3311
+ const cadenceContext = {
3312
+ ...typeof contextRecord.ticketId === "string" ? { ticketId: contextRecord.ticketId } : {},
3313
+ ...typeof contextRecord.sessionId === "string" ? { sessionId: contextRecord.sessionId } : {},
3314
+ ...typeof contextRecord.changesetId === "string" ? { changesetId: contextRecord.changesetId } : {},
3315
+ ...typeof contextRecord.capturedAt === "string" ? { capturedAt: contextRecord.capturedAt } : {}
3316
+ };
3317
+ return cadenceContext.ticketId || cadenceContext.sessionId || cadenceContext.changesetId ? { cadenceContext } : {};
3318
+ }
2775
3319
  function readAgentLoopDiagnostics(record) {
2776
3320
  return {
2777
3321
  ...typeof record.missingSessionIdCount === "number" && Number.isInteger(record.missingSessionIdCount) && record.missingSessionIdCount > 0 ? { missingSessionIdCount: record.missingSessionIdCount } : {},
@@ -2806,6 +3350,15 @@ async function writeAgentEventFile(parsed, options, event) {
2806
3350
  `);
2807
3351
  return filePath;
2808
3352
  }
3353
+ async function writeAgentCheckpointAuditFile(parsed, options, audit) {
3354
+ const checkpointsDirectory = join(agentLoopDirectory(parsed, options), "checkpoints");
3355
+ const fileName = `${new Date().toISOString().replace(/[:.]/g, "-")}-${randomUUID()}.json`;
3356
+ const filePath = join(checkpointsDirectory, fileName);
3357
+ await mkdir(checkpointsDirectory, { recursive: true });
3358
+ await writeFile(filePath, `${JSON.stringify(audit, null, 2)}
3359
+ `);
3360
+ return filePath;
3361
+ }
2809
3362
  async function removeStaleAgentLoopLock(lockPath) {
2810
3363
  try {
2811
3364
  const lockStats = await stat(lockPath);
@@ -2891,7 +3444,7 @@ function currentCliWorkerInvocation() {
2891
3444
  argsPrefix: []
2892
3445
  };
2893
3446
  }
2894
- async function spawnAgentRunCloseout(args, options) {
3447
+ async function spawnAgentRunWorker(args, options) {
2895
3448
  const cwd = options.cwd ?? process.cwd();
2896
3449
  const invocation = currentCliWorkerInvocation();
2897
3450
  const env = {
@@ -2910,47 +3463,91 @@ async function spawnAgentRunCloseout(args, options) {
2910
3463
  });
2911
3464
  child.unref();
2912
3465
  }
3466
+ async function spawnAgentRunCheckpoint(args, options) {
3467
+ await spawnAgentRunWorker(args, options);
3468
+ }
3469
+ function currentRecordTime(value, keys) {
3470
+ for (const key of keys) {
3471
+ const candidate = value[key];
3472
+ if (typeof candidate !== "string") {
3473
+ continue;
3474
+ }
3475
+ const time = new Date(candidate).getTime();
3476
+ if (!Number.isNaN(time)) {
3477
+ return time;
3478
+ }
3479
+ }
3480
+ return 0;
3481
+ }
2913
3482
  function activeSessionFromCurrent(current) {
2914
3483
  const record = current && typeof current === "object" ? current : null;
2915
3484
  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
3485
  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) => {
3486
+ const activeLease = activeLeases.filter((lease) => {
2928
3487
  if (!lease || typeof lease !== "object") {
2929
3488
  return false;
2930
3489
  }
2931
- const leaseRecord2 = lease;
2932
- return typeof leaseRecord2.ticketId === "string";
2933
- });
2934
- if (!activeLease || typeof activeLease !== "object") {
2935
- return null;
3490
+ const leaseRecord = lease;
3491
+ return typeof leaseRecord.ticketId === "string";
3492
+ }).map((lease) => lease).sort((left, right) => currentRecordTime(right, ["lastSeenAt", "claimedAt", "expiresAt"]) - currentRecordTime(left, ["lastSeenAt", "claimedAt", "expiresAt"]))[0];
3493
+ if (activeLease && typeof activeLease === "object") {
3494
+ const leaseRecord = activeLease;
3495
+ const sessionId = typeof leaseRecord.sessionId === "string" ? leaseRecord.sessionId : undefined;
3496
+ const matchingSession = sessions.find((session) => {
3497
+ if (!session || typeof session !== "object") {
3498
+ return false;
3499
+ }
3500
+ return sessionId && session.id === sessionId;
3501
+ });
3502
+ const sessionRecord = matchingSession && typeof matchingSession === "object" ? matchingSession : {};
3503
+ return {
3504
+ ...sessionRecord,
3505
+ ...sessionId ? { id: sessionId } : {},
3506
+ ticketId: leaseRecord.ticketId,
3507
+ ...typeof leaseRecord.changesetId === "string" ? { changesetId: leaseRecord.changesetId } : typeof sessionRecord.changesetId === "string" ? { changesetId: sessionRecord.changesetId } : {},
3508
+ status: "active"
3509
+ };
2936
3510
  }
2937
- const leaseRecord = activeLease;
2938
- const sessionId = typeof leaseRecord.sessionId === "string" ? leaseRecord.sessionId : undefined;
2939
- const matchingSession = sessions.find((session) => {
3511
+ const activeSessions = sessions.filter((session) => {
2940
3512
  if (!session || typeof session !== "object") {
2941
3513
  return false;
2942
3514
  }
2943
- return sessionId && session.id === sessionId;
3515
+ const sessionRecord = session;
3516
+ return sessionRecord.status === "active" && typeof sessionRecord.ticketId === "string";
2944
3517
  });
2945
- const sessionRecord = matchingSession && typeof matchingSession === "object" ? matchingSession : {};
3518
+ const activeSession = activeSessions.map((session) => session).sort((left, right) => currentRecordTime(right, ["lastActivityAt", "startedAt"]) - currentRecordTime(left, ["lastActivityAt", "startedAt"]))[0];
3519
+ if (activeSession && typeof activeSession === "object") {
3520
+ return activeSession;
3521
+ }
3522
+ return null;
3523
+ }
3524
+ function cadenceContextFromCurrent(current) {
3525
+ const activeSession = activeSessionFromCurrent(current);
3526
+ if (!activeSession) {
3527
+ return;
3528
+ }
3529
+ const ticketId = typeof activeSession.ticketId === "string" ? activeSession.ticketId : undefined;
3530
+ const sessionId = typeof activeSession.id === "string" ? activeSession.id : undefined;
3531
+ const changesetId = typeof activeSession.changesetId === "string" ? activeSession.changesetId : undefined;
3532
+ if (!ticketId && !sessionId && !changesetId) {
3533
+ return;
3534
+ }
2946
3535
  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"
3536
+ ...ticketId ? { ticketId } : {},
3537
+ ...sessionId ? { sessionId } : {},
3538
+ ...changesetId ? { changesetId } : {},
3539
+ capturedAt: new Date().toISOString()
2952
3540
  };
2953
3541
  }
3542
+ async function readCurrentCadenceContext(client, projectId) {
3543
+ const current = await client.sessions.current({
3544
+ projectId,
3545
+ filters: {
3546
+ limit: 100
3547
+ }
3548
+ });
3549
+ return cadenceContextFromCurrent(current);
3550
+ }
2954
3551
  function fingerprintForCheckpoint(event, options) {
2955
3552
  const status = gitOutput(["status", "--short"], options);
2956
3553
  const changedFiles = gitOutput(["diff", "--name-only", "origin/dev..."], options);
@@ -2958,6 +3555,7 @@ function fingerprintForCheckpoint(event, options) {
2958
3555
  source: event.source,
2959
3556
  event: event.event,
2960
3557
  lastAssistantMessage: event.lastAssistantMessage ?? "",
3558
+ recentTurns: event.recentTurns ?? [],
2961
3559
  status,
2962
3560
  changedFiles
2963
3561
  }));
@@ -2972,60 +3570,548 @@ function shouldSkipForCooldown(state, cooldownSeconds) {
2972
3570
  function synthesizeAgentEventFromSession(agentSessionKeyValue, session, options) {
2973
3571
  return {
2974
3572
  source: session.source,
2975
- event: "closeout",
3573
+ event: "checkpoint",
2976
3574
  workspacePath: options.cwd ?? process.cwd(),
2977
3575
  occurredAt: new Date().toISOString(),
2978
3576
  agentSessionKey: agentSessionKeyValue,
2979
3577
  ...session.lastAssistantMessage ? { lastAssistantMessage: session.lastAssistantMessage } : {},
3578
+ ...session.recentTurns?.length ? { recentTurns: session.recentTurns } : {},
2980
3579
  payloadKeys: []
2981
3580
  };
2982
3581
  }
2983
- function parseCheckpointJson(raw) {
2984
- const trimmed = raw.trim();
2985
- try {
2986
- const parsed = JSON.parse(trimmed);
2987
- 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
- };
2993
- }
2994
- } catch {
2995
- const start = trimmed.indexOf("{");
2996
- const end = trimmed.lastIndexOf("}");
2997
- if (start >= 0 && end > start) {
2998
- try {
2999
- const parsed = JSON.parse(trimmed.slice(start, end + 1));
3000
- 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
- };
3006
- }
3007
- } catch {}
3582
+ var checkpointRouteActions = ["current", "noop", "intake_create", "intake_attach", "switch_existing", "needs_human"];
3583
+ var checkpointConfidenceLevels = ["low", "medium", "high"];
3584
+ var checkpointSessionActions = ["keep", "handoff", "end", "complete_ticket"];
3585
+ var highAutonomyRouteActions = new Set(["intake_create", "intake_attach", "switch_existing"]);
3586
+ var checkpointServerTextLimit = 1200;
3587
+ function checkpointEntrySummary(body) {
3588
+ return truncateText(body.replace(/\s+/g, " ").trim(), 500);
3589
+ }
3590
+ function checkpointString(record, keys) {
3591
+ for (const key of keys) {
3592
+ const value = record[key];
3593
+ if (typeof value === "string" && value.trim()) {
3594
+ return value.trim();
3008
3595
  }
3009
3596
  }
3010
- return {
3011
- body: trimmed,
3012
- summary: undefined
3013
- };
3597
+ return;
3014
3598
  }
3015
- function buildCheckpointPrompt(input) {
3016
- 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.",
3021
- "",
3599
+ function parseCheckpointParent(value) {
3600
+ if (typeof value !== "string" || !value.trim()) {
3601
+ return {};
3602
+ }
3603
+ try {
3604
+ return parseWorkLogParent(value.trim());
3605
+ } catch {
3606
+ return {};
3607
+ }
3608
+ }
3609
+ function parseCheckpointEntry(value, fallbackKind) {
3610
+ if (typeof value === "string" && value.trim()) {
3611
+ const body2 = value.trim();
3612
+ return {
3613
+ kind: fallbackKind,
3614
+ body: body2,
3615
+ summary: checkpointEntrySummary(body2)
3616
+ };
3617
+ }
3618
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
3619
+ return;
3620
+ }
3621
+ const record = value;
3622
+ const body = checkpointString(record, ["body", "text", "content"]);
3623
+ if (!body) {
3624
+ return;
3625
+ }
3626
+ let kind = fallbackKind;
3627
+ const rawKind = checkpointString(record, ["kind", "entryKind", "type"]);
3628
+ if (rawKind && workLogEntryKinds.includes(rawKind)) {
3629
+ kind = rawKind;
3630
+ }
3631
+ const parent = parseCheckpointParent(record.under ?? record.parent ?? record.parentSelector ?? record.parentEntryId);
3632
+ const summary = checkpointString(record, ["summary", "title"]);
3633
+ return {
3634
+ kind,
3635
+ body,
3636
+ summary: summary ?? checkpointEntrySummary(body),
3637
+ ...parent
3638
+ };
3639
+ }
3640
+ function parseCheckpointSummaryUpdate(value) {
3641
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
3642
+ return;
3643
+ }
3644
+ const record = value;
3645
+ const update = record.update === true;
3646
+ const summary = checkpointString(record, ["value", "summary", "currentSummary"]);
3647
+ const reason = checkpointString(record, ["reason"]);
3648
+ return {
3649
+ update,
3650
+ ...summary ? { value: summary } : {},
3651
+ ...reason ? { reason } : {}
3652
+ };
3653
+ }
3654
+ function parseCheckpointRecord(record, rawText, fallbackKind) {
3655
+ const rawEntries = Array.isArray(record.entries) ? record.entries : [];
3656
+ const entries = rawEntries.map((entry) => parseCheckpointEntry(entry, fallbackKind)).filter((entry) => Boolean(entry));
3657
+ const legacyBody = checkpointString(record, ["body"]);
3658
+ const summary = checkpointString(record, ["summary"]);
3659
+ const parsedEntries = entries.length > 0 ? entries : legacyBody ? [parseCheckpointEntry({ kind: fallbackKind, body: legacyBody, summary }, fallbackKind)].filter((entry) => Boolean(entry)) : [];
3660
+ const currentSummary = parseCheckpointSummaryUpdate(record.currentSummary);
3661
+ return {
3662
+ summary: summary ?? parsedEntries[0]?.summary ?? checkpointEntrySummary(rawText),
3663
+ entries: parsedEntries,
3664
+ ...currentSummary ? { currentSummary } : {}
3665
+ };
3666
+ }
3667
+ function parseCheckpointJson(raw, fallbackKind) {
3668
+ const trimmed = raw.trim();
3669
+ try {
3670
+ const parsed = JSON.parse(trimmed);
3671
+ if (parsed && typeof parsed === "object") {
3672
+ return parseCheckpointRecord(parsed, trimmed, fallbackKind);
3673
+ }
3674
+ } catch {
3675
+ const start = trimmed.indexOf("{");
3676
+ const end = trimmed.lastIndexOf("}");
3677
+ if (start >= 0 && end > start) {
3678
+ try {
3679
+ const parsed = JSON.parse(trimmed.slice(start, end + 1));
3680
+ if (parsed && typeof parsed === "object") {
3681
+ return parseCheckpointRecord(parsed, trimmed, fallbackKind);
3682
+ }
3683
+ } catch {}
3684
+ }
3685
+ }
3686
+ return {
3687
+ summary: checkpointEntrySummary(trimmed),
3688
+ entries: trimmed ? [
3689
+ {
3690
+ kind: fallbackKind,
3691
+ body: trimmed,
3692
+ summary: checkpointEntrySummary(trimmed)
3693
+ }
3694
+ ] : []
3695
+ };
3696
+ }
3697
+ function checkpointValidationError(message, details) {
3698
+ return new CliError("AGENT_RUN_CHECKPOINT_INVALID_PLAN", message, details);
3699
+ }
3700
+ function checkpointPlanString(record, key, limit = checkpointServerTextLimit) {
3701
+ const value = record[key];
3702
+ if (typeof value !== "string") {
3703
+ return;
3704
+ }
3705
+ const trimmed = value.trim();
3706
+ if (!trimmed) {
3707
+ return;
3708
+ }
3709
+ if (trimmed.length > limit) {
3710
+ throw checkpointValidationError(`Checkpoint plan field "${key}" is too long.`, { key, limit });
3711
+ }
3712
+ return trimmed;
3713
+ }
3714
+ function parseCheckpointRoute(value) {
3715
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
3716
+ throw checkpointValidationError("Checkpoint plan requires route.");
3717
+ }
3718
+ const record = value;
3719
+ const actionValue = checkpointPlanString(record, "action", 80);
3720
+ const confidenceValue = checkpointPlanString(record, "confidence", 20) ?? "medium";
3721
+ if (!actionValue || !checkpointRouteActions.includes(actionValue)) {
3722
+ throw checkpointValidationError("Checkpoint plan route.action is invalid.", { action: actionValue });
3723
+ }
3724
+ if (!checkpointConfidenceLevels.includes(confidenceValue)) {
3725
+ throw checkpointValidationError("Checkpoint plan route.confidence is invalid.", { confidence: confidenceValue });
3726
+ }
3727
+ const reason = checkpointPlanString(record, "reason");
3728
+ const request = checkpointPlanString(record, "request");
3729
+ const targetTicketId = checkpointPlanString(record, "targetTicketId", 100);
3730
+ return {
3731
+ action: actionValue,
3732
+ confidence: confidenceValue,
3733
+ ...reason ? { reason } : {},
3734
+ ...request ? { request } : {},
3735
+ ...targetTicketId ? { targetTicketId } : {}
3736
+ };
3737
+ }
3738
+ function parseCheckpointPlanEntry(value) {
3739
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
3740
+ throw checkpointValidationError("Checkpoint plan entries must be objects.");
3741
+ }
3742
+ const record = value;
3743
+ const rawKind = checkpointPlanString(record, "kind", 40) ?? checkpointPlanString(record, "entryKind", 40) ?? checkpointPlanString(record, "type", 40);
3744
+ const body = checkpointPlanString(record, "body") ?? checkpointPlanString(record, "text") ?? checkpointPlanString(record, "content");
3745
+ if (!rawKind || !workLogEntryKinds.includes(rawKind)) {
3746
+ throw checkpointValidationError("Checkpoint plan entry kind is invalid.", { kind: rawKind });
3747
+ }
3748
+ if (!body) {
3749
+ throw checkpointValidationError("Checkpoint plan entry body is required.");
3750
+ }
3751
+ const parent = parseCheckpointParent(record.under ?? record.parent ?? record.parentSelector ?? record.parentEntryId);
3752
+ const summary = checkpointPlanString(record, "summary", 300) ?? checkpointPlanString(record, "title", 300);
3753
+ return {
3754
+ kind: rawKind,
3755
+ body,
3756
+ summary: summary ?? checkpointEntrySummary(body),
3757
+ ...parent
3758
+ };
3759
+ }
3760
+ function parseCheckpointPlanSummaryUpdate(value) {
3761
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
3762
+ return;
3763
+ }
3764
+ const record = value;
3765
+ const update = record.update === true;
3766
+ const summary = checkpointPlanString(record, "value") ?? checkpointPlanString(record, "summary") ?? checkpointPlanString(record, "currentSummary");
3767
+ const reason = checkpointPlanString(record, "reason");
3768
+ return {
3769
+ update,
3770
+ ...summary ? { value: summary } : {},
3771
+ ...reason ? { reason } : {}
3772
+ };
3773
+ }
3774
+ function parseCheckpointPlanSession(value) {
3775
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
3776
+ return;
3777
+ }
3778
+ const record = value;
3779
+ const action = checkpointPlanString(record, "action", 40) ?? "keep";
3780
+ if (!checkpointSessionActions.includes(action)) {
3781
+ throw checkpointValidationError("Checkpoint plan session.action is invalid.", { action });
3782
+ }
3783
+ const summary = checkpointPlanString(record, "summary");
3784
+ const reason = checkpointPlanString(record, "reason");
3785
+ return {
3786
+ action,
3787
+ ...summary ? { summary } : {},
3788
+ ...reason ? { reason } : {}
3789
+ };
3790
+ }
3791
+ function parseCheckpointPlanFiles(value) {
3792
+ if (!Array.isArray(value)) {
3793
+ return [];
3794
+ }
3795
+ return value.map((item) => {
3796
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
3797
+ throw checkpointValidationError("Checkpoint plan files must be objects.");
3798
+ }
3799
+ const record = item;
3800
+ const path = checkpointPlanString(record, "path", 500);
3801
+ if (!path || isAbsolute(path) || path.split("/").includes("..")) {
3802
+ throw checkpointValidationError("Checkpoint plan file path must be a safe relative path.", { path });
3803
+ }
3804
+ return {
3805
+ path,
3806
+ kind: parseSessionFileChangeKind(checkpointPlanString(record, "kind", 40) ?? checkpointPlanString(record, "changeKind", 40))
3807
+ };
3808
+ });
3809
+ }
3810
+ function forceNeedsHuman(plan, reason) {
3811
+ return {
3812
+ ...plan,
3813
+ route: {
3814
+ ...plan.route,
3815
+ action: "needs_human",
3816
+ reason
3817
+ },
3818
+ validationWarnings: [...plan.validationWarnings, reason]
3819
+ };
3820
+ }
3821
+ function normalizeCheckpointPlan(plan) {
3822
+ if (plan.route.action === "noop") {
3823
+ return {
3824
+ ...plan,
3825
+ entries: []
3826
+ };
3827
+ }
3828
+ return plan;
3829
+ }
3830
+ function checkpointPlanWithRoute(plan, route, warning) {
3831
+ return {
3832
+ ...plan,
3833
+ route,
3834
+ ...route.action === "noop" ? { entries: [] } : {},
3835
+ validationWarnings: warning ? [...plan.validationWarnings, warning] : plan.validationWarnings
3836
+ };
3837
+ }
3838
+ function safeAutomaticRoutePlan(plan, hasCurrentContext, reason) {
3839
+ return checkpointPlanWithRoute(plan, {
3840
+ action: hasCurrentContext ? "current" : "noop",
3841
+ confidence: plan.route.confidence,
3842
+ reason
3843
+ }, reason);
3844
+ }
3845
+ function routePlanAllowsLifecycle(plan) {
3846
+ return checkpointRouteRequiresIntake(plan.route.action) && plan.route.confidence === "high";
3847
+ }
3848
+ function parseCheckpointPlanRecord(record, rawText, fallbackKind) {
3849
+ if (!("route" in record)) {
3850
+ const legacy = parseCheckpointRecord(record, rawText, fallbackKind);
3851
+ const reason = legacy.currentSummary?.reason ?? legacy.summary;
3852
+ const route2 = {
3853
+ action: legacy.entries.length ? "current" : "noop",
3854
+ confidence: "high",
3855
+ ...reason ? { reason } : {}
3856
+ };
3857
+ return normalizeCheckpointPlan({
3858
+ ...legacy.summary ? { summary: legacy.summary } : {},
3859
+ route: route2,
3860
+ entries: legacy.entries,
3861
+ ...legacy.currentSummary ? { summaryUpdate: legacy.currentSummary } : {},
3862
+ files: [],
3863
+ legacy: true,
3864
+ validationWarnings: []
3865
+ });
3866
+ }
3867
+ const route = parseCheckpointRoute(record.route);
3868
+ const entries = Array.isArray(record.entries) ? record.entries.map(parseCheckpointPlanEntry) : [];
3869
+ const summary = checkpointPlanString(record, "summary");
3870
+ const summaryUpdate = parseCheckpointPlanSummaryUpdate(record.summaryUpdate ?? record.currentSummary);
3871
+ const session = parseCheckpointPlanSession(record.session);
3872
+ return normalizeCheckpointPlan({
3873
+ ...summary ? { summary } : {},
3874
+ route,
3875
+ entries,
3876
+ ...summaryUpdate ? { summaryUpdate } : {},
3877
+ ...session ? { session } : {},
3878
+ files: parseCheckpointPlanFiles(record.files),
3879
+ legacy: false,
3880
+ validationWarnings: []
3881
+ });
3882
+ }
3883
+ function parseCheckpointPlanJson(raw, fallbackKind) {
3884
+ const trimmed = raw.trim();
3885
+ try {
3886
+ const parsed = JSON.parse(trimmed);
3887
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
3888
+ return parseCheckpointPlanRecord(parsed, trimmed, fallbackKind);
3889
+ }
3890
+ } catch (error) {
3891
+ if (error instanceof CliError) {
3892
+ throw error;
3893
+ }
3894
+ const start = trimmed.indexOf("{");
3895
+ const end = trimmed.lastIndexOf("}");
3896
+ if (start >= 0 && end > start) {
3897
+ try {
3898
+ const parsed = JSON.parse(trimmed.slice(start, end + 1));
3899
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
3900
+ return parseCheckpointPlanRecord(parsed, trimmed, fallbackKind);
3901
+ }
3902
+ } catch (innerError) {
3903
+ if (innerError instanceof CliError) {
3904
+ throw innerError;
3905
+ }
3906
+ }
3907
+ }
3908
+ }
3909
+ const legacy = parseCheckpointJson(trimmed, fallbackKind);
3910
+ const reason = legacy.currentSummary?.reason ?? legacy.summary;
3911
+ return normalizeCheckpointPlan({
3912
+ ...legacy.summary ? { summary: legacy.summary } : {},
3913
+ route: {
3914
+ action: legacy.entries.length ? "current" : "noop",
3915
+ confidence: "high",
3916
+ ...reason ? { reason } : {}
3917
+ },
3918
+ entries: legacy.entries,
3919
+ ...legacy.currentSummary ? { summaryUpdate: legacy.currentSummary } : {},
3920
+ files: [],
3921
+ legacy: true,
3922
+ validationWarnings: []
3923
+ });
3924
+ }
3925
+ function checkpointRouteRequiresIntake(action) {
3926
+ return action === "intake_create" || action === "intake_attach" || action === "switch_existing";
3927
+ }
3928
+ function checkpointPlanCompletionAllowed(event, summary) {
3929
+ const text = [
3930
+ summary,
3931
+ event.lastAssistantMessage ?? "",
3932
+ ...(event.recentTurns ?? []).flatMap((turn) => [turn.user ?? "", turn.assistant ?? ""])
3933
+ ].join(`
3934
+ `).toLowerCase();
3935
+ return /\b(done|complete|completed|finish|finished|close|closed|ship|shipped|push|pushed|pr|pull request|merged|clean branch|verified)\b/.test(text);
3936
+ }
3937
+ function checkpointPlanTitle(plan) {
3938
+ const intent = plan.entries.find((entry) => entry.kind === "intent");
3939
+ return truncateText(intent?.summary ?? plan.summary ?? plan.route.request ?? "Checkpoint routed work", 120);
3940
+ }
3941
+ function checkpointPlanDescription(plan) {
3942
+ return truncateText(plan.route.request ?? plan.summary ?? plan.route.reason ?? checkpointPlanTitle(plan), 1000);
3943
+ }
3944
+ function checkpointHasConflictingIntakeResult(intake, plan) {
3945
+ if (!intake || typeof intake !== "object" || Array.isArray(intake)) {
3946
+ return false;
3947
+ }
3948
+ const record = intake;
3949
+ const classification = typeof record.classification === "string" ? record.classification : undefined;
3950
+ const candidates = Array.isArray(record.candidates) ? record.candidates.filter((candidate) => candidate && typeof candidate === "object") : [];
3951
+ if (plan.route.action === "intake_create" && classification && classification !== "new") {
3952
+ return true;
3953
+ }
3954
+ if ((plan.route.action === "intake_attach" || plan.route.action === "switch_existing") && plan.route.targetTicketId && candidates.length > 0) {
3955
+ return !candidates.some((candidate) => {
3956
+ const candidateRecord = candidate;
3957
+ return candidateRecord.id === plan.route.targetTicketId || candidateRecord.ticketId === plan.route.targetTicketId;
3958
+ });
3959
+ }
3960
+ return false;
3961
+ }
3962
+ function checkpointPlanNeedsHuman(plan, reason) {
3963
+ return forceNeedsHuman(plan, reason);
3964
+ }
3965
+ function checkpointLifecycleOperation(type, success, details = {}) {
3966
+ return {
3967
+ type,
3968
+ success,
3969
+ ...details
3970
+ };
3971
+ }
3972
+ function checkpointRecentTurns(event) {
3973
+ return event.recentTurns?.length ? event.recentTurns.slice(-3) : [];
3974
+ }
3975
+ function formatCheckpointRecentTurns(turns) {
3976
+ return turns.map((turn, index) => [
3977
+ `Turn ${index + 1}${turn.occurredAt ? ` (${turn.occurredAt})` : ""}:`,
3978
+ turn.user ? `User:
3979
+ ${truncateText(turn.user, 3000)}` : "User: unavailable",
3980
+ turn.assistant ? `Assistant:
3981
+ ${truncateText(turn.assistant, 3000)}` : "Assistant: unavailable"
3982
+ ].join(`
3983
+ `)).join(`
3984
+
3985
+ `);
3986
+ }
3987
+ function checkpointCapturedContextTokenCounts(event) {
3988
+ const turns = checkpointRecentTurns(event);
3989
+ if (!turns.length) {
3990
+ const fallbackText = event.lastAssistantMessage ? truncateText(event.lastAssistantMessage, 3000) : "";
3991
+ const fallbackTokens = fallbackText ? estimateTokenCount(fallbackText) : 0;
3992
+ return {
3993
+ mode: fallbackText ? "last_assistant_message" : "unavailable",
3994
+ turnCount: 0,
3995
+ userPromptTokens: 0,
3996
+ assistantResponseTokens: fallbackTokens,
3997
+ totalTokens: fallbackTokens
3998
+ };
3999
+ }
4000
+ const userPromptTokens = turns.reduce((total, turn) => total + (turn.user ? estimateTokenCount(truncateText(turn.user, 3000)) : 0), 0);
4001
+ const assistantResponseTokens = turns.reduce((total, turn) => total + (turn.assistant ? estimateTokenCount(truncateText(turn.assistant, 3000)) : 0), 0);
4002
+ return {
4003
+ mode: "recent_turns",
4004
+ turnCount: turns.length,
4005
+ userPromptTokens,
4006
+ assistantResponseTokens,
4007
+ totalTokens: estimateTokenCount(formatCheckpointRecentTurns(turns))
4008
+ };
4009
+ }
4010
+ function buildCheckpointTokenAccounting(event, prompt, tokenUsage) {
4011
+ const capturedContext = checkpointCapturedContextTokenCounts(event);
4012
+ const explicitCheckpointPromptTokens = estimateTokenCount(prompt);
4013
+ const explicitCadenceOverheadTokens = Math.max(0, explicitCheckpointPromptTokens - capturedContext.totalTokens);
4014
+ const reported = tokenUsageTotal(tokenUsage);
4015
+ const reportedInputTokens = reported?.inputTokens;
4016
+ const reportedCadenceOverheadTokens = typeof reportedInputTokens === "number" ? Math.max(0, reportedInputTokens - capturedContext.totalTokens) : undefined;
4017
+ return {
4018
+ source: "codex_token_count_plus_local_estimates",
4019
+ estimator: "cadence-simple-estimate-v1",
4020
+ capturedContext,
4021
+ explicitCheckpointPromptTokens,
4022
+ explicitCadenceOverheadTokens,
4023
+ explicitCadenceOverheadRatio: roundedRatio(explicitCadenceOverheadTokens, Math.max(1, capturedContext.totalTokens)),
4024
+ ...reported ? {
4025
+ reported: {
4026
+ ...reported.inputTokens !== undefined ? { inputTokens: reported.inputTokens } : {},
4027
+ ...reported.cachedInputTokens !== undefined ? { cachedInputTokens: reported.cachedInputTokens } : {},
4028
+ ...reported.outputTokens !== undefined ? { outputTokens: reported.outputTokens } : {},
4029
+ ...reported.reasoningOutputTokens !== undefined ? { reasoningOutputTokens: reported.reasoningOutputTokens } : {},
4030
+ ...reported.totalTokens !== undefined ? { totalTokens: reported.totalTokens } : {},
4031
+ ...tokenUsage?.modelContextWindow !== undefined ? { modelContextWindow: tokenUsage.modelContextWindow } : {}
4032
+ }
4033
+ } : {},
4034
+ ...reportedCadenceOverheadTokens !== undefined ? {
4035
+ reportedCadenceOverheadTokens,
4036
+ reportedCadenceOverheadRatio: roundedRatio(reportedCadenceOverheadTokens, Math.max(1, capturedContext.totalTokens))
4037
+ } : {}
4038
+ };
4039
+ }
4040
+ function buildCheckpointPrompt(input) {
4041
+ const recentTurns = checkpointRecentTurns(input.event);
4042
+ const recentTurnsText = recentTurns.length ? formatCheckpointRecentTurns(recentTurns) : "";
4043
+ const modeInstructions = input.mode === "closeout" ? [
4044
+ "Mode: closeout. Produce final session memory, not an interim pulse.",
4045
+ "Closeout should cover the completed work segment: outcome, reviewable implementation actions, decisions/corrections, verification, blockers/follow-ups, scope or attribution notes, and handoff when needed.",
4046
+ "Write the minimum entries needed for coverage. Target 2-6 entries; avoid more than 8 by merging related facts or writing one concise session catch-up entry.",
4047
+ "Use session.action handoff, end, or keep when appropriate. Use complete_ticket only when explicit completion evidence is present."
4048
+ ] : [
4049
+ "Mode: checkpoint. Produce sparse incremental memory for an active session, not a full-session closeout.",
4050
+ "Checkpoint should commonly return route.action noop with entries [] when there is nothing durable to record.",
4051
+ "Target 0-2 entries. Only exceed that for a blocker or failed verification. Do not repeat unchanged intent, repeated dirty workspace warnings, or routine process actions."
4052
+ ];
4053
+ return [
4054
+ `You are generating a compact Cadence dogfood operation plan for an agent-run ${input.mode} worker.`,
4055
+ "The model judges; the Cadence CLI validates and executes. Return JSON only. Do not call tools to mutate Cadence yourself.",
4056
+ "Preserve durable ticket purpose. Identify user intent, changed intent, corrections, decisions, rationale, implementation actions, verification, blockers, and useful notes.",
4057
+ "Do not include raw diffs, raw transcripts, terminal logs, tool streams, secrets, file contents, or model reasoning in server-bound fields.",
4058
+ 'Return this sparse JSON shape: {"summary":"short checkpoint summary","route":{"action":"current|noop|intake_create|intake_attach|switch_existing","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"}]}',
4059
+ "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.",
4060
+ "Use route.action current for the same ticket. Use noop when routing is ambiguous or there is nothing durable to record. Use intake_create, intake_attach, or switch_existing only when recent work clearly belongs somewhere else; the CLI will delegate those actions to agent-run route.",
4061
+ "Set route.confidence high only when the recent user/assistant context makes the route and lifecycle action clear. Lifecycle mutations require high confidence.",
4062
+ "Use note only as a last-resort context kind. Prefer intent, decision, rationale, action, verification, correction, or blocker when those fit.",
4063
+ "Each entry body should be concise narrative, not a transcript. Keep each entry under 800 characters and avoid duplicating the same fact across entries.",
4064
+ "Set summaryUpdate.update true only when the durable current work summary is missing or misleading; otherwise omit summaryUpdate or leave update false.",
4065
+ "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.",
4066
+ ...modeInstructions,
4067
+ "",
3022
4068
  `Ticket: ${input.ticketId}`,
3023
4069
  input.sessionId ? `Session: ${input.sessionId}` : "",
3024
4070
  input.changesetId ? `ChangeSet: ${input.changesetId}` : "",
4071
+ input.currentWorkSummary ? `Current Work Summary:
4072
+ ${truncateText(input.currentWorkSummary, 1200)}` : "",
4073
+ input.recentWorkLog ? `Recent high-signal Work Log entries:
4074
+ ${truncateText(input.recentWorkLog, 3000)}` : "",
4075
+ input.coverage ? `Prior coverage: ${JSON.stringify(input.coverage)}` : "",
4076
+ `Agent event: ${input.event.source}/${input.event.event}`,
4077
+ input.previousCheckpointSummary ? `Previous checkpoint summary: ${input.previousCheckpointSummary}` : "",
4078
+ recentTurnsText ? `Recent user/assistant turns (most recent 3, local checkpoint context only):
4079
+ ${recentTurnsText}` : input.event.lastAssistantMessage ? `Last assistant message:
4080
+ ${truncateText(input.event.lastAssistantMessage, 3000)}` : "Recent turns: unavailable",
4081
+ input.gitStatus ? `Git status --short:
4082
+ ${truncateText(input.gitStatus, 2000)}` : "Git status --short: clean or unavailable",
4083
+ input.gitDiffStat ? `Git diff --stat origin/dev...:
4084
+ ${truncateText(input.gitDiffStat, 2000)}` : "Git diff stat: unavailable",
4085
+ input.changedFiles ? `Changed files:
4086
+ ${truncateText(input.changedFiles, 2000)}` : "Changed files: unavailable"
4087
+ ].filter(Boolean).join(`
4088
+ `);
4089
+ }
4090
+ function buildRoutePrompt(input) {
4091
+ const recentTurns = checkpointRecentTurns(input.event);
4092
+ const recentTurnsText = recentTurns.length ? formatCheckpointRecentTurns(recentTurns) : "";
4093
+ const contextText = input.savedContext?.ticketId ? `Saved Cadence context: ticket ${input.savedContext.ticketId}${input.savedContext.sessionId ? `, session ${input.savedContext.sessionId}` : ""}${input.savedContext.changesetId ? `, ChangeSet ${input.savedContext.changesetId}` : ""}` : input.currentContext?.ticketId ? `No saved agent-session context. Current active Cadence context: ticket ${input.currentContext.ticketId}${input.currentContext.sessionId ? `, session ${input.currentContext.sessionId}` : ""}${input.currentContext.changesetId ? `, ChangeSet ${input.currentContext.changesetId}` : ""}` : "No saved agent-session context and no active Cadence context was found.";
4094
+ return [
4095
+ "You are generating a compact Cadence dogfood routing plan for an agent-run worker.",
4096
+ "The model judges; the Cadence CLI validates and executes. Return JSON only. Do not call tools to mutate Cadence yourself.",
4097
+ "Decide where this agent session belongs before checkpoint memory is written.",
4098
+ "Do not include raw diffs, raw transcripts, terminal logs, tool streams, secrets, file contents, or model reasoning in server-bound fields.",
4099
+ 'Return this sparse JSON shape: {"summary":"short routing summary","route":{"action":"current|noop|intake_create|intake_attach|switch_existing","confidence":"low|medium|high","reason":"short reason","request":"natural language intake 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"}],"files":[{"path":"relative/path.ts","kind":"added|modified|deleted|renamed|unknown"}]}',
4100
+ "Use noop for filler, acknowledgements, setup chatter, or anything not durable enough for Cadence.",
4101
+ "Use current only when an existing Cadence context clearly fits the recent work.",
4102
+ "Use intake_create for a clearly durable new task without a good existing ticket.",
4103
+ "Use intake_attach or switch_existing only when a specific targetTicketId is clearly the correct ticket.",
4104
+ "Set confidence high only when the route is clear. Low or medium confidence lifecycle actions will be ignored by the CLI.",
4105
+ "Use note only as a last-resort context kind. Prefer intent for new routed work.",
4106
+ "Each entry body should be concise narrative, not a transcript. Keep each entry under 800 characters.",
4107
+ "",
4108
+ `Route reason: ${input.reason ?? "manual"}`,
4109
+ contextText,
3025
4110
  `Agent event: ${input.event.source}/${input.event.event}`,
3026
4111
  input.previousCheckpointSummary ? `Previous checkpoint summary: ${input.previousCheckpointSummary}` : "",
3027
- input.event.lastAssistantMessage ? `Last assistant message:
3028
- ${truncateText(input.event.lastAssistantMessage, 3000)}` : "Last assistant message: unavailable",
4112
+ recentTurnsText ? `Recent user/assistant turns (most recent 3, local routing context only):
4113
+ ${recentTurnsText}` : input.event.lastAssistantMessage ? `Last assistant message:
4114
+ ${truncateText(input.event.lastAssistantMessage, 3000)}` : "Recent turns: unavailable",
3029
4115
  input.gitStatus ? `Git status --short:
3030
4116
  ${truncateText(input.gitStatus, 2000)}` : "Git status --short: clean or unavailable",
3031
4117
  input.gitDiffStat ? `Git diff --stat origin/dev...:
@@ -3035,6 +4121,263 @@ ${truncateText(input.changedFiles, 2000)}` : "Changed files: unavailable"
3035
4121
  ].filter(Boolean).join(`
3036
4122
  `);
3037
4123
  }
4124
+ function agentRunFingerprint(value) {
4125
+ return stableHash(JSON.stringify(value));
4126
+ }
4127
+ function buildAgentRunFingerprints(input) {
4128
+ return {
4129
+ event: agentRunFingerprint({
4130
+ source: input.event.source,
4131
+ event: input.event.event,
4132
+ agentSessionKey: input.event.agentSessionKey,
4133
+ agentSessionId: input.event.agentSessionId,
4134
+ threadId: input.event.threadId,
4135
+ turnId: input.event.turnId,
4136
+ lastAssistantMessage: input.event.lastAssistantMessage,
4137
+ recentTurns: input.event.recentTurns,
4138
+ payloadKeys: input.event.payloadKeys
4139
+ }),
4140
+ git: agentRunFingerprint({
4141
+ status: input.gitStatus,
4142
+ diffStat: input.gitDiffStat,
4143
+ changedFiles: input.changedFiles
4144
+ }),
4145
+ cadenceContext: agentRunFingerprint({
4146
+ ticketId: input.ticketId,
4147
+ sessionId: input.sessionId,
4148
+ changesetId: input.changesetId
4149
+ }),
4150
+ ...input.coverage?.hasDebt ? { coverageDebt: agentRunFingerprint(input.coverage) } : {}
4151
+ };
4152
+ }
4153
+ function fingerprintsEqual(left, right) {
4154
+ return Boolean(left && left.event === right.event && left.git === right.git && left.cadenceContext === right.cadenceContext && left.verification === right.verification && left.coverageDebt === right.coverageDebt);
4155
+ }
4156
+ function checkpointReasonBypassesFingerprintGate(reason) {
4157
+ return reason === "manual" || reason === "idle" || reason === "ticket_switch" || reason === "closeout";
4158
+ }
4159
+ function entryText(entry) {
4160
+ return `${entry.summary ?? ""}
4161
+ ${entry.body}`.toLowerCase();
4162
+ }
4163
+ function entryReductionSummary(entry) {
4164
+ return truncateText(entry.summary ?? checkpointEntrySummary(entry.body), 120);
4165
+ }
4166
+ function isRepeatedScopeNote(entry) {
4167
+ const text = entryText(entry);
4168
+ return entry.kind === "note" && (text.includes("dirty workspace") || text.includes("dirty files") || text.includes("unattributed"));
4169
+ }
4170
+ function isRepeatedNoVerificationNote(entry) {
4171
+ const text = entryText(entry);
4172
+ return entry.kind === "verification" && text.includes("no fresh") && (text.includes("test") || text.includes("verification"));
4173
+ }
4174
+ function isUnchangedIntent(entry) {
4175
+ const text = entryText(entry);
4176
+ return entry.kind === "intent" && (text.includes("remain") || text.includes("still")) && text.includes("active");
4177
+ }
4178
+ function isProcessOnlyAction(entry) {
4179
+ if (entry.kind !== "action") {
4180
+ return false;
4181
+ }
4182
+ const text = entryText(entry);
4183
+ const processSignals = ["read ", "inspected", "looked at", "ran git status", "gathered context", "paused", "discussed", "answered"];
4184
+ const reviewableSignals = [
4185
+ "added",
4186
+ "implemented",
4187
+ "updated",
4188
+ "changed",
4189
+ "removed",
4190
+ "refactored",
4191
+ "fixed",
4192
+ "moved",
4193
+ "persisted",
4194
+ "configured",
4195
+ "wrote",
4196
+ "split"
4197
+ ];
4198
+ return processSignals.some((signal) => text.includes(signal)) && !reviewableSignals.some((signal) => text.includes(signal));
4199
+ }
4200
+ function entryDedupeKey(entry) {
4201
+ if (isRepeatedScopeNote(entry)) {
4202
+ return "scope-note";
4203
+ }
4204
+ if (isRepeatedNoVerificationNote(entry)) {
4205
+ return "no-verification";
4206
+ }
4207
+ if (isUnchangedIntent(entry)) {
4208
+ return "unchanged-intent";
4209
+ }
4210
+ return `${entry.kind}:${entry.summary ?? checkpointEntrySummary(entry.body)}`.toLowerCase();
4211
+ }
4212
+ function mergeDecisionRationaleEntries(entries) {
4213
+ const merged = [];
4214
+ const records = [];
4215
+ for (let index = 0;index < entries.length; index += 1) {
4216
+ const entry = entries[index];
4217
+ const next = entries[index + 1];
4218
+ if (entry.kind === "decision" && next?.kind === "rationale") {
4219
+ merged.push({
4220
+ ...entry,
4221
+ body: truncateText(`${entry.body}
4222
+
4223
+ Rationale: ${next.body.replace(/^Rationale:\s*/i, "")}`, checkpointServerTextLimit)
4224
+ });
4225
+ records.push({
4226
+ summary: entryReductionSummary(next),
4227
+ reason: "merged_adjacent_decision_rationale"
4228
+ });
4229
+ index += 1;
4230
+ continue;
4231
+ }
4232
+ merged.push(entry);
4233
+ }
4234
+ return { entries: merged, records };
4235
+ }
4236
+ function checkpointAllowsExtraEntries(entries) {
4237
+ return entries.some((entry) => entry.kind === "blocker" || entry.kind === "verification" && /\b(fail|failed|failing|error|blocked)\b/i.test(entry.body));
4238
+ }
4239
+ function reduceCheckpointEntries(entries, mode) {
4240
+ const dropped = [];
4241
+ const seen = new Set;
4242
+ const filtered = [];
4243
+ for (const entry of entries) {
4244
+ if (isProcessOnlyAction(entry)) {
4245
+ dropped.push({ summary: entryReductionSummary(entry), reason: "process_only_action" });
4246
+ continue;
4247
+ }
4248
+ const dedupeKey = entryDedupeKey(entry);
4249
+ if (seen.has(dedupeKey)) {
4250
+ dropped.push({ summary: entryReductionSummary(entry), reason: "duplicate_entry" });
4251
+ continue;
4252
+ }
4253
+ seen.add(dedupeKey);
4254
+ filtered.push(entry);
4255
+ }
4256
+ const merged = mergeDecisionRationaleEntries(filtered);
4257
+ const maxEntries = mode === "closeout" ? 8 : checkpointAllowsExtraEntries(merged.entries) ? 4 : 3;
4258
+ const capped = merged.entries.length > maxEntries;
4259
+ const kept = capped ? merged.entries.slice(0, maxEntries) : merged.entries;
4260
+ for (const entry of merged.entries.slice(maxEntries)) {
4261
+ dropped.push({ summary: entryReductionSummary(entry), reason: "entry_budget" });
4262
+ }
4263
+ return {
4264
+ entries: kept,
4265
+ reduction: {
4266
+ mode,
4267
+ originalCount: entries.length,
4268
+ keptCount: kept.length,
4269
+ dropped,
4270
+ merged: merged.records,
4271
+ capped
4272
+ }
4273
+ };
4274
+ }
4275
+ function buildAgentRunCoverage(plan, mode) {
4276
+ const entries = plan.entries;
4277
+ const bodyText = [plan.summary ?? "", plan.session?.summary ?? "", ...entries.map((entry) => `${entry.summary ?? ""} ${entry.body}`)].join(`
4278
+ `).toLowerCase();
4279
+ const outcome = Boolean(plan.summary || plan.session?.summary);
4280
+ const implementation = entries.some((entry) => entry.kind === "action");
4281
+ const decisions = entries.some((entry) => entry.kind === "decision" || entry.kind === "rationale");
4282
+ const corrections = entries.some((entry) => entry.kind === "correction");
4283
+ const verification = entries.some((entry) => entry.kind === "verification");
4284
+ const blockers = entries.some((entry) => entry.kind === "blocker");
4285
+ const scope = /\b(scope|attribut|dirty workspace|dirty files|unassigned|unattributed)\b/.test(bodyText);
4286
+ const handoff = mode === "closeout" && Boolean(plan.session && plan.session.action !== "keep");
4287
+ const hasDebt = mode === "closeout" && (!outcome || !implementation || !verification || !handoff);
4288
+ return {
4289
+ outcome,
4290
+ implementation,
4291
+ decisions,
4292
+ corrections,
4293
+ verification,
4294
+ blockers,
4295
+ scope,
4296
+ handoff,
4297
+ hasDebt
4298
+ };
4299
+ }
4300
+ function applyCloseoutSessionDefaults(plan, sessionAction, completeTicket) {
4301
+ const requestedAction = completeTicket ? "complete_ticket" : sessionAction;
4302
+ const session = plan.session && (completeTicket || plan.session.action !== "complete_ticket") ? plan.session : {
4303
+ action: requestedAction,
4304
+ ...plan.summary ? { summary: plan.summary } : {},
4305
+ reason: "Closeout defaults to ending or handing off the active session."
4306
+ };
4307
+ if (!completeTicket && session.action === "complete_ticket") {
4308
+ return {
4309
+ ...plan,
4310
+ session: {
4311
+ ...session,
4312
+ action: sessionAction,
4313
+ reason: "Ticket completion was not requested for this closeout."
4314
+ }
4315
+ };
4316
+ }
4317
+ return {
4318
+ ...plan,
4319
+ session
4320
+ };
4321
+ }
4322
+ function parseCloseoutSessionAction(value) {
4323
+ if (!value) {
4324
+ return "handoff";
4325
+ }
4326
+ if (!closeoutSessionActions.includes(value)) {
4327
+ throw new CliError("CLI_USAGE", "--session-action must be one of handoff, end, or keep.");
4328
+ }
4329
+ return value;
4330
+ }
4331
+ function checkpointModeForCommand(parsed) {
4332
+ return parsed.command.name === "agent-run.closeout" ? "closeout" : "checkpoint";
4333
+ }
4334
+ function formatCloseoutWorkLogEvents(events) {
4335
+ if (!Array.isArray(events)) {
4336
+ return;
4337
+ }
4338
+ const entries = events.filter((event) => event && typeof event === "object").map((event) => {
4339
+ const record = event;
4340
+ const payload = record.payload && typeof record.payload === "object" && !Array.isArray(record.payload) ? record.payload : {};
4341
+ const type = typeof record.type === "string" ? record.type : undefined;
4342
+ const kind = typeof payload.entryKind === "string" ? payload.entryKind : undefined;
4343
+ const summary = typeof payload.summary === "string" ? payload.summary : undefined;
4344
+ const body = typeof payload.body === "string" ? payload.body : undefined;
4345
+ if (type !== "ticket.work_log_appended" || !summary && !body) {
4346
+ return;
4347
+ }
4348
+ return `- ${kind ?? "note"}: ${truncateText(summary ?? body ?? "", 180)}${body && summary ? ` \u2014 ${truncateText(body, 300)}` : ""}`;
4349
+ }).filter((entry) => Boolean(entry)).slice(-12);
4350
+ return entries.length ? entries.join(`
4351
+ `) : undefined;
4352
+ }
4353
+ async function readCloseoutPromptContext(input) {
4354
+ let currentWorkSummary;
4355
+ let recentWorkLog;
4356
+ try {
4357
+ const ticket = await input.client.tickets.get({
4358
+ projectId: input.projectId,
4359
+ ticketId: input.ticketId
4360
+ });
4361
+ if (ticket && typeof ticket === "object" && "currentSummary" in ticket && typeof ticket.currentSummary === "string") {
4362
+ currentWorkSummary = ticket.currentSummary;
4363
+ }
4364
+ } catch {}
4365
+ try {
4366
+ const events = await input.client.events.list({
4367
+ projectId: input.projectId,
4368
+ filters: {
4369
+ ticketId: input.ticketId,
4370
+ ...input.sessionId ? { sessionId: input.sessionId } : {},
4371
+ limit: 50
4372
+ }
4373
+ });
4374
+ recentWorkLog = formatCloseoutWorkLogEvents(events);
4375
+ } catch {}
4376
+ return {
4377
+ ...currentWorkSummary ? { currentWorkSummary } : {},
4378
+ ...recentWorkLog ? { recentWorkLog } : {}
4379
+ };
4380
+ }
3038
4381
  async function findRepoRoot(cwd) {
3039
4382
  const result = spawnSync("git", ["rev-parse", "--show-toplevel"], {
3040
4383
  cwd,
@@ -3058,6 +4401,22 @@ function codexHookEntry(command) {
3058
4401
  ]
3059
4402
  };
3060
4403
  }
4404
+ function codexHookCommands(entry) {
4405
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
4406
+ return [];
4407
+ }
4408
+ const record = entry;
4409
+ const directCommand = typeof record.command === "string" ? [record.command] : [];
4410
+ const handlers = Array.isArray(record.hooks) ? record.hooks : [];
4411
+ const handlerCommands = handlers.flatMap((handler) => {
4412
+ if (!handler || typeof handler !== "object" || Array.isArray(handler)) {
4413
+ return [];
4414
+ }
4415
+ const command = handler.command;
4416
+ return typeof command === "string" ? [command] : [];
4417
+ });
4418
+ return [...directCommand, ...handlerCommands];
4419
+ }
3061
4420
  async function readJsonObjectFile(filePath) {
3062
4421
  const file = Bun.file(filePath);
3063
4422
  if (!await file.exists()) {
@@ -3071,8 +4430,9 @@ async function installCodexHookFile(filePath, command) {
3071
4430
  const hooks = hooksValue && typeof hooksValue === "object" && !Array.isArray(hooksValue) ? hooksValue : {};
3072
4431
  const stopValue = hooks.Stop;
3073
4432
  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)];
4433
+ const alreadyInstalled = stopHooks.some((entry) => codexHookCommands(entry).includes(command));
4434
+ 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);
4435
+ 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
4436
  const nextConfig = {
3077
4437
  ...existing,
3078
4438
  hooks: {
@@ -3086,7 +4446,8 @@ async function installCodexHookFile(filePath, command) {
3086
4446
  return {
3087
4447
  path: filePath,
3088
4448
  installed: !alreadyInstalled,
3089
- alreadyInstalled
4449
+ alreadyInstalled,
4450
+ updated: !alreadyInstalled && cadenceHookIndexes.length > 0
3090
4451
  };
3091
4452
  }
3092
4453
  async function codexHookPaths(scope, options) {
@@ -3108,7 +4469,7 @@ async function codexHookInstalled(filePath, command) {
3108
4469
  const existing = await readJsonObjectFile(filePath);
3109
4470
  const hooks = existing.hooks;
3110
4471
  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));
4472
+ return stopHooks.some((entry) => codexHookCommands(entry).includes(command));
3112
4473
  }
3113
4474
  async function runStatus(parsed, options) {
3114
4475
  const config = await resolveCliConfig(parsed.flags, options);
@@ -3255,14 +4616,19 @@ async function runAgentRunCommand(parsed, options) {
3255
4616
  switch (parsed.command.name) {
3256
4617
  case "agent-run.ingest-stop":
3257
4618
  return await runAgentRunIngestStop(parsed, options, config, meta);
4619
+ case "agent-run.route":
4620
+ return await runAgentRunRoute(parsed, options, config, meta);
4621
+ case "agent-run.checkpoint":
3258
4622
  case "agent-run.closeout":
3259
- return await runAgentRunCloseout(parsed, options, config, meta);
4623
+ return await runAgentRunCheckpoint(parsed, options, config, meta);
3260
4624
  case "agent-run.sweep":
3261
- return await runAgentRunSweep(parsed, options, meta);
4625
+ return await runAgentRunSweep(parsed, options, config, meta);
3262
4626
  case "agent-run.doctor": {
3263
4627
  const state = await readAgentLoopState(parsed, options);
4628
+ const settings = await readAgentLoopSettings(parsed, options);
3264
4629
  const data = {
3265
4630
  action: "doctor",
4631
+ settings,
3266
4632
  state,
3267
4633
  sessionCount: Object.keys(state.sessions).length,
3268
4634
  pendingSessions: Object.entries(state.sessions).filter(([, session]) => session.stopCount > 0).map(([agentSessionKeyValue, session]) => ({
@@ -3337,8 +4703,18 @@ async function runAgentRunIngestStop(parsed, options, config, meta) {
3337
4703
  const dryRun = parseBooleanOption(parsed.options["dry-run"], false);
3338
4704
  const existingSession = state.sessions[normalized.agentSessionKey];
3339
4705
  const now = new Date().toISOString();
3340
- const threshold = forcedThreshold ?? existingSession?.threshold ?? randomCheckpointThreshold();
3341
- const nextCount = (existingSession?.stopCount ?? 0) + 1;
4706
+ const threshold = forcedThreshold ?? existingSession?.threshold ?? defaultCheckpointThresholdValue();
4707
+ const duplicateTurn = Boolean(normalized.turnId && existingSession?.lastObservedTurnId === normalized.turnId);
4708
+ const nextCount = (existingSession?.stopCount ?? 0) + (duplicateTurn ? 0 : 1);
4709
+ const recentTurns = mergeRecentAgentTurns(existingSession?.recentTurns, normalized.recentTurns);
4710
+ const savedCadenceContext = existingSession?.cadenceContext;
4711
+ let cadenceContext = savedCadenceContext;
4712
+ if (savedCadenceContext?.ticketId) {
4713
+ try {
4714
+ const client = await createClient(config, options);
4715
+ cadenceContext = await readCurrentCadenceContext(client, projectId) ?? cadenceContext;
4716
+ } catch {}
4717
+ }
3342
4718
  const observedSession = {
3343
4719
  ...clearAgentSessionReason(existingSession ?? {
3344
4720
  source: normalized.source,
@@ -3350,8 +4726,11 @@ async function runAgentRunIngestStop(parsed, options, config, meta) {
3350
4726
  threshold,
3351
4727
  firstObservedAt: existingSession?.firstObservedAt ?? now,
3352
4728
  lastObservedAt: now,
4729
+ ...normalized.turnId ? { lastObservedTurnId: normalized.turnId } : {},
3353
4730
  lastAction: "counted",
3354
- ...normalized.lastAssistantMessage ? { lastAssistantMessage: normalized.lastAssistantMessage } : {}
4731
+ ...cadenceContext ? { cadenceContext } : {},
4732
+ ...normalized.lastAssistantMessage ? { lastAssistantMessage: normalized.lastAssistantMessage } : {},
4733
+ ...recentTurns ? { recentTurns } : {}
3355
4734
  };
3356
4735
  const countedState = {
3357
4736
  ...state,
@@ -3360,10 +4739,11 @@ async function runAgentRunIngestStop(parsed, options, config, meta) {
3360
4739
  [normalized.agentSessionKey]: observedSession
3361
4740
  }
3362
4741
  };
3363
- if (nextCount < threshold) {
4742
+ if (duplicateTurn) {
3364
4743
  await writeAgentLoopState(parsed, options, countedState);
3365
4744
  const data2 = {
3366
- action: "counted",
4745
+ action: "updated",
4746
+ reason: "duplicate_turn",
3367
4747
  agentSessionKey: normalized.agentSessionKey,
3368
4748
  stopCount: nextCount,
3369
4749
  threshold
@@ -3375,10 +4755,128 @@ async function runAgentRunIngestStop(parsed, options, config, meta) {
3375
4755
  exitCode: 0
3376
4756
  };
3377
4757
  }
3378
- if (shouldSkipForCooldown(observedSession, cooldownSeconds)) {
3379
- await writeAgentLoopState(parsed, options, {
3380
- ...state,
3381
- sessions: {
4758
+ if (!savedCadenceContext?.ticketId) {
4759
+ const eventFile2 = await writeAgentEventFile(parsed, options, normalized);
4760
+ const lockPath2 = agentLoopLockPath(parsed, options, normalized.agentSessionKey);
4761
+ const workerArgs2 = [
4762
+ "agent-run",
4763
+ "route",
4764
+ "--agent-session-key",
4765
+ normalized.agentSessionKey,
4766
+ "--reason",
4767
+ "missing_context",
4768
+ "--event-file",
4769
+ eventFile2,
4770
+ "--lock",
4771
+ lockPath2,
4772
+ ...parsed.flags.project ? ["--project", parsed.flags.project] : [],
4773
+ ...parsed.flags.server ? ["--server", parsed.flags.server] : []
4774
+ ];
4775
+ if (dryRun) {
4776
+ await writeAgentLoopState(parsed, options, {
4777
+ ...state,
4778
+ sessions: {
4779
+ ...state.sessions,
4780
+ [normalized.agentSessionKey]: {
4781
+ ...observedSession,
4782
+ lastAction: "would_route",
4783
+ lastEventFile: eventFile2
4784
+ }
4785
+ }
4786
+ });
4787
+ const data3 = {
4788
+ action: "would_route",
4789
+ agentSessionKey: normalized.agentSessionKey,
4790
+ eventFile: eventFile2,
4791
+ workerArgs: workerArgs2
4792
+ };
4793
+ return {
4794
+ stdout: parsed.flags.json ? formatJson(successEnvelope(data3, meta)) : `${JSON.stringify(data3, null, 2)}
4795
+ `,
4796
+ stderr: "",
4797
+ exitCode: 0
4798
+ };
4799
+ }
4800
+ const lock2 = await acquireAgentLoopLock(parsed, options, normalized.agentSessionKey);
4801
+ if (!lock2.acquired) {
4802
+ await writeAgentLoopState(parsed, options, {
4803
+ ...state,
4804
+ sessions: {
4805
+ ...state.sessions,
4806
+ [normalized.agentSessionKey]: {
4807
+ ...observedSession,
4808
+ lastAction: "skipped",
4809
+ lastReason: "lock_held"
4810
+ }
4811
+ }
4812
+ });
4813
+ const data3 = {
4814
+ action: "skipped",
4815
+ reason: "lock_held",
4816
+ lockPath: lock2.lockPath,
4817
+ agentSessionKey: normalized.agentSessionKey
4818
+ };
4819
+ return {
4820
+ stdout: parsed.flags.json ? formatJson(successEnvelope(data3, meta)) : `${JSON.stringify(data3, null, 2)}
4821
+ `,
4822
+ stderr: "",
4823
+ exitCode: 0
4824
+ };
4825
+ }
4826
+ try {
4827
+ await spawnAgentRunWorker(workerArgs2, options);
4828
+ } catch (error) {
4829
+ await releaseAgentLoopLock(lock2.lockPath);
4830
+ throw error;
4831
+ }
4832
+ await writeAgentLoopState(parsed, options, {
4833
+ ...state,
4834
+ sessions: {
4835
+ ...state.sessions,
4836
+ [normalized.agentSessionKey]: {
4837
+ ...observedSession,
4838
+ stopCount: 0,
4839
+ threshold: defaultCheckpointThresholdValue(),
4840
+ lastAction: "route_spawned",
4841
+ lastEventFile: eventFile2,
4842
+ lastCheckpointAt: new Date().toISOString(),
4843
+ lastCheckpointFingerprint: fingerprintForCheckpoint(normalized, options)
4844
+ }
4845
+ }
4846
+ });
4847
+ const data2 = {
4848
+ action: "route_spawned",
4849
+ agentSessionKey: normalized.agentSessionKey,
4850
+ eventFile: eventFile2,
4851
+ lockPath: lock2.lockPath
4852
+ };
4853
+ return {
4854
+ stdout: parsed.flags.json ? formatJson(successEnvelope(data2, meta)) : `${JSON.stringify(data2, null, 2)}
4855
+ `,
4856
+ stderr: "",
4857
+ exitCode: 0
4858
+ };
4859
+ }
4860
+ if (nextCount < threshold) {
4861
+ await writeAgentLoopState(parsed, options, countedState);
4862
+ const data2 = {
4863
+ action: duplicateTurn ? "updated" : "counted",
4864
+ ...duplicateTurn ? { reason: "duplicate_turn" } : {},
4865
+ agentSessionKey: normalized.agentSessionKey,
4866
+ stopCount: nextCount,
4867
+ threshold
4868
+ };
4869
+ return {
4870
+ stdout: parsed.flags.json ? formatJson(successEnvelope(data2, meta)) : `${JSON.stringify(data2, null, 2)}
4871
+ `,
4872
+ stderr: "",
4873
+ exitCode: 0
4874
+ };
4875
+ }
4876
+ if (shouldSkipForCooldown(observedSession, cooldownSeconds)) {
4877
+ await writeAgentLoopState(parsed, options, {
4878
+ ...state,
4879
+ sessions: {
3382
4880
  ...state.sessions,
3383
4881
  [normalized.agentSessionKey]: {
3384
4882
  ...observedSession,
@@ -3410,7 +4908,7 @@ async function runAgentRunIngestStop(parsed, options, config, meta) {
3410
4908
  [normalized.agentSessionKey]: {
3411
4909
  ...observedSession,
3412
4910
  stopCount: 0,
3413
- threshold: randomCheckpointThreshold(),
4911
+ threshold: defaultCheckpointThresholdValue(),
3414
4912
  lastAction: "skipped",
3415
4913
  lastReason: "unchanged"
3416
4914
  }
@@ -3432,7 +4930,7 @@ async function runAgentRunIngestStop(parsed, options, config, meta) {
3432
4930
  const lockPath = agentLoopLockPath(parsed, options, normalized.agentSessionKey);
3433
4931
  const workerArgs = [
3434
4932
  "agent-run",
3435
- "closeout",
4933
+ "checkpoint",
3436
4934
  "--agent-session-key",
3437
4935
  normalized.agentSessionKey,
3438
4936
  "--reason",
@@ -3498,7 +4996,7 @@ async function runAgentRunIngestStop(parsed, options, config, meta) {
3498
4996
  };
3499
4997
  }
3500
4998
  try {
3501
- await spawnAgentRunCloseout(workerArgs, options);
4999
+ await spawnAgentRunCheckpoint(workerArgs, options);
3502
5000
  } catch (error) {
3503
5001
  await releaseAgentLoopLock(lock.lockPath);
3504
5002
  throw error;
@@ -3509,8 +5007,9 @@ async function runAgentRunIngestStop(parsed, options, config, meta) {
3509
5007
  ...state.sessions,
3510
5008
  [normalized.agentSessionKey]: {
3511
5009
  ...observedSession,
5010
+ ...cadenceContext ? { cadenceContext } : {},
3512
5011
  stopCount: 0,
3513
- threshold: randomCheckpointThreshold(),
5012
+ threshold: defaultCheckpointThresholdValue(),
3514
5013
  lastAction: "spawned",
3515
5014
  lastEventFile: eventFile,
3516
5015
  lastCheckpointAt: new Date().toISOString(),
@@ -3531,7 +5030,10 @@ async function runAgentRunIngestStop(parsed, options, config, meta) {
3531
5030
  exitCode: 0
3532
5031
  };
3533
5032
  }
3534
- async function runAgentRunCloseout(parsed, options, config, meta) {
5033
+ function objectStringId(value) {
5034
+ return value && typeof value === "object" && "id" in value && typeof value.id === "string" ? value.id : undefined;
5035
+ }
5036
+ async function runAgentRunRoute(parsed, options, config, meta) {
3535
5037
  const projectId = requireProjectId(config);
3536
5038
  const client = await createClient(config, options);
3537
5039
  const agentSessionKeyValue = requireOption(parsed, "agent-session-key");
@@ -3542,16 +5044,383 @@ async function runAgentRunCloseout(parsed, options, config, meta) {
3542
5044
  agentSessionKey: agentSessionKeyValue
3543
5045
  });
3544
5046
  }
3545
- const current = await client.sessions.current({
3546
- projectId,
3547
- filters: {
3548
- limit: 100
3549
- }
5047
+ const savedContext = sessionState.cadenceContext;
5048
+ const currentContext = savedContext?.ticketId ? undefined : await readCurrentCadenceContext(client, projectId);
5049
+ const eventFile = parsed.options["event-file"] ?? sessionState.lastEventFile;
5050
+ const event = eventFile ? tryParseJsonObject(await readFile(eventFile, "utf8"), eventFile) : synthesizeAgentEventFromSession(agentSessionKeyValue, sessionState, options);
5051
+ const checkpointSettings = await resolveCheckpointSettings(parsed, options);
5052
+ const gitStatus = gitOutput(["status", "--short"], options);
5053
+ const gitDiffStat = gitOutput(["diff", "--stat", "origin/dev..."], options);
5054
+ const changedFiles = gitOutput(["diff", "--name-only", "origin/dev..."], options);
5055
+ const prompt = buildRoutePrompt({
5056
+ event,
5057
+ ...parsed.options.reason ? { reason: parsed.options.reason } : {},
5058
+ ...savedContext ? { savedContext } : {},
5059
+ ...currentContext ? { currentContext } : {},
5060
+ gitStatus,
5061
+ gitDiffStat,
5062
+ changedFiles,
5063
+ ...sessionState.previousCheckpointSummary ? { previousCheckpointSummary: sessionState.previousCheckpointSummary } : {}
3550
5064
  });
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);
5065
+ const promptTokenAccounting = buildCheckpointTokenAccounting(event, prompt, undefined);
5066
+ const dryRun = parseBooleanOption(parsed.options["dry-run"], false);
5067
+ const lockPath = parsed.options.lock;
5068
+ if (dryRun) {
5069
+ const auditFile = await writeAgentCheckpointAuditFile(parsed, options, {
5070
+ version: 1,
5071
+ mode: "route",
5072
+ action: "would_route",
5073
+ createdAt: new Date().toISOString(),
5074
+ localOnly: true,
5075
+ localOnlyReason: "Raw hook context and route prompts stay on this machine and are not uploaded to Cadence.",
5076
+ agentSessionKey: agentSessionKeyValue,
5077
+ ...savedContext?.ticketId ? { ticketId: savedContext.ticketId } : currentContext?.ticketId ? { ticketId: currentContext.ticketId } : {},
5078
+ ...eventFile ? { eventFile } : {},
5079
+ event,
5080
+ prompt,
5081
+ checkpointSettings,
5082
+ tokenAccounting: promptTokenAccounting,
5083
+ cadenceWrites: []
5084
+ });
5085
+ const data = {
5086
+ action: "would_route",
5087
+ prompt,
5088
+ auditFile,
5089
+ agentSessionKey: agentSessionKeyValue
5090
+ };
5091
+ return {
5092
+ stdout: parsed.flags.json ? formatJson(successEnvelope(data, meta)) : `${JSON.stringify(data, null, 2)}
5093
+ `,
5094
+ stderr: "",
5095
+ exitCode: 0
5096
+ };
5097
+ }
5098
+ try {
5099
+ const codexCommand = parsed.options["codex-command"] ?? "codex";
5100
+ const codexStartedAtMs = Date.now();
5101
+ const codexCwd = options.cwd ?? process.cwd();
5102
+ const codexArgs = [
5103
+ "exec",
5104
+ ...checkpointSettings.model ? ["-m", checkpointSettings.model] : [],
5105
+ "--disable",
5106
+ "hooks",
5107
+ "--sandbox",
5108
+ "read-only",
5109
+ "-C",
5110
+ codexCwd,
5111
+ prompt
5112
+ ];
5113
+ const codex = runLocalCommand(codexCommand, codexArgs, options, {
5114
+ cwd: codexCwd,
5115
+ env: {
5116
+ [agentLoopSuppressEnv]: "1",
5117
+ CADENCE_HOOK_SUPPRESS: "1"
5118
+ },
5119
+ timeoutMs: defaultCheckpointWorkerTimeoutMs
5120
+ });
5121
+ const codexSessionTranscript = readCodexSessionTranscript(findCodexSessionFileCreatedAfter(options, codexStartedAtMs, codexCwd));
5122
+ const tokenAccounting = buildCheckpointTokenAccounting(event, prompt, codexSessionTranscript?.tokenUsage);
5123
+ if (codex.status !== 0 || codex.error) {
5124
+ throw new CliError("AGENT_RUN_ROUTE_FAILED", "Codex route generation failed.", {
5125
+ status: codex.status,
5126
+ stderr: truncateText(codex.stderr, 2000),
5127
+ error: codex.error?.message
5128
+ });
5129
+ }
5130
+ let routePlan = parseCheckpointPlanJson(codex.stdout, "intent");
5131
+ const currentTicketId = savedContext?.ticketId ?? currentContext?.ticketId;
5132
+ const currentSessionId = savedContext?.sessionId ?? currentContext?.sessionId;
5133
+ const currentChangesetId = savedContext?.changesetId ?? currentContext?.changesetId;
5134
+ if (routePlan.route.action === "needs_human") {
5135
+ routePlan = safeAutomaticRoutePlan(routePlan, Boolean(currentTicketId), routePlan.route.reason ?? "Route was uncertain.");
5136
+ } else if (checkpointRouteRequiresIntake(routePlan.route.action) && routePlan.route.confidence !== "high") {
5137
+ routePlan = safeAutomaticRoutePlan(routePlan, Boolean(currentTicketId), "Automatic routing requires high confidence.");
5138
+ } else if ((routePlan.route.action === "intake_attach" || routePlan.route.action === "switch_existing") && !routePlan.route.targetTicketId) {
5139
+ routePlan = safeAutomaticRoutePlan(routePlan, Boolean(currentTicketId), "Automatic routing to existing work requires a target ticket id.");
5140
+ } else if (routePlan.route.action === "current" && !currentTicketId) {
5141
+ routePlan = checkpointPlanWithRoute(routePlan, {
5142
+ action: "noop",
5143
+ confidence: routePlan.route.confidence,
5144
+ reason: routePlan.route.reason ?? "No current Cadence context exists."
5145
+ });
5146
+ }
5147
+ const cadenceWrites = [];
5148
+ const lifecycleOperations = [];
5149
+ let targetTicketId = currentTicketId;
5150
+ let targetSessionId = currentSessionId;
5151
+ let targetChangesetId = currentChangesetId;
5152
+ let intakeResult;
5153
+ let selectedTicket;
5154
+ let summary = routePlan.summary ?? routePlan.entries[0]?.summary ?? routePlan.route.reason ?? "Agent run route";
5155
+ const modelAudit = {
5156
+ provider: checkpointSettings.provider,
5157
+ command: codexCommand,
5158
+ ...checkpointSettings.model ? { model: checkpointSettings.model } : {},
5159
+ status: codex.status,
5160
+ stdout: codex.stdout.trim(),
5161
+ stderr: truncateText(codex.stderr.trim(), 2000),
5162
+ tokenAccounting,
5163
+ ...codexSessionTranscript?.tokenUsage ? { tokenUsage: codexSessionTranscript.tokenUsage } : {},
5164
+ ...codexSessionTranscript ? { sessionTranscript: codexSessionTranscript } : {}
5165
+ };
5166
+ const finishRoute = async (action, reason) => {
5167
+ const checkedAt = new Date().toISOString();
5168
+ const auditFile = await writeAgentCheckpointAuditFile(parsed, options, {
5169
+ version: 1,
5170
+ mode: "route",
5171
+ action,
5172
+ createdAt: checkedAt,
5173
+ localOnly: true,
5174
+ localOnlyReason: action === "noop" || action === "current" ? "Raw hook context, route prompts, and model output stay on this machine; no Cadence writes were needed." : "Raw hook context, route prompts, and model output stay on this machine; Cadence receives only the structured writes listed here.",
5175
+ agentSessionKey: agentSessionKeyValue,
5176
+ ...targetTicketId ? { ticketId: targetTicketId } : {},
5177
+ ...targetSessionId ? { sessionId: targetSessionId } : {},
5178
+ ...targetChangesetId ? { changesetId: targetChangesetId } : {},
5179
+ ...eventFile ? { eventFile } : {},
5180
+ event,
5181
+ prompt,
5182
+ model: modelAudit,
5183
+ checkpoint: routePlan,
5184
+ route: routePlan.route,
5185
+ intakeResult,
5186
+ selectedTicket,
5187
+ lifecycleOperations,
5188
+ cadenceWrites,
5189
+ summary,
5190
+ ...reason ? { reason } : {},
5191
+ entryCount: routePlan.entries.length
5192
+ });
5193
+ const nextSessionState = {
5194
+ ...sessionState,
5195
+ stopCount: 0,
5196
+ threshold: defaultCheckpointThresholdValue(),
5197
+ previousCheckpointSummary: summary,
5198
+ lastAction: action,
5199
+ ...reason ? { lastReason: reason } : {},
5200
+ ...eventFile ? { lastEventFile: eventFile } : {},
5201
+ ...targetTicketId ? {
5202
+ cadenceContext: {
5203
+ ticketId: targetTicketId,
5204
+ ...targetSessionId ? { sessionId: targetSessionId } : {},
5205
+ ...targetChangesetId ? { changesetId: targetChangesetId } : {},
5206
+ capturedAt: checkedAt
5207
+ }
5208
+ } : {},
5209
+ lastCheckpointAt: checkedAt,
5210
+ lastCheckpointMode: "checkpoint",
5211
+ lastCheckpointAuditFile: auditFile
5212
+ };
5213
+ await writeAgentLoopState(parsed, options, {
5214
+ ...state,
5215
+ sessions: {
5216
+ ...state.sessions,
5217
+ [agentSessionKeyValue]: nextSessionState
5218
+ }
5219
+ });
5220
+ const data = {
5221
+ action,
5222
+ route: routePlan.route,
5223
+ ...reason ? { reason } : {},
5224
+ ...targetTicketId ? { ticketId: targetTicketId } : {},
5225
+ summary,
5226
+ entryCount: routePlan.entries.length,
5227
+ mode: "route",
5228
+ auditFile,
5229
+ agentSessionKey: agentSessionKeyValue,
5230
+ ...targetSessionId ? { sessionId: targetSessionId } : {},
5231
+ ...targetChangesetId ? { changesetId: targetChangesetId } : {}
5232
+ };
5233
+ return {
5234
+ stdout: parsed.flags.json ? formatJson(successEnvelope(data, meta)) : `${JSON.stringify(data, null, 2)}
5235
+ `,
5236
+ stderr: "",
5237
+ exitCode: 0
5238
+ };
5239
+ };
5240
+ if (routePlan.route.action === "noop") {
5241
+ return await finishRoute("noop", routePlan.route.reason ?? routePlan.summary ?? "Nothing durable to route.");
5242
+ }
5243
+ if (routePlan.route.action === "current") {
5244
+ return await finishRoute("current", routePlan.route.reason ?? "Kept current Cadence context.");
5245
+ }
5246
+ if (routePlanAllowsLifecycle(routePlan)) {
5247
+ const intakeRequest = routePlan.route.request ?? checkpointPlanDescription(routePlan);
5248
+ intakeResult = await client.intake.create({
5249
+ projectId,
5250
+ intake: {
5251
+ request: intakeRequest,
5252
+ ...commandMetadata()
5253
+ }
5254
+ });
5255
+ lifecycleOperations.push(checkpointLifecycleOperation("intake.created", true, { request: intakeRequest, result: intakeResult }));
5256
+ if (checkpointHasConflictingIntakeResult(intakeResult, routePlan)) {
5257
+ routePlan = safeAutomaticRoutePlan(routePlan, Boolean(currentTicketId), "Intake returned conflicting duplicate, overlap, or completed-before candidates.");
5258
+ return await finishRoute(routePlan.route.action, routePlan.route.reason);
5259
+ }
5260
+ if (routePlan.route.action === "intake_create") {
5261
+ selectedTicket = await client.tickets.create({
5262
+ projectId,
5263
+ ticket: {
5264
+ title: checkpointPlanTitle(routePlan),
5265
+ description: checkpointPlanDescription(routePlan),
5266
+ fromIntakeId: objectStringId(intakeResult),
5267
+ ...commandMetadata()
5268
+ }
5269
+ });
5270
+ lifecycleOperations.push(checkpointLifecycleOperation("ticket.created", true, { ticket: selectedTicket }));
5271
+ targetTicketId = objectStringId(selectedTicket);
5272
+ } else {
5273
+ const selectedTicketId = routePlan.route.targetTicketId;
5274
+ if (!selectedTicketId) {
5275
+ routePlan = safeAutomaticRoutePlan(routePlan, Boolean(currentTicketId), "Automatic routing to existing work requires a target ticket id.");
5276
+ return await finishRoute(routePlan.route.action, routePlan.route.reason);
5277
+ }
5278
+ targetTicketId = selectedTicketId;
5279
+ selectedTicket = await client.tickets.get({ projectId, ticketId: targetTicketId });
5280
+ lifecycleOperations.push(checkpointLifecycleOperation("ticket.selected", true, { ticketId: targetTicketId, ticket: selectedTicket }));
5281
+ const selectedVersion = selectedTicket && typeof selectedTicket === "object" && typeof selectedTicket.projectionVersion === "number" ? selectedTicket.projectionVersion : undefined;
5282
+ const intakeId = objectStringId(intakeResult);
5283
+ if (selectedVersion !== undefined && intakeId) {
5284
+ await client.tickets.attach({
5285
+ projectId,
5286
+ ticketId: targetTicketId,
5287
+ fromIntakeId: intakeId,
5288
+ ifVersion: selectedVersion
5289
+ });
5290
+ lifecycleOperations.push(checkpointLifecycleOperation("intake.attached", true, { ticketId: targetTicketId, intakeId }));
5291
+ }
5292
+ }
5293
+ if (!targetTicketId) {
5294
+ routePlan = checkpointPlanWithRoute(routePlan, {
5295
+ action: currentTicketId ? "current" : "noop",
5296
+ confidence: routePlan.route.confidence,
5297
+ reason: "Automatic routing did not return a target ticket."
5298
+ });
5299
+ return await finishRoute(routePlan.route.action, routePlan.route.reason);
5300
+ }
5301
+ if (currentSessionId && currentTicketId && targetTicketId !== currentTicketId) {
5302
+ await client.sessions.end({
5303
+ projectId,
5304
+ sessionId: currentSessionId,
5305
+ session: {
5306
+ summary: routePlan.route.reason ?? summary,
5307
+ ...commandMetadata()
5308
+ }
5309
+ });
5310
+ lifecycleOperations.push(checkpointLifecycleOperation("session.ended", true, { sessionId: currentSessionId, reason: "ticket_switch" }));
5311
+ targetSessionId = undefined;
5312
+ targetChangesetId = undefined;
5313
+ }
5314
+ const startedSession = await client.sessions.start({
5315
+ projectId,
5316
+ session: {
5317
+ ticketId: targetTicketId,
5318
+ ...commandMetadata()
5319
+ }
5320
+ });
5321
+ lifecycleOperations.push(checkpointLifecycleOperation("session.started", true, { ticketId: targetTicketId, session: startedSession }));
5322
+ targetSessionId = objectStringId(startedSession);
5323
+ if (targetSessionId) {
5324
+ const lease = await client.sessions.leases.create({
5325
+ projectId,
5326
+ lease: {
5327
+ ticketId: targetTicketId,
5328
+ sessionId: targetSessionId,
5329
+ expiresAt: leaseExpiresAt(defaultLeaseTtlSeconds),
5330
+ ...commandMetadata()
5331
+ }
5332
+ });
5333
+ lifecycleOperations.push(checkpointLifecycleOperation("lease.claimed", true, { ticketId: targetTicketId, sessionId: targetSessionId, lease }));
5334
+ }
5335
+ if (targetSessionId) {
5336
+ try {
5337
+ const branchName = await resolveCurrentBranch(options);
5338
+ const changeset = await client.changesets.create({
5339
+ projectId,
5340
+ changeset: {
5341
+ ticketId: targetTicketId,
5342
+ branchName,
5343
+ baseBranch: "dev",
5344
+ sessionId: targetSessionId,
5345
+ ...commandMetadata()
5346
+ }
5347
+ });
5348
+ lifecycleOperations.push(checkpointLifecycleOperation("changeset.linked", true, { ticketId: targetTicketId, branchName, changeset }));
5349
+ targetChangesetId = objectStringId(changeset);
5350
+ } catch (error) {
5351
+ lifecycleOperations.push(checkpointLifecycleOperation("changeset.linked", false, { error: error instanceof Error ? error.message : String(error) }));
5352
+ }
5353
+ }
5354
+ const routeMetadata = checkpointWorkLogMetadata(checkpointSettings, "route", tokenAccounting);
5355
+ for (const entry of routePlan.entries) {
5356
+ const logEntry = {
5357
+ entryKind: entry.kind,
5358
+ body: entry.body,
5359
+ summary: entry.summary ?? checkpointEntrySummary(entry.body),
5360
+ ...targetSessionId ? { sessionId: targetSessionId } : {},
5361
+ ...targetChangesetId ? { changesetId: targetChangesetId } : {},
5362
+ metadata: routeMetadata,
5363
+ ...commandMetadata()
5364
+ };
5365
+ await client.tickets.log({
5366
+ projectId,
5367
+ ticketId: targetTicketId,
5368
+ entry: logEntry
5369
+ });
5370
+ cadenceWrites.push({
5371
+ type: "ticket.work_log_appended",
5372
+ success: true,
5373
+ ticketId: targetTicketId,
5374
+ entry: logEntry
5375
+ });
5376
+ }
5377
+ if (routePlan.files.length && targetSessionId) {
5378
+ const filesByKind = new Map;
5379
+ for (const file of routePlan.files) {
5380
+ filesByKind.set(file.kind, [...filesByKind.get(file.kind) ?? [], file.path]);
5381
+ }
5382
+ for (const [kind, paths] of filesByKind) {
5383
+ const filesPayload = {
5384
+ ...targetChangesetId ? { changesetId: targetChangesetId } : {},
5385
+ files: paths.map((path) => ({
5386
+ path,
5387
+ changeKind: kind
5388
+ })),
5389
+ ...commandMetadata()
5390
+ };
5391
+ await client.sessions.files({
5392
+ projectId,
5393
+ sessionId: targetSessionId,
5394
+ files: filesPayload
5395
+ });
5396
+ lifecycleOperations.push(checkpointLifecycleOperation("session.files_attached", true, { sessionId: targetSessionId, kind, files: paths }));
5397
+ }
5398
+ }
5399
+ summary = routePlan.summary ?? routePlan.entries[0]?.summary ?? routePlan.route.reason ?? "Agent run routed";
5400
+ return await finishRoute(routePlan.route.action === "intake_create" ? "routed" : "switched");
5401
+ }
5402
+ routePlan = safeAutomaticRoutePlan(routePlan, Boolean(currentTicketId), "Route did not request an executable automatic action.");
5403
+ return await finishRoute(routePlan.route.action, routePlan.route.reason);
5404
+ } finally {
5405
+ await releaseAgentLoopLock(lockPath);
5406
+ }
5407
+ }
5408
+ async function runAgentRunCheckpoint(parsed, options, config, meta) {
5409
+ const projectId = requireProjectId(config);
5410
+ const client = await createClient(config, options);
5411
+ const agentSessionKeyValue = requireOption(parsed, "agent-session-key");
5412
+ const state = await readAgentLoopState(parsed, options);
5413
+ const sessionState = state.sessions[agentSessionKeyValue];
5414
+ if (!sessionState) {
5415
+ throw new CliError("AGENT_RUN_SESSION_NOT_FOUND", "No local agent-run session state exists for --agent-session-key.", {
5416
+ agentSessionKey: agentSessionKeyValue
5417
+ });
5418
+ }
5419
+ const savedContext = sessionState.cadenceContext;
5420
+ const currentContext = parsed.options.ticket || savedContext?.ticketId ? undefined : await readCurrentCadenceContext(client, projectId);
5421
+ const ticketId = parsed.options.ticket ?? savedContext?.ticketId ?? currentContext?.ticketId;
5422
+ const sessionId = parsed.options.session ?? savedContext?.sessionId ?? currentContext?.sessionId;
5423
+ const changesetId = parsed.options.changeset ?? savedContext?.changesetId ?? currentContext?.changesetId;
3555
5424
  if (!ticketId) {
3556
5425
  await writeAgentLoopState(parsed, options, {
3557
5426
  ...state,
@@ -3578,14 +5447,36 @@ async function runAgentRunCloseout(parsed, options, config, meta) {
3578
5447
  }
3579
5448
  const eventFile = parsed.options["event-file"] ?? sessionState.lastEventFile;
3580
5449
  const event = eventFile ? tryParseJsonObject(await readFile(eventFile, "utf8"), eventFile) : synthesizeAgentEventFromSession(agentSessionKeyValue, sessionState, options);
5450
+ const mode = checkpointModeForCommand(parsed);
3581
5451
  const logKind = parseWorkLogEntryKind(parsed.options["log-kind"] ?? "note");
3582
5452
  const updateSummary = parseBooleanOption(parsed.options["update-summary"], false);
3583
5453
  const dryRun = parseBooleanOption(parsed.options["dry-run"], false);
5454
+ const closeoutSessionAction = parseCloseoutSessionAction(parsed.options["session-action"]);
5455
+ const completeTicket = parseBooleanOption(parsed.options["complete-ticket"], false);
3584
5456
  const lockPath = parsed.options.lock;
5457
+ const checkpointSettings = await resolveCheckpointSettings(parsed, options);
3585
5458
  const gitStatus = gitOutput(["status", "--short"], options);
3586
5459
  const gitDiffStat = gitOutput(["diff", "--stat", "origin/dev..."], options);
3587
5460
  const changedFiles = gitOutput(["diff", "--name-only", "origin/dev..."], options);
5461
+ const priorCoverage = sessionState.lastCheckpointCoverage;
5462
+ const fingerprints = buildAgentRunFingerprints({
5463
+ event,
5464
+ ticketId,
5465
+ ...sessionId ? { sessionId } : {},
5466
+ ...changesetId ? { changesetId } : {},
5467
+ gitStatus,
5468
+ gitDiffStat,
5469
+ changedFiles,
5470
+ ...priorCoverage ? { coverage: priorCoverage } : {}
5471
+ });
5472
+ const closeoutContext = mode === "closeout" ? await readCloseoutPromptContext({
5473
+ client,
5474
+ projectId,
5475
+ ticketId,
5476
+ ...sessionId ? { sessionId } : {}
5477
+ }) : {};
3588
5478
  const prompt = buildCheckpointPrompt({
5479
+ mode,
3589
5480
  event,
3590
5481
  ticketId,
3591
5482
  ...sessionId ? { sessionId } : {},
@@ -3593,12 +5484,89 @@ async function runAgentRunCloseout(parsed, options, config, meta) {
3593
5484
  gitStatus,
3594
5485
  gitDiffStat,
3595
5486
  changedFiles,
3596
- ...sessionState.previousCheckpointSummary ? { previousCheckpointSummary: sessionState.previousCheckpointSummary } : {}
5487
+ ...sessionState.previousCheckpointSummary ? { previousCheckpointSummary: sessionState.previousCheckpointSummary } : {},
5488
+ ...priorCoverage ? { coverage: priorCoverage } : {},
5489
+ ...closeoutContext
3597
5490
  });
5491
+ const promptTokenAccounting = buildCheckpointTokenAccounting(event, prompt, undefined);
5492
+ if (mode === "checkpoint" && !dryRun && !priorCoverage?.hasDebt && fingerprintsEqual(sessionState.lastCheckpointFingerprints, fingerprints) && !checkpointReasonBypassesFingerprintGate(parsed.options.reason)) {
5493
+ const checkedAt = new Date().toISOString();
5494
+ const auditFile = await writeAgentCheckpointAuditFile(parsed, options, {
5495
+ version: 1,
5496
+ mode,
5497
+ action: "skipped",
5498
+ reason: "unchanged_fingerprints",
5499
+ createdAt: checkedAt,
5500
+ localOnly: true,
5501
+ localOnlyReason: "Raw hook context and checkpoint prompts stay on this machine; unchanged fingerprints did not need a model call.",
5502
+ agentSessionKey: agentSessionKeyValue,
5503
+ ticketId,
5504
+ ...sessionId ? { sessionId } : {},
5505
+ ...changesetId ? { changesetId } : {},
5506
+ ...eventFile ? { eventFile } : {},
5507
+ event,
5508
+ fingerprints,
5509
+ cadenceWrites: []
5510
+ });
5511
+ await writeAgentLoopState(parsed, options, {
5512
+ ...state,
5513
+ sessions: {
5514
+ ...state.sessions,
5515
+ [agentSessionKeyValue]: {
5516
+ ...sessionState,
5517
+ stopCount: 0,
5518
+ threshold: defaultCheckpointThresholdValue(),
5519
+ lastAction: "skipped",
5520
+ lastReason: "unchanged_fingerprints",
5521
+ lastCheckpointAt: checkedAt,
5522
+ lastCheckpointMode: mode,
5523
+ lastCheckpointFingerprints: fingerprints,
5524
+ lastCheckpointAuditFile: auditFile
5525
+ }
5526
+ }
5527
+ });
5528
+ const data = {
5529
+ action: "skipped",
5530
+ reason: "unchanged_fingerprints",
5531
+ mode,
5532
+ ticketId,
5533
+ auditFile,
5534
+ agentSessionKey: agentSessionKeyValue,
5535
+ ...sessionId ? { sessionId } : {},
5536
+ ...changesetId ? { changesetId } : {}
5537
+ };
5538
+ return {
5539
+ stdout: parsed.flags.json ? formatJson(successEnvelope(data, meta)) : `${JSON.stringify(data, null, 2)}
5540
+ `,
5541
+ stderr: "",
5542
+ exitCode: 0
5543
+ };
5544
+ }
3598
5545
  if (dryRun) {
5546
+ const auditFile = await writeAgentCheckpointAuditFile(parsed, options, {
5547
+ version: 1,
5548
+ mode,
5549
+ action: "would_checkpoint",
5550
+ createdAt: new Date().toISOString(),
5551
+ localOnly: true,
5552
+ localOnlyReason: "Raw hook context and checkpoint prompts stay on this machine and are not uploaded to Cadence.",
5553
+ agentSessionKey: agentSessionKeyValue,
5554
+ ticketId,
5555
+ ...sessionId ? { sessionId } : {},
5556
+ ...changesetId ? { changesetId } : {},
5557
+ ...eventFile ? { eventFile } : {},
5558
+ event,
5559
+ prompt,
5560
+ fingerprints,
5561
+ ...mode === "closeout" ? { coverage: priorCoverage ?? null } : {},
5562
+ checkpointSettings,
5563
+ tokenAccounting: promptTokenAccounting,
5564
+ cadenceWrites: []
5565
+ });
3599
5566
  const data = {
3600
- action: "would_closeout",
5567
+ action: "would_checkpoint",
3601
5568
  prompt,
5569
+ auditFile,
3602
5570
  ticketId,
3603
5571
  agentSessionKey: agentSessionKeyValue,
3604
5572
  ...sessionId ? { sessionId } : {},
@@ -3613,84 +5581,329 @@ async function runAgentRunCloseout(parsed, options, config, meta) {
3613
5581
  }
3614
5582
  try {
3615
5583
  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(),
5584
+ const codexStartedAtMs = Date.now();
5585
+ const codexCwd = options.cwd ?? process.cwd();
5586
+ const codexArgs = [
5587
+ "exec",
5588
+ ...checkpointSettings.model ? ["-m", checkpointSettings.model] : [],
5589
+ "--disable",
5590
+ "hooks",
5591
+ "--sandbox",
5592
+ "read-only",
5593
+ "-C",
5594
+ codexCwd,
5595
+ prompt
5596
+ ];
5597
+ const codex = runLocalCommand(codexCommand, codexArgs, options, {
5598
+ cwd: codexCwd,
3618
5599
  env: {
3619
5600
  [agentLoopSuppressEnv]: "1",
3620
5601
  CADENCE_HOOK_SUPPRESS: "1"
3621
5602
  },
3622
5603
  timeoutMs: defaultCheckpointWorkerTimeoutMs
3623
5604
  });
5605
+ const codexSessionTranscript = readCodexSessionTranscript(findCodexSessionFileCreatedAfter(options, codexStartedAtMs, codexCwd));
5606
+ const tokenAccounting = buildCheckpointTokenAccounting(event, prompt, codexSessionTranscript?.tokenUsage);
3624
5607
  if (codex.status !== 0 || codex.error) {
3625
- throw new CliError("AGENT_RUN_CLOSEOUT_FAILED", "Codex closeout generation failed.", {
5608
+ throw new CliError("AGENT_RUN_CHECKPOINT_FAILED", "Codex checkpoint generation failed.", {
3626
5609
  status: codex.status,
3627
5610
  stderr: truncateText(codex.stderr, 2000),
3628
5611
  error: codex.error?.message
3629
5612
  });
3630
5613
  }
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.");
5614
+ let checkpoint = parseCheckpointPlanJson(codex.stdout, logKind);
5615
+ if (mode === "closeout") {
5616
+ checkpoint = applyCloseoutSessionDefaults(checkpoint, closeoutSessionAction, completeTicket);
3636
5617
  }
3637
- await client.tickets.log({
3638
- projectId,
3639
- ticketId,
3640
- entry: {
3641
- entryKind: logKind,
3642
- body,
5618
+ const reductionResult = reduceCheckpointEntries(checkpoint.entries, mode);
5619
+ checkpoint = {
5620
+ ...checkpoint,
5621
+ entries: reductionResult.entries
5622
+ };
5623
+ const coverage = buildAgentRunCoverage(checkpoint, mode);
5624
+ let summary = checkpoint.summary ?? checkpoint.entries[0]?.summary ?? checkpoint.route.reason ?? "Agent run checkpoint";
5625
+ let targetTicketId = ticketId;
5626
+ let targetSessionId = sessionId;
5627
+ let targetChangesetId = changesetId;
5628
+ const cadenceWrites = [];
5629
+ const lifecycleOperations = [];
5630
+ let intakeResult;
5631
+ let selectedTicket;
5632
+ const modelAudit = {
5633
+ provider: checkpointSettings.provider,
5634
+ command: codexCommand,
5635
+ ...checkpointSettings.model ? { model: checkpointSettings.model } : {},
5636
+ status: codex.status,
5637
+ stdout: codex.stdout.trim(),
5638
+ stderr: truncateText(codex.stderr.trim(), 2000),
5639
+ tokenAccounting,
5640
+ ...codexSessionTranscript?.tokenUsage ? { tokenUsage: codexSessionTranscript.tokenUsage } : {},
5641
+ ...codexSessionTranscript ? { sessionTranscript: codexSessionTranscript } : {}
5642
+ };
5643
+ const finishCheckpoint = async (action, reason) => {
5644
+ const checkedAt = new Date().toISOString();
5645
+ const auditFile = await writeAgentCheckpointAuditFile(parsed, options, {
5646
+ version: 1,
5647
+ action,
5648
+ createdAt: checkedAt,
5649
+ localOnly: true,
5650
+ localOnlyReason: action === "noop" || action === "needs_human" || action === "reroute" ? "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.",
5651
+ agentSessionKey: agentSessionKeyValue,
5652
+ ticketId: targetTicketId,
5653
+ ...targetSessionId ? { sessionId: targetSessionId } : {},
5654
+ ...targetChangesetId ? { changesetId: targetChangesetId } : {},
5655
+ ...eventFile ? { eventFile } : {},
5656
+ event,
5657
+ prompt,
5658
+ model: modelAudit,
5659
+ checkpoint,
5660
+ route: checkpoint.route,
5661
+ mode,
5662
+ fingerprints,
5663
+ reduction: reductionResult.reduction,
5664
+ ...mode === "closeout" ? { coverage } : {},
5665
+ ...intakeResult ? { intakeResult } : {},
5666
+ ...selectedTicket ? { selectedTicket } : {},
5667
+ lifecycleOperations,
5668
+ cadenceWrites,
5669
+ summary,
5670
+ ...reason ? { reason } : {},
5671
+ entryCount: checkpoint.entries.length,
5672
+ needsHuman: action === "needs_human"
5673
+ });
5674
+ await writeAgentLoopState(parsed, options, {
5675
+ ...state,
5676
+ sessions: {
5677
+ ...state.sessions,
5678
+ [agentSessionKeyValue]: {
5679
+ ...sessionState,
5680
+ stopCount: 0,
5681
+ threshold: defaultCheckpointThresholdValue(),
5682
+ previousCheckpointSummary: summary,
5683
+ lastAction: action,
5684
+ ...reason ? { lastReason: reason } : {},
5685
+ ...eventFile ? { lastEventFile: eventFile } : {},
5686
+ cadenceContext: {
5687
+ ticketId: targetTicketId,
5688
+ ...targetSessionId ? { sessionId: targetSessionId } : {},
5689
+ ...targetChangesetId ? { changesetId: targetChangesetId } : {},
5690
+ capturedAt: checkedAt
5691
+ },
5692
+ lastCheckpointAt: checkedAt,
5693
+ lastCheckpointMode: mode,
5694
+ lastCheckpointFingerprints: fingerprints,
5695
+ lastCheckpointCoverage: coverage,
5696
+ lastCheckpointAuditFile: auditFile
5697
+ }
5698
+ }
5699
+ });
5700
+ const data = {
5701
+ action,
5702
+ route: checkpoint.route,
5703
+ ...reason ? { reason } : {},
5704
+ ticketId: targetTicketId,
3643
5705
  summary,
3644
- ...sessionId ? { sessionId } : {},
3645
- ...changesetId ? { changesetId } : {},
5706
+ entryCount: checkpoint.entries.length,
5707
+ mode,
5708
+ auditFile,
5709
+ agentSessionKey: agentSessionKeyValue,
5710
+ ...targetSessionId ? { sessionId: targetSessionId } : {},
5711
+ ...targetChangesetId ? { changesetId: targetChangesetId } : {},
5712
+ ...action === "needs_human" ? { needsHuman: true } : {}
5713
+ };
5714
+ return {
5715
+ stdout: parsed.flags.json ? formatJson(successEnvelope(data, meta)) : `${JSON.stringify(data, null, 2)}
5716
+ `,
5717
+ stderr: "",
5718
+ exitCode: 0
5719
+ };
5720
+ };
5721
+ if (checkpoint.session?.action === "complete_ticket" && mode === "closeout" && !completeTicket) {
5722
+ checkpoint = checkpointPlanNeedsHuman(checkpoint, "Closeout ticket completion requires --complete-ticket true.");
5723
+ }
5724
+ if (checkpoint.session?.action === "complete_ticket" && !checkpointPlanCompletionAllowed(event, summary)) {
5725
+ checkpoint = checkpointPlanNeedsHuman(checkpoint, "Completion requires explicit recent completion, push, PR, merge, clean branch, or verification intent.");
5726
+ }
5727
+ if (checkpoint.route.action === "noop") {
5728
+ return await finishCheckpoint("noop", checkpoint.route.reason ?? checkpoint.summary ?? "Nothing durable to record.");
5729
+ }
5730
+ if (checkpoint.route.action === "needs_human") {
5731
+ checkpoint = checkpointPlanWithRoute(checkpoint, {
5732
+ action: "noop",
5733
+ confidence: checkpoint.route.confidence,
5734
+ reason: checkpoint.route.reason ?? "Checkpoint routing was uncertain."
5735
+ });
5736
+ return await finishCheckpoint("noop", checkpoint.route.reason);
5737
+ }
5738
+ if (checkpointRouteRequiresIntake(checkpoint.route.action)) {
5739
+ if (checkpoint.route.confidence !== "high") {
5740
+ checkpoint = checkpointPlanWithRoute(checkpoint, {
5741
+ action: "noop",
5742
+ confidence: checkpoint.route.confidence,
5743
+ reason: "Checkpoint reroute requires high confidence."
5744
+ });
5745
+ return await finishCheckpoint("noop", checkpoint.route.reason);
5746
+ }
5747
+ const routeArgs = [
5748
+ "agent-run",
5749
+ "route",
5750
+ "--agent-session-key",
5751
+ agentSessionKeyValue,
5752
+ "--reason",
5753
+ "checkpoint_reroute",
5754
+ ...eventFile ? ["--event-file", eventFile] : [],
5755
+ ...parsed.flags.project ? ["--project", parsed.flags.project] : [],
5756
+ ...parsed.flags.server ? ["--server", parsed.flags.server] : []
5757
+ ];
5758
+ const rerouteResult = await finishCheckpoint("reroute", checkpoint.route.reason ?? "Checkpoint delegated routing to agent-run route.");
5759
+ await spawnAgentRunWorker(routeArgs, options);
5760
+ return rerouteResult;
5761
+ }
5762
+ const checkpointMetadata = checkpointWorkLogMetadata(checkpointSettings, mode, tokenAccounting);
5763
+ for (const entry of checkpoint.entries) {
5764
+ const logEntry = {
5765
+ entryKind: entry.kind,
5766
+ body: entry.body,
5767
+ summary: entry.summary ?? checkpointEntrySummary(entry.body),
5768
+ ...entry.parentSelector ? { parentSelector: entry.parentSelector } : {},
5769
+ ...entry.parentEntryId ? { parentEntryId: entry.parentEntryId } : {},
5770
+ ...targetSessionId ? { sessionId: targetSessionId } : {},
5771
+ ...targetChangesetId ? { changesetId: targetChangesetId } : {},
5772
+ metadata: checkpointMetadata,
3646
5773
  ...commandMetadata()
5774
+ };
5775
+ try {
5776
+ await client.tickets.log({
5777
+ projectId,
5778
+ ticketId: targetTicketId,
5779
+ entry: logEntry
5780
+ });
5781
+ cadenceWrites.push({
5782
+ type: "ticket.work_log_appended",
5783
+ success: true,
5784
+ ticketId: targetTicketId,
5785
+ entry: logEntry
5786
+ });
5787
+ } catch (error) {
5788
+ if (!entry.parentSelector && !entry.parentEntryId) {
5789
+ cadenceWrites.push({
5790
+ type: "ticket.work_log_appended",
5791
+ success: false,
5792
+ ticketId: targetTicketId,
5793
+ entry: logEntry,
5794
+ error: error instanceof Error ? error.message : String(error)
5795
+ });
5796
+ throw error;
5797
+ }
5798
+ const { parentSelector: _parentSelector, parentEntryId: _parentEntryId, ...entryWithoutParent } = logEntry;
5799
+ cadenceWrites.push({
5800
+ type: "ticket.work_log_appended",
5801
+ success: false,
5802
+ ticketId: targetTicketId,
5803
+ entry: logEntry,
5804
+ error: error instanceof Error ? error.message : String(error)
5805
+ });
5806
+ await client.tickets.log({
5807
+ projectId,
5808
+ ticketId: targetTicketId,
5809
+ entry: entryWithoutParent
5810
+ });
5811
+ cadenceWrites.push({
5812
+ type: "ticket.work_log_appended",
5813
+ success: true,
5814
+ ticketId: targetTicketId,
5815
+ entry: entryWithoutParent,
5816
+ fallback: "removed unresolved parent reference"
5817
+ });
3647
5818
  }
3648
- });
3649
- if (updateSummary) {
3650
- const ticket = await client.tickets.get({ projectId, ticketId });
5819
+ }
5820
+ if (checkpoint.files.length && targetSessionId) {
5821
+ const filesByKind = new Map;
5822
+ for (const file of checkpoint.files) {
5823
+ filesByKind.set(file.kind, [...filesByKind.get(file.kind) ?? [], file.path]);
5824
+ }
5825
+ for (const [kind, paths] of filesByKind) {
5826
+ const filesPayload = {
5827
+ ...targetChangesetId ? { changesetId: targetChangesetId } : {},
5828
+ files: paths.map((path) => ({
5829
+ path,
5830
+ changeKind: kind
5831
+ })),
5832
+ ...commandMetadata()
5833
+ };
5834
+ await client.sessions.files({
5835
+ projectId,
5836
+ sessionId: targetSessionId,
5837
+ files: filesPayload
5838
+ });
5839
+ lifecycleOperations.push(checkpointLifecycleOperation("session.files_attached", true, { sessionId: targetSessionId, kind, files: paths }));
5840
+ }
5841
+ }
5842
+ if ((updateSummary || checkpoint.summaryUpdate?.update) && checkpoint.summaryUpdate?.value) {
5843
+ const ticket = await client.tickets.get({ projectId, ticketId: targetTicketId });
3651
5844
  if (typeof ticket.projectionVersion === "number") {
3652
5845
  await client.tickets.update({
3653
5846
  projectId,
3654
- ticketId,
5847
+ ticketId: targetTicketId,
3655
5848
  ifVersion: ticket.projectionVersion,
3656
5849
  ticket: {
3657
- currentSummary: summary,
5850
+ currentSummary: checkpoint.summaryUpdate.value,
3658
5851
  ...commandMetadata()
3659
5852
  }
3660
5853
  });
5854
+ cadenceWrites.push({
5855
+ type: "ticket.updated",
5856
+ success: true,
5857
+ ticketId: targetTicketId,
5858
+ ifVersion: ticket.projectionVersion,
5859
+ ticket: {
5860
+ currentSummary: checkpoint.summaryUpdate.value
5861
+ }
5862
+ });
3661
5863
  }
3662
5864
  }
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
- }
5865
+ if (checkpoint.session?.action === "handoff" || checkpoint.session?.action === "end" || checkpoint.session?.action === "complete_ticket") {
5866
+ if (targetSessionId) {
5867
+ await client.sessions.end({
5868
+ projectId,
5869
+ sessionId: targetSessionId,
5870
+ session: {
5871
+ summary: checkpoint.session.summary ?? summary,
5872
+ ...commandMetadata()
5873
+ }
5874
+ });
5875
+ lifecycleOperations.push(checkpointLifecycleOperation("session.ended", true, { sessionId: targetSessionId, action: checkpoint.session.action }));
3673
5876
  }
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
- };
5877
+ }
5878
+ if (checkpoint.session?.action === "complete_ticket") {
5879
+ const latestTicket = await client.tickets.get({ projectId, ticketId: targetTicketId });
5880
+ if (typeof latestTicket.projectionVersion !== "number") {
5881
+ checkpoint = checkpointPlanNeedsHuman(checkpoint, "Ticket completion requires a latest ticket projection version.");
5882
+ return await finishCheckpoint("needs_human", checkpoint.route.reason);
5883
+ }
5884
+ await client.tickets.complete({
5885
+ projectId,
5886
+ ticketId: targetTicketId,
5887
+ ifVersion: latestTicket.projectionVersion,
5888
+ completion: {
5889
+ currentSummary: checkpoint.session.summary ?? checkpoint.summaryUpdate?.value ?? summary,
5890
+ ...commandMetadata()
5891
+ }
5892
+ });
5893
+ cadenceWrites.push({
5894
+ type: "ticket.completed",
5895
+ success: true,
5896
+ ticketId: targetTicketId,
5897
+ ifVersion: latestTicket.projectionVersion,
5898
+ summary: checkpoint.session.summary ?? checkpoint.summaryUpdate?.value ?? summary
5899
+ });
5900
+ }
5901
+ return await finishCheckpoint("checkpointed");
3689
5902
  } finally {
3690
5903
  await releaseAgentLoopLock(lockPath);
3691
5904
  }
3692
5905
  }
3693
- async function runAgentRunSweep(parsed, options, meta) {
5906
+ async function runAgentRunSweep(parsed, options, config, meta) {
3694
5907
  const state = await readAgentLoopState(parsed, options);
3695
5908
  const idleAfterSeconds = parsePositiveInteger(parsed.options["idle-after-seconds"], "--idle-after-seconds") ?? 5 * 60;
3696
5909
  const dryRun = parseBooleanOption(parsed.options["dry-run"], false);
@@ -3709,14 +5922,30 @@ async function runAgentRunSweep(parsed, options, meta) {
3709
5922
  lastObservedAt: session.lastObservedAt
3710
5923
  }));
3711
5924
  if (!dryRun) {
5925
+ let nextState = state;
3712
5926
  for (const staleSession of staleSessions) {
3713
- await spawnAgentRunCloseout([
5927
+ const existingSession = nextState.sessions[staleSession.agentSessionKey];
5928
+ const hasContext = Boolean(existingSession?.cadenceContext?.ticketId);
5929
+ if (existingSession) {
5930
+ nextState = {
5931
+ ...nextState,
5932
+ sessions: {
5933
+ ...nextState.sessions,
5934
+ [staleSession.agentSessionKey]: {
5935
+ ...existingSession,
5936
+ lastAction: "sweep_spawned"
5937
+ }
5938
+ }
5939
+ };
5940
+ await writeAgentLoopState(parsed, options, nextState);
5941
+ }
5942
+ await spawnAgentRunCheckpoint([
3714
5943
  "agent-run",
3715
- "closeout",
5944
+ hasContext ? "checkpoint" : "route",
3716
5945
  "--agent-session-key",
3717
5946
  staleSession.agentSessionKey,
3718
5947
  "--reason",
3719
- "idle",
5948
+ hasContext ? "idle" : "missing_context",
3720
5949
  "--lock",
3721
5950
  agentLoopLockPath(parsed, options, staleSession.agentSessionKey),
3722
5951
  ...parsed.flags.project ? ["--project", parsed.flags.project] : [],
@@ -4158,6 +6387,7 @@ async function runIntakeCommand(parsed, options) {
4158
6387
  ...parsed.options.changeset ? { changesetId: parsed.options.changeset } : {},
4159
6388
  ...parsed.options.actor ? { actorId: parsed.options.actor } : {},
4160
6389
  expiresAt: leaseExpiresAt(parsePositiveInteger(parsed.options["ttl-seconds"], "--ttl-seconds") ?? defaultLeaseTtlSeconds),
6390
+ ...parseBooleanOption(parsed.options["replace-own-active"], false) ? { replaceOwnActiveLease: true } : {},
4161
6391
  ...commandMetadata()
4162
6392
  }
4163
6393
  });
@@ -4383,7 +6613,7 @@ async function runCli(argv, options = {}) {
4383
6613
  if (parsed.command.name === "projects.list") {
4384
6614
  return await runProjectCommand(parsed, options);
4385
6615
  }
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") {
6616
+ if (parsed.command.name === "agent-run.ingest-stop" || parsed.command.name === "agent-run.route" || 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
6617
  return await runAgentRunCommand(parsed, options);
4388
6618
  }
4389
6619
  if (parsed.command.name === "hooks.install" || parsed.command.name === "hooks.doctor") {