@trycadence/cli 0.1.14-dev.0 → 0.1.18-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 +1422 -196
- 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.18-dev.0",
|
|
1524
1524
|
private: false,
|
|
1525
1525
|
type: "module",
|
|
1526
1526
|
bin: {
|
|
@@ -1554,12 +1554,15 @@ 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";
|
|
1560
1562
|
var defaultCheckpointThreshold = 3;
|
|
1561
|
-
var defaultCheckpointCooldownSeconds =
|
|
1563
|
+
var defaultCheckpointCooldownSeconds = 0;
|
|
1562
1564
|
var defaultCheckpointWorkerTimeoutMs = 10 * 60 * 1000;
|
|
1565
|
+
var defaultCheckpointWorkerMaxAttempts = 3;
|
|
1563
1566
|
var defaultHookCommand = `/bin/sh -lc 'root=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0; cd "$root" || exit 0; [ -f .cadence/config.json ] || [ -f .cadence/config.local.json ] || exit 0; exec cadence agent-run ingest-stop --source codex --event stop >/dev/null'`;
|
|
1564
1567
|
var agentLoopSuppressEnv = "CADENCE_AGENT_EVENT_SUPPRESS";
|
|
1565
1568
|
var credentialRefreshSkewMs = 60 * 1000;
|
|
@@ -1599,6 +1602,7 @@ var knownCommandPaths = [
|
|
|
1599
1602
|
["changesets", "notes", "put"],
|
|
1600
1603
|
["changesets", "notes", "apply"],
|
|
1601
1604
|
["agent-run", "ingest-stop"],
|
|
1605
|
+
["agent-run", "route"],
|
|
1602
1606
|
["agent-run", "checkpoint"],
|
|
1603
1607
|
["agent-run", "closeout"],
|
|
1604
1608
|
["agent-run", "sweep"],
|
|
@@ -2279,7 +2283,9 @@ function helpText() {
|
|
|
2279
2283
|
" 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
2284
|
" cadence changesets notes apply [--changeset <id>|--branch current|<branch>] --provider github --pr-number <n> --pr-url <url> [--project <project-id>] [--json]",
|
|
2281
2285
|
" cadence agent-run ingest-stop --source <codex|claude-code|opencode|openrouter> [--event <event>] [--threshold <n>] [--dry-run true|false] [--project <project-id>] [--json]",
|
|
2286
|
+
" 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
2287
|
" 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]",
|
|
2288
|
+
" 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
2289
|
" cadence agent-run sweep [--idle-after-seconds <n>] [--dry-run true|false] [--json]",
|
|
2284
2290
|
" cadence agent-run doctor [--json]",
|
|
2285
2291
|
" cadence hooks install --provider codex [--scope global] [--command <command>] [--json]",
|
|
@@ -3085,15 +3091,6 @@ function normalizeGenericAgentEvent(input, base) {
|
|
|
3085
3091
|
function agentSessionKey(source, agentSessionId) {
|
|
3086
3092
|
return `${source}:${stableHash(agentSessionId)}`;
|
|
3087
3093
|
}
|
|
3088
|
-
function checkpointWorkLogMetadata(settings, tokenAccounting) {
|
|
3089
|
-
return {
|
|
3090
|
-
checkpoint: {
|
|
3091
|
-
provider: settings.provider,
|
|
3092
|
-
...settings.model ? { model: settings.model } : {},
|
|
3093
|
-
...tokenAccounting ? { tokenAccounting } : {}
|
|
3094
|
-
}
|
|
3095
|
-
};
|
|
3096
|
-
}
|
|
3097
3094
|
function defaultAgentLoopState() {
|
|
3098
3095
|
return {
|
|
3099
3096
|
version: 2,
|
|
@@ -3248,12 +3245,59 @@ function readAgentLoopSessions(rawSessions) {
|
|
|
3248
3245
|
...recentTurns ? { recentTurns } : {},
|
|
3249
3246
|
...typeof record.lastCheckpointAt === "string" ? { lastCheckpointAt: record.lastCheckpointAt } : {},
|
|
3250
3247
|
...typeof record.lastCheckpointFingerprint === "string" ? { lastCheckpointFingerprint: record.lastCheckpointFingerprint } : {},
|
|
3248
|
+
...readAgentRunFingerprints(record.lastCheckpointFingerprints),
|
|
3249
|
+
...typeof record.lastCheckpointMode === "string" && agentRunMemoryModes.includes(record.lastCheckpointMode) ? { lastCheckpointMode: record.lastCheckpointMode } : {},
|
|
3250
|
+
...readAgentRunCoverage(record.lastCheckpointCoverage),
|
|
3251
3251
|
...typeof record.previousCheckpointSummary === "string" ? { previousCheckpointSummary: record.previousCheckpointSummary } : {},
|
|
3252
|
+
...typeof record.displayTitle === "string" ? { displayTitle: record.displayTitle } : {},
|
|
3253
|
+
...typeof record.displayTitleUpdatedAt === "string" ? { displayTitleUpdatedAt: record.displayTitleUpdatedAt } : {},
|
|
3254
|
+
...record.displayTitleSource === "route" ? { displayTitleSource: "route" } : {},
|
|
3255
|
+
...typeof record.displayTitleConfidence === "string" && checkpointConfidenceLevels.includes(record.displayTitleConfidence) ? { displayTitleConfidence: record.displayTitleConfidence } : {},
|
|
3256
|
+
...typeof record.displayTitleReason === "string" ? { displayTitleReason: record.displayTitleReason } : {},
|
|
3252
3257
|
...typeof record.lastCheckpointAuditFile === "string" ? { lastCheckpointAuditFile: record.lastCheckpointAuditFile } : {}
|
|
3253
3258
|
};
|
|
3254
3259
|
}
|
|
3255
3260
|
return sessions;
|
|
3256
3261
|
}
|
|
3262
|
+
function readAgentRunFingerprints(value) {
|
|
3263
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
3264
|
+
return {};
|
|
3265
|
+
}
|
|
3266
|
+
const record = value;
|
|
3267
|
+
const event = typeof record.event === "string" ? record.event : undefined;
|
|
3268
|
+
const git = typeof record.git === "string" ? record.git : undefined;
|
|
3269
|
+
const cadenceContext = typeof record.cadenceContext === "string" ? record.cadenceContext : undefined;
|
|
3270
|
+
if (!event || !git || !cadenceContext) {
|
|
3271
|
+
return {};
|
|
3272
|
+
}
|
|
3273
|
+
return {
|
|
3274
|
+
lastCheckpointFingerprints: {
|
|
3275
|
+
event,
|
|
3276
|
+
git,
|
|
3277
|
+
cadenceContext,
|
|
3278
|
+
...typeof record.verification === "string" ? { verification: record.verification } : {},
|
|
3279
|
+
...typeof record.coverageDebt === "string" ? { coverageDebt: record.coverageDebt } : {}
|
|
3280
|
+
}
|
|
3281
|
+
};
|
|
3282
|
+
}
|
|
3283
|
+
function readAgentRunCoverage(value) {
|
|
3284
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
3285
|
+
return {};
|
|
3286
|
+
}
|
|
3287
|
+
const record = value;
|
|
3288
|
+
const coverage = {
|
|
3289
|
+
outcome: record.outcome === true,
|
|
3290
|
+
implementation: record.implementation === true,
|
|
3291
|
+
decisions: record.decisions === true,
|
|
3292
|
+
corrections: record.corrections === true,
|
|
3293
|
+
verification: record.verification === true,
|
|
3294
|
+
blockers: record.blockers === true,
|
|
3295
|
+
scope: record.scope === true,
|
|
3296
|
+
handoff: record.handoff === true,
|
|
3297
|
+
hasDebt: record.hasDebt === true
|
|
3298
|
+
};
|
|
3299
|
+
return { lastCheckpointCoverage: coverage };
|
|
3300
|
+
}
|
|
3257
3301
|
function readAgentSessionCadenceContext(record) {
|
|
3258
3302
|
const value = record.cadenceContext;
|
|
3259
3303
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -3396,7 +3440,7 @@ function currentCliWorkerInvocation() {
|
|
|
3396
3440
|
argsPrefix: []
|
|
3397
3441
|
};
|
|
3398
3442
|
}
|
|
3399
|
-
async function
|
|
3443
|
+
async function spawnAgentRunWorker(args, options) {
|
|
3400
3444
|
const cwd = options.cwd ?? process.cwd();
|
|
3401
3445
|
const invocation = currentCliWorkerInvocation();
|
|
3402
3446
|
const env = {
|
|
@@ -3415,6 +3459,9 @@ async function spawnAgentRunCheckpoint(args, options) {
|
|
|
3415
3459
|
});
|
|
3416
3460
|
child.unref();
|
|
3417
3461
|
}
|
|
3462
|
+
async function spawnAgentRunCheckpoint(args, options) {
|
|
3463
|
+
await spawnAgentRunWorker(args, options);
|
|
3464
|
+
}
|
|
3418
3465
|
function currentRecordTime(value, keys) {
|
|
3419
3466
|
for (const key of keys) {
|
|
3420
3467
|
const candidate = value[key];
|
|
@@ -3510,6 +3557,9 @@ function fingerprintForCheckpoint(event, options) {
|
|
|
3510
3557
|
}));
|
|
3511
3558
|
}
|
|
3512
3559
|
function shouldSkipForCooldown(state, cooldownSeconds) {
|
|
3560
|
+
if (cooldownSeconds <= 0) {
|
|
3561
|
+
return false;
|
|
3562
|
+
}
|
|
3513
3563
|
if (!state.lastCheckpointAt) {
|
|
3514
3564
|
return false;
|
|
3515
3565
|
}
|
|
@@ -3737,6 +3787,43 @@ function parseCheckpointPlanSession(value) {
|
|
|
3737
3787
|
...reason ? { reason } : {}
|
|
3738
3788
|
};
|
|
3739
3789
|
}
|
|
3790
|
+
function cleanAgentSessionTitleText(value) {
|
|
3791
|
+
const cleaned = value.replace(/\s+/g, " ").trim();
|
|
3792
|
+
if (!cleaned) {
|
|
3793
|
+
return;
|
|
3794
|
+
}
|
|
3795
|
+
const normalized = cleaned.toLowerCase();
|
|
3796
|
+
const genericTitles = new Set(["agent session", "session", "work session", "cadence session", "current session"]);
|
|
3797
|
+
if (genericTitles.has(normalized)) {
|
|
3798
|
+
return;
|
|
3799
|
+
}
|
|
3800
|
+
return cleaned.length > 80 ? cleaned.slice(0, 77).trimEnd() + "..." : cleaned;
|
|
3801
|
+
}
|
|
3802
|
+
function agentSessionTitleString(record, key) {
|
|
3803
|
+
const value = record[key];
|
|
3804
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
3805
|
+
}
|
|
3806
|
+
function parseAgentSessionTitle(value) {
|
|
3807
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
3808
|
+
return;
|
|
3809
|
+
}
|
|
3810
|
+
const record = value;
|
|
3811
|
+
const text = agentSessionTitleString(record, "text") ?? agentSessionTitleString(record, "title") ?? agentSessionTitleString(record, "label");
|
|
3812
|
+
const cleanedText = text ? cleanAgentSessionTitleText(text) : undefined;
|
|
3813
|
+
if (!cleanedText) {
|
|
3814
|
+
return;
|
|
3815
|
+
}
|
|
3816
|
+
const confidenceValue = agentSessionTitleString(record, "confidence") ?? "medium";
|
|
3817
|
+
if (!checkpointConfidenceLevels.includes(confidenceValue)) {
|
|
3818
|
+
return;
|
|
3819
|
+
}
|
|
3820
|
+
const reason = agentSessionTitleString(record, "reason")?.replace(/\s+/g, " ").trim();
|
|
3821
|
+
return {
|
|
3822
|
+
text: cleanedText,
|
|
3823
|
+
confidence: confidenceValue,
|
|
3824
|
+
...reason ? { reason: reason.length > 300 ? `${reason.slice(0, 297).trimEnd()}...` : reason } : {}
|
|
3825
|
+
};
|
|
3826
|
+
}
|
|
3740
3827
|
function parseCheckpointPlanFiles(value) {
|
|
3741
3828
|
if (!Array.isArray(value)) {
|
|
3742
3829
|
return [];
|
|
@@ -3756,9 +3843,6 @@ function parseCheckpointPlanFiles(value) {
|
|
|
3756
3843
|
};
|
|
3757
3844
|
});
|
|
3758
3845
|
}
|
|
3759
|
-
function routeRequiresHighConfidence(route, session) {
|
|
3760
|
-
return highAutonomyRouteActions.has(route.action) || session?.action === "complete_ticket";
|
|
3761
|
-
}
|
|
3762
3846
|
function forceNeedsHuman(plan, reason) {
|
|
3763
3847
|
return {
|
|
3764
3848
|
...plan,
|
|
@@ -3771,9 +3855,6 @@ function forceNeedsHuman(plan, reason) {
|
|
|
3771
3855
|
};
|
|
3772
3856
|
}
|
|
3773
3857
|
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
3858
|
if (plan.route.action === "noop") {
|
|
3778
3859
|
return {
|
|
3779
3860
|
...plan,
|
|
@@ -3782,6 +3863,51 @@ function normalizeCheckpointPlan(plan) {
|
|
|
3782
3863
|
}
|
|
3783
3864
|
return plan;
|
|
3784
3865
|
}
|
|
3866
|
+
function checkpointPlanWithRoute(plan, route, warning) {
|
|
3867
|
+
return {
|
|
3868
|
+
...plan,
|
|
3869
|
+
route,
|
|
3870
|
+
...route.action === "noop" ? { entries: [] } : {},
|
|
3871
|
+
validationWarnings: warning ? [...plan.validationWarnings, warning] : plan.validationWarnings
|
|
3872
|
+
};
|
|
3873
|
+
}
|
|
3874
|
+
function safeAutomaticRoutePlan(plan, hasCurrentContext, reason) {
|
|
3875
|
+
return checkpointPlanWithRoute(plan, {
|
|
3876
|
+
action: hasCurrentContext ? "current" : "noop",
|
|
3877
|
+
confidence: plan.route.confidence,
|
|
3878
|
+
reason
|
|
3879
|
+
}, reason);
|
|
3880
|
+
}
|
|
3881
|
+
function routePlanAllowsLifecycle(plan) {
|
|
3882
|
+
return checkpointRouteRequiresIntake(plan.route.action) && plan.route.confidence === "high";
|
|
3883
|
+
}
|
|
3884
|
+
function planWithCandidateSelection(original, selected, currentTicketId, candidateTicketIds) {
|
|
3885
|
+
let nextPlan = selected;
|
|
3886
|
+
if (nextPlan.route.action === "needs_human") {
|
|
3887
|
+
nextPlan = safeAutomaticRoutePlan(nextPlan, Boolean(currentTicketId), nextPlan.route.reason ?? "Candidate routing was uncertain.");
|
|
3888
|
+
} else if (checkpointRouteRequiresIntake(nextPlan.route.action) && nextPlan.route.confidence !== "high") {
|
|
3889
|
+
nextPlan = safeAutomaticRoutePlan(nextPlan, Boolean(currentTicketId), "Candidate routing requires high confidence.");
|
|
3890
|
+
} else if ((nextPlan.route.action === "intake_attach" || nextPlan.route.action === "switch_existing") && !nextPlan.route.targetTicketId) {
|
|
3891
|
+
nextPlan = safeAutomaticRoutePlan(nextPlan, Boolean(currentTicketId), "Candidate routing to existing work requires a target ticket id.");
|
|
3892
|
+
} else if ((nextPlan.route.action === "intake_attach" || nextPlan.route.action === "switch_existing") && nextPlan.route.targetTicketId && !candidateTicketIds.has(nextPlan.route.targetTicketId)) {
|
|
3893
|
+
nextPlan = safeAutomaticRoutePlan(nextPlan, Boolean(currentTicketId), "Candidate routing selected a ticket outside the intake candidate list.");
|
|
3894
|
+
} else if (nextPlan.route.action === "current" && !currentTicketId) {
|
|
3895
|
+
nextPlan = checkpointPlanWithRoute(nextPlan, {
|
|
3896
|
+
action: "noop",
|
|
3897
|
+
confidence: nextPlan.route.confidence,
|
|
3898
|
+
reason: nextPlan.route.reason ?? "No current Cadence context exists."
|
|
3899
|
+
});
|
|
3900
|
+
}
|
|
3901
|
+
const summary = nextPlan.summary ?? original.summary;
|
|
3902
|
+
return {
|
|
3903
|
+
...nextPlan,
|
|
3904
|
+
...summary ? { summary } : {},
|
|
3905
|
+
...nextPlan.sessionTitle ?? original.sessionTitle ? { sessionTitle: nextPlan.sessionTitle ?? original.sessionTitle } : {},
|
|
3906
|
+
entries: nextPlan.entries.length ? nextPlan.entries : original.entries,
|
|
3907
|
+
files: nextPlan.files.length ? nextPlan.files : original.files,
|
|
3908
|
+
validationWarnings: [...original.validationWarnings, ...nextPlan.validationWarnings]
|
|
3909
|
+
};
|
|
3910
|
+
}
|
|
3785
3911
|
function parseCheckpointPlanRecord(record, rawText, fallbackKind) {
|
|
3786
3912
|
if (!("route" in record)) {
|
|
3787
3913
|
const legacy = parseCheckpointRecord(record, rawText, fallbackKind);
|
|
@@ -3805,12 +3931,14 @@ function parseCheckpointPlanRecord(record, rawText, fallbackKind) {
|
|
|
3805
3931
|
const entries = Array.isArray(record.entries) ? record.entries.map(parseCheckpointPlanEntry) : [];
|
|
3806
3932
|
const summary = checkpointPlanString(record, "summary");
|
|
3807
3933
|
const summaryUpdate = parseCheckpointPlanSummaryUpdate(record.summaryUpdate ?? record.currentSummary);
|
|
3934
|
+
const sessionTitle = parseAgentSessionTitle(record.sessionTitle ?? record.displayTitle);
|
|
3808
3935
|
const session = parseCheckpointPlanSession(record.session);
|
|
3809
3936
|
return normalizeCheckpointPlan({
|
|
3810
3937
|
...summary ? { summary } : {},
|
|
3811
3938
|
route,
|
|
3812
3939
|
entries,
|
|
3813
3940
|
...summaryUpdate ? { summaryUpdate } : {},
|
|
3941
|
+
...sessionTitle ? { sessionTitle } : {},
|
|
3814
3942
|
...session ? { session } : {},
|
|
3815
3943
|
files: parseCheckpointPlanFiles(record.files),
|
|
3816
3944
|
legacy: false,
|
|
@@ -3862,6 +3990,62 @@ function parseCheckpointPlanJson(raw, fallbackKind) {
|
|
|
3862
3990
|
function checkpointRouteRequiresIntake(action) {
|
|
3863
3991
|
return action === "intake_create" || action === "intake_attach" || action === "switch_existing";
|
|
3864
3992
|
}
|
|
3993
|
+
function intakeCandidates(intake) {
|
|
3994
|
+
if (!intake || typeof intake !== "object" || Array.isArray(intake)) {
|
|
3995
|
+
return [];
|
|
3996
|
+
}
|
|
3997
|
+
const candidates = intake.candidates;
|
|
3998
|
+
return Array.isArray(candidates) ? candidates.filter((candidate) => candidate && typeof candidate === "object" && !Array.isArray(candidate)) : [];
|
|
3999
|
+
}
|
|
4000
|
+
function intakeCandidateTicketId(candidate) {
|
|
4001
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
|
|
4002
|
+
return;
|
|
4003
|
+
}
|
|
4004
|
+
const record = candidate;
|
|
4005
|
+
const nestedTicket = record.ticket && typeof record.ticket === "object" && !Array.isArray(record.ticket) ? record.ticket : null;
|
|
4006
|
+
const value = record.ticketId ?? nestedTicket?.id ?? record.id;
|
|
4007
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
4008
|
+
}
|
|
4009
|
+
function intakeCandidateTicketIds(intake) {
|
|
4010
|
+
return new Set(intakeCandidates(intake).map(intakeCandidateTicketId).filter((id) => Boolean(id)));
|
|
4011
|
+
}
|
|
4012
|
+
function compactIntakeCandidate(candidate, index) {
|
|
4013
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
|
|
4014
|
+
return { index };
|
|
4015
|
+
}
|
|
4016
|
+
const record = candidate;
|
|
4017
|
+
const ticket = record.ticket && typeof record.ticket === "object" && !Array.isArray(record.ticket) ? record.ticket : {};
|
|
4018
|
+
const ticketId = intakeCandidateTicketId(candidate);
|
|
4019
|
+
const title = record.title ?? ticket.title;
|
|
4020
|
+
const status = record.status ?? ticket.status;
|
|
4021
|
+
const summary = record.summary ?? ticket.currentSummary ?? ticket.summary;
|
|
4022
|
+
return {
|
|
4023
|
+
index,
|
|
4024
|
+
...ticketId ? { ticketId } : {},
|
|
4025
|
+
...typeof title === "string" ? { title: truncateText(title, 200) } : {},
|
|
4026
|
+
...typeof status === "string" ? { status } : {},
|
|
4027
|
+
...typeof summary === "string" ? { summary: truncateText(summary, 600) } : {},
|
|
4028
|
+
...typeof record.classification === "string" ? { classification: record.classification } : {},
|
|
4029
|
+
...typeof record.reason === "string" ? { reason: truncateText(record.reason, 300) } : {},
|
|
4030
|
+
...typeof record.score === "number" ? { score: record.score } : {},
|
|
4031
|
+
...typeof record.confidence === "string" ? { confidence: record.confidence } : {}
|
|
4032
|
+
};
|
|
4033
|
+
}
|
|
4034
|
+
function compactIntakeResultForRouteSelection(intake) {
|
|
4035
|
+
if (!intake || typeof intake !== "object" || Array.isArray(intake)) {
|
|
4036
|
+
return null;
|
|
4037
|
+
}
|
|
4038
|
+
const record = intake;
|
|
4039
|
+
return {
|
|
4040
|
+
...typeof record.id === "string" ? { id: record.id } : {},
|
|
4041
|
+
...typeof record.status === "string" ? { status: record.status } : {},
|
|
4042
|
+
...typeof record.classification === "string" ? { classification: record.classification } : {},
|
|
4043
|
+
...typeof record.recommendedAction === "string" ? { recommendedAction: record.recommendedAction } : {},
|
|
4044
|
+
...typeof record.attachedTicketId === "string" ? { attachedTicketId: record.attachedTicketId } : {},
|
|
4045
|
+
...typeof record.convertedTicketId === "string" ? { convertedTicketId: record.convertedTicketId } : {},
|
|
4046
|
+
candidates: intakeCandidates(intake).map(compactIntakeCandidate)
|
|
4047
|
+
};
|
|
4048
|
+
}
|
|
3865
4049
|
function checkpointPlanCompletionAllowed(event, summary) {
|
|
3866
4050
|
const text = [
|
|
3867
4051
|
summary,
|
|
@@ -3977,23 +4161,39 @@ function buildCheckpointTokenAccounting(event, prompt, tokenUsage) {
|
|
|
3977
4161
|
function buildCheckpointPrompt(input) {
|
|
3978
4162
|
const recentTurns = checkpointRecentTurns(input.event);
|
|
3979
4163
|
const recentTurnsText = recentTurns.length ? formatCheckpointRecentTurns(recentTurns) : "";
|
|
4164
|
+
const modeInstructions = input.mode === "closeout" ? [
|
|
4165
|
+
"Mode: closeout. Produce final session memory, not an interim pulse.",
|
|
4166
|
+
"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.",
|
|
4167
|
+
"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.",
|
|
4168
|
+
"Use session.action handoff, end, or keep when appropriate. Use complete_ticket only when explicit completion evidence is present."
|
|
4169
|
+
] : [
|
|
4170
|
+
"Mode: checkpoint. Produce sparse incremental memory for an active session, not a full-session closeout.",
|
|
4171
|
+
"Checkpoint should commonly return route.action noop with entries [] when there is nothing durable to record.",
|
|
4172
|
+
"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."
|
|
4173
|
+
];
|
|
3980
4174
|
return [
|
|
3981
|
-
|
|
4175
|
+
`You are generating a compact Cadence dogfood operation plan for an agent-run ${input.mode} worker.`,
|
|
3982
4176
|
"The model judges; the Cadence CLI validates and executes. Return JSON only. Do not call tools to mutate Cadence yourself.",
|
|
3983
4177
|
"Preserve durable ticket purpose. Identify user intent, changed intent, corrections, decisions, rationale, implementation actions, verification, blockers, and useful notes.",
|
|
3984
4178
|
"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
|
|
4179
|
+
'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
4180
|
"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
|
|
4181
|
+
"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
4182
|
"Set route.confidence high only when the recent user/assistant context makes the route and lifecycle action clear. Lifecycle mutations require high confidence.",
|
|
3989
4183
|
"Use note only as a last-resort context kind. Prefer intent, decision, rationale, action, verification, correction, or blocker when those fit.",
|
|
3990
4184
|
"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
4185
|
"Set summaryUpdate.update true only when the durable current work summary is missing or misleading; otherwise omit summaryUpdate or leave update false.",
|
|
3992
4186
|
"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.",
|
|
4187
|
+
...modeInstructions,
|
|
3993
4188
|
"",
|
|
3994
4189
|
`Ticket: ${input.ticketId}`,
|
|
3995
4190
|
input.sessionId ? `Session: ${input.sessionId}` : "",
|
|
3996
4191
|
input.changesetId ? `ChangeSet: ${input.changesetId}` : "",
|
|
4192
|
+
input.currentWorkSummary ? `Current Work Summary:
|
|
4193
|
+
${truncateText(input.currentWorkSummary, 1200)}` : "",
|
|
4194
|
+
input.recentWorkLog ? `Recent high-signal Work Log entries:
|
|
4195
|
+
${truncateText(input.recentWorkLog, 3000)}` : "",
|
|
4196
|
+
input.coverage ? `Prior coverage: ${JSON.stringify(input.coverage)}` : "",
|
|
3997
4197
|
`Agent event: ${input.event.source}/${input.event.event}`,
|
|
3998
4198
|
input.previousCheckpointSummary ? `Previous checkpoint summary: ${input.previousCheckpointSummary}` : "",
|
|
3999
4199
|
recentTurnsText ? `Recent user/assistant turns (most recent 3, local checkpoint context only):
|
|
@@ -4008,6 +4208,327 @@ ${truncateText(input.changedFiles, 2000)}` : "Changed files: unavailable"
|
|
|
4008
4208
|
].filter(Boolean).join(`
|
|
4009
4209
|
`);
|
|
4010
4210
|
}
|
|
4211
|
+
function buildRoutePrompt(input) {
|
|
4212
|
+
const recentTurns = checkpointRecentTurns(input.event);
|
|
4213
|
+
const recentTurnsText = recentTurns.length ? formatCheckpointRecentTurns(recentTurns) : "";
|
|
4214
|
+
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.";
|
|
4215
|
+
return [
|
|
4216
|
+
"You are generating a compact Cadence dogfood routing plan for an agent-run worker.",
|
|
4217
|
+
"The model judges; the Cadence CLI validates and executes. Return JSON only. Do not call tools to mutate Cadence yourself.",
|
|
4218
|
+
"Decide where this agent session belongs before checkpoint memory is written.",
|
|
4219
|
+
"Do not include raw diffs, raw transcripts, terminal logs, tool streams, secrets, file contents, or model reasoning in server-bound fields.",
|
|
4220
|
+
'Return this sparse JSON shape: {"summary":"short routing summary","sessionTitle":{"text":"short GUI session title","confidence":"low|medium|high","reason":"why this title names the routed focus"},"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"}]}',
|
|
4221
|
+
"Generate sessionTitle only when the routed focus is durable enough to name a GUI row. Omit it for noop/filler. Keep it under 80 characters and do not reuse the latest checkpoint summary as the title.",
|
|
4222
|
+
"Use noop for filler, acknowledgements, setup chatter, or anything not durable enough for Cadence.",
|
|
4223
|
+
"Use current only when an existing Cadence context clearly fits the recent work.",
|
|
4224
|
+
"Use intake_create for a clearly durable new task without a good existing ticket.",
|
|
4225
|
+
"Use intake_attach or switch_existing only when a specific targetTicketId is clearly the correct ticket.",
|
|
4226
|
+
"Set confidence high only when the route is clear. Low or medium confidence lifecycle actions will be ignored by the CLI.",
|
|
4227
|
+
"Use note only as a last-resort context kind. Prefer intent for new routed work.",
|
|
4228
|
+
"Each entry body should be concise narrative, not a transcript. Keep each entry under 800 characters.",
|
|
4229
|
+
"",
|
|
4230
|
+
`Route reason: ${input.reason ?? "manual"}`,
|
|
4231
|
+
contextText,
|
|
4232
|
+
`Agent event: ${input.event.source}/${input.event.event}`,
|
|
4233
|
+
input.previousCheckpointSummary ? `Previous checkpoint summary: ${input.previousCheckpointSummary}` : "",
|
|
4234
|
+
recentTurnsText ? `Recent user/assistant turns (most recent 3, local routing context only):
|
|
4235
|
+
${recentTurnsText}` : input.event.lastAssistantMessage ? `Last assistant message:
|
|
4236
|
+
${truncateText(input.event.lastAssistantMessage, 3000)}` : "Recent turns: unavailable",
|
|
4237
|
+
input.gitStatus ? `Git status --short:
|
|
4238
|
+
${truncateText(input.gitStatus, 2000)}` : "Git status --short: clean or unavailable",
|
|
4239
|
+
input.gitDiffStat ? `Git diff --stat origin/dev...:
|
|
4240
|
+
${truncateText(input.gitDiffStat, 2000)}` : "Git diff stat: unavailable",
|
|
4241
|
+
input.changedFiles ? `Changed files:
|
|
4242
|
+
${truncateText(input.changedFiles, 2000)}` : "Changed files: unavailable"
|
|
4243
|
+
].filter(Boolean).join(`
|
|
4244
|
+
`);
|
|
4245
|
+
}
|
|
4246
|
+
function buildRouteCandidatePrompt(input) {
|
|
4247
|
+
const recentTurns = checkpointRecentTurns(input.event);
|
|
4248
|
+
const recentTurnsText = recentTurns.length ? formatCheckpointRecentTurns(recentTurns) : "";
|
|
4249
|
+
const intake = compactIntakeResultForRouteSelection(input.intakeResult);
|
|
4250
|
+
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.";
|
|
4251
|
+
return [
|
|
4252
|
+
"You are resolving Cadence agent-run routing after intake returned possible existing work.",
|
|
4253
|
+
"Intake is retrieval, not the final decision. The model judges; the Cadence CLI validates and executes. Return JSON only.",
|
|
4254
|
+
"Choose whether this agent session should create a new ticket, attach/switch to one concrete candidate ticket, keep the current context, or no-op.",
|
|
4255
|
+
"Use intake_create when the recent request is durable and the candidates are weak, adjacent, completed, or not concrete matches.",
|
|
4256
|
+
"Use intake_attach or switch_existing only when one candidate ticket is clearly the same work. You must include targetTicketId from the candidate list.",
|
|
4257
|
+
"Use current only when the saved/current Cadence context clearly fits the recent work.",
|
|
4258
|
+
"Use noop only for filler or if the work is not durable enough for Cadence.",
|
|
4259
|
+
"Do not include raw transcripts, raw diffs, terminal logs, tool streams, secrets, file contents, or model reasoning in server-bound fields.",
|
|
4260
|
+
'Return this sparse JSON shape: {"summary":"short routing summary","sessionTitle":{"text":"short GUI session title","confidence":"low|medium|high","reason":"why this title names the selected routed focus"},"route":{"action":"current|noop|intake_create|intake_attach|switch_existing","confidence":"low|medium|high","reason":"short reason","request":"natural language intake request when creating","targetTicketId":"required for attach/switch"},"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"}]}',
|
|
4261
|
+
"The final candidate plan owns sessionTitle. Generate it only for the selected durable focus, keep it under 80 characters, and do not reuse the latest checkpoint summary as the title.",
|
|
4262
|
+
"Prefer preserving the initial plan's entries unless they are wrong for the selected ticket.",
|
|
4263
|
+
"",
|
|
4264
|
+
contextText,
|
|
4265
|
+
`Initial route plan:
|
|
4266
|
+
${truncateText(JSON.stringify(input.initialPlan), 4000)}`,
|
|
4267
|
+
`Intake result and candidates:
|
|
4268
|
+
${truncateText(JSON.stringify(intake), 6000)}`,
|
|
4269
|
+
recentTurnsText ? `Recent user/assistant turns (most recent 3, local routing context only):
|
|
4270
|
+
${recentTurnsText}` : input.event.lastAssistantMessage ? `Last assistant message:
|
|
4271
|
+
${truncateText(input.event.lastAssistantMessage, 3000)}` : "Recent turns: unavailable"
|
|
4272
|
+
].filter(Boolean).join(`
|
|
4273
|
+
`);
|
|
4274
|
+
}
|
|
4275
|
+
function agentRunFingerprint(value) {
|
|
4276
|
+
return stableHash(JSON.stringify(value));
|
|
4277
|
+
}
|
|
4278
|
+
function buildAgentRunFingerprints(input) {
|
|
4279
|
+
return {
|
|
4280
|
+
event: agentRunFingerprint({
|
|
4281
|
+
source: input.event.source,
|
|
4282
|
+
event: input.event.event,
|
|
4283
|
+
agentSessionKey: input.event.agentSessionKey,
|
|
4284
|
+
agentSessionId: input.event.agentSessionId,
|
|
4285
|
+
threadId: input.event.threadId,
|
|
4286
|
+
turnId: input.event.turnId,
|
|
4287
|
+
lastAssistantMessage: input.event.lastAssistantMessage,
|
|
4288
|
+
recentTurns: input.event.recentTurns,
|
|
4289
|
+
payloadKeys: input.event.payloadKeys
|
|
4290
|
+
}),
|
|
4291
|
+
git: agentRunFingerprint({
|
|
4292
|
+
status: input.gitStatus,
|
|
4293
|
+
diffStat: input.gitDiffStat,
|
|
4294
|
+
changedFiles: input.changedFiles
|
|
4295
|
+
}),
|
|
4296
|
+
cadenceContext: agentRunFingerprint({
|
|
4297
|
+
ticketId: input.ticketId,
|
|
4298
|
+
sessionId: input.sessionId,
|
|
4299
|
+
changesetId: input.changesetId
|
|
4300
|
+
}),
|
|
4301
|
+
...input.coverage?.hasDebt ? { coverageDebt: agentRunFingerprint(input.coverage) } : {}
|
|
4302
|
+
};
|
|
4303
|
+
}
|
|
4304
|
+
function fingerprintsEqual(left, right) {
|
|
4305
|
+
return Boolean(left && left.event === right.event && left.git === right.git && left.cadenceContext === right.cadenceContext && left.verification === right.verification && left.coverageDebt === right.coverageDebt);
|
|
4306
|
+
}
|
|
4307
|
+
function checkpointReasonBypassesFingerprintGate(reason) {
|
|
4308
|
+
return reason === "manual" || reason === "idle" || reason === "ticket_switch" || reason === "closeout";
|
|
4309
|
+
}
|
|
4310
|
+
function entryText(entry) {
|
|
4311
|
+
return `${entry.summary ?? ""}
|
|
4312
|
+
${entry.body}`.toLowerCase();
|
|
4313
|
+
}
|
|
4314
|
+
function entryReductionSummary(entry) {
|
|
4315
|
+
return truncateText(entry.summary ?? checkpointEntrySummary(entry.body), 120);
|
|
4316
|
+
}
|
|
4317
|
+
function isRepeatedScopeNote(entry) {
|
|
4318
|
+
const text = entryText(entry);
|
|
4319
|
+
return entry.kind === "note" && (text.includes("dirty workspace") || text.includes("dirty files") || text.includes("unattributed"));
|
|
4320
|
+
}
|
|
4321
|
+
function isRepeatedNoVerificationNote(entry) {
|
|
4322
|
+
const text = entryText(entry);
|
|
4323
|
+
return entry.kind === "verification" && text.includes("no fresh") && (text.includes("test") || text.includes("verification"));
|
|
4324
|
+
}
|
|
4325
|
+
function isUnchangedIntent(entry) {
|
|
4326
|
+
const text = entryText(entry);
|
|
4327
|
+
return entry.kind === "intent" && (text.includes("remain") || text.includes("still")) && text.includes("active");
|
|
4328
|
+
}
|
|
4329
|
+
function isProcessOnlyAction(entry) {
|
|
4330
|
+
if (entry.kind !== "action") {
|
|
4331
|
+
return false;
|
|
4332
|
+
}
|
|
4333
|
+
const text = entryText(entry);
|
|
4334
|
+
const processSignals = ["read ", "inspected", "looked at", "ran git status", "gathered context", "paused", "discussed", "answered"];
|
|
4335
|
+
const reviewableSignals = [
|
|
4336
|
+
"added",
|
|
4337
|
+
"implemented",
|
|
4338
|
+
"updated",
|
|
4339
|
+
"changed",
|
|
4340
|
+
"removed",
|
|
4341
|
+
"refactored",
|
|
4342
|
+
"fixed",
|
|
4343
|
+
"moved",
|
|
4344
|
+
"persisted",
|
|
4345
|
+
"configured",
|
|
4346
|
+
"wrote",
|
|
4347
|
+
"split"
|
|
4348
|
+
];
|
|
4349
|
+
return processSignals.some((signal) => text.includes(signal)) && !reviewableSignals.some((signal) => text.includes(signal));
|
|
4350
|
+
}
|
|
4351
|
+
function entryDedupeKey(entry) {
|
|
4352
|
+
if (isRepeatedScopeNote(entry)) {
|
|
4353
|
+
return "scope-note";
|
|
4354
|
+
}
|
|
4355
|
+
if (isRepeatedNoVerificationNote(entry)) {
|
|
4356
|
+
return "no-verification";
|
|
4357
|
+
}
|
|
4358
|
+
if (isUnchangedIntent(entry)) {
|
|
4359
|
+
return "unchanged-intent";
|
|
4360
|
+
}
|
|
4361
|
+
return `${entry.kind}:${entry.summary ?? checkpointEntrySummary(entry.body)}`.toLowerCase();
|
|
4362
|
+
}
|
|
4363
|
+
function mergeDecisionRationaleEntries(entries) {
|
|
4364
|
+
const merged = [];
|
|
4365
|
+
const records = [];
|
|
4366
|
+
for (let index = 0;index < entries.length; index += 1) {
|
|
4367
|
+
const entry = entries[index];
|
|
4368
|
+
const next = entries[index + 1];
|
|
4369
|
+
if (entry.kind === "decision" && next?.kind === "rationale") {
|
|
4370
|
+
merged.push({
|
|
4371
|
+
...entry,
|
|
4372
|
+
body: truncateText(`${entry.body}
|
|
4373
|
+
|
|
4374
|
+
Rationale: ${next.body.replace(/^Rationale:\s*/i, "")}`, checkpointServerTextLimit)
|
|
4375
|
+
});
|
|
4376
|
+
records.push({
|
|
4377
|
+
summary: entryReductionSummary(next),
|
|
4378
|
+
reason: "merged_adjacent_decision_rationale"
|
|
4379
|
+
});
|
|
4380
|
+
index += 1;
|
|
4381
|
+
continue;
|
|
4382
|
+
}
|
|
4383
|
+
merged.push(entry);
|
|
4384
|
+
}
|
|
4385
|
+
return { entries: merged, records };
|
|
4386
|
+
}
|
|
4387
|
+
function checkpointAllowsExtraEntries(entries) {
|
|
4388
|
+
return entries.some((entry) => entry.kind === "blocker" || entry.kind === "verification" && /\b(fail|failed|failing|error|blocked)\b/i.test(entry.body));
|
|
4389
|
+
}
|
|
4390
|
+
function reduceCheckpointEntries(entries, mode) {
|
|
4391
|
+
const dropped = [];
|
|
4392
|
+
const seen = new Set;
|
|
4393
|
+
const filtered = [];
|
|
4394
|
+
for (const entry of entries) {
|
|
4395
|
+
if (isProcessOnlyAction(entry)) {
|
|
4396
|
+
dropped.push({ summary: entryReductionSummary(entry), reason: "process_only_action" });
|
|
4397
|
+
continue;
|
|
4398
|
+
}
|
|
4399
|
+
const dedupeKey = entryDedupeKey(entry);
|
|
4400
|
+
if (seen.has(dedupeKey)) {
|
|
4401
|
+
dropped.push({ summary: entryReductionSummary(entry), reason: "duplicate_entry" });
|
|
4402
|
+
continue;
|
|
4403
|
+
}
|
|
4404
|
+
seen.add(dedupeKey);
|
|
4405
|
+
filtered.push(entry);
|
|
4406
|
+
}
|
|
4407
|
+
const merged = mergeDecisionRationaleEntries(filtered);
|
|
4408
|
+
const maxEntries = mode === "closeout" ? 8 : checkpointAllowsExtraEntries(merged.entries) ? 4 : 3;
|
|
4409
|
+
const capped = merged.entries.length > maxEntries;
|
|
4410
|
+
const kept = capped ? merged.entries.slice(0, maxEntries) : merged.entries;
|
|
4411
|
+
for (const entry of merged.entries.slice(maxEntries)) {
|
|
4412
|
+
dropped.push({ summary: entryReductionSummary(entry), reason: "entry_budget" });
|
|
4413
|
+
}
|
|
4414
|
+
return {
|
|
4415
|
+
entries: kept,
|
|
4416
|
+
reduction: {
|
|
4417
|
+
mode,
|
|
4418
|
+
originalCount: entries.length,
|
|
4419
|
+
keptCount: kept.length,
|
|
4420
|
+
dropped,
|
|
4421
|
+
merged: merged.records,
|
|
4422
|
+
capped
|
|
4423
|
+
}
|
|
4424
|
+
};
|
|
4425
|
+
}
|
|
4426
|
+
function buildAgentRunCoverage(plan, mode) {
|
|
4427
|
+
const entries = plan.entries;
|
|
4428
|
+
const bodyText = [plan.summary ?? "", plan.session?.summary ?? "", ...entries.map((entry) => `${entry.summary ?? ""} ${entry.body}`)].join(`
|
|
4429
|
+
`).toLowerCase();
|
|
4430
|
+
const outcome = Boolean(plan.summary || plan.session?.summary);
|
|
4431
|
+
const implementation = entries.some((entry) => entry.kind === "action");
|
|
4432
|
+
const decisions = entries.some((entry) => entry.kind === "decision" || entry.kind === "rationale");
|
|
4433
|
+
const corrections = entries.some((entry) => entry.kind === "correction");
|
|
4434
|
+
const verification = entries.some((entry) => entry.kind === "verification");
|
|
4435
|
+
const blockers = entries.some((entry) => entry.kind === "blocker");
|
|
4436
|
+
const scope = /\b(scope|attribut|dirty workspace|dirty files|unassigned|unattributed)\b/.test(bodyText);
|
|
4437
|
+
const handoff = mode === "closeout" && Boolean(plan.session && plan.session.action !== "keep");
|
|
4438
|
+
const hasDebt = mode === "closeout" && (!outcome || !implementation || !verification || !handoff);
|
|
4439
|
+
return {
|
|
4440
|
+
outcome,
|
|
4441
|
+
implementation,
|
|
4442
|
+
decisions,
|
|
4443
|
+
corrections,
|
|
4444
|
+
verification,
|
|
4445
|
+
blockers,
|
|
4446
|
+
scope,
|
|
4447
|
+
handoff,
|
|
4448
|
+
hasDebt
|
|
4449
|
+
};
|
|
4450
|
+
}
|
|
4451
|
+
function applyCloseoutSessionDefaults(plan, sessionAction, completeTicket) {
|
|
4452
|
+
const requestedAction = completeTicket ? "complete_ticket" : sessionAction;
|
|
4453
|
+
const session = plan.session && (completeTicket || plan.session.action !== "complete_ticket") ? plan.session : {
|
|
4454
|
+
action: requestedAction,
|
|
4455
|
+
...plan.summary ? { summary: plan.summary } : {},
|
|
4456
|
+
reason: "Closeout defaults to ending or handing off the active session."
|
|
4457
|
+
};
|
|
4458
|
+
if (!completeTicket && session.action === "complete_ticket") {
|
|
4459
|
+
return {
|
|
4460
|
+
...plan,
|
|
4461
|
+
session: {
|
|
4462
|
+
...session,
|
|
4463
|
+
action: sessionAction,
|
|
4464
|
+
reason: "Ticket completion was not requested for this closeout."
|
|
4465
|
+
}
|
|
4466
|
+
};
|
|
4467
|
+
}
|
|
4468
|
+
return {
|
|
4469
|
+
...plan,
|
|
4470
|
+
session
|
|
4471
|
+
};
|
|
4472
|
+
}
|
|
4473
|
+
function parseCloseoutSessionAction(value) {
|
|
4474
|
+
if (!value) {
|
|
4475
|
+
return "handoff";
|
|
4476
|
+
}
|
|
4477
|
+
if (!closeoutSessionActions.includes(value)) {
|
|
4478
|
+
throw new CliError("CLI_USAGE", "--session-action must be one of handoff, end, or keep.");
|
|
4479
|
+
}
|
|
4480
|
+
return value;
|
|
4481
|
+
}
|
|
4482
|
+
function checkpointModeForCommand(parsed) {
|
|
4483
|
+
return parsed.command.name === "agent-run.closeout" ? "closeout" : "checkpoint";
|
|
4484
|
+
}
|
|
4485
|
+
function formatCloseoutWorkLogEvents(events) {
|
|
4486
|
+
if (!Array.isArray(events)) {
|
|
4487
|
+
return;
|
|
4488
|
+
}
|
|
4489
|
+
const entries = events.filter((event) => event && typeof event === "object").map((event) => {
|
|
4490
|
+
const record = event;
|
|
4491
|
+
const payload = record.payload && typeof record.payload === "object" && !Array.isArray(record.payload) ? record.payload : {};
|
|
4492
|
+
const type = typeof record.type === "string" ? record.type : undefined;
|
|
4493
|
+
const kind = typeof payload.entryKind === "string" ? payload.entryKind : undefined;
|
|
4494
|
+
const summary = typeof payload.summary === "string" ? payload.summary : undefined;
|
|
4495
|
+
const body = typeof payload.body === "string" ? payload.body : undefined;
|
|
4496
|
+
if (type !== "ticket.work_log_appended" || !summary && !body) {
|
|
4497
|
+
return;
|
|
4498
|
+
}
|
|
4499
|
+
return `- ${kind ?? "note"}: ${truncateText(summary ?? body ?? "", 180)}${body && summary ? ` \u2014 ${truncateText(body, 300)}` : ""}`;
|
|
4500
|
+
}).filter((entry) => Boolean(entry)).slice(-12);
|
|
4501
|
+
return entries.length ? entries.join(`
|
|
4502
|
+
`) : undefined;
|
|
4503
|
+
}
|
|
4504
|
+
async function readCloseoutPromptContext(input) {
|
|
4505
|
+
let currentWorkSummary;
|
|
4506
|
+
let recentWorkLog;
|
|
4507
|
+
try {
|
|
4508
|
+
const ticket = await input.client.tickets.get({
|
|
4509
|
+
projectId: input.projectId,
|
|
4510
|
+
ticketId: input.ticketId
|
|
4511
|
+
});
|
|
4512
|
+
if (ticket && typeof ticket === "object" && "currentSummary" in ticket && typeof ticket.currentSummary === "string") {
|
|
4513
|
+
currentWorkSummary = ticket.currentSummary;
|
|
4514
|
+
}
|
|
4515
|
+
} catch {}
|
|
4516
|
+
try {
|
|
4517
|
+
const events = await input.client.events.list({
|
|
4518
|
+
projectId: input.projectId,
|
|
4519
|
+
filters: {
|
|
4520
|
+
ticketId: input.ticketId,
|
|
4521
|
+
...input.sessionId ? { sessionId: input.sessionId } : {},
|
|
4522
|
+
limit: 50
|
|
4523
|
+
}
|
|
4524
|
+
});
|
|
4525
|
+
recentWorkLog = formatCloseoutWorkLogEvents(events);
|
|
4526
|
+
} catch {}
|
|
4527
|
+
return {
|
|
4528
|
+
...currentWorkSummary ? { currentWorkSummary } : {},
|
|
4529
|
+
...recentWorkLog ? { recentWorkLog } : {}
|
|
4530
|
+
};
|
|
4531
|
+
}
|
|
4011
4532
|
async function findRepoRoot(cwd) {
|
|
4012
4533
|
const result = spawnSync("git", ["rev-parse", "--show-toplevel"], {
|
|
4013
4534
|
cwd,
|
|
@@ -4246,6 +4767,8 @@ async function runAgentRunCommand(parsed, options) {
|
|
|
4246
4767
|
switch (parsed.command.name) {
|
|
4247
4768
|
case "agent-run.ingest-stop":
|
|
4248
4769
|
return await runAgentRunIngestStop(parsed, options, config, meta);
|
|
4770
|
+
case "agent-run.route":
|
|
4771
|
+
return await runAgentRunRoute(parsed, options, config, meta);
|
|
4249
4772
|
case "agent-run.checkpoint":
|
|
4250
4773
|
case "agent-run.closeout":
|
|
4251
4774
|
return await runAgentRunCheckpoint(parsed, options, config, meta);
|
|
@@ -4335,11 +4858,14 @@ async function runAgentRunIngestStop(parsed, options, config, meta) {
|
|
|
4335
4858
|
const duplicateTurn = Boolean(normalized.turnId && existingSession?.lastObservedTurnId === normalized.turnId);
|
|
4336
4859
|
const nextCount = (existingSession?.stopCount ?? 0) + (duplicateTurn ? 0 : 1);
|
|
4337
4860
|
const recentTurns = mergeRecentAgentTurns(existingSession?.recentTurns, normalized.recentTurns);
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4861
|
+
const savedCadenceContext = existingSession?.cadenceContext;
|
|
4862
|
+
let cadenceContext = savedCadenceContext;
|
|
4863
|
+
if (savedCadenceContext?.ticketId) {
|
|
4864
|
+
try {
|
|
4865
|
+
const client = await createClient(config, options);
|
|
4866
|
+
cadenceContext = await readCurrentCadenceContext(client, projectId) ?? cadenceContext;
|
|
4867
|
+
} catch {}
|
|
4868
|
+
}
|
|
4343
4869
|
const observedSession = {
|
|
4344
4870
|
...clearAgentSessionReason(existingSession ?? {
|
|
4345
4871
|
source: normalized.source,
|
|
@@ -4364,6 +4890,124 @@ async function runAgentRunIngestStop(parsed, options, config, meta) {
|
|
|
4364
4890
|
[normalized.agentSessionKey]: observedSession
|
|
4365
4891
|
}
|
|
4366
4892
|
};
|
|
4893
|
+
if (duplicateTurn) {
|
|
4894
|
+
await writeAgentLoopState(parsed, options, countedState);
|
|
4895
|
+
const data2 = {
|
|
4896
|
+
action: "updated",
|
|
4897
|
+
reason: "duplicate_turn",
|
|
4898
|
+
agentSessionKey: normalized.agentSessionKey,
|
|
4899
|
+
stopCount: nextCount,
|
|
4900
|
+
threshold
|
|
4901
|
+
};
|
|
4902
|
+
return {
|
|
4903
|
+
stdout: parsed.flags.json ? formatJson(successEnvelope(data2, meta)) : `${JSON.stringify(data2, null, 2)}
|
|
4904
|
+
`,
|
|
4905
|
+
stderr: "",
|
|
4906
|
+
exitCode: 0
|
|
4907
|
+
};
|
|
4908
|
+
}
|
|
4909
|
+
if (!savedCadenceContext?.ticketId) {
|
|
4910
|
+
const eventFile2 = await writeAgentEventFile(parsed, options, normalized);
|
|
4911
|
+
const lockPath2 = agentLoopLockPath(parsed, options, normalized.agentSessionKey);
|
|
4912
|
+
const workerArgs2 = [
|
|
4913
|
+
"agent-run",
|
|
4914
|
+
"route",
|
|
4915
|
+
"--agent-session-key",
|
|
4916
|
+
normalized.agentSessionKey,
|
|
4917
|
+
"--reason",
|
|
4918
|
+
"missing_context",
|
|
4919
|
+
"--event-file",
|
|
4920
|
+
eventFile2,
|
|
4921
|
+
"--lock",
|
|
4922
|
+
lockPath2,
|
|
4923
|
+
...parsed.flags.project ? ["--project", parsed.flags.project] : [],
|
|
4924
|
+
...parsed.flags.server ? ["--server", parsed.flags.server] : []
|
|
4925
|
+
];
|
|
4926
|
+
if (dryRun) {
|
|
4927
|
+
await writeAgentLoopState(parsed, options, {
|
|
4928
|
+
...state,
|
|
4929
|
+
sessions: {
|
|
4930
|
+
...state.sessions,
|
|
4931
|
+
[normalized.agentSessionKey]: {
|
|
4932
|
+
...observedSession,
|
|
4933
|
+
lastAction: "would_route",
|
|
4934
|
+
lastEventFile: eventFile2
|
|
4935
|
+
}
|
|
4936
|
+
}
|
|
4937
|
+
});
|
|
4938
|
+
const data3 = {
|
|
4939
|
+
action: "would_route",
|
|
4940
|
+
agentSessionKey: normalized.agentSessionKey,
|
|
4941
|
+
eventFile: eventFile2,
|
|
4942
|
+
workerArgs: workerArgs2
|
|
4943
|
+
};
|
|
4944
|
+
return {
|
|
4945
|
+
stdout: parsed.flags.json ? formatJson(successEnvelope(data3, meta)) : `${JSON.stringify(data3, null, 2)}
|
|
4946
|
+
`,
|
|
4947
|
+
stderr: "",
|
|
4948
|
+
exitCode: 0
|
|
4949
|
+
};
|
|
4950
|
+
}
|
|
4951
|
+
const lock2 = await acquireAgentLoopLock(parsed, options, normalized.agentSessionKey);
|
|
4952
|
+
if (!lock2.acquired) {
|
|
4953
|
+
await writeAgentLoopState(parsed, options, {
|
|
4954
|
+
...state,
|
|
4955
|
+
sessions: {
|
|
4956
|
+
...state.sessions,
|
|
4957
|
+
[normalized.agentSessionKey]: {
|
|
4958
|
+
...observedSession,
|
|
4959
|
+
lastAction: "skipped",
|
|
4960
|
+
lastReason: "lock_held"
|
|
4961
|
+
}
|
|
4962
|
+
}
|
|
4963
|
+
});
|
|
4964
|
+
const data3 = {
|
|
4965
|
+
action: "skipped",
|
|
4966
|
+
reason: "lock_held",
|
|
4967
|
+
lockPath: lock2.lockPath,
|
|
4968
|
+
agentSessionKey: normalized.agentSessionKey
|
|
4969
|
+
};
|
|
4970
|
+
return {
|
|
4971
|
+
stdout: parsed.flags.json ? formatJson(successEnvelope(data3, meta)) : `${JSON.stringify(data3, null, 2)}
|
|
4972
|
+
`,
|
|
4973
|
+
stderr: "",
|
|
4974
|
+
exitCode: 0
|
|
4975
|
+
};
|
|
4976
|
+
}
|
|
4977
|
+
try {
|
|
4978
|
+
await spawnAgentRunWorker(workerArgs2, options);
|
|
4979
|
+
} catch (error) {
|
|
4980
|
+
await releaseAgentLoopLock(lock2.lockPath);
|
|
4981
|
+
throw error;
|
|
4982
|
+
}
|
|
4983
|
+
await writeAgentLoopState(parsed, options, {
|
|
4984
|
+
...state,
|
|
4985
|
+
sessions: {
|
|
4986
|
+
...state.sessions,
|
|
4987
|
+
[normalized.agentSessionKey]: {
|
|
4988
|
+
...observedSession,
|
|
4989
|
+
stopCount: 0,
|
|
4990
|
+
threshold: defaultCheckpointThresholdValue(),
|
|
4991
|
+
lastAction: "route_spawned",
|
|
4992
|
+
lastEventFile: eventFile2,
|
|
4993
|
+
lastCheckpointAt: new Date().toISOString(),
|
|
4994
|
+
lastCheckpointFingerprint: fingerprintForCheckpoint(normalized, options)
|
|
4995
|
+
}
|
|
4996
|
+
}
|
|
4997
|
+
});
|
|
4998
|
+
const data2 = {
|
|
4999
|
+
action: "route_spawned",
|
|
5000
|
+
agentSessionKey: normalized.agentSessionKey,
|
|
5001
|
+
eventFile: eventFile2,
|
|
5002
|
+
lockPath: lock2.lockPath
|
|
5003
|
+
};
|
|
5004
|
+
return {
|
|
5005
|
+
stdout: parsed.flags.json ? formatJson(successEnvelope(data2, meta)) : `${JSON.stringify(data2, null, 2)}
|
|
5006
|
+
`,
|
|
5007
|
+
stderr: "",
|
|
5008
|
+
exitCode: 0
|
|
5009
|
+
};
|
|
5010
|
+
}
|
|
4367
5011
|
if (nextCount < threshold) {
|
|
4368
5012
|
await writeAgentLoopState(parsed, options, countedState);
|
|
4369
5013
|
const data2 = {
|
|
@@ -4537,9 +5181,450 @@ async function runAgentRunIngestStop(parsed, options, config, meta) {
|
|
|
4537
5181
|
exitCode: 0
|
|
4538
5182
|
};
|
|
4539
5183
|
}
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
5184
|
+
function objectStringId(value) {
|
|
5185
|
+
return value && typeof value === "object" && "id" in value && typeof value.id === "string" ? value.id : undefined;
|
|
5186
|
+
}
|
|
5187
|
+
function objectStringTitle(value) {
|
|
5188
|
+
return value && typeof value === "object" && "title" in value && typeof value.title === "string" ? cleanAgentSessionTitleText(value.title) : undefined;
|
|
5189
|
+
}
|
|
5190
|
+
function routeActionAllowsDisplayTitle(action, plan) {
|
|
5191
|
+
return action === "routed" || action === "switched" || action === "current" && plan.route.confidence === "high";
|
|
5192
|
+
}
|
|
5193
|
+
function routeDisplayTitle(action, plan, selectedTicket) {
|
|
5194
|
+
if (!routeActionAllowsDisplayTitle(action, plan)) {
|
|
5195
|
+
return;
|
|
5196
|
+
}
|
|
5197
|
+
const fallbackTitle = objectStringTitle(selectedTicket);
|
|
5198
|
+
const text = plan.sessionTitle?.text ?? fallbackTitle;
|
|
5199
|
+
if (!text) {
|
|
5200
|
+
return;
|
|
5201
|
+
}
|
|
5202
|
+
return {
|
|
5203
|
+
text,
|
|
5204
|
+
confidence: plan.sessionTitle?.confidence ?? plan.route.confidence,
|
|
5205
|
+
reason: plan.sessionTitle?.reason ?? plan.route.reason ?? (fallbackTitle ? "Recent route selected this ticket." : undefined)
|
|
5206
|
+
};
|
|
5207
|
+
}
|
|
5208
|
+
async function runAgentRunRoute(parsed, options, config, meta) {
|
|
5209
|
+
const projectId = requireProjectId(config);
|
|
5210
|
+
const client = await createClient(config, options);
|
|
5211
|
+
const agentSessionKeyValue = requireOption(parsed, "agent-session-key");
|
|
5212
|
+
const state = await readAgentLoopState(parsed, options);
|
|
5213
|
+
const sessionState = state.sessions[agentSessionKeyValue];
|
|
5214
|
+
if (!sessionState) {
|
|
5215
|
+
throw new CliError("AGENT_RUN_SESSION_NOT_FOUND", "No local agent-run session state exists for --agent-session-key.", {
|
|
5216
|
+
agentSessionKey: agentSessionKeyValue
|
|
5217
|
+
});
|
|
5218
|
+
}
|
|
5219
|
+
const savedContext = sessionState.cadenceContext;
|
|
5220
|
+
const currentContext = savedContext?.ticketId ? undefined : await readCurrentCadenceContext(client, projectId);
|
|
5221
|
+
const eventFile = parsed.options["event-file"] ?? sessionState.lastEventFile;
|
|
5222
|
+
const event = eventFile ? tryParseJsonObject(await readFile(eventFile, "utf8"), eventFile) : synthesizeAgentEventFromSession(agentSessionKeyValue, sessionState, options);
|
|
5223
|
+
const checkpointSettings = await resolveCheckpointSettings(parsed, options);
|
|
5224
|
+
const gitStatus = gitOutput(["status", "--short"], options);
|
|
5225
|
+
const gitDiffStat = gitOutput(["diff", "--stat", "origin/dev..."], options);
|
|
5226
|
+
const changedFiles = gitOutput(["diff", "--name-only", "origin/dev..."], options);
|
|
5227
|
+
const prompt = buildRoutePrompt({
|
|
5228
|
+
event,
|
|
5229
|
+
...parsed.options.reason ? { reason: parsed.options.reason } : {},
|
|
5230
|
+
...savedContext ? { savedContext } : {},
|
|
5231
|
+
...currentContext ? { currentContext } : {},
|
|
5232
|
+
gitStatus,
|
|
5233
|
+
gitDiffStat,
|
|
5234
|
+
changedFiles,
|
|
5235
|
+
...sessionState.previousCheckpointSummary ? { previousCheckpointSummary: sessionState.previousCheckpointSummary } : {}
|
|
5236
|
+
});
|
|
5237
|
+
const promptTokenAccounting = buildCheckpointTokenAccounting(event, prompt, undefined);
|
|
5238
|
+
const dryRun = parseBooleanOption(parsed.options["dry-run"], false);
|
|
5239
|
+
const lockPath = parsed.options.lock;
|
|
5240
|
+
if (dryRun) {
|
|
5241
|
+
const auditFile = await writeAgentCheckpointAuditFile(parsed, options, {
|
|
5242
|
+
version: 1,
|
|
5243
|
+
mode: "route",
|
|
5244
|
+
action: "would_route",
|
|
5245
|
+
createdAt: new Date().toISOString(),
|
|
5246
|
+
localOnly: true,
|
|
5247
|
+
localOnlyReason: "Raw hook context and route prompts stay on this machine and are not uploaded to Cadence.",
|
|
5248
|
+
agentSessionKey: agentSessionKeyValue,
|
|
5249
|
+
...savedContext?.ticketId ? { ticketId: savedContext.ticketId } : currentContext?.ticketId ? { ticketId: currentContext.ticketId } : {},
|
|
5250
|
+
...eventFile ? { eventFile } : {},
|
|
5251
|
+
event,
|
|
5252
|
+
prompt,
|
|
5253
|
+
checkpointSettings,
|
|
5254
|
+
tokenAccounting: promptTokenAccounting,
|
|
5255
|
+
cadenceWrites: []
|
|
5256
|
+
});
|
|
5257
|
+
const data = {
|
|
5258
|
+
action: "would_route",
|
|
5259
|
+
prompt,
|
|
5260
|
+
auditFile,
|
|
5261
|
+
agentSessionKey: agentSessionKeyValue
|
|
5262
|
+
};
|
|
5263
|
+
return {
|
|
5264
|
+
stdout: parsed.flags.json ? formatJson(successEnvelope(data, meta)) : `${JSON.stringify(data, null, 2)}
|
|
5265
|
+
`,
|
|
5266
|
+
stderr: "",
|
|
5267
|
+
exitCode: 0
|
|
5268
|
+
};
|
|
5269
|
+
}
|
|
5270
|
+
try {
|
|
5271
|
+
const codexCommand = parsed.options["codex-command"] ?? "codex";
|
|
5272
|
+
const codexCwd = options.cwd ?? process.cwd();
|
|
5273
|
+
const runRouteWorker = (workerPrompt, failureMessage) => {
|
|
5274
|
+
const codexStartedAtMs = Date.now();
|
|
5275
|
+
const codexArgs = [
|
|
5276
|
+
"exec",
|
|
5277
|
+
...checkpointSettings.model ? ["-m", checkpointSettings.model] : [],
|
|
5278
|
+
"--disable",
|
|
5279
|
+
"hooks",
|
|
5280
|
+
"--sandbox",
|
|
5281
|
+
"read-only",
|
|
5282
|
+
"-C",
|
|
5283
|
+
codexCwd,
|
|
5284
|
+
workerPrompt
|
|
5285
|
+
];
|
|
5286
|
+
const codex = runLocalCommand(codexCommand, codexArgs, options, {
|
|
5287
|
+
cwd: codexCwd,
|
|
5288
|
+
env: {
|
|
5289
|
+
[agentLoopSuppressEnv]: "1",
|
|
5290
|
+
CADENCE_HOOK_SUPPRESS: "1"
|
|
5291
|
+
},
|
|
5292
|
+
timeoutMs: defaultCheckpointWorkerTimeoutMs
|
|
5293
|
+
});
|
|
5294
|
+
const codexSessionTranscript = readCodexSessionTranscript(findCodexSessionFileCreatedAfter(options, codexStartedAtMs, codexCwd));
|
|
5295
|
+
const tokenAccounting2 = buildCheckpointTokenAccounting(event, workerPrompt, codexSessionTranscript?.tokenUsage);
|
|
5296
|
+
if (codex.status !== 0 || codex.error) {
|
|
5297
|
+
throw new CliError("AGENT_RUN_ROUTE_FAILED", failureMessage, {
|
|
5298
|
+
status: codex.status,
|
|
5299
|
+
stderr: truncateText(codex.stderr, 2000),
|
|
5300
|
+
error: codex.error?.message
|
|
5301
|
+
});
|
|
5302
|
+
}
|
|
5303
|
+
return { codex, codexSessionTranscript, tokenAccounting: tokenAccounting2 };
|
|
5304
|
+
};
|
|
5305
|
+
const initialRouteWorker = runRouteWorker(prompt, "Codex route generation failed.");
|
|
5306
|
+
let routePlan = parseCheckpointPlanJson(initialRouteWorker.codex.stdout, "intent");
|
|
5307
|
+
const currentTicketId = savedContext?.ticketId ?? currentContext?.ticketId;
|
|
5308
|
+
const currentSessionId = savedContext?.sessionId ?? currentContext?.sessionId;
|
|
5309
|
+
const currentChangesetId = savedContext?.changesetId ?? currentContext?.changesetId;
|
|
5310
|
+
if (routePlan.route.action === "needs_human") {
|
|
5311
|
+
routePlan = safeAutomaticRoutePlan(routePlan, Boolean(currentTicketId), routePlan.route.reason ?? "Route was uncertain.");
|
|
5312
|
+
} else if (checkpointRouteRequiresIntake(routePlan.route.action) && routePlan.route.confidence !== "high") {
|
|
5313
|
+
routePlan = safeAutomaticRoutePlan(routePlan, Boolean(currentTicketId), "Automatic routing requires high confidence.");
|
|
5314
|
+
} else if ((routePlan.route.action === "intake_attach" || routePlan.route.action === "switch_existing") && !routePlan.route.targetTicketId) {
|
|
5315
|
+
routePlan = safeAutomaticRoutePlan(routePlan, Boolean(currentTicketId), "Automatic routing to existing work requires a target ticket id.");
|
|
5316
|
+
} else if (routePlan.route.action === "current" && !currentTicketId) {
|
|
5317
|
+
routePlan = checkpointPlanWithRoute(routePlan, {
|
|
5318
|
+
action: "noop",
|
|
5319
|
+
confidence: routePlan.route.confidence,
|
|
5320
|
+
reason: routePlan.route.reason ?? "No current Cadence context exists."
|
|
5321
|
+
});
|
|
5322
|
+
}
|
|
5323
|
+
const cadenceWrites = [];
|
|
5324
|
+
const lifecycleOperations = [];
|
|
5325
|
+
let targetTicketId = currentTicketId;
|
|
5326
|
+
let targetSessionId = currentSessionId;
|
|
5327
|
+
let targetChangesetId = currentChangesetId;
|
|
5328
|
+
let intakeResult;
|
|
5329
|
+
let selectedTicket;
|
|
5330
|
+
let summary = routePlan.summary ?? routePlan.entries[0]?.summary ?? routePlan.route.reason ?? "Agent run route";
|
|
5331
|
+
let tokenAccounting = initialRouteWorker.tokenAccounting;
|
|
5332
|
+
const modelAudit = {
|
|
5333
|
+
provider: checkpointSettings.provider,
|
|
5334
|
+
command: codexCommand,
|
|
5335
|
+
...checkpointSettings.model ? { model: checkpointSettings.model } : {},
|
|
5336
|
+
status: initialRouteWorker.codex.status,
|
|
5337
|
+
stdout: initialRouteWorker.codex.stdout.trim(),
|
|
5338
|
+
stderr: truncateText(initialRouteWorker.codex.stderr.trim(), 2000),
|
|
5339
|
+
tokenAccounting,
|
|
5340
|
+
...initialRouteWorker.codexSessionTranscript?.tokenUsage ? { tokenUsage: initialRouteWorker.codexSessionTranscript.tokenUsage } : {},
|
|
5341
|
+
...initialRouteWorker.codexSessionTranscript ? { sessionTranscript: initialRouteWorker.codexSessionTranscript } : {}
|
|
5342
|
+
};
|
|
5343
|
+
const finishRoute = async (action, reason) => {
|
|
5344
|
+
const checkedAt = new Date().toISOString();
|
|
5345
|
+
const displayTitle = routeDisplayTitle(action, routePlan, selectedTicket);
|
|
5346
|
+
const auditFile = await writeAgentCheckpointAuditFile(parsed, options, {
|
|
5347
|
+
version: 1,
|
|
5348
|
+
mode: "route",
|
|
5349
|
+
action,
|
|
5350
|
+
createdAt: checkedAt,
|
|
5351
|
+
localOnly: true,
|
|
5352
|
+
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.",
|
|
5353
|
+
agentSessionKey: agentSessionKeyValue,
|
|
5354
|
+
...targetTicketId ? { ticketId: targetTicketId } : {},
|
|
5355
|
+
...targetSessionId ? { sessionId: targetSessionId } : {},
|
|
5356
|
+
...targetChangesetId ? { changesetId: targetChangesetId } : {},
|
|
5357
|
+
...eventFile ? { eventFile } : {},
|
|
5358
|
+
event,
|
|
5359
|
+
prompt,
|
|
5360
|
+
model: modelAudit,
|
|
5361
|
+
checkpoint: routePlan,
|
|
5362
|
+
route: routePlan.route,
|
|
5363
|
+
...displayTitle ? { sessionTitle: displayTitle } : {},
|
|
5364
|
+
intakeResult,
|
|
5365
|
+
selectedTicket,
|
|
5366
|
+
lifecycleOperations,
|
|
5367
|
+
cadenceWrites,
|
|
5368
|
+
summary,
|
|
5369
|
+
...reason ? { reason } : {},
|
|
5370
|
+
entryCount: routePlan.entries.length
|
|
5371
|
+
});
|
|
5372
|
+
const nextSessionState = {
|
|
5373
|
+
...sessionState,
|
|
5374
|
+
stopCount: 0,
|
|
5375
|
+
threshold: defaultCheckpointThresholdValue(),
|
|
5376
|
+
previousCheckpointSummary: summary,
|
|
5377
|
+
lastAction: action,
|
|
5378
|
+
...reason ? { lastReason: reason } : {},
|
|
5379
|
+
...eventFile ? { lastEventFile: eventFile } : {},
|
|
5380
|
+
...targetTicketId ? {
|
|
5381
|
+
cadenceContext: {
|
|
5382
|
+
ticketId: targetTicketId,
|
|
5383
|
+
...targetSessionId ? { sessionId: targetSessionId } : {},
|
|
5384
|
+
...targetChangesetId ? { changesetId: targetChangesetId } : {},
|
|
5385
|
+
capturedAt: checkedAt
|
|
5386
|
+
}
|
|
5387
|
+
} : {},
|
|
5388
|
+
lastCheckpointAt: checkedAt,
|
|
5389
|
+
lastCheckpointMode: "checkpoint",
|
|
5390
|
+
...displayTitle ? {
|
|
5391
|
+
displayTitle: displayTitle.text,
|
|
5392
|
+
displayTitleUpdatedAt: checkedAt,
|
|
5393
|
+
displayTitleSource: "route",
|
|
5394
|
+
displayTitleConfidence: displayTitle.confidence,
|
|
5395
|
+
...displayTitle.reason ? { displayTitleReason: displayTitle.reason } : {}
|
|
5396
|
+
} : {},
|
|
5397
|
+
lastCheckpointAuditFile: auditFile
|
|
5398
|
+
};
|
|
5399
|
+
await writeAgentLoopState(parsed, options, {
|
|
5400
|
+
...state,
|
|
5401
|
+
sessions: {
|
|
5402
|
+
...state.sessions,
|
|
5403
|
+
[agentSessionKeyValue]: nextSessionState
|
|
5404
|
+
}
|
|
5405
|
+
});
|
|
5406
|
+
const data = {
|
|
5407
|
+
action,
|
|
5408
|
+
route: routePlan.route,
|
|
5409
|
+
...reason ? { reason } : {},
|
|
5410
|
+
...targetTicketId ? { ticketId: targetTicketId } : {},
|
|
5411
|
+
summary,
|
|
5412
|
+
...displayTitle ? { sessionTitle: displayTitle } : {},
|
|
5413
|
+
entryCount: routePlan.entries.length,
|
|
5414
|
+
mode: "route",
|
|
5415
|
+
auditFile,
|
|
5416
|
+
agentSessionKey: agentSessionKeyValue,
|
|
5417
|
+
...targetSessionId ? { sessionId: targetSessionId } : {},
|
|
5418
|
+
...targetChangesetId ? { changesetId: targetChangesetId } : {}
|
|
5419
|
+
};
|
|
5420
|
+
return {
|
|
5421
|
+
stdout: parsed.flags.json ? formatJson(successEnvelope(data, meta)) : `${JSON.stringify(data, null, 2)}
|
|
5422
|
+
`,
|
|
5423
|
+
stderr: "",
|
|
5424
|
+
exitCode: 0
|
|
5425
|
+
};
|
|
5426
|
+
};
|
|
5427
|
+
if (routePlan.route.action === "noop") {
|
|
5428
|
+
return await finishRoute("noop", routePlan.route.reason ?? routePlan.summary ?? "Nothing durable to route.");
|
|
5429
|
+
}
|
|
5430
|
+
if (routePlan.route.action === "current") {
|
|
5431
|
+
return await finishRoute("current", routePlan.route.reason ?? "Kept current Cadence context.");
|
|
5432
|
+
}
|
|
5433
|
+
if (routePlanAllowsLifecycle(routePlan)) {
|
|
5434
|
+
const intakeRequest = routePlan.route.request ?? checkpointPlanDescription(routePlan);
|
|
5435
|
+
intakeResult = await client.intake.create({
|
|
5436
|
+
projectId,
|
|
5437
|
+
intake: {
|
|
5438
|
+
request: intakeRequest,
|
|
5439
|
+
...commandMetadata()
|
|
5440
|
+
}
|
|
5441
|
+
});
|
|
5442
|
+
lifecycleOperations.push(checkpointLifecycleOperation("intake.created", true, { request: intakeRequest, result: intakeResult }));
|
|
5443
|
+
if (intakeCandidates(intakeResult).length > 0) {
|
|
5444
|
+
const candidatePrompt = buildRouteCandidatePrompt({
|
|
5445
|
+
event,
|
|
5446
|
+
initialPlan: routePlan,
|
|
5447
|
+
intakeResult,
|
|
5448
|
+
...savedContext ? { savedContext } : {},
|
|
5449
|
+
...currentContext ? { currentContext } : {}
|
|
5450
|
+
});
|
|
5451
|
+
const candidateWorker = runRouteWorker(candidatePrompt, "Codex route candidate selection failed.");
|
|
5452
|
+
const candidatePlan = parseCheckpointPlanJson(candidateWorker.codex.stdout, "intent");
|
|
5453
|
+
routePlan = planWithCandidateSelection(routePlan, candidatePlan, currentTicketId, intakeCandidateTicketIds(intakeResult));
|
|
5454
|
+
summary = routePlan.summary ?? routePlan.entries[0]?.summary ?? routePlan.route.reason ?? summary;
|
|
5455
|
+
tokenAccounting = candidateWorker.tokenAccounting;
|
|
5456
|
+
modelAudit.candidateSelection = {
|
|
5457
|
+
prompt: candidatePrompt,
|
|
5458
|
+
status: candidateWorker.codex.status,
|
|
5459
|
+
stdout: candidateWorker.codex.stdout.trim(),
|
|
5460
|
+
stderr: truncateText(candidateWorker.codex.stderr.trim(), 2000),
|
|
5461
|
+
tokenAccounting: candidateWorker.tokenAccounting,
|
|
5462
|
+
...candidateWorker.codexSessionTranscript?.tokenUsage ? { tokenUsage: candidateWorker.codexSessionTranscript.tokenUsage } : {},
|
|
5463
|
+
...candidateWorker.codexSessionTranscript ? { sessionTranscript: candidateWorker.codexSessionTranscript } : {}
|
|
5464
|
+
};
|
|
5465
|
+
if (routePlan.route.action === "noop") {
|
|
5466
|
+
return await finishRoute("noop", routePlan.route.reason ?? routePlan.summary ?? "Candidate routing produced no durable route.");
|
|
5467
|
+
}
|
|
5468
|
+
if (routePlan.route.action === "current") {
|
|
5469
|
+
return await finishRoute("current", routePlan.route.reason ?? "Candidate routing kept current Cadence context.");
|
|
5470
|
+
}
|
|
5471
|
+
if (!routePlanAllowsLifecycle(routePlan)) {
|
|
5472
|
+
routePlan = safeAutomaticRoutePlan(routePlan, Boolean(currentTicketId), routePlan.route.reason ?? "Candidate routing did not request an executable automatic action.");
|
|
5473
|
+
return await finishRoute(routePlan.route.action, routePlan.route.reason);
|
|
5474
|
+
}
|
|
5475
|
+
} else if (checkpointHasConflictingIntakeResult(intakeResult, routePlan)) {
|
|
5476
|
+
routePlan = safeAutomaticRoutePlan(routePlan, Boolean(currentTicketId), "Intake returned conflicting duplicate, overlap, or completed-before candidates.");
|
|
5477
|
+
return await finishRoute(routePlan.route.action, routePlan.route.reason);
|
|
5478
|
+
}
|
|
5479
|
+
if (routePlan.route.action === "intake_create") {
|
|
5480
|
+
selectedTicket = await client.tickets.create({
|
|
5481
|
+
projectId,
|
|
5482
|
+
ticket: {
|
|
5483
|
+
title: checkpointPlanTitle(routePlan),
|
|
5484
|
+
description: checkpointPlanDescription(routePlan),
|
|
5485
|
+
fromIntakeId: objectStringId(intakeResult),
|
|
5486
|
+
...commandMetadata()
|
|
5487
|
+
}
|
|
5488
|
+
});
|
|
5489
|
+
lifecycleOperations.push(checkpointLifecycleOperation("ticket.created", true, { ticket: selectedTicket }));
|
|
5490
|
+
targetTicketId = objectStringId(selectedTicket);
|
|
5491
|
+
} else {
|
|
5492
|
+
const selectedTicketId = routePlan.route.targetTicketId;
|
|
5493
|
+
if (!selectedTicketId) {
|
|
5494
|
+
routePlan = safeAutomaticRoutePlan(routePlan, Boolean(currentTicketId), "Automatic routing to existing work requires a target ticket id.");
|
|
5495
|
+
return await finishRoute(routePlan.route.action, routePlan.route.reason);
|
|
5496
|
+
}
|
|
5497
|
+
targetTicketId = selectedTicketId;
|
|
5498
|
+
selectedTicket = await client.tickets.get({ projectId, ticketId: targetTicketId });
|
|
5499
|
+
lifecycleOperations.push(checkpointLifecycleOperation("ticket.selected", true, { ticketId: targetTicketId, ticket: selectedTicket }));
|
|
5500
|
+
const selectedVersion = selectedTicket && typeof selectedTicket === "object" && typeof selectedTicket.projectionVersion === "number" ? selectedTicket.projectionVersion : undefined;
|
|
5501
|
+
const intakeId = objectStringId(intakeResult);
|
|
5502
|
+
if (selectedVersion !== undefined && intakeId) {
|
|
5503
|
+
await client.tickets.attach({
|
|
5504
|
+
projectId,
|
|
5505
|
+
ticketId: targetTicketId,
|
|
5506
|
+
fromIntakeId: intakeId,
|
|
5507
|
+
ifVersion: selectedVersion
|
|
5508
|
+
});
|
|
5509
|
+
lifecycleOperations.push(checkpointLifecycleOperation("intake.attached", true, { ticketId: targetTicketId, intakeId }));
|
|
5510
|
+
}
|
|
5511
|
+
}
|
|
5512
|
+
if (!targetTicketId) {
|
|
5513
|
+
routePlan = checkpointPlanWithRoute(routePlan, {
|
|
5514
|
+
action: currentTicketId ? "current" : "noop",
|
|
5515
|
+
confidence: routePlan.route.confidence,
|
|
5516
|
+
reason: "Automatic routing did not return a target ticket."
|
|
5517
|
+
});
|
|
5518
|
+
return await finishRoute(routePlan.route.action, routePlan.route.reason);
|
|
5519
|
+
}
|
|
5520
|
+
if (currentSessionId && currentTicketId && targetTicketId !== currentTicketId) {
|
|
5521
|
+
await client.sessions.end({
|
|
5522
|
+
projectId,
|
|
5523
|
+
sessionId: currentSessionId,
|
|
5524
|
+
session: {
|
|
5525
|
+
summary: routePlan.route.reason ?? summary,
|
|
5526
|
+
...commandMetadata()
|
|
5527
|
+
}
|
|
5528
|
+
});
|
|
5529
|
+
lifecycleOperations.push(checkpointLifecycleOperation("session.ended", true, { sessionId: currentSessionId, reason: "ticket_switch" }));
|
|
5530
|
+
targetSessionId = undefined;
|
|
5531
|
+
targetChangesetId = undefined;
|
|
5532
|
+
}
|
|
5533
|
+
const startedSession = await client.sessions.start({
|
|
5534
|
+
projectId,
|
|
5535
|
+
session: {
|
|
5536
|
+
ticketId: targetTicketId,
|
|
5537
|
+
...commandMetadata()
|
|
5538
|
+
}
|
|
5539
|
+
});
|
|
5540
|
+
lifecycleOperations.push(checkpointLifecycleOperation("session.started", true, { ticketId: targetTicketId, session: startedSession }));
|
|
5541
|
+
targetSessionId = objectStringId(startedSession);
|
|
5542
|
+
if (targetSessionId) {
|
|
5543
|
+
const lease = await client.sessions.leases.create({
|
|
5544
|
+
projectId,
|
|
5545
|
+
lease: {
|
|
5546
|
+
ticketId: targetTicketId,
|
|
5547
|
+
sessionId: targetSessionId,
|
|
5548
|
+
expiresAt: leaseExpiresAt(defaultLeaseTtlSeconds),
|
|
5549
|
+
...commandMetadata()
|
|
5550
|
+
}
|
|
5551
|
+
});
|
|
5552
|
+
lifecycleOperations.push(checkpointLifecycleOperation("lease.claimed", true, { ticketId: targetTicketId, sessionId: targetSessionId, lease }));
|
|
5553
|
+
}
|
|
5554
|
+
if (targetSessionId) {
|
|
5555
|
+
try {
|
|
5556
|
+
const branchName = await resolveCurrentBranch(options);
|
|
5557
|
+
const changeset = await client.changesets.create({
|
|
5558
|
+
projectId,
|
|
5559
|
+
changeset: {
|
|
5560
|
+
ticketId: targetTicketId,
|
|
5561
|
+
branchName,
|
|
5562
|
+
baseBranch: "dev",
|
|
5563
|
+
sessionId: targetSessionId,
|
|
5564
|
+
...commandMetadata()
|
|
5565
|
+
}
|
|
5566
|
+
});
|
|
5567
|
+
lifecycleOperations.push(checkpointLifecycleOperation("changeset.linked", true, { ticketId: targetTicketId, branchName, changeset }));
|
|
5568
|
+
targetChangesetId = objectStringId(changeset);
|
|
5569
|
+
} catch (error) {
|
|
5570
|
+
lifecycleOperations.push(checkpointLifecycleOperation("changeset.linked", false, { error: error instanceof Error ? error.message : String(error) }));
|
|
5571
|
+
}
|
|
5572
|
+
}
|
|
5573
|
+
for (const entry of routePlan.entries) {
|
|
5574
|
+
const logEntry = {
|
|
5575
|
+
entryKind: entry.kind,
|
|
5576
|
+
body: entry.body,
|
|
5577
|
+
summary: entry.summary ?? checkpointEntrySummary(entry.body),
|
|
5578
|
+
...targetSessionId ? { sessionId: targetSessionId } : {},
|
|
5579
|
+
...targetChangesetId ? { changesetId: targetChangesetId } : {},
|
|
5580
|
+
...commandMetadata()
|
|
5581
|
+
};
|
|
5582
|
+
await client.tickets.log({
|
|
5583
|
+
projectId,
|
|
5584
|
+
ticketId: targetTicketId,
|
|
5585
|
+
entry: logEntry
|
|
5586
|
+
});
|
|
5587
|
+
cadenceWrites.push({
|
|
5588
|
+
type: "ticket.work_log_appended",
|
|
5589
|
+
success: true,
|
|
5590
|
+
ticketId: targetTicketId,
|
|
5591
|
+
entry: logEntry
|
|
5592
|
+
});
|
|
5593
|
+
}
|
|
5594
|
+
if (routePlan.files.length && targetSessionId) {
|
|
5595
|
+
const filesByKind = new Map;
|
|
5596
|
+
for (const file of routePlan.files) {
|
|
5597
|
+
filesByKind.set(file.kind, [...filesByKind.get(file.kind) ?? [], file.path]);
|
|
5598
|
+
}
|
|
5599
|
+
for (const [kind, paths] of filesByKind) {
|
|
5600
|
+
const filesPayload = {
|
|
5601
|
+
...targetChangesetId ? { changesetId: targetChangesetId } : {},
|
|
5602
|
+
files: paths.map((path) => ({
|
|
5603
|
+
path,
|
|
5604
|
+
changeKind: kind
|
|
5605
|
+
})),
|
|
5606
|
+
...commandMetadata()
|
|
5607
|
+
};
|
|
5608
|
+
await client.sessions.files({
|
|
5609
|
+
projectId,
|
|
5610
|
+
sessionId: targetSessionId,
|
|
5611
|
+
files: filesPayload
|
|
5612
|
+
});
|
|
5613
|
+
lifecycleOperations.push(checkpointLifecycleOperation("session.files_attached", true, { sessionId: targetSessionId, kind, files: paths }));
|
|
5614
|
+
}
|
|
5615
|
+
}
|
|
5616
|
+
summary = routePlan.summary ?? routePlan.entries[0]?.summary ?? routePlan.route.reason ?? "Agent run routed";
|
|
5617
|
+
return await finishRoute(routePlan.route.action === "intake_create" ? "routed" : "switched");
|
|
5618
|
+
}
|
|
5619
|
+
routePlan = safeAutomaticRoutePlan(routePlan, Boolean(currentTicketId), "Route did not request an executable automatic action.");
|
|
5620
|
+
return await finishRoute(routePlan.route.action, routePlan.route.reason);
|
|
5621
|
+
} finally {
|
|
5622
|
+
await releaseAgentLoopLock(lockPath);
|
|
5623
|
+
}
|
|
5624
|
+
}
|
|
5625
|
+
async function runAgentRunCheckpoint(parsed, options, config, meta) {
|
|
5626
|
+
const projectId = requireProjectId(config);
|
|
5627
|
+
const client = await createClient(config, options);
|
|
4543
5628
|
const agentSessionKeyValue = requireOption(parsed, "agent-session-key");
|
|
4544
5629
|
const state = await readAgentLoopState(parsed, options);
|
|
4545
5630
|
const sessionState = state.sessions[agentSessionKeyValue];
|
|
@@ -4579,15 +5664,36 @@ async function runAgentRunCheckpoint(parsed, options, config, meta) {
|
|
|
4579
5664
|
}
|
|
4580
5665
|
const eventFile = parsed.options["event-file"] ?? sessionState.lastEventFile;
|
|
4581
5666
|
const event = eventFile ? tryParseJsonObject(await readFile(eventFile, "utf8"), eventFile) : synthesizeAgentEventFromSession(agentSessionKeyValue, sessionState, options);
|
|
5667
|
+
const mode = checkpointModeForCommand(parsed);
|
|
4582
5668
|
const logKind = parseWorkLogEntryKind(parsed.options["log-kind"] ?? "note");
|
|
4583
5669
|
const updateSummary = parseBooleanOption(parsed.options["update-summary"], false);
|
|
4584
5670
|
const dryRun = parseBooleanOption(parsed.options["dry-run"], false);
|
|
5671
|
+
const closeoutSessionAction = parseCloseoutSessionAction(parsed.options["session-action"]);
|
|
5672
|
+
const completeTicket = parseBooleanOption(parsed.options["complete-ticket"], false);
|
|
4585
5673
|
const lockPath = parsed.options.lock;
|
|
4586
5674
|
const checkpointSettings = await resolveCheckpointSettings(parsed, options);
|
|
4587
5675
|
const gitStatus = gitOutput(["status", "--short"], options);
|
|
4588
5676
|
const gitDiffStat = gitOutput(["diff", "--stat", "origin/dev..."], options);
|
|
4589
5677
|
const changedFiles = gitOutput(["diff", "--name-only", "origin/dev..."], options);
|
|
5678
|
+
const priorCoverage = sessionState.lastCheckpointCoverage;
|
|
5679
|
+
const fingerprints = buildAgentRunFingerprints({
|
|
5680
|
+
event,
|
|
5681
|
+
ticketId,
|
|
5682
|
+
...sessionId ? { sessionId } : {},
|
|
5683
|
+
...changesetId ? { changesetId } : {},
|
|
5684
|
+
gitStatus,
|
|
5685
|
+
gitDiffStat,
|
|
5686
|
+
changedFiles,
|
|
5687
|
+
...priorCoverage ? { coverage: priorCoverage } : {}
|
|
5688
|
+
});
|
|
5689
|
+
const closeoutContext = mode === "closeout" ? await readCloseoutPromptContext({
|
|
5690
|
+
client,
|
|
5691
|
+
projectId,
|
|
5692
|
+
ticketId,
|
|
5693
|
+
...sessionId ? { sessionId } : {}
|
|
5694
|
+
}) : {};
|
|
4590
5695
|
const prompt = buildCheckpointPrompt({
|
|
5696
|
+
mode,
|
|
4591
5697
|
event,
|
|
4592
5698
|
ticketId,
|
|
4593
5699
|
...sessionId ? { sessionId } : {},
|
|
@@ -4595,12 +5701,68 @@ async function runAgentRunCheckpoint(parsed, options, config, meta) {
|
|
|
4595
5701
|
gitStatus,
|
|
4596
5702
|
gitDiffStat,
|
|
4597
5703
|
changedFiles,
|
|
4598
|
-
...sessionState.previousCheckpointSummary ? { previousCheckpointSummary: sessionState.previousCheckpointSummary } : {}
|
|
5704
|
+
...sessionState.previousCheckpointSummary ? { previousCheckpointSummary: sessionState.previousCheckpointSummary } : {},
|
|
5705
|
+
...priorCoverage ? { coverage: priorCoverage } : {},
|
|
5706
|
+
...closeoutContext
|
|
4599
5707
|
});
|
|
4600
5708
|
const promptTokenAccounting = buildCheckpointTokenAccounting(event, prompt, undefined);
|
|
5709
|
+
if (mode === "checkpoint" && !dryRun && !priorCoverage?.hasDebt && fingerprintsEqual(sessionState.lastCheckpointFingerprints, fingerprints) && !checkpointReasonBypassesFingerprintGate(parsed.options.reason)) {
|
|
5710
|
+
const checkedAt = new Date().toISOString();
|
|
5711
|
+
const auditFile = await writeAgentCheckpointAuditFile(parsed, options, {
|
|
5712
|
+
version: 1,
|
|
5713
|
+
mode,
|
|
5714
|
+
action: "skipped",
|
|
5715
|
+
reason: "unchanged_fingerprints",
|
|
5716
|
+
createdAt: checkedAt,
|
|
5717
|
+
localOnly: true,
|
|
5718
|
+
localOnlyReason: "Raw hook context and checkpoint prompts stay on this machine; unchanged fingerprints did not need a model call.",
|
|
5719
|
+
agentSessionKey: agentSessionKeyValue,
|
|
5720
|
+
ticketId,
|
|
5721
|
+
...sessionId ? { sessionId } : {},
|
|
5722
|
+
...changesetId ? { changesetId } : {},
|
|
5723
|
+
...eventFile ? { eventFile } : {},
|
|
5724
|
+
event,
|
|
5725
|
+
fingerprints,
|
|
5726
|
+
cadenceWrites: []
|
|
5727
|
+
});
|
|
5728
|
+
await writeAgentLoopState(parsed, options, {
|
|
5729
|
+
...state,
|
|
5730
|
+
sessions: {
|
|
5731
|
+
...state.sessions,
|
|
5732
|
+
[agentSessionKeyValue]: {
|
|
5733
|
+
...sessionState,
|
|
5734
|
+
stopCount: 0,
|
|
5735
|
+
threshold: defaultCheckpointThresholdValue(),
|
|
5736
|
+
lastAction: "skipped",
|
|
5737
|
+
lastReason: "unchanged_fingerprints",
|
|
5738
|
+
lastCheckpointAt: checkedAt,
|
|
5739
|
+
lastCheckpointMode: mode,
|
|
5740
|
+
lastCheckpointFingerprints: fingerprints,
|
|
5741
|
+
lastCheckpointAuditFile: auditFile
|
|
5742
|
+
}
|
|
5743
|
+
}
|
|
5744
|
+
});
|
|
5745
|
+
const data = {
|
|
5746
|
+
action: "skipped",
|
|
5747
|
+
reason: "unchanged_fingerprints",
|
|
5748
|
+
mode,
|
|
5749
|
+
ticketId,
|
|
5750
|
+
auditFile,
|
|
5751
|
+
agentSessionKey: agentSessionKeyValue,
|
|
5752
|
+
...sessionId ? { sessionId } : {},
|
|
5753
|
+
...changesetId ? { changesetId } : {}
|
|
5754
|
+
};
|
|
5755
|
+
return {
|
|
5756
|
+
stdout: parsed.flags.json ? formatJson(successEnvelope(data, meta)) : `${JSON.stringify(data, null, 2)}
|
|
5757
|
+
`,
|
|
5758
|
+
stderr: "",
|
|
5759
|
+
exitCode: 0
|
|
5760
|
+
};
|
|
5761
|
+
}
|
|
4601
5762
|
if (dryRun) {
|
|
4602
5763
|
const auditFile = await writeAgentCheckpointAuditFile(parsed, options, {
|
|
4603
5764
|
version: 1,
|
|
5765
|
+
mode,
|
|
4604
5766
|
action: "would_checkpoint",
|
|
4605
5767
|
createdAt: new Date().toISOString(),
|
|
4606
5768
|
localOnly: true,
|
|
@@ -4612,6 +5774,8 @@ async function runAgentRunCheckpoint(parsed, options, config, meta) {
|
|
|
4612
5774
|
...eventFile ? { eventFile } : {},
|
|
4613
5775
|
event,
|
|
4614
5776
|
prompt,
|
|
5777
|
+
fingerprints,
|
|
5778
|
+
...mode === "closeout" ? { coverage: priorCoverage ?? null } : {},
|
|
4615
5779
|
checkpointSettings,
|
|
4616
5780
|
tokenAccounting: promptTokenAccounting,
|
|
4617
5781
|
cadenceWrites: []
|
|
@@ -4634,7 +5798,6 @@ async function runAgentRunCheckpoint(parsed, options, config, meta) {
|
|
|
4634
5798
|
}
|
|
4635
5799
|
try {
|
|
4636
5800
|
const codexCommand = parsed.options["codex-command"] ?? "codex";
|
|
4637
|
-
const codexStartedAtMs = Date.now();
|
|
4638
5801
|
const codexCwd = options.cwd ?? process.cwd();
|
|
4639
5802
|
const codexArgs = [
|
|
4640
5803
|
"exec",
|
|
@@ -4647,25 +5810,165 @@ async function runAgentRunCheckpoint(parsed, options, config, meta) {
|
|
|
4647
5810
|
codexCwd,
|
|
4648
5811
|
prompt
|
|
4649
5812
|
];
|
|
4650
|
-
const
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
5813
|
+
const generationAttempts = [];
|
|
5814
|
+
let codex = null;
|
|
5815
|
+
let codexSessionTranscript = null;
|
|
5816
|
+
let tokenAccounting = promptTokenAccounting;
|
|
5817
|
+
let checkpoint = null;
|
|
5818
|
+
let reductionResult = null;
|
|
5819
|
+
let lastFailureReason = "Codex checkpoint generation failed.";
|
|
5820
|
+
for (let attempt = 1;attempt <= defaultCheckpointWorkerMaxAttempts; attempt += 1) {
|
|
5821
|
+
const codexStartedAtMs = Date.now();
|
|
5822
|
+
codex = runLocalCommand(codexCommand, codexArgs, options, {
|
|
5823
|
+
cwd: codexCwd,
|
|
5824
|
+
env: {
|
|
5825
|
+
[agentLoopSuppressEnv]: "1",
|
|
5826
|
+
CADENCE_HOOK_SUPPRESS: "1"
|
|
5827
|
+
},
|
|
5828
|
+
timeoutMs: defaultCheckpointWorkerTimeoutMs
|
|
4665
5829
|
});
|
|
5830
|
+
codexSessionTranscript = readCodexSessionTranscript(findCodexSessionFileCreatedAfter(options, codexStartedAtMs, codexCwd));
|
|
5831
|
+
tokenAccounting = buildCheckpointTokenAccounting(event, prompt, codexSessionTranscript?.tokenUsage);
|
|
5832
|
+
if (codex.status !== 0 || codex.error) {
|
|
5833
|
+
lastFailureReason = codex.error?.message ?? `Codex exited with status ${codex.status ?? "unknown"}.`;
|
|
5834
|
+
generationAttempts.push({
|
|
5835
|
+
attempt,
|
|
5836
|
+
success: false,
|
|
5837
|
+
status: codex.status,
|
|
5838
|
+
stderr: truncateText(codex.stderr.trim(), 2000),
|
|
5839
|
+
...codex.error ? { error: codex.error.message } : {}
|
|
5840
|
+
});
|
|
5841
|
+
continue;
|
|
5842
|
+
}
|
|
5843
|
+
try {
|
|
5844
|
+
let parsedCheckpoint = parseCheckpointPlanJson(codex.stdout, logKind);
|
|
5845
|
+
if (mode === "closeout") {
|
|
5846
|
+
parsedCheckpoint = applyCloseoutSessionDefaults(parsedCheckpoint, closeoutSessionAction, completeTicket);
|
|
5847
|
+
}
|
|
5848
|
+
const parsedReductionResult = reduceCheckpointEntries(parsedCheckpoint.entries, mode);
|
|
5849
|
+
checkpoint = {
|
|
5850
|
+
...parsedCheckpoint,
|
|
5851
|
+
entries: parsedReductionResult.entries
|
|
5852
|
+
};
|
|
5853
|
+
reductionResult = parsedReductionResult;
|
|
5854
|
+
generationAttempts.push({
|
|
5855
|
+
attempt,
|
|
5856
|
+
success: true,
|
|
5857
|
+
status: codex.status
|
|
5858
|
+
});
|
|
5859
|
+
break;
|
|
5860
|
+
} catch (error) {
|
|
5861
|
+
if (error instanceof CliError && error.code === "AGENT_RUN_CHECKPOINT_INVALID_PLAN") {
|
|
5862
|
+
throw error;
|
|
5863
|
+
}
|
|
5864
|
+
lastFailureReason = error instanceof Error ? error.message : String(error);
|
|
5865
|
+
generationAttempts.push({
|
|
5866
|
+
attempt,
|
|
5867
|
+
success: false,
|
|
5868
|
+
status: codex.status,
|
|
5869
|
+
error: lastFailureReason,
|
|
5870
|
+
stdout: truncateText(codex.stdout.trim(), 2000),
|
|
5871
|
+
stderr: truncateText(codex.stderr.trim(), 2000)
|
|
5872
|
+
});
|
|
5873
|
+
}
|
|
4666
5874
|
}
|
|
4667
|
-
|
|
4668
|
-
|
|
5875
|
+
if (!codex || !checkpoint || !reductionResult) {
|
|
5876
|
+
const failedCodex = codex ?? { status: null, stdout: "", stderr: "" };
|
|
5877
|
+
const checkedAt = new Date().toISOString();
|
|
5878
|
+
const summary2 = `Checkpoint worker failed after ${generationAttempts.length} ${generationAttempts.length === 1 ? "attempt" : "attempts"}.`;
|
|
5879
|
+
const route = {
|
|
5880
|
+
action: "failed",
|
|
5881
|
+
confidence: "high",
|
|
5882
|
+
reason: lastFailureReason
|
|
5883
|
+
};
|
|
5884
|
+
const auditFile = await writeAgentCheckpointAuditFile(parsed, options, {
|
|
5885
|
+
version: 1,
|
|
5886
|
+
action: "failed",
|
|
5887
|
+
reason: lastFailureReason,
|
|
5888
|
+
createdAt: checkedAt,
|
|
5889
|
+
localOnly: true,
|
|
5890
|
+
localOnlyReason: "Raw hook context, checkpoint prompts, and model output stay on this machine; failed checkpoint attempts are recorded locally for UI visibility and retry diagnosis.",
|
|
5891
|
+
agentSessionKey: agentSessionKeyValue,
|
|
5892
|
+
ticketId,
|
|
5893
|
+
...sessionId ? { sessionId } : {},
|
|
5894
|
+
...changesetId ? { changesetId } : {},
|
|
5895
|
+
...eventFile ? { eventFile } : {},
|
|
5896
|
+
event,
|
|
5897
|
+
prompt,
|
|
5898
|
+
model: {
|
|
5899
|
+
provider: checkpointSettings.provider,
|
|
5900
|
+
command: codexCommand,
|
|
5901
|
+
...checkpointSettings.model ? { model: checkpointSettings.model } : {},
|
|
5902
|
+
status: failedCodex.status,
|
|
5903
|
+
stderr: truncateText(failedCodex.stderr.trim(), 2000),
|
|
5904
|
+
stdout: truncateText(failedCodex.stdout.trim(), 2000),
|
|
5905
|
+
...failedCodex.error ? { error: failedCodex.error.message } : {},
|
|
5906
|
+
tokenAccounting,
|
|
5907
|
+
attempts: generationAttempts,
|
|
5908
|
+
...codexSessionTranscript?.tokenUsage ? { tokenUsage: codexSessionTranscript.tokenUsage } : {},
|
|
5909
|
+
...codexSessionTranscript ? { sessionTranscript: codexSessionTranscript } : {}
|
|
5910
|
+
},
|
|
5911
|
+
checkpointSettings,
|
|
5912
|
+
route,
|
|
5913
|
+
mode,
|
|
5914
|
+
fingerprints,
|
|
5915
|
+
lifecycleOperations: [],
|
|
5916
|
+
cadenceWrites: [],
|
|
5917
|
+
summary: summary2,
|
|
5918
|
+
entryCount: 0,
|
|
5919
|
+
needsHuman: true
|
|
5920
|
+
});
|
|
5921
|
+
await writeAgentLoopState(parsed, options, {
|
|
5922
|
+
...state,
|
|
5923
|
+
sessions: {
|
|
5924
|
+
...state.sessions,
|
|
5925
|
+
[agentSessionKeyValue]: {
|
|
5926
|
+
...sessionState,
|
|
5927
|
+
stopCount: 0,
|
|
5928
|
+
threshold: defaultCheckpointThresholdValue(),
|
|
5929
|
+
previousCheckpointSummary: summary2,
|
|
5930
|
+
lastAction: "failed",
|
|
5931
|
+
lastReason: lastFailureReason,
|
|
5932
|
+
...eventFile ? { lastEventFile: eventFile } : {},
|
|
5933
|
+
cadenceContext: {
|
|
5934
|
+
ticketId,
|
|
5935
|
+
...sessionId ? { sessionId } : {},
|
|
5936
|
+
...changesetId ? { changesetId } : {},
|
|
5937
|
+
capturedAt: checkedAt
|
|
5938
|
+
},
|
|
5939
|
+
lastCheckpointAt: checkedAt,
|
|
5940
|
+
lastCheckpointMode: mode,
|
|
5941
|
+
lastCheckpointFingerprints: fingerprints,
|
|
5942
|
+
lastCheckpointAuditFile: auditFile
|
|
5943
|
+
}
|
|
5944
|
+
}
|
|
5945
|
+
});
|
|
5946
|
+
const data = {
|
|
5947
|
+
action: "failed",
|
|
5948
|
+
reason: lastFailureReason,
|
|
5949
|
+
ticketId,
|
|
5950
|
+
summary: summary2,
|
|
5951
|
+
entryCount: 0,
|
|
5952
|
+
mode,
|
|
5953
|
+
auditFile,
|
|
5954
|
+
agentSessionKey: agentSessionKeyValue,
|
|
5955
|
+
attempts: generationAttempts.length,
|
|
5956
|
+
...sessionId ? { sessionId } : {},
|
|
5957
|
+
...changesetId ? { changesetId } : {},
|
|
5958
|
+
needsHuman: true
|
|
5959
|
+
};
|
|
5960
|
+
return {
|
|
5961
|
+
stdout: parsed.flags.json ? formatJson(successEnvelope(data, meta)) : `${JSON.stringify(data, null, 2)}
|
|
5962
|
+
`,
|
|
5963
|
+
stderr: "",
|
|
5964
|
+
exitCode: 0
|
|
5965
|
+
};
|
|
5966
|
+
}
|
|
5967
|
+
const completedCodex = codex;
|
|
5968
|
+
let checkpointPlan = checkpoint;
|
|
5969
|
+
const checkpointReductionResult = reductionResult;
|
|
5970
|
+
const coverage = buildAgentRunCoverage(checkpointPlan, mode);
|
|
5971
|
+
let summary = checkpointPlan.summary ?? checkpointPlan.entries[0]?.summary ?? checkpointPlan.route.reason ?? "Agent run checkpoint";
|
|
4669
5972
|
let targetTicketId = ticketId;
|
|
4670
5973
|
let targetSessionId = sessionId;
|
|
4671
5974
|
let targetChangesetId = changesetId;
|
|
@@ -4677,10 +5980,11 @@ async function runAgentRunCheckpoint(parsed, options, config, meta) {
|
|
|
4677
5980
|
provider: checkpointSettings.provider,
|
|
4678
5981
|
command: codexCommand,
|
|
4679
5982
|
...checkpointSettings.model ? { model: checkpointSettings.model } : {},
|
|
4680
|
-
status:
|
|
4681
|
-
stdout:
|
|
4682
|
-
stderr: truncateText(
|
|
5983
|
+
status: completedCodex.status,
|
|
5984
|
+
stdout: completedCodex.stdout.trim(),
|
|
5985
|
+
stderr: truncateText(completedCodex.stderr.trim(), 2000),
|
|
4683
5986
|
tokenAccounting,
|
|
5987
|
+
attempts: generationAttempts,
|
|
4684
5988
|
...codexSessionTranscript?.tokenUsage ? { tokenUsage: codexSessionTranscript.tokenUsage } : {},
|
|
4685
5989
|
...codexSessionTranscript ? { sessionTranscript: codexSessionTranscript } : {}
|
|
4686
5990
|
};
|
|
@@ -4691,7 +5995,7 @@ async function runAgentRunCheckpoint(parsed, options, config, meta) {
|
|
|
4691
5995
|
action,
|
|
4692
5996
|
createdAt: checkedAt,
|
|
4693
5997
|
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.",
|
|
5998
|
+
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.",
|
|
4695
5999
|
agentSessionKey: agentSessionKeyValue,
|
|
4696
6000
|
ticketId: targetTicketId,
|
|
4697
6001
|
...targetSessionId ? { sessionId: targetSessionId } : {},
|
|
@@ -4700,15 +6004,19 @@ async function runAgentRunCheckpoint(parsed, options, config, meta) {
|
|
|
4700
6004
|
event,
|
|
4701
6005
|
prompt,
|
|
4702
6006
|
model: modelAudit,
|
|
4703
|
-
checkpoint,
|
|
4704
|
-
route:
|
|
6007
|
+
checkpoint: checkpointPlan,
|
|
6008
|
+
route: checkpointPlan.route,
|
|
6009
|
+
mode,
|
|
6010
|
+
fingerprints,
|
|
6011
|
+
reduction: checkpointReductionResult.reduction,
|
|
6012
|
+
...mode === "closeout" ? { coverage } : {},
|
|
4705
6013
|
...intakeResult ? { intakeResult } : {},
|
|
4706
6014
|
...selectedTicket ? { selectedTicket } : {},
|
|
4707
6015
|
lifecycleOperations,
|
|
4708
6016
|
cadenceWrites,
|
|
4709
6017
|
summary,
|
|
4710
6018
|
...reason ? { reason } : {},
|
|
4711
|
-
entryCount:
|
|
6019
|
+
entryCount: checkpointPlan.entries.length,
|
|
4712
6020
|
needsHuman: action === "needs_human"
|
|
4713
6021
|
});
|
|
4714
6022
|
await writeAgentLoopState(parsed, options, {
|
|
@@ -4730,17 +6038,21 @@ async function runAgentRunCheckpoint(parsed, options, config, meta) {
|
|
|
4730
6038
|
capturedAt: checkedAt
|
|
4731
6039
|
},
|
|
4732
6040
|
lastCheckpointAt: checkedAt,
|
|
6041
|
+
lastCheckpointMode: mode,
|
|
6042
|
+
lastCheckpointFingerprints: fingerprints,
|
|
6043
|
+
lastCheckpointCoverage: coverage,
|
|
4733
6044
|
lastCheckpointAuditFile: auditFile
|
|
4734
6045
|
}
|
|
4735
6046
|
}
|
|
4736
6047
|
});
|
|
4737
6048
|
const data = {
|
|
4738
6049
|
action,
|
|
4739
|
-
route:
|
|
6050
|
+
route: checkpointPlan.route,
|
|
4740
6051
|
...reason ? { reason } : {},
|
|
4741
6052
|
ticketId: targetTicketId,
|
|
4742
6053
|
summary,
|
|
4743
|
-
entryCount:
|
|
6054
|
+
entryCount: checkpointPlan.entries.length,
|
|
6055
|
+
mode,
|
|
4744
6056
|
auditFile,
|
|
4745
6057
|
agentSessionKey: agentSessionKeyValue,
|
|
4746
6058
|
...targetSessionId ? { sessionId: targetSessionId } : {},
|
|
@@ -4754,124 +6066,48 @@ async function runAgentRunCheckpoint(parsed, options, config, meta) {
|
|
|
4754
6066
|
exitCode: 0
|
|
4755
6067
|
};
|
|
4756
6068
|
};
|
|
4757
|
-
if (
|
|
4758
|
-
|
|
6069
|
+
if (checkpointPlan.session?.action === "complete_ticket" && mode === "closeout" && !completeTicket) {
|
|
6070
|
+
checkpointPlan = checkpointPlanNeedsHuman(checkpointPlan, "Closeout ticket completion requires --complete-ticket true.");
|
|
4759
6071
|
}
|
|
4760
|
-
if (
|
|
4761
|
-
|
|
6072
|
+
if (checkpointPlan.session?.action === "complete_ticket" && !checkpointPlanCompletionAllowed(event, summary)) {
|
|
6073
|
+
checkpointPlan = checkpointPlanNeedsHuman(checkpointPlan, "Completion requires explicit recent completion, push, PR, merge, clean branch, or verification intent.");
|
|
4762
6074
|
}
|
|
4763
|
-
if (
|
|
4764
|
-
return await finishCheckpoint("
|
|
6075
|
+
if (checkpointPlan.route.action === "noop") {
|
|
6076
|
+
return await finishCheckpoint("noop", checkpointPlan.route.reason ?? checkpointPlan.summary ?? "Nothing durable to record.");
|
|
4765
6077
|
}
|
|
4766
|
-
if (
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
request: intakeRequest,
|
|
4772
|
-
...commandMetadata()
|
|
4773
|
-
}
|
|
4774
|
-
});
|
|
4775
|
-
lifecycleOperations.push(checkpointLifecycleOperation("intake.created", true, { request: intakeRequest, result: intakeResult }));
|
|
4776
|
-
if (checkpointHasConflictingIntakeResult(intakeResult, checkpoint)) {
|
|
4777
|
-
checkpoint = checkpointPlanNeedsHuman(checkpoint, "Intake returned conflicting duplicate, overlap, or completed-before candidates.");
|
|
4778
|
-
return await finishCheckpoint("needs_human", checkpoint.route.reason);
|
|
4779
|
-
}
|
|
4780
|
-
if (checkpoint.route.action === "intake_create") {
|
|
4781
|
-
selectedTicket = await client.tickets.create({
|
|
4782
|
-
projectId,
|
|
4783
|
-
ticket: {
|
|
4784
|
-
title: checkpointPlanTitle(checkpoint),
|
|
4785
|
-
description: checkpointPlanDescription(checkpoint),
|
|
4786
|
-
fromIntakeId: typeof intakeResult === "object" && intakeResult && "id" in intakeResult ? String(intakeResult.id) : undefined,
|
|
4787
|
-
...commandMetadata()
|
|
4788
|
-
}
|
|
4789
|
-
});
|
|
4790
|
-
lifecycleOperations.push(checkpointLifecycleOperation("ticket.created", true, { ticket: selectedTicket }));
|
|
4791
|
-
if (selectedTicket && typeof selectedTicket === "object" && "id" in selectedTicket) {
|
|
4792
|
-
targetTicketId = String(selectedTicket.id);
|
|
4793
|
-
}
|
|
4794
|
-
} else {
|
|
4795
|
-
if (!checkpoint.route.targetTicketId) {
|
|
4796
|
-
checkpoint = checkpointPlanNeedsHuman(checkpoint, "Checkpoint route requires a target ticket id.");
|
|
4797
|
-
return await finishCheckpoint("needs_human", checkpoint.route.reason);
|
|
4798
|
-
}
|
|
4799
|
-
targetTicketId = checkpoint.route.targetTicketId;
|
|
4800
|
-
selectedTicket = await client.tickets.get({ projectId, ticketId: targetTicketId });
|
|
4801
|
-
lifecycleOperations.push(checkpointLifecycleOperation("ticket.selected", true, { ticketId: targetTicketId, ticket: selectedTicket }));
|
|
4802
|
-
const selectedVersion = selectedTicket && typeof selectedTicket === "object" && typeof selectedTicket.projectionVersion === "number" ? selectedTicket.projectionVersion : undefined;
|
|
4803
|
-
const intakeId = intakeResult && typeof intakeResult === "object" && "id" in intakeResult ? String(intakeResult.id) : undefined;
|
|
4804
|
-
if (selectedVersion !== undefined && intakeId) {
|
|
4805
|
-
await client.tickets.attach({
|
|
4806
|
-
projectId,
|
|
4807
|
-
ticketId: targetTicketId,
|
|
4808
|
-
fromIntakeId: intakeId,
|
|
4809
|
-
ifVersion: selectedVersion
|
|
4810
|
-
});
|
|
4811
|
-
lifecycleOperations.push(checkpointLifecycleOperation("intake.attached", true, { ticketId: targetTicketId, intakeId }));
|
|
4812
|
-
}
|
|
4813
|
-
}
|
|
4814
|
-
if (targetTicketId !== ticketId && targetSessionId) {
|
|
4815
|
-
await client.sessions.end({
|
|
4816
|
-
projectId,
|
|
4817
|
-
sessionId: targetSessionId,
|
|
4818
|
-
session: {
|
|
4819
|
-
summary: checkpoint.session?.summary ?? checkpoint.route.reason ?? summary,
|
|
4820
|
-
...commandMetadata()
|
|
4821
|
-
}
|
|
4822
|
-
});
|
|
4823
|
-
lifecycleOperations.push(checkpointLifecycleOperation("session.ended", true, { sessionId: targetSessionId, reason: "ticket_switch" }));
|
|
4824
|
-
targetSessionId = undefined;
|
|
4825
|
-
targetChangesetId = undefined;
|
|
4826
|
-
}
|
|
4827
|
-
const startedSession = await client.sessions.start({
|
|
4828
|
-
projectId,
|
|
4829
|
-
session: {
|
|
4830
|
-
ticketId: targetTicketId,
|
|
4831
|
-
...commandMetadata()
|
|
4832
|
-
}
|
|
6078
|
+
if (checkpointPlan.route.action === "needs_human") {
|
|
6079
|
+
checkpointPlan = checkpointPlanWithRoute(checkpointPlan, {
|
|
6080
|
+
action: "noop",
|
|
6081
|
+
confidence: checkpointPlan.route.confidence,
|
|
6082
|
+
reason: checkpointPlan.route.reason ?? "Checkpoint routing was uncertain."
|
|
4833
6083
|
});
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
ticketId: targetTicketId,
|
|
4843
|
-
sessionId: targetSessionId,
|
|
4844
|
-
expiresAt: leaseExpiresAt(defaultLeaseTtlSeconds),
|
|
4845
|
-
replaceOwnActiveLease: true,
|
|
4846
|
-
...commandMetadata()
|
|
4847
|
-
}
|
|
6084
|
+
return await finishCheckpoint("noop", checkpointPlan.route.reason);
|
|
6085
|
+
}
|
|
6086
|
+
if (checkpointRouteRequiresIntake(checkpointPlan.route.action)) {
|
|
6087
|
+
if (checkpointPlan.route.confidence !== "high") {
|
|
6088
|
+
checkpointPlan = checkpointPlanWithRoute(checkpointPlan, {
|
|
6089
|
+
action: "noop",
|
|
6090
|
+
confidence: checkpointPlan.route.confidence,
|
|
6091
|
+
reason: "Checkpoint reroute requires high confidence."
|
|
4848
6092
|
});
|
|
4849
|
-
|
|
4850
|
-
}
|
|
4851
|
-
if (targetSessionId) {
|
|
4852
|
-
try {
|
|
4853
|
-
const branchName = await resolveCurrentBranch(options);
|
|
4854
|
-
const changeset = await client.changesets.create({
|
|
4855
|
-
projectId,
|
|
4856
|
-
changeset: {
|
|
4857
|
-
ticketId: targetTicketId,
|
|
4858
|
-
branchName,
|
|
4859
|
-
baseBranch: "dev",
|
|
4860
|
-
sessionId: targetSessionId,
|
|
4861
|
-
...commandMetadata()
|
|
4862
|
-
}
|
|
4863
|
-
});
|
|
4864
|
-
lifecycleOperations.push(checkpointLifecycleOperation("changeset.linked", true, { ticketId: targetTicketId, branchName, changeset }));
|
|
4865
|
-
if (changeset && typeof changeset === "object" && "id" in changeset) {
|
|
4866
|
-
targetChangesetId = String(changeset.id);
|
|
4867
|
-
}
|
|
4868
|
-
} catch (error) {
|
|
4869
|
-
lifecycleOperations.push(checkpointLifecycleOperation("changeset.linked", false, { error: error instanceof Error ? error.message : String(error) }));
|
|
4870
|
-
}
|
|
6093
|
+
return await finishCheckpoint("noop", checkpointPlan.route.reason);
|
|
4871
6094
|
}
|
|
6095
|
+
const routeArgs = [
|
|
6096
|
+
"agent-run",
|
|
6097
|
+
"route",
|
|
6098
|
+
"--agent-session-key",
|
|
6099
|
+
agentSessionKeyValue,
|
|
6100
|
+
"--reason",
|
|
6101
|
+
"checkpoint_reroute",
|
|
6102
|
+
...eventFile ? ["--event-file", eventFile] : [],
|
|
6103
|
+
...parsed.flags.project ? ["--project", parsed.flags.project] : [],
|
|
6104
|
+
...parsed.flags.server ? ["--server", parsed.flags.server] : []
|
|
6105
|
+
];
|
|
6106
|
+
const rerouteResult = await finishCheckpoint("reroute", checkpointPlan.route.reason ?? "Checkpoint delegated routing to agent-run route.");
|
|
6107
|
+
await spawnAgentRunWorker(routeArgs, options);
|
|
6108
|
+
return rerouteResult;
|
|
4872
6109
|
}
|
|
4873
|
-
const
|
|
4874
|
-
for (const entry of checkpoint.entries) {
|
|
6110
|
+
for (const entry of checkpointPlan.entries) {
|
|
4875
6111
|
const logEntry = {
|
|
4876
6112
|
entryKind: entry.kind,
|
|
4877
6113
|
body: entry.body,
|
|
@@ -4880,7 +6116,6 @@ async function runAgentRunCheckpoint(parsed, options, config, meta) {
|
|
|
4880
6116
|
...entry.parentEntryId ? { parentEntryId: entry.parentEntryId } : {},
|
|
4881
6117
|
...targetSessionId ? { sessionId: targetSessionId } : {},
|
|
4882
6118
|
...targetChangesetId ? { changesetId: targetChangesetId } : {},
|
|
4883
|
-
metadata: checkpointMetadata,
|
|
4884
6119
|
...commandMetadata()
|
|
4885
6120
|
};
|
|
4886
6121
|
try {
|
|
@@ -4928,9 +6163,9 @@ async function runAgentRunCheckpoint(parsed, options, config, meta) {
|
|
|
4928
6163
|
});
|
|
4929
6164
|
}
|
|
4930
6165
|
}
|
|
4931
|
-
if (
|
|
6166
|
+
if (checkpointPlan.files.length && targetSessionId) {
|
|
4932
6167
|
const filesByKind = new Map;
|
|
4933
|
-
for (const file of
|
|
6168
|
+
for (const file of checkpointPlan.files) {
|
|
4934
6169
|
filesByKind.set(file.kind, [...filesByKind.get(file.kind) ?? [], file.path]);
|
|
4935
6170
|
}
|
|
4936
6171
|
for (const [kind, paths] of filesByKind) {
|
|
@@ -4950,7 +6185,7 @@ async function runAgentRunCheckpoint(parsed, options, config, meta) {
|
|
|
4950
6185
|
lifecycleOperations.push(checkpointLifecycleOperation("session.files_attached", true, { sessionId: targetSessionId, kind, files: paths }));
|
|
4951
6186
|
}
|
|
4952
6187
|
}
|
|
4953
|
-
if ((updateSummary ||
|
|
6188
|
+
if ((updateSummary || checkpointPlan.summaryUpdate?.update) && checkpointPlan.summaryUpdate?.value) {
|
|
4954
6189
|
const ticket = await client.tickets.get({ projectId, ticketId: targetTicketId });
|
|
4955
6190
|
if (typeof ticket.projectionVersion === "number") {
|
|
4956
6191
|
await client.tickets.update({
|
|
@@ -4958,7 +6193,7 @@ async function runAgentRunCheckpoint(parsed, options, config, meta) {
|
|
|
4958
6193
|
ticketId: targetTicketId,
|
|
4959
6194
|
ifVersion: ticket.projectionVersion,
|
|
4960
6195
|
ticket: {
|
|
4961
|
-
currentSummary:
|
|
6196
|
+
currentSummary: checkpointPlan.summaryUpdate.value,
|
|
4962
6197
|
...commandMetadata()
|
|
4963
6198
|
}
|
|
4964
6199
|
});
|
|
@@ -4968,36 +6203,36 @@ async function runAgentRunCheckpoint(parsed, options, config, meta) {
|
|
|
4968
6203
|
ticketId: targetTicketId,
|
|
4969
6204
|
ifVersion: ticket.projectionVersion,
|
|
4970
6205
|
ticket: {
|
|
4971
|
-
currentSummary:
|
|
6206
|
+
currentSummary: checkpointPlan.summaryUpdate.value
|
|
4972
6207
|
}
|
|
4973
6208
|
});
|
|
4974
6209
|
}
|
|
4975
6210
|
}
|
|
4976
|
-
if (
|
|
6211
|
+
if (checkpointPlan.session?.action === "handoff" || checkpointPlan.session?.action === "end" || checkpointPlan.session?.action === "complete_ticket") {
|
|
4977
6212
|
if (targetSessionId) {
|
|
4978
6213
|
await client.sessions.end({
|
|
4979
6214
|
projectId,
|
|
4980
6215
|
sessionId: targetSessionId,
|
|
4981
6216
|
session: {
|
|
4982
|
-
summary:
|
|
6217
|
+
summary: checkpointPlan.session.summary ?? summary,
|
|
4983
6218
|
...commandMetadata()
|
|
4984
6219
|
}
|
|
4985
6220
|
});
|
|
4986
|
-
lifecycleOperations.push(checkpointLifecycleOperation("session.ended", true, { sessionId: targetSessionId, action:
|
|
6221
|
+
lifecycleOperations.push(checkpointLifecycleOperation("session.ended", true, { sessionId: targetSessionId, action: checkpointPlan.session.action }));
|
|
4987
6222
|
}
|
|
4988
6223
|
}
|
|
4989
|
-
if (
|
|
6224
|
+
if (checkpointPlan.session?.action === "complete_ticket") {
|
|
4990
6225
|
const latestTicket = await client.tickets.get({ projectId, ticketId: targetTicketId });
|
|
4991
6226
|
if (typeof latestTicket.projectionVersion !== "number") {
|
|
4992
|
-
|
|
4993
|
-
return await finishCheckpoint("needs_human",
|
|
6227
|
+
checkpointPlan = checkpointPlanNeedsHuman(checkpointPlan, "Ticket completion requires a latest ticket projection version.");
|
|
6228
|
+
return await finishCheckpoint("needs_human", checkpointPlan.route.reason);
|
|
4994
6229
|
}
|
|
4995
6230
|
await client.tickets.complete({
|
|
4996
6231
|
projectId,
|
|
4997
6232
|
ticketId: targetTicketId,
|
|
4998
6233
|
ifVersion: latestTicket.projectionVersion,
|
|
4999
6234
|
completion: {
|
|
5000
|
-
currentSummary:
|
|
6235
|
+
currentSummary: checkpointPlan.session.summary ?? checkpointPlan.summaryUpdate?.value ?? summary,
|
|
5001
6236
|
...commandMetadata()
|
|
5002
6237
|
}
|
|
5003
6238
|
});
|
|
@@ -5006,7 +6241,7 @@ async function runAgentRunCheckpoint(parsed, options, config, meta) {
|
|
|
5006
6241
|
success: true,
|
|
5007
6242
|
ticketId: targetTicketId,
|
|
5008
6243
|
ifVersion: latestTicket.projectionVersion,
|
|
5009
|
-
summary:
|
|
6244
|
+
summary: checkpointPlan.session.summary ?? checkpointPlan.summaryUpdate?.value ?? summary
|
|
5010
6245
|
});
|
|
5011
6246
|
}
|
|
5012
6247
|
return await finishCheckpoint("checkpointed");
|
|
@@ -5033,26 +6268,17 @@ async function runAgentRunSweep(parsed, options, config, meta) {
|
|
|
5033
6268
|
lastObservedAt: session.lastObservedAt
|
|
5034
6269
|
}));
|
|
5035
6270
|
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
6271
|
let nextState = state;
|
|
5045
6272
|
for (const staleSession of staleSessions) {
|
|
5046
6273
|
const existingSession = nextState.sessions[staleSession.agentSessionKey];
|
|
5047
|
-
const
|
|
5048
|
-
if (existingSession
|
|
6274
|
+
const hasContext = Boolean(existingSession?.cadenceContext?.ticketId);
|
|
6275
|
+
if (existingSession) {
|
|
5049
6276
|
nextState = {
|
|
5050
6277
|
...nextState,
|
|
5051
6278
|
sessions: {
|
|
5052
6279
|
...nextState.sessions,
|
|
5053
6280
|
[staleSession.agentSessionKey]: {
|
|
5054
6281
|
...existingSession,
|
|
5055
|
-
cadenceContext,
|
|
5056
6282
|
lastAction: "sweep_spawned"
|
|
5057
6283
|
}
|
|
5058
6284
|
}
|
|
@@ -5061,11 +6287,11 @@ async function runAgentRunSweep(parsed, options, config, meta) {
|
|
|
5061
6287
|
}
|
|
5062
6288
|
await spawnAgentRunCheckpoint([
|
|
5063
6289
|
"agent-run",
|
|
5064
|
-
"checkpoint",
|
|
6290
|
+
hasContext ? "checkpoint" : "route",
|
|
5065
6291
|
"--agent-session-key",
|
|
5066
6292
|
staleSession.agentSessionKey,
|
|
5067
6293
|
"--reason",
|
|
5068
|
-
"idle",
|
|
6294
|
+
hasContext ? "idle" : "missing_context",
|
|
5069
6295
|
"--lock",
|
|
5070
6296
|
agentLoopLockPath(parsed, options, staleSession.agentSessionKey),
|
|
5071
6297
|
...parsed.flags.project ? ["--project", parsed.flags.project] : [],
|
|
@@ -5733,7 +6959,7 @@ async function runCli(argv, options = {}) {
|
|
|
5733
6959
|
if (parsed.command.name === "projects.list") {
|
|
5734
6960
|
return await runProjectCommand(parsed, options);
|
|
5735
6961
|
}
|
|
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") {
|
|
6962
|
+
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
6963
|
return await runAgentRunCommand(parsed, options);
|
|
5738
6964
|
}
|
|
5739
6965
|
if (parsed.command.name === "hooks.install" || parsed.command.name === "hooks.doctor") {
|