@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.
- package/dist/cadence +1031 -151
- 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.
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
5055
|
+
const prompt = buildRoutePrompt({
|
|
4591
5056
|
event,
|
|
4592
|
-
|
|
4593
|
-
...
|
|
4594
|
-
...
|
|
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
|
-
|
|
5071
|
+
mode: "route",
|
|
5072
|
+
action: "would_route",
|
|
4605
5073
|
createdAt: new Date().toISOString(),
|
|
4606
5074
|
localOnly: true,
|
|
4607
|
-
localOnlyReason: "Raw hook context and
|
|
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: "
|
|
5086
|
+
action: "would_route",
|
|
4621
5087
|
prompt,
|
|
4622
5088
|
auditFile,
|
|
4623
|
-
|
|
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("
|
|
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
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
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
|
|
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 === "
|
|
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:
|
|
4705
|
-
|
|
4706
|
-
|
|
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:
|
|
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:
|
|
5222
|
+
route: routePlan.route,
|
|
4740
5223
|
...reason ? { reason } : {},
|
|
4741
|
-
ticketId: targetTicketId,
|
|
5224
|
+
...targetTicketId ? { ticketId: targetTicketId } : {},
|
|
4742
5225
|
summary,
|
|
4743
|
-
entryCount:
|
|
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 (
|
|
4758
|
-
|
|
5240
|
+
if (routePlan.route.action === "noop") {
|
|
5241
|
+
return await finishRoute("noop", routePlan.route.reason ?? routePlan.summary ?? "Nothing durable to route.");
|
|
4759
5242
|
}
|
|
4760
|
-
if (
|
|
4761
|
-
return await
|
|
5243
|
+
if (routePlan.route.action === "current") {
|
|
5244
|
+
return await finishRoute("current", routePlan.route.reason ?? "Kept current Cadence context.");
|
|
4762
5245
|
}
|
|
4763
|
-
if (
|
|
4764
|
-
|
|
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,
|
|
4777
|
-
|
|
4778
|
-
return await
|
|
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 (
|
|
5260
|
+
if (routePlan.route.action === "intake_create") {
|
|
4781
5261
|
selectedTicket = await client.tickets.create({
|
|
4782
5262
|
projectId,
|
|
4783
5263
|
ticket: {
|
|
4784
|
-
title: checkpointPlanTitle(
|
|
4785
|
-
description: checkpointPlanDescription(
|
|
4786
|
-
fromIntakeId:
|
|
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
|
-
|
|
4792
|
-
targetTicketId = String(selectedTicket.id);
|
|
4793
|
-
}
|
|
5271
|
+
targetTicketId = objectStringId(selectedTicket);
|
|
4794
5272
|
} else {
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
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:
|
|
5304
|
+
sessionId: currentSessionId,
|
|
4818
5305
|
session: {
|
|
4819
|
-
summary:
|
|
5306
|
+
summary: routePlan.route.reason ?? summary,
|
|
4820
5307
|
...commandMetadata()
|
|
4821
5308
|
}
|
|
4822
5309
|
});
|
|
4823
|
-
lifecycleOperations.push(checkpointLifecycleOperation("session.ended", true, { sessionId:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
5048
|
-
if (existingSession
|
|
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") {
|