@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.
- package/README.md +32 -0
- package/dist/cadence +2395 -165
- 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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2285
|
-
" cadence hooks doctor --provider codex --scope
|
|
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
|
|
2702
|
-
return
|
|
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 ? `
|
|
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 :
|
|
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
|
-
...
|
|
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
|
|
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.
|
|
3486
|
+
const activeLease = activeLeases.filter((lease) => {
|
|
2928
3487
|
if (!lease || typeof lease !== "object") {
|
|
2929
3488
|
return false;
|
|
2930
3489
|
}
|
|
2931
|
-
const
|
|
2932
|
-
return typeof
|
|
2933
|
-
});
|
|
2934
|
-
if (
|
|
2935
|
-
|
|
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
|
|
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
|
-
|
|
3515
|
+
const sessionRecord = session;
|
|
3516
|
+
return sessionRecord.status === "active" && typeof sessionRecord.ticketId === "string";
|
|
2944
3517
|
});
|
|
2945
|
-
const
|
|
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
|
-
...
|
|
2948
|
-
...sessionId ? {
|
|
2949
|
-
|
|
2950
|
-
|
|
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: "
|
|
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
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
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
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
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
|
-
|
|
3028
|
-
${
|
|
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) =>
|
|
3075
|
-
const
|
|
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) =>
|
|
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
|
|
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 ??
|
|
3341
|
-
const
|
|
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
|
-
...
|
|
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 (
|
|
4742
|
+
if (duplicateTurn) {
|
|
3364
4743
|
await writeAgentLoopState(parsed, options, countedState);
|
|
3365
4744
|
const data2 = {
|
|
3366
|
-
action: "
|
|
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 (
|
|
3379
|
-
await
|
|
3380
|
-
|
|
3381
|
-
|
|
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:
|
|
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
|
-
"
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
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
|
|
3552
|
-
const
|
|
3553
|
-
const
|
|
3554
|
-
|
|
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: "
|
|
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
|
|
3617
|
-
|
|
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("
|
|
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
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
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
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
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
|
-
|
|
3645
|
-
|
|
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 (
|
|
3650
|
-
const
|
|
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:
|
|
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
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
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
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
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") {
|