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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cadence +1031 -151
  2. package/package.json +1 -1
package/dist/cadence CHANGED
@@ -1520,7 +1520,7 @@ import { createInterface } from "readline/promises";
1520
1520
  // package.json
1521
1521
  var package_default = {
1522
1522
  name: "@trycadence/cli",
1523
- version: "0.1.14-dev.0",
1523
+ version: "0.1.16-dev.0",
1524
1524
  private: false,
1525
1525
  type: "module",
1526
1526
  bin: {
@@ -1554,6 +1554,8 @@ var workLogParentSelectors = ["last", "ticket-last", "session-last", "last-decis
1554
1554
  var changesetPrNoteSources = ["agent", "human", "system"];
1555
1555
  var hookScopes = ["repo", "global", "both"];
1556
1556
  var agentEventSources = ["codex", "claude-code", "opencode", "openrouter", "unknown"];
1557
+ var agentRunMemoryModes = ["checkpoint", "closeout"];
1558
+ var closeoutSessionActions = ["handoff", "end", "keep"];
1557
1559
  var defaultLeaseTtlSeconds = 15 * 60;
1558
1560
  var defaultCliApiBaseUrl = "https://cadenceapi.deploy.lvl8studios.com";
1559
1561
  var defaultCliWebBaseUrl = "https://cadence.deploy.lvl8studios.com";
@@ -1599,6 +1601,7 @@ var knownCommandPaths = [
1599
1601
  ["changesets", "notes", "put"],
1600
1602
  ["changesets", "notes", "apply"],
1601
1603
  ["agent-run", "ingest-stop"],
1604
+ ["agent-run", "route"],
1602
1605
  ["agent-run", "checkpoint"],
1603
1606
  ["agent-run", "closeout"],
1604
1607
  ["agent-run", "sweep"],
@@ -2279,7 +2282,9 @@ function helpText() {
2279
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]",
2280
2283
  " cadence changesets notes apply [--changeset <id>|--branch current|<branch>] --provider github --pr-number <n> --pr-url <url> [--project <project-id>] [--json]",
2281
2284
  " cadence agent-run ingest-stop --source <codex|claude-code|opencode|openrouter> [--event <event>] [--threshold <n>] [--dry-run true|false] [--project <project-id>] [--json]",
2285
+ " cadence agent-run route --agent-session-key <key> --reason <missing_context|checkpoint_reroute|manual> [--event-file <path>] [--checkpoint-provider codex] [--checkpoint-model <model>] [--json]",
2282
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]",
2283
2288
  " cadence agent-run sweep [--idle-after-seconds <n>] [--dry-run true|false] [--json]",
2284
2289
  " cadence agent-run doctor [--json]",
2285
2290
  " cadence hooks install --provider codex [--scope global] [--command <command>] [--json]",
@@ -3085,9 +3090,10 @@ function normalizeGenericAgentEvent(input, base) {
3085
3090
  function agentSessionKey(source, agentSessionId) {
3086
3091
  return `${source}:${stableHash(agentSessionId)}`;
3087
3092
  }
3088
- function checkpointWorkLogMetadata(settings, tokenAccounting) {
3093
+ function checkpointWorkLogMetadata(settings, mode, tokenAccounting) {
3089
3094
  return {
3090
3095
  checkpoint: {
3096
+ ...mode ? { mode } : {},
3091
3097
  provider: settings.provider,
3092
3098
  ...settings.model ? { model: settings.model } : {},
3093
3099
  ...tokenAccounting ? { tokenAccounting } : {}
@@ -3248,12 +3254,54 @@ function readAgentLoopSessions(rawSessions) {
3248
3254
  ...recentTurns ? { recentTurns } : {},
3249
3255
  ...typeof record.lastCheckpointAt === "string" ? { lastCheckpointAt: record.lastCheckpointAt } : {},
3250
3256
  ...typeof record.lastCheckpointFingerprint === "string" ? { lastCheckpointFingerprint: record.lastCheckpointFingerprint } : {},
3257
+ ...readAgentRunFingerprints(record.lastCheckpointFingerprints),
3258
+ ...typeof record.lastCheckpointMode === "string" && agentRunMemoryModes.includes(record.lastCheckpointMode) ? { lastCheckpointMode: record.lastCheckpointMode } : {},
3259
+ ...readAgentRunCoverage(record.lastCheckpointCoverage),
3251
3260
  ...typeof record.previousCheckpointSummary === "string" ? { previousCheckpointSummary: record.previousCheckpointSummary } : {},
3252
3261
  ...typeof record.lastCheckpointAuditFile === "string" ? { lastCheckpointAuditFile: record.lastCheckpointAuditFile } : {}
3253
3262
  };
3254
3263
  }
3255
3264
  return sessions;
3256
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
+ }
3257
3305
  function readAgentSessionCadenceContext(record) {
3258
3306
  const value = record.cadenceContext;
3259
3307
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -3396,7 +3444,7 @@ function currentCliWorkerInvocation() {
3396
3444
  argsPrefix: []
3397
3445
  };
3398
3446
  }
3399
- async function spawnAgentRunCheckpoint(args, options) {
3447
+ async function spawnAgentRunWorker(args, options) {
3400
3448
  const cwd = options.cwd ?? process.cwd();
3401
3449
  const invocation = currentCliWorkerInvocation();
3402
3450
  const env = {
@@ -3415,6 +3463,9 @@ async function spawnAgentRunCheckpoint(args, options) {
3415
3463
  });
3416
3464
  child.unref();
3417
3465
  }
3466
+ async function spawnAgentRunCheckpoint(args, options) {
3467
+ await spawnAgentRunWorker(args, options);
3468
+ }
3418
3469
  function currentRecordTime(value, keys) {
3419
3470
  for (const key of keys) {
3420
3471
  const candidate = value[key];
@@ -3756,9 +3807,6 @@ function parseCheckpointPlanFiles(value) {
3756
3807
  };
3757
3808
  });
3758
3809
  }
3759
- function routeRequiresHighConfidence(route, session) {
3760
- return highAutonomyRouteActions.has(route.action) || session?.action === "complete_ticket";
3761
- }
3762
3810
  function forceNeedsHuman(plan, reason) {
3763
3811
  return {
3764
3812
  ...plan,
@@ -3771,9 +3819,6 @@ function forceNeedsHuman(plan, reason) {
3771
3819
  };
3772
3820
  }
3773
3821
  function normalizeCheckpointPlan(plan) {
3774
- if (routeRequiresHighConfidence(plan.route, plan.session) && plan.route.confidence !== "high") {
3775
- return forceNeedsHuman(plan, "Lifecycle mutation requires high confidence.");
3776
- }
3777
3822
  if (plan.route.action === "noop") {
3778
3823
  return {
3779
3824
  ...plan,
@@ -3782,6 +3827,24 @@ function normalizeCheckpointPlan(plan) {
3782
3827
  }
3783
3828
  return plan;
3784
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
+ }
3785
3848
  function parseCheckpointPlanRecord(record, rawText, fallbackKind) {
3786
3849
  if (!("route" in record)) {
3787
3850
  const legacy = parseCheckpointRecord(record, rawText, fallbackKind);
@@ -3977,23 +4040,39 @@ function buildCheckpointTokenAccounting(event, prompt, tokenUsage) {
3977
4040
  function buildCheckpointPrompt(input) {
3978
4041
  const recentTurns = checkpointRecentTurns(input.event);
3979
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
+ ];
3980
4053
  return [
3981
- "You are generating a compact Cadence dogfood operation plan for a hook-spawned checkpoint worker.",
4054
+ `You are generating a compact Cadence dogfood operation plan for an agent-run ${input.mode} worker.`,
3982
4055
  "The model judges; the Cadence CLI validates and executes. Return JSON only. Do not call tools to mutate Cadence yourself.",
3983
4056
  "Preserve durable ticket purpose. Identify user intent, changed intent, corrections, decisions, rationale, implementation actions, verification, blockers, and useful notes.",
3984
4057
  "Do not include raw diffs, raw transcripts, terminal logs, tool streams, secrets, file contents, or model reasoning in server-bound fields.",
3985
- 'Return this sparse JSON shape: {"summary":"short checkpoint summary","route":{"action":"current|noop|intake_create|intake_attach|switch_existing|needs_human","confidence":"low|medium|high","reason":"short reason","request":"natural language work request when intake is needed","targetTicketId":"optional existing ticket id"},"entries":[{"kind":"intent|decision|rationale|action|verification|blocker|correction|note","summary":"short entry summary","body":"safe Cadence work-log body","under":"optional: last, ticket-last, session-last, last-decision, last-correction, last-action, or entry UUID"}],"summaryUpdate":{"update":false,"value":null,"reason":"short reason"},"session":{"action":"keep|handoff|end|complete_ticket","summary":"optional handoff or completion summary","reason":"short reason"},"files":[{"path":"relative/path.ts","kind":"added|modified|deleted|renamed|unknown"}]}',
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"}]}',
3986
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.",
3987
- "Use route.action current for the same ticket. Use intake_create for clearly unrelated new work. Use intake_attach or switch_existing only when a specific existing ticket is clearly the right target. Use needs_human when routing is ambiguous.",
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.",
3988
4061
  "Set route.confidence high only when the recent user/assistant context makes the route and lifecycle action clear. Lifecycle mutations require high confidence.",
3989
4062
  "Use note only as a last-resort context kind. Prefer intent, decision, rationale, action, verification, correction, or blocker when those fit.",
3990
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.",
3991
4064
  "Set summaryUpdate.update true only when the durable current work summary is missing or misleading; otherwise omit summaryUpdate or leave update false.",
3992
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,
3993
4067
  "",
3994
4068
  `Ticket: ${input.ticketId}`,
3995
4069
  input.sessionId ? `Session: ${input.sessionId}` : "",
3996
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)}` : "",
3997
4076
  `Agent event: ${input.event.source}/${input.event.event}`,
3998
4077
  input.previousCheckpointSummary ? `Previous checkpoint summary: ${input.previousCheckpointSummary}` : "",
3999
4078
  recentTurnsText ? `Recent user/assistant turns (most recent 3, local checkpoint context only):
@@ -4008,6 +4087,297 @@ ${truncateText(input.changedFiles, 2000)}` : "Changed files: unavailable"
4008
4087
  ].filter(Boolean).join(`
4009
4088
  `);
4010
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,
4110
+ `Agent event: ${input.event.source}/${input.event.event}`,
4111
+ input.previousCheckpointSummary ? `Previous checkpoint summary: ${input.previousCheckpointSummary}` : "",
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",
4115
+ input.gitStatus ? `Git status --short:
4116
+ ${truncateText(input.gitStatus, 2000)}` : "Git status --short: clean or unavailable",
4117
+ input.gitDiffStat ? `Git diff --stat origin/dev...:
4118
+ ${truncateText(input.gitDiffStat, 2000)}` : "Git diff stat: unavailable",
4119
+ input.changedFiles ? `Changed files:
4120
+ ${truncateText(input.changedFiles, 2000)}` : "Changed files: unavailable"
4121
+ ].filter(Boolean).join(`
4122
+ `);
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
+ }
4011
4381
  async function findRepoRoot(cwd) {
4012
4382
  const result = spawnSync("git", ["rev-parse", "--show-toplevel"], {
4013
4383
  cwd,
@@ -4246,6 +4616,8 @@ async function runAgentRunCommand(parsed, options) {
4246
4616
  switch (parsed.command.name) {
4247
4617
  case "agent-run.ingest-stop":
4248
4618
  return await runAgentRunIngestStop(parsed, options, config, meta);
4619
+ case "agent-run.route":
4620
+ return await runAgentRunRoute(parsed, options, config, meta);
4249
4621
  case "agent-run.checkpoint":
4250
4622
  case "agent-run.closeout":
4251
4623
  return await runAgentRunCheckpoint(parsed, options, config, meta);
@@ -4335,11 +4707,14 @@ async function runAgentRunIngestStop(parsed, options, config, meta) {
4335
4707
  const duplicateTurn = Boolean(normalized.turnId && existingSession?.lastObservedTurnId === normalized.turnId);
4336
4708
  const nextCount = (existingSession?.stopCount ?? 0) + (duplicateTurn ? 0 : 1);
4337
4709
  const recentTurns = mergeRecentAgentTurns(existingSession?.recentTurns, normalized.recentTurns);
4338
- let cadenceContext = existingSession?.cadenceContext;
4339
- try {
4340
- const client = await createClient(config, options);
4341
- cadenceContext = await readCurrentCadenceContext(client, projectId) ?? cadenceContext;
4342
- } catch {}
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
+ }
4343
4718
  const observedSession = {
4344
4719
  ...clearAgentSessionReason(existingSession ?? {
4345
4720
  source: normalized.source,
@@ -4364,6 +4739,124 @@ async function runAgentRunIngestStop(parsed, options, config, meta) {
4364
4739
  [normalized.agentSessionKey]: observedSession
4365
4740
  }
4366
4741
  };
4742
+ if (duplicateTurn) {
4743
+ await writeAgentLoopState(parsed, options, countedState);
4744
+ const data2 = {
4745
+ action: "updated",
4746
+ reason: "duplicate_turn",
4747
+ agentSessionKey: normalized.agentSessionKey,
4748
+ stopCount: nextCount,
4749
+ threshold
4750
+ };
4751
+ return {
4752
+ stdout: parsed.flags.json ? formatJson(successEnvelope(data2, meta)) : `${JSON.stringify(data2, null, 2)}
4753
+ `,
4754
+ stderr: "",
4755
+ exitCode: 0
4756
+ };
4757
+ }
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
+ }
4367
4860
  if (nextCount < threshold) {
4368
4861
  await writeAgentLoopState(parsed, options, countedState);
4369
4862
  const data2 = {
@@ -4537,7 +5030,10 @@ async function runAgentRunIngestStop(parsed, options, config, meta) {
4537
5030
  exitCode: 0
4538
5031
  };
4539
5032
  }
4540
- async function runAgentRunCheckpoint(parsed, options, config, meta) {
5033
+ function objectStringId(value) {
5034
+ return value && typeof value === "object" && "id" in value && typeof value.id === "string" ? value.id : undefined;
5035
+ }
5036
+ async function runAgentRunRoute(parsed, options, config, meta) {
4541
5037
  const projectId = requireProjectId(config);
4542
5038
  const client = await createClient(config, options);
4543
5039
  const agentSessionKeyValue = requireOption(parsed, "agent-session-key");
@@ -4549,66 +5045,36 @@ async function runAgentRunCheckpoint(parsed, options, config, meta) {
4549
5045
  });
4550
5046
  }
4551
5047
  const savedContext = sessionState.cadenceContext;
4552
- const currentContext = parsed.options.ticket || savedContext?.ticketId ? undefined : await readCurrentCadenceContext(client, projectId);
4553
- const ticketId = parsed.options.ticket ?? savedContext?.ticketId ?? currentContext?.ticketId;
4554
- const sessionId = parsed.options.session ?? savedContext?.sessionId ?? currentContext?.sessionId;
4555
- const changesetId = parsed.options.changeset ?? savedContext?.changesetId ?? currentContext?.changesetId;
4556
- if (!ticketId) {
4557
- await writeAgentLoopState(parsed, options, {
4558
- ...state,
4559
- sessions: {
4560
- ...state.sessions,
4561
- [agentSessionKeyValue]: {
4562
- ...sessionState,
4563
- lastAction: "skipped",
4564
- lastReason: "no_active_ticket"
4565
- }
4566
- }
4567
- });
4568
- const data = {
4569
- action: "skipped",
4570
- reason: "no_active_ticket",
4571
- agentSessionKey: agentSessionKeyValue
4572
- };
4573
- return {
4574
- stdout: parsed.flags.json ? formatJson(successEnvelope(data, meta)) : `${JSON.stringify(data, null, 2)}
4575
- `,
4576
- stderr: "",
4577
- exitCode: 0
4578
- };
4579
- }
5048
+ const currentContext = savedContext?.ticketId ? undefined : await readCurrentCadenceContext(client, projectId);
4580
5049
  const eventFile = parsed.options["event-file"] ?? sessionState.lastEventFile;
4581
5050
  const event = eventFile ? tryParseJsonObject(await readFile(eventFile, "utf8"), eventFile) : synthesizeAgentEventFromSession(agentSessionKeyValue, sessionState, options);
4582
- const logKind = parseWorkLogEntryKind(parsed.options["log-kind"] ?? "note");
4583
- const updateSummary = parseBooleanOption(parsed.options["update-summary"], false);
4584
- const dryRun = parseBooleanOption(parsed.options["dry-run"], false);
4585
- const lockPath = parsed.options.lock;
4586
5051
  const checkpointSettings = await resolveCheckpointSettings(parsed, options);
4587
5052
  const gitStatus = gitOutput(["status", "--short"], options);
4588
5053
  const gitDiffStat = gitOutput(["diff", "--stat", "origin/dev..."], options);
4589
5054
  const changedFiles = gitOutput(["diff", "--name-only", "origin/dev..."], options);
4590
- const prompt = buildCheckpointPrompt({
5055
+ const prompt = buildRoutePrompt({
4591
5056
  event,
4592
- ticketId,
4593
- ...sessionId ? { sessionId } : {},
4594
- ...changesetId ? { changesetId } : {},
5057
+ ...parsed.options.reason ? { reason: parsed.options.reason } : {},
5058
+ ...savedContext ? { savedContext } : {},
5059
+ ...currentContext ? { currentContext } : {},
4595
5060
  gitStatus,
4596
5061
  gitDiffStat,
4597
5062
  changedFiles,
4598
5063
  ...sessionState.previousCheckpointSummary ? { previousCheckpointSummary: sessionState.previousCheckpointSummary } : {}
4599
5064
  });
4600
5065
  const promptTokenAccounting = buildCheckpointTokenAccounting(event, prompt, undefined);
5066
+ const dryRun = parseBooleanOption(parsed.options["dry-run"], false);
5067
+ const lockPath = parsed.options.lock;
4601
5068
  if (dryRun) {
4602
5069
  const auditFile = await writeAgentCheckpointAuditFile(parsed, options, {
4603
5070
  version: 1,
4604
- action: "would_checkpoint",
5071
+ mode: "route",
5072
+ action: "would_route",
4605
5073
  createdAt: new Date().toISOString(),
4606
5074
  localOnly: true,
4607
- localOnlyReason: "Raw hook context and checkpoint prompts stay on this machine and are not uploaded to Cadence.",
5075
+ localOnlyReason: "Raw hook context and route prompts stay on this machine and are not uploaded to Cadence.",
4608
5076
  agentSessionKey: agentSessionKeyValue,
4609
- ticketId,
4610
- ...sessionId ? { sessionId } : {},
4611
- ...changesetId ? { changesetId } : {},
5077
+ ...savedContext?.ticketId ? { ticketId: savedContext.ticketId } : currentContext?.ticketId ? { ticketId: currentContext.ticketId } : {},
4612
5078
  ...eventFile ? { eventFile } : {},
4613
5079
  event,
4614
5080
  prompt,
@@ -4617,13 +5083,10 @@ async function runAgentRunCheckpoint(parsed, options, config, meta) {
4617
5083
  cadenceWrites: []
4618
5084
  });
4619
5085
  const data = {
4620
- action: "would_checkpoint",
5086
+ action: "would_route",
4621
5087
  prompt,
4622
5088
  auditFile,
4623
- ticketId,
4624
- agentSessionKey: agentSessionKeyValue,
4625
- ...sessionId ? { sessionId } : {},
4626
- ...changesetId ? { changesetId } : {}
5089
+ agentSessionKey: agentSessionKeyValue
4627
5090
  };
4628
5091
  return {
4629
5092
  stdout: parsed.flags.json ? formatJson(successEnvelope(data, meta)) : `${JSON.stringify(data, null, 2)}
@@ -4658,21 +5121,37 @@ async function runAgentRunCheckpoint(parsed, options, config, meta) {
4658
5121
  const codexSessionTranscript = readCodexSessionTranscript(findCodexSessionFileCreatedAfter(options, codexStartedAtMs, codexCwd));
4659
5122
  const tokenAccounting = buildCheckpointTokenAccounting(event, prompt, codexSessionTranscript?.tokenUsage);
4660
5123
  if (codex.status !== 0 || codex.error) {
4661
- throw new CliError("AGENT_RUN_CHECKPOINT_FAILED", "Codex checkpoint generation failed.", {
5124
+ throw new CliError("AGENT_RUN_ROUTE_FAILED", "Codex route generation failed.", {
4662
5125
  status: codex.status,
4663
5126
  stderr: truncateText(codex.stderr, 2000),
4664
5127
  error: codex.error?.message
4665
5128
  });
4666
5129
  }
4667
- let checkpoint = parseCheckpointPlanJson(codex.stdout, logKind);
4668
- let summary = checkpoint.summary ?? checkpoint.entries[0]?.summary ?? checkpoint.route.reason ?? "Agent run checkpoint";
4669
- let targetTicketId = ticketId;
4670
- let targetSessionId = sessionId;
4671
- let targetChangesetId = changesetId;
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
+ }
4672
5147
  const cadenceWrites = [];
4673
5148
  const lifecycleOperations = [];
5149
+ let targetTicketId = currentTicketId;
5150
+ let targetSessionId = currentSessionId;
5151
+ let targetChangesetId = currentChangesetId;
4674
5152
  let intakeResult;
4675
5153
  let selectedTicket;
5154
+ let summary = routePlan.summary ?? routePlan.entries[0]?.summary ?? routePlan.route.reason ?? "Agent run route";
4676
5155
  const modelAudit = {
4677
5156
  provider: checkpointSettings.provider,
4678
5157
  command: codexCommand,
@@ -4684,68 +5163,72 @@ async function runAgentRunCheckpoint(parsed, options, config, meta) {
4684
5163
  ...codexSessionTranscript?.tokenUsage ? { tokenUsage: codexSessionTranscript.tokenUsage } : {},
4685
5164
  ...codexSessionTranscript ? { sessionTranscript: codexSessionTranscript } : {}
4686
5165
  };
4687
- const finishCheckpoint = async (action, reason) => {
5166
+ const finishRoute = async (action, reason) => {
4688
5167
  const checkedAt = new Date().toISOString();
4689
5168
  const auditFile = await writeAgentCheckpointAuditFile(parsed, options, {
4690
5169
  version: 1,
5170
+ mode: "route",
4691
5171
  action,
4692
5172
  createdAt: checkedAt,
4693
5173
  localOnly: true,
4694
- localOnlyReason: action === "noop" || action === "needs_human" ? "Raw hook context, checkpoint prompts, and model output stay on this machine; no Cadence writes were needed." : "Raw hook context, checkpoint prompts, and model output stay on this machine; Cadence receives only the structured writes listed here.",
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.",
4695
5175
  agentSessionKey: agentSessionKeyValue,
4696
- ticketId: targetTicketId,
5176
+ ...targetTicketId ? { ticketId: targetTicketId } : {},
4697
5177
  ...targetSessionId ? { sessionId: targetSessionId } : {},
4698
5178
  ...targetChangesetId ? { changesetId: targetChangesetId } : {},
4699
5179
  ...eventFile ? { eventFile } : {},
4700
5180
  event,
4701
5181
  prompt,
4702
5182
  model: modelAudit,
4703
- checkpoint,
4704
- route: checkpoint.route,
4705
- ...intakeResult ? { intakeResult } : {},
4706
- ...selectedTicket ? { selectedTicket } : {},
5183
+ checkpoint: routePlan,
5184
+ route: routePlan.route,
5185
+ intakeResult,
5186
+ selectedTicket,
4707
5187
  lifecycleOperations,
4708
5188
  cadenceWrites,
4709
5189
  summary,
4710
5190
  ...reason ? { reason } : {},
4711
- entryCount: checkpoint.entries.length,
4712
- needsHuman: action === "needs_human"
5191
+ entryCount: routePlan.entries.length
4713
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
+ };
4714
5213
  await writeAgentLoopState(parsed, options, {
4715
5214
  ...state,
4716
5215
  sessions: {
4717
5216
  ...state.sessions,
4718
- [agentSessionKeyValue]: {
4719
- ...sessionState,
4720
- stopCount: 0,
4721
- threshold: defaultCheckpointThresholdValue(),
4722
- previousCheckpointSummary: summary,
4723
- lastAction: action,
4724
- ...reason ? { lastReason: reason } : {},
4725
- ...eventFile ? { lastEventFile: eventFile } : {},
4726
- cadenceContext: {
4727
- ticketId: targetTicketId,
4728
- ...targetSessionId ? { sessionId: targetSessionId } : {},
4729
- ...targetChangesetId ? { changesetId: targetChangesetId } : {},
4730
- capturedAt: checkedAt
4731
- },
4732
- lastCheckpointAt: checkedAt,
4733
- lastCheckpointAuditFile: auditFile
4734
- }
5217
+ [agentSessionKeyValue]: nextSessionState
4735
5218
  }
4736
5219
  });
4737
5220
  const data = {
4738
5221
  action,
4739
- route: checkpoint.route,
5222
+ route: routePlan.route,
4740
5223
  ...reason ? { reason } : {},
4741
- ticketId: targetTicketId,
5224
+ ...targetTicketId ? { ticketId: targetTicketId } : {},
4742
5225
  summary,
4743
- entryCount: checkpoint.entries.length,
5226
+ entryCount: routePlan.entries.length,
5227
+ mode: "route",
4744
5228
  auditFile,
4745
5229
  agentSessionKey: agentSessionKeyValue,
4746
5230
  ...targetSessionId ? { sessionId: targetSessionId } : {},
4747
- ...targetChangesetId ? { changesetId: targetChangesetId } : {},
4748
- ...action === "needs_human" ? { needsHuman: true } : {}
5231
+ ...targetChangesetId ? { changesetId: targetChangesetId } : {}
4749
5232
  };
4750
5233
  return {
4751
5234
  stdout: parsed.flags.json ? formatJson(successEnvelope(data, meta)) : `${JSON.stringify(data, null, 2)}
@@ -4754,17 +5237,14 @@ async function runAgentRunCheckpoint(parsed, options, config, meta) {
4754
5237
  exitCode: 0
4755
5238
  };
4756
5239
  };
4757
- if (checkpoint.session?.action === "complete_ticket" && !checkpointPlanCompletionAllowed(event, summary)) {
4758
- checkpoint = checkpointPlanNeedsHuman(checkpoint, "Completion requires explicit recent completion, push, PR, merge, clean branch, or verification intent.");
5240
+ if (routePlan.route.action === "noop") {
5241
+ return await finishRoute("noop", routePlan.route.reason ?? routePlan.summary ?? "Nothing durable to route.");
4759
5242
  }
4760
- if (checkpoint.route.action === "noop") {
4761
- return await finishCheckpoint("noop", checkpoint.route.reason ?? checkpoint.summary ?? "Nothing durable to record.");
5243
+ if (routePlan.route.action === "current") {
5244
+ return await finishRoute("current", routePlan.route.reason ?? "Kept current Cadence context.");
4762
5245
  }
4763
- if (checkpoint.route.action === "needs_human") {
4764
- return await finishCheckpoint("needs_human", checkpoint.route.reason ?? "Checkpoint routing needs human review.");
4765
- }
4766
- if (checkpointRouteRequiresIntake(checkpoint.route.action)) {
4767
- const intakeRequest = checkpoint.route.request ?? checkpointPlanDescription(checkpoint);
5246
+ if (routePlanAllowsLifecycle(routePlan)) {
5247
+ const intakeRequest = routePlan.route.request ?? checkpointPlanDescription(routePlan);
4768
5248
  intakeResult = await client.intake.create({
4769
5249
  projectId,
4770
5250
  intake: {
@@ -4773,34 +5253,33 @@ async function runAgentRunCheckpoint(parsed, options, config, meta) {
4773
5253
  }
4774
5254
  });
4775
5255
  lifecycleOperations.push(checkpointLifecycleOperation("intake.created", true, { request: intakeRequest, result: intakeResult }));
4776
- if (checkpointHasConflictingIntakeResult(intakeResult, checkpoint)) {
4777
- checkpoint = checkpointPlanNeedsHuman(checkpoint, "Intake returned conflicting duplicate, overlap, or completed-before candidates.");
4778
- return await finishCheckpoint("needs_human", checkpoint.route.reason);
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);
4779
5259
  }
4780
- if (checkpoint.route.action === "intake_create") {
5260
+ if (routePlan.route.action === "intake_create") {
4781
5261
  selectedTicket = await client.tickets.create({
4782
5262
  projectId,
4783
5263
  ticket: {
4784
- title: checkpointPlanTitle(checkpoint),
4785
- description: checkpointPlanDescription(checkpoint),
4786
- fromIntakeId: typeof intakeResult === "object" && intakeResult && "id" in intakeResult ? String(intakeResult.id) : undefined,
5264
+ title: checkpointPlanTitle(routePlan),
5265
+ description: checkpointPlanDescription(routePlan),
5266
+ fromIntakeId: objectStringId(intakeResult),
4787
5267
  ...commandMetadata()
4788
5268
  }
4789
5269
  });
4790
5270
  lifecycleOperations.push(checkpointLifecycleOperation("ticket.created", true, { ticket: selectedTicket }));
4791
- if (selectedTicket && typeof selectedTicket === "object" && "id" in selectedTicket) {
4792
- targetTicketId = String(selectedTicket.id);
4793
- }
5271
+ targetTicketId = objectStringId(selectedTicket);
4794
5272
  } else {
4795
- if (!checkpoint.route.targetTicketId) {
4796
- checkpoint = checkpointPlanNeedsHuman(checkpoint, "Checkpoint route requires a target ticket id.");
4797
- return await finishCheckpoint("needs_human", checkpoint.route.reason);
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);
4798
5277
  }
4799
- targetTicketId = checkpoint.route.targetTicketId;
5278
+ targetTicketId = selectedTicketId;
4800
5279
  selectedTicket = await client.tickets.get({ projectId, ticketId: targetTicketId });
4801
5280
  lifecycleOperations.push(checkpointLifecycleOperation("ticket.selected", true, { ticketId: targetTicketId, ticket: selectedTicket }));
4802
5281
  const selectedVersion = selectedTicket && typeof selectedTicket === "object" && typeof selectedTicket.projectionVersion === "number" ? selectedTicket.projectionVersion : undefined;
4803
- const intakeId = intakeResult && typeof intakeResult === "object" && "id" in intakeResult ? String(intakeResult.id) : undefined;
5282
+ const intakeId = objectStringId(intakeResult);
4804
5283
  if (selectedVersion !== undefined && intakeId) {
4805
5284
  await client.tickets.attach({
4806
5285
  projectId,
@@ -4811,16 +5290,24 @@ async function runAgentRunCheckpoint(parsed, options, config, meta) {
4811
5290
  lifecycleOperations.push(checkpointLifecycleOperation("intake.attached", true, { ticketId: targetTicketId, intakeId }));
4812
5291
  }
4813
5292
  }
4814
- if (targetTicketId !== ticketId && targetSessionId) {
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) {
4815
5302
  await client.sessions.end({
4816
5303
  projectId,
4817
- sessionId: targetSessionId,
5304
+ sessionId: currentSessionId,
4818
5305
  session: {
4819
- summary: checkpoint.session?.summary ?? checkpoint.route.reason ?? summary,
5306
+ summary: routePlan.route.reason ?? summary,
4820
5307
  ...commandMetadata()
4821
5308
  }
4822
5309
  });
4823
- lifecycleOperations.push(checkpointLifecycleOperation("session.ended", true, { sessionId: targetSessionId, reason: "ticket_switch" }));
5310
+ lifecycleOperations.push(checkpointLifecycleOperation("session.ended", true, { sessionId: currentSessionId, reason: "ticket_switch" }));
4824
5311
  targetSessionId = undefined;
4825
5312
  targetChangesetId = undefined;
4826
5313
  }
@@ -4832,9 +5319,7 @@ async function runAgentRunCheckpoint(parsed, options, config, meta) {
4832
5319
  }
4833
5320
  });
4834
5321
  lifecycleOperations.push(checkpointLifecycleOperation("session.started", true, { ticketId: targetTicketId, session: startedSession }));
4835
- if (startedSession && typeof startedSession === "object" && "id" in startedSession) {
4836
- targetSessionId = String(startedSession.id);
4837
- }
5322
+ targetSessionId = objectStringId(startedSession);
4838
5323
  if (targetSessionId) {
4839
5324
  const lease = await client.sessions.leases.create({
4840
5325
  projectId,
@@ -4842,7 +5327,6 @@ async function runAgentRunCheckpoint(parsed, options, config, meta) {
4842
5327
  ticketId: targetTicketId,
4843
5328
  sessionId: targetSessionId,
4844
5329
  expiresAt: leaseExpiresAt(defaultLeaseTtlSeconds),
4845
- replaceOwnActiveLease: true,
4846
5330
  ...commandMetadata()
4847
5331
  }
4848
5332
  });
@@ -4862,15 +5346,420 @@ async function runAgentRunCheckpoint(parsed, options, config, meta) {
4862
5346
  }
4863
5347
  });
4864
5348
  lifecycleOperations.push(checkpointLifecycleOperation("changeset.linked", true, { ticketId: targetTicketId, branchName, changeset }));
4865
- if (changeset && typeof changeset === "object" && "id" in changeset) {
4866
- targetChangesetId = String(changeset.id);
4867
- }
5349
+ targetChangesetId = objectStringId(changeset);
4868
5350
  } catch (error) {
4869
5351
  lifecycleOperations.push(checkpointLifecycleOperation("changeset.linked", false, { error: error instanceof Error ? error.message : String(error) }));
4870
5352
  }
4871
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;
5424
+ if (!ticketId) {
5425
+ await writeAgentLoopState(parsed, options, {
5426
+ ...state,
5427
+ sessions: {
5428
+ ...state.sessions,
5429
+ [agentSessionKeyValue]: {
5430
+ ...sessionState,
5431
+ lastAction: "skipped",
5432
+ lastReason: "no_active_ticket"
5433
+ }
5434
+ }
5435
+ });
5436
+ const data = {
5437
+ action: "skipped",
5438
+ reason: "no_active_ticket",
5439
+ agentSessionKey: agentSessionKeyValue
5440
+ };
5441
+ return {
5442
+ stdout: parsed.flags.json ? formatJson(successEnvelope(data, meta)) : `${JSON.stringify(data, null, 2)}
5443
+ `,
5444
+ stderr: "",
5445
+ exitCode: 0
5446
+ };
5447
+ }
5448
+ const eventFile = parsed.options["event-file"] ?? sessionState.lastEventFile;
5449
+ const event = eventFile ? tryParseJsonObject(await readFile(eventFile, "utf8"), eventFile) : synthesizeAgentEventFromSession(agentSessionKeyValue, sessionState, options);
5450
+ const mode = checkpointModeForCommand(parsed);
5451
+ const logKind = parseWorkLogEntryKind(parsed.options["log-kind"] ?? "note");
5452
+ const updateSummary = parseBooleanOption(parsed.options["update-summary"], false);
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);
5456
+ const lockPath = parsed.options.lock;
5457
+ const checkpointSettings = await resolveCheckpointSettings(parsed, options);
5458
+ const gitStatus = gitOutput(["status", "--short"], options);
5459
+ const gitDiffStat = gitOutput(["diff", "--stat", "origin/dev..."], options);
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
+ }) : {};
5478
+ const prompt = buildCheckpointPrompt({
5479
+ mode,
5480
+ event,
5481
+ ticketId,
5482
+ ...sessionId ? { sessionId } : {},
5483
+ ...changesetId ? { changesetId } : {},
5484
+ gitStatus,
5485
+ gitDiffStat,
5486
+ changedFiles,
5487
+ ...sessionState.previousCheckpointSummary ? { previousCheckpointSummary: sessionState.previousCheckpointSummary } : {},
5488
+ ...priorCoverage ? { coverage: priorCoverage } : {},
5489
+ ...closeoutContext
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
+ }
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
+ });
5566
+ const data = {
5567
+ action: "would_checkpoint",
5568
+ prompt,
5569
+ auditFile,
5570
+ ticketId,
5571
+ agentSessionKey: agentSessionKeyValue,
5572
+ ...sessionId ? { sessionId } : {},
5573
+ ...changesetId ? { changesetId } : {}
5574
+ };
5575
+ return {
5576
+ stdout: parsed.flags.json ? formatJson(successEnvelope(data, meta)) : `${JSON.stringify(data, null, 2)}
5577
+ `,
5578
+ stderr: "",
5579
+ exitCode: 0
5580
+ };
5581
+ }
5582
+ try {
5583
+ const codexCommand = parsed.options["codex-command"] ?? "codex";
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,
5599
+ env: {
5600
+ [agentLoopSuppressEnv]: "1",
5601
+ CADENCE_HOOK_SUPPRESS: "1"
5602
+ },
5603
+ timeoutMs: defaultCheckpointWorkerTimeoutMs
5604
+ });
5605
+ const codexSessionTranscript = readCodexSessionTranscript(findCodexSessionFileCreatedAfter(options, codexStartedAtMs, codexCwd));
5606
+ const tokenAccounting = buildCheckpointTokenAccounting(event, prompt, codexSessionTranscript?.tokenUsage);
5607
+ if (codex.status !== 0 || codex.error) {
5608
+ throw new CliError("AGENT_RUN_CHECKPOINT_FAILED", "Codex checkpoint generation failed.", {
5609
+ status: codex.status,
5610
+ stderr: truncateText(codex.stderr, 2000),
5611
+ error: codex.error?.message
5612
+ });
5613
+ }
5614
+ let checkpoint = parseCheckpointPlanJson(codex.stdout, logKind);
5615
+ if (mode === "closeout") {
5616
+ checkpoint = applyCloseoutSessionDefaults(checkpoint, closeoutSessionAction, completeTicket);
5617
+ }
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,
5705
+ summary,
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;
4872
5761
  }
4873
- const checkpointMetadata = checkpointWorkLogMetadata(checkpointSettings, tokenAccounting);
5762
+ const checkpointMetadata = checkpointWorkLogMetadata(checkpointSettings, mode, tokenAccounting);
4874
5763
  for (const entry of checkpoint.entries) {
4875
5764
  const logEntry = {
4876
5765
  entryKind: entry.kind,
@@ -5033,26 +5922,17 @@ async function runAgentRunSweep(parsed, options, config, meta) {
5033
5922
  lastObservedAt: session.lastObservedAt
5034
5923
  }));
5035
5924
  if (!dryRun) {
5036
- const needsFallbackContext = staleSessions.some((staleSession) => !state.sessions[staleSession.agentSessionKey]?.cadenceContext);
5037
- let fallbackContext;
5038
- if (needsFallbackContext && config.projectId) {
5039
- try {
5040
- const client = await createClient(config, options);
5041
- fallbackContext = await readCurrentCadenceContext(client, config.projectId);
5042
- } catch {}
5043
- }
5044
5925
  let nextState = state;
5045
5926
  for (const staleSession of staleSessions) {
5046
5927
  const existingSession = nextState.sessions[staleSession.agentSessionKey];
5047
- const cadenceContext = existingSession?.cadenceContext ?? fallbackContext;
5048
- if (existingSession && cadenceContext) {
5928
+ const hasContext = Boolean(existingSession?.cadenceContext?.ticketId);
5929
+ if (existingSession) {
5049
5930
  nextState = {
5050
5931
  ...nextState,
5051
5932
  sessions: {
5052
5933
  ...nextState.sessions,
5053
5934
  [staleSession.agentSessionKey]: {
5054
5935
  ...existingSession,
5055
- cadenceContext,
5056
5936
  lastAction: "sweep_spawned"
5057
5937
  }
5058
5938
  }
@@ -5061,11 +5941,11 @@ async function runAgentRunSweep(parsed, options, config, meta) {
5061
5941
  }
5062
5942
  await spawnAgentRunCheckpoint([
5063
5943
  "agent-run",
5064
- "checkpoint",
5944
+ hasContext ? "checkpoint" : "route",
5065
5945
  "--agent-session-key",
5066
5946
  staleSession.agentSessionKey,
5067
5947
  "--reason",
5068
- "idle",
5948
+ hasContext ? "idle" : "missing_context",
5069
5949
  "--lock",
5070
5950
  agentLoopLockPath(parsed, options, staleSession.agentSessionKey),
5071
5951
  ...parsed.flags.project ? ["--project", parsed.flags.project] : [],
@@ -5733,7 +6613,7 @@ async function runCli(argv, options = {}) {
5733
6613
  if (parsed.command.name === "projects.list") {
5734
6614
  return await runProjectCommand(parsed, options);
5735
6615
  }
5736
- if (parsed.command.name === "agent-run.ingest-stop" || parsed.command.name === "agent-run.checkpoint" || parsed.command.name === "agent-run.closeout" || parsed.command.name === "agent-run.sweep" || parsed.command.name === "agent-run.doctor") {
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") {
5737
6617
  return await runAgentRunCommand(parsed, options);
5738
6618
  }
5739
6619
  if (parsed.command.name === "hooks.install" || parsed.command.name === "hooks.doctor") {