@trycadence/cli 0.1.8 → 0.1.11-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 +1401 -328
- package/package.json +1 -1
package/dist/cadence
CHANGED
|
@@ -1511,24 +1511,62 @@ function createCadenceClient(options = {}) {
|
|
|
1511
1511
|
}
|
|
1512
1512
|
|
|
1513
1513
|
// src/index.ts
|
|
1514
|
-
import { spawnSync } from "child_process";
|
|
1514
|
+
import { spawn, spawnSync } from "child_process";
|
|
1515
1515
|
import { createHash, randomUUID } from "crypto";
|
|
1516
1516
|
import { mkdir, readFile, rm, stat, writeFile } from "fs/promises";
|
|
1517
1517
|
import { basename, dirname, isAbsolute, join, parse } from "path";
|
|
1518
1518
|
import { createInterface } from "readline/promises";
|
|
1519
|
+
// package.json
|
|
1520
|
+
var package_default = {
|
|
1521
|
+
name: "@trycadence/cli",
|
|
1522
|
+
version: "0.1.11-dev.0",
|
|
1523
|
+
private: false,
|
|
1524
|
+
type: "module",
|
|
1525
|
+
bin: {
|
|
1526
|
+
cadence: "dist/cadence"
|
|
1527
|
+
},
|
|
1528
|
+
files: [
|
|
1529
|
+
"dist",
|
|
1530
|
+
"README.md"
|
|
1531
|
+
],
|
|
1532
|
+
scripts: {
|
|
1533
|
+
build: "bun build src/index.ts --target=bun --outfile=dist/cadence",
|
|
1534
|
+
dev: "bun run src/index.ts",
|
|
1535
|
+
prepack: "bun run build",
|
|
1536
|
+
test: "bun test",
|
|
1537
|
+
typecheck: "tsc --noEmit -p tsconfig.json"
|
|
1538
|
+
},
|
|
1539
|
+
engines: {
|
|
1540
|
+
bun: ">=1.3.0"
|
|
1541
|
+
},
|
|
1542
|
+
publishConfig: {
|
|
1543
|
+
access: "public"
|
|
1544
|
+
}
|
|
1545
|
+
};
|
|
1546
|
+
|
|
1547
|
+
// src/index.ts
|
|
1519
1548
|
var ticketPriorities = ["low", "normal", "high", "urgent"];
|
|
1520
1549
|
var ticketStatuses = ["backlog", "ready", "in_progress", "blocked", "review", "done", "abandoned"];
|
|
1521
1550
|
var sessionFileChangeKinds = ["added", "modified", "deleted", "renamed", "unknown"];
|
|
1522
1551
|
var workLogEntryKinds = ["intent", "decision", "rationale", "action", "verification", "blocker", "correction", "note"];
|
|
1523
1552
|
var workLogParentSelectors = ["last", "ticket-last", "session-last", "last-decision", "last-correction", "last-action"];
|
|
1524
1553
|
var changesetPrNoteSources = ["agent", "human", "system"];
|
|
1554
|
+
var hookScopes = ["repo", "global", "both"];
|
|
1555
|
+
var agentEventSources = ["codex", "claude-code", "opencode", "openrouter", "unknown"];
|
|
1525
1556
|
var defaultLeaseTtlSeconds = 5 * 60 * 60;
|
|
1526
1557
|
var defaultCliApiBaseUrl = "https://cadenceapi.deploy.lvl8studios.com";
|
|
1527
1558
|
var defaultCliWebBaseUrl = "https://cadence.deploy.lvl8studios.com";
|
|
1559
|
+
var defaultCheckpointThresholdMin = 3;
|
|
1560
|
+
var defaultCheckpointThresholdMax = 5;
|
|
1561
|
+
var defaultCheckpointCooldownSeconds = 10 * 60;
|
|
1562
|
+
var defaultCheckpointWorkerTimeoutMs = 10 * 60 * 1000;
|
|
1563
|
+
var defaultHookCommand = "cadence agent-run ingest-stop --source codex --event stop";
|
|
1564
|
+
var agentLoopSuppressEnv = "CADENCE_AGENT_EVENT_SUPPRESS";
|
|
1528
1565
|
var credentialRefreshSkewMs = 60 * 1000;
|
|
1529
1566
|
var credentialRefreshLockTimeoutMs = 10 * 1000;
|
|
1530
1567
|
var credentialRefreshLockStaleMs = 60 * 1000;
|
|
1531
1568
|
var credentialRefreshLockPollMs = 100;
|
|
1569
|
+
var cliVersion = package_default.version;
|
|
1532
1570
|
|
|
1533
1571
|
class CliError extends Error {
|
|
1534
1572
|
code;
|
|
@@ -1560,6 +1598,12 @@ var knownCommandPaths = [
|
|
|
1560
1598
|
["changesets", "notes", "get"],
|
|
1561
1599
|
["changesets", "notes", "put"],
|
|
1562
1600
|
["changesets", "notes", "apply"],
|
|
1601
|
+
["agent-run", "ingest-stop"],
|
|
1602
|
+
["agent-run", "closeout"],
|
|
1603
|
+
["agent-run", "sweep"],
|
|
1604
|
+
["agent-run", "doctor"],
|
|
1605
|
+
["hooks", "doctor"],
|
|
1606
|
+
["hooks", "install"],
|
|
1563
1607
|
["sessions", "start"],
|
|
1564
1608
|
["sessions", "end"],
|
|
1565
1609
|
["sessions", "files"],
|
|
@@ -1578,9 +1622,9 @@ var knownCommandPaths = [
|
|
|
1578
1622
|
["events", "list"],
|
|
1579
1623
|
["work", "overview"],
|
|
1580
1624
|
["projects", "list"],
|
|
1581
|
-
["projects", "use"],
|
|
1582
1625
|
["init"],
|
|
1583
1626
|
["status"],
|
|
1627
|
+
["version"],
|
|
1584
1628
|
["help"]
|
|
1585
1629
|
];
|
|
1586
1630
|
function isCommandMatch(words, commandPath) {
|
|
@@ -1610,6 +1654,7 @@ function parseCliArgs(argv) {
|
|
|
1610
1654
|
const options = {};
|
|
1611
1655
|
let json = false;
|
|
1612
1656
|
let help = false;
|
|
1657
|
+
let version = false;
|
|
1613
1658
|
let server;
|
|
1614
1659
|
let project;
|
|
1615
1660
|
for (let index = 0;index < argv.length; index += 1) {
|
|
@@ -1625,6 +1670,10 @@ function parseCliArgs(argv) {
|
|
|
1625
1670
|
help = true;
|
|
1626
1671
|
continue;
|
|
1627
1672
|
}
|
|
1673
|
+
if (arg === "--version" || arg === "-v") {
|
|
1674
|
+
version = true;
|
|
1675
|
+
continue;
|
|
1676
|
+
}
|
|
1628
1677
|
if (arg === "--server") {
|
|
1629
1678
|
server = readFlagValue(argv, index, arg);
|
|
1630
1679
|
index += 1;
|
|
@@ -1655,6 +1704,7 @@ ${value}` : value;
|
|
|
1655
1704
|
flags: {
|
|
1656
1705
|
json,
|
|
1657
1706
|
help,
|
|
1707
|
+
version,
|
|
1658
1708
|
...server ? { server } : {},
|
|
1659
1709
|
...project ? { project } : {}
|
|
1660
1710
|
},
|
|
@@ -1827,39 +1877,6 @@ async function findRepoCadenceDirectory(cwd) {
|
|
|
1827
1877
|
current = parent;
|
|
1828
1878
|
}
|
|
1829
1879
|
}
|
|
1830
|
-
async function findRepoConfigDirectory(cwd) {
|
|
1831
|
-
let current = cwd;
|
|
1832
|
-
while (true) {
|
|
1833
|
-
const repoConfig = join(current, ".cadence", "config.json");
|
|
1834
|
-
if (await Bun.file(repoConfig).exists()) {
|
|
1835
|
-
return join(current, ".cadence");
|
|
1836
|
-
}
|
|
1837
|
-
const parent = dirname(current);
|
|
1838
|
-
if (parent === current) {
|
|
1839
|
-
return null;
|
|
1840
|
-
}
|
|
1841
|
-
current = parent;
|
|
1842
|
-
}
|
|
1843
|
-
}
|
|
1844
|
-
function resolveGitRootFromCommand(cwd) {
|
|
1845
|
-
const result = spawnSync("git", ["rev-parse", "--show-toplevel"], {
|
|
1846
|
-
cwd,
|
|
1847
|
-
encoding: "utf8"
|
|
1848
|
-
});
|
|
1849
|
-
if (result.status !== 0) {
|
|
1850
|
-
return null;
|
|
1851
|
-
}
|
|
1852
|
-
return result.stdout.trim() || null;
|
|
1853
|
-
}
|
|
1854
|
-
async function repoConfigPathForWrite(options) {
|
|
1855
|
-
const cwd = options.cwd ?? process.cwd();
|
|
1856
|
-
const existingConfigDirectory = await findRepoConfigDirectory(cwd);
|
|
1857
|
-
if (existingConfigDirectory) {
|
|
1858
|
-
return join(existingConfigDirectory, "config.json");
|
|
1859
|
-
}
|
|
1860
|
-
const gitRoot = options.resolveGitRoot ? await options.resolveGitRoot() : resolveGitRootFromCommand(cwd);
|
|
1861
|
-
return join(gitRoot ?? cwd, ".cadence", "config.json");
|
|
1862
|
-
}
|
|
1863
1880
|
function getConfigHome(env) {
|
|
1864
1881
|
return env.CADENCE_CONFIG_HOME ?? (env.HOME ? join(env.HOME, ".config", "cadence") : join(parse(process.cwd()).root, ".config", "cadence"));
|
|
1865
1882
|
}
|
|
@@ -1944,6 +1961,77 @@ function isInteractive(options) {
|
|
|
1944
1961
|
function getCliWebBaseUrl(config, parsed, options) {
|
|
1945
1962
|
return parsed.options["web-base-url"] ?? options.env?.CADENCE_WEB_BASE_URL ?? process.env.CADENCE_WEB_BASE_URL ?? config.webBaseUrl;
|
|
1946
1963
|
}
|
|
1964
|
+
function normalizeBaseUrl(value, label) {
|
|
1965
|
+
try {
|
|
1966
|
+
return new URL(value).toString().replace(/\/$/, "");
|
|
1967
|
+
} catch {
|
|
1968
|
+
throw new CliError("CLI_USAGE", `${label} must be a valid URL.`);
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
function cliConfigDiscoveryUrl(webBaseUrl) {
|
|
1972
|
+
return new URL("/cli/config", webBaseUrl).toString();
|
|
1973
|
+
}
|
|
1974
|
+
function hasExplicitWebBaseUrl(parsed, options) {
|
|
1975
|
+
return Boolean(parsed.options["web-base-url"] ?? options.env?.CADENCE_WEB_BASE_URL ?? process.env.CADENCE_WEB_BASE_URL);
|
|
1976
|
+
}
|
|
1977
|
+
async function discoverApiBaseUrlFromWeb(webBaseUrl, options) {
|
|
1978
|
+
const configUrl = cliConfigDiscoveryUrl(webBaseUrl);
|
|
1979
|
+
const requestFetch = options.fetch ?? fetch;
|
|
1980
|
+
let response;
|
|
1981
|
+
try {
|
|
1982
|
+
response = await requestFetch(configUrl, {
|
|
1983
|
+
headers: {
|
|
1984
|
+
accept: "application/json"
|
|
1985
|
+
}
|
|
1986
|
+
});
|
|
1987
|
+
} catch (error) {
|
|
1988
|
+
throw new CliError("WEB_CONFIG_DISCOVERY_FAILED", "Could not discover Cadence API URL from the web app.", {
|
|
1989
|
+
webBaseUrl,
|
|
1990
|
+
configUrl,
|
|
1991
|
+
cause: error instanceof Error ? error.message : String(error)
|
|
1992
|
+
});
|
|
1993
|
+
}
|
|
1994
|
+
if (!response.ok) {
|
|
1995
|
+
throw new CliError("WEB_CONFIG_DISCOVERY_FAILED", "Could not discover Cadence API URL from the web app.", {
|
|
1996
|
+
webBaseUrl,
|
|
1997
|
+
configUrl,
|
|
1998
|
+
status: response.status
|
|
1999
|
+
});
|
|
2000
|
+
}
|
|
2001
|
+
const parsed = await response.json();
|
|
2002
|
+
const apiBaseUrl = parsed && typeof parsed === "object" ? parsed.apiBaseUrl : undefined;
|
|
2003
|
+
if (typeof apiBaseUrl !== "string") {
|
|
2004
|
+
throw new CliError("WEB_CONFIG_DISCOVERY_FAILED", "Cadence web app returned invalid CLI configuration.", {
|
|
2005
|
+
webBaseUrl,
|
|
2006
|
+
configUrl
|
|
2007
|
+
});
|
|
2008
|
+
}
|
|
2009
|
+
return normalizeBaseUrl(apiBaseUrl, "Discovered API base URL");
|
|
2010
|
+
}
|
|
2011
|
+
async function resolveAuthLoginConfig(config, parsed, options) {
|
|
2012
|
+
const webBaseUrl = normalizeBaseUrl(getCliWebBaseUrl(config, parsed, options), "--web-base-url");
|
|
2013
|
+
if (parsed.flags.server && !hasExplicitWebBaseUrl(parsed, options)) {
|
|
2014
|
+
return {
|
|
2015
|
+
...config,
|
|
2016
|
+
webBaseUrl
|
|
2017
|
+
};
|
|
2018
|
+
}
|
|
2019
|
+
try {
|
|
2020
|
+
return {
|
|
2021
|
+
...config,
|
|
2022
|
+
server: await discoverApiBaseUrlFromWeb(webBaseUrl, options),
|
|
2023
|
+
webBaseUrl
|
|
2024
|
+
};
|
|
2025
|
+
} catch (error) {
|
|
2026
|
+
if (hasExplicitWebBaseUrl(parsed, options)) {
|
|
2027
|
+
throw error;
|
|
2028
|
+
}
|
|
2029
|
+
return {
|
|
2030
|
+
...config,
|
|
2031
|
+
webBaseUrl
|
|
2032
|
+
};
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
1947
2035
|
function deriveWebBaseUrl(server) {
|
|
1948
2036
|
try {
|
|
1949
2037
|
const url = new URL(server);
|
|
@@ -2151,123 +2239,13 @@ function formatJson(envelope) {
|
|
|
2151
2239
|
return `${JSON.stringify(envelope, null, 2)}
|
|
2152
2240
|
`;
|
|
2153
2241
|
}
|
|
2154
|
-
function isRecord(value) {
|
|
2155
|
-
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
2156
|
-
}
|
|
2157
|
-
function stringField(value, field) {
|
|
2158
|
-
if (!isRecord(value)) {
|
|
2159
|
-
return null;
|
|
2160
|
-
}
|
|
2161
|
-
const fieldValue = value[field];
|
|
2162
|
-
return typeof fieldValue === "string" ? fieldValue : null;
|
|
2163
|
-
}
|
|
2164
|
-
function countLabel(value, singular, plural = `${singular}s`) {
|
|
2165
|
-
const count = Array.isArray(value) ? value.length : 0;
|
|
2166
|
-
return `${count} ${count === 1 ? singular : plural}`;
|
|
2167
|
-
}
|
|
2168
|
-
function humanCommandOutput(commandName, data) {
|
|
2169
|
-
switch (commandName) {
|
|
2170
|
-
case "auth.status":
|
|
2171
|
-
return [
|
|
2172
|
-
`Server: ${stringField(data, "server") ?? "unknown"}`,
|
|
2173
|
-
`Project: ${stringField(data, "projectId") ?? "not configured"}`,
|
|
2174
|
-
`Credential: ${isRecord(data) && data.credentialConfigured ? "configured" : "not configured"}`
|
|
2175
|
-
].join(`
|
|
2176
|
-
`) + `
|
|
2177
|
-
`;
|
|
2178
|
-
case "auth.logout":
|
|
2179
|
-
return `Cadence CLI credential removed.
|
|
2180
|
-
`;
|
|
2181
|
-
case "events.list":
|
|
2182
|
-
return `Found ${countLabel(data, "event")}.
|
|
2183
|
-
`;
|
|
2184
|
-
case "work.overview":
|
|
2185
|
-
return `Loaded work overview.
|
|
2186
|
-
`;
|
|
2187
|
-
case "tickets.get":
|
|
2188
|
-
return `Loaded ticket ${stringField(data, "id") ?? ""}`.trimEnd() + `.
|
|
2189
|
-
`;
|
|
2190
|
-
case "tickets.list":
|
|
2191
|
-
return `Found ${countLabel(data, "ticket")}.
|
|
2192
|
-
`;
|
|
2193
|
-
case "sessions.current":
|
|
2194
|
-
return `Loaded current session context.
|
|
2195
|
-
`;
|
|
2196
|
-
case "changesets.get":
|
|
2197
|
-
case "changesets.context":
|
|
2198
|
-
case "changesets.current":
|
|
2199
|
-
return `Loaded ChangeSet context.
|
|
2200
|
-
`;
|
|
2201
|
-
case "changesets.list":
|
|
2202
|
-
return `Found ${countLabel(data, "ChangeSet")}.
|
|
2203
|
-
`;
|
|
2204
|
-
case "changesets.notes.get":
|
|
2205
|
-
return `Loaded ChangeSet PR notes.
|
|
2206
|
-
`;
|
|
2207
|
-
case "projects.list":
|
|
2208
|
-
return `Found ${countLabel(data, "project")}.
|
|
2209
|
-
`;
|
|
2210
|
-
case "actors.ensure-workspace-agent": {
|
|
2211
|
-
const displayName = stringField(data, "displayName");
|
|
2212
|
-
const actorId = stringField(data, "actorId");
|
|
2213
|
-
return `Workspace agent ready${displayName ? `: ${displayName}` : ""}${actorId ? ` (${actorId})` : ""}.
|
|
2214
|
-
`;
|
|
2215
|
-
}
|
|
2216
|
-
case "intake":
|
|
2217
|
-
return `Created intake ${stringField(data, "id") ?? ""}`.trimEnd() + `.
|
|
2218
|
-
`;
|
|
2219
|
-
case "intake.dismiss":
|
|
2220
|
-
return `Dismissed intake.
|
|
2221
|
-
`;
|
|
2222
|
-
case "tickets.attach":
|
|
2223
|
-
return `Attached intake to ticket.
|
|
2224
|
-
`;
|
|
2225
|
-
case "tickets.create":
|
|
2226
|
-
return `Created ticket ${stringField(data, "id") ?? ""}`.trimEnd() + `.
|
|
2227
|
-
`;
|
|
2228
|
-
case "tickets.update":
|
|
2229
|
-
return `Updated ticket ${stringField(data, "id") ?? ""}`.trimEnd() + `.
|
|
2230
|
-
`;
|
|
2231
|
-
case "tickets.claim":
|
|
2232
|
-
return `Claimed ticket lease ${stringField(data, "id") ?? ""}`.trimEnd() + `.
|
|
2233
|
-
`;
|
|
2234
|
-
case "tickets.release":
|
|
2235
|
-
return `Released ticket lease ${stringField(data, "id") ?? ""}`.trimEnd() + `.
|
|
2236
|
-
`;
|
|
2237
|
-
case "tickets.log":
|
|
2238
|
-
return `Added ticket work-log entry.
|
|
2239
|
-
`;
|
|
2240
|
-
case "tickets.complete":
|
|
2241
|
-
return `Completed ticket ${stringField(data, "id") ?? ""}`.trimEnd() + `.
|
|
2242
|
-
`;
|
|
2243
|
-
case "sessions.start":
|
|
2244
|
-
return `Started session ${stringField(data, "id") ?? ""}`.trimEnd() + `.
|
|
2245
|
-
`;
|
|
2246
|
-
case "sessions.end":
|
|
2247
|
-
return `Ended session ${stringField(data, "id") ?? ""}`.trimEnd() + `.
|
|
2248
|
-
`;
|
|
2249
|
-
case "sessions.files":
|
|
2250
|
-
return `Recorded session files.
|
|
2251
|
-
`;
|
|
2252
|
-
case "changesets.create":
|
|
2253
|
-
return `Created ChangeSet ${stringField(data, "id") ?? ""}`.trimEnd() + `.
|
|
2254
|
-
`;
|
|
2255
|
-
case "changesets.notes.put":
|
|
2256
|
-
return `Saved ChangeSet PR notes.
|
|
2257
|
-
`;
|
|
2258
|
-
case "changesets.notes.apply":
|
|
2259
|
-
return `Marked ChangeSet PR notes applied.
|
|
2260
|
-
`;
|
|
2261
|
-
default:
|
|
2262
|
-
return `Done.
|
|
2263
|
-
`;
|
|
2264
|
-
}
|
|
2265
|
-
}
|
|
2266
2242
|
function helpText() {
|
|
2267
2243
|
return [
|
|
2268
2244
|
"Cadence CLI",
|
|
2269
2245
|
"",
|
|
2270
2246
|
"Usage:",
|
|
2247
|
+
" cadence --version",
|
|
2248
|
+
" cadence version [--json]",
|
|
2271
2249
|
" cadence init [--project <project-id|org/project>] [--json]",
|
|
2272
2250
|
" cadence auth login [--json]",
|
|
2273
2251
|
" cadence auth status [--json]",
|
|
@@ -2275,7 +2253,6 @@ function helpText() {
|
|
|
2275
2253
|
" cadence actors ensure-workspace-agent --agent-kind <kind> [--workspace-name <name>] [--workspace-ref <ref>] [--display-name <name>] [--project <project-id>] [--json]",
|
|
2276
2254
|
" cadence status [--project <project-id>] [--json]",
|
|
2277
2255
|
" cadence projects list [--json]",
|
|
2278
|
-
" cadence projects use <project-id|org/project> [--server <url>] [--json]",
|
|
2279
2256
|
" cadence work overview [--project <project-id>] [--json]",
|
|
2280
2257
|
" cadence tickets get <ticket-id> [--project <project-id>] [--json]",
|
|
2281
2258
|
" cadence tickets list [--project <project-id>] [--status <status>] [--json]",
|
|
@@ -2300,11 +2277,18 @@ function helpText() {
|
|
|
2300
2277
|
" cadence changesets notes get [--changeset <id>|--branch current|<branch>] [--project <project-id>] [--json]",
|
|
2301
2278
|
" 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]",
|
|
2302
2279
|
" cadence changesets notes apply [--changeset <id>|--branch current|<branch>] --provider github --pr-number <n> --pr-url <url> [--project <project-id>] [--json]",
|
|
2280
|
+
" cadence agent-run ingest-stop --source <codex|claude-code|opencode|openrouter> [--event <event>] [--threshold <n>] [--dry-run true|false] [--project <project-id>] [--json]",
|
|
2281
|
+
" cadence agent-run closeout --agent-session-key <key> [--reason <threshold|idle|manual>] [--event-file <path>] [--log-kind <kind>] [--update-summary true|false] [--json]",
|
|
2282
|
+
" cadence agent-run sweep [--idle-after-seconds <n>] [--dry-run true|false] [--json]",
|
|
2283
|
+
" cadence agent-run doctor [--json]",
|
|
2284
|
+
" cadence hooks install --provider codex --scope <repo|global|both> [--command <command>] [--json]",
|
|
2285
|
+
" cadence hooks doctor --provider codex --scope <repo|global|both> [--json]",
|
|
2303
2286
|
" cadence events list [--project <project-id>] [--ticket <ticket-id>] [--changeset <changeset-id>] [--session <session-id>] [--json]",
|
|
2304
2287
|
"",
|
|
2305
2288
|
"Global flags:",
|
|
2306
|
-
" --project <id
|
|
2289
|
+
" --project <id> Cadence project ID or org/project slug",
|
|
2307
2290
|
" --server <url> Cadence API server override",
|
|
2291
|
+
" --version, -v Print the Cadence CLI version",
|
|
2308
2292
|
" --json Print stable JSON envelope",
|
|
2309
2293
|
"",
|
|
2310
2294
|
"Work log parent selectors:",
|
|
@@ -2560,135 +2544,679 @@ async function readBodyFile(path, options) {
|
|
|
2560
2544
|
const resolvedPath = isAbsolute(path) ? path : join(options.cwd ?? process.cwd(), path);
|
|
2561
2545
|
return readFile(resolvedPath, "utf8");
|
|
2562
2546
|
}
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2547
|
+
function parseHookScope(value) {
|
|
2548
|
+
if (!value) {
|
|
2549
|
+
return "repo";
|
|
2550
|
+
}
|
|
2551
|
+
if (hookScopes.includes(value)) {
|
|
2552
|
+
return value;
|
|
2553
|
+
}
|
|
2554
|
+
throw new CliError("CLI_USAGE", "--scope must be one of repo, global, or both.");
|
|
2555
|
+
}
|
|
2556
|
+
function parseAgentEventSource(value) {
|
|
2557
|
+
if (!value) {
|
|
2558
|
+
return "unknown";
|
|
2559
|
+
}
|
|
2560
|
+
if (agentEventSources.includes(value)) {
|
|
2561
|
+
return value;
|
|
2562
|
+
}
|
|
2563
|
+
throw new CliError("CLI_USAGE", "--source must be one of codex, claude-code, opencode, openrouter, or unknown.");
|
|
2564
|
+
}
|
|
2565
|
+
function parseBooleanOption(value, defaultValue) {
|
|
2566
|
+
if (!value) {
|
|
2567
|
+
return defaultValue;
|
|
2568
|
+
}
|
|
2569
|
+
if (value === "true") {
|
|
2570
|
+
return true;
|
|
2571
|
+
}
|
|
2572
|
+
if (value === "false") {
|
|
2573
|
+
return false;
|
|
2574
|
+
}
|
|
2575
|
+
throw new CliError("CLI_USAGE", "Boolean options must be true or false.");
|
|
2576
|
+
}
|
|
2577
|
+
function truncateText(value, maxLength) {
|
|
2578
|
+
if (value.length <= maxLength) {
|
|
2579
|
+
return value;
|
|
2580
|
+
}
|
|
2581
|
+
return `${value.slice(0, maxLength - 15)}
|
|
2582
|
+
[truncated]`;
|
|
2583
|
+
}
|
|
2584
|
+
function stableHash(value) {
|
|
2585
|
+
return createHash("sha256").update(value).digest("hex");
|
|
2586
|
+
}
|
|
2587
|
+
async function readStdin(options) {
|
|
2588
|
+
if (options.readStdin) {
|
|
2589
|
+
return await options.readStdin();
|
|
2590
|
+
}
|
|
2591
|
+
return await Bun.stdin.text();
|
|
2592
|
+
}
|
|
2593
|
+
function tryParseJsonObject(source, label) {
|
|
2594
|
+
if (!source.trim()) {
|
|
2595
|
+
return {};
|
|
2596
|
+
}
|
|
2597
|
+
const parsed = JSON.parse(source);
|
|
2598
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2599
|
+
throw new CliError("CLI_USAGE", `${label} must be a JSON object.`);
|
|
2600
|
+
}
|
|
2601
|
+
return parsed;
|
|
2602
|
+
}
|
|
2603
|
+
function readNestedString(record, path) {
|
|
2604
|
+
let value = record;
|
|
2605
|
+
for (const key of path) {
|
|
2606
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
2607
|
+
return;
|
|
2608
|
+
}
|
|
2609
|
+
value = value[key];
|
|
2610
|
+
}
|
|
2611
|
+
return typeof value === "string" ? value : undefined;
|
|
2612
|
+
}
|
|
2613
|
+
function firstString(record, paths) {
|
|
2614
|
+
for (const path of paths) {
|
|
2615
|
+
const value = readNestedString(record, path);
|
|
2616
|
+
if (value?.trim()) {
|
|
2617
|
+
return value.trim();
|
|
2582
2618
|
}
|
|
2583
|
-
};
|
|
2584
|
-
const meta = commandMeta(parsed, config);
|
|
2585
|
-
if (parsed.flags.json) {
|
|
2586
|
-
return {
|
|
2587
|
-
stdout: formatJson(successEnvelope(data, meta)),
|
|
2588
|
-
stderr: "",
|
|
2589
|
-
exitCode: 0
|
|
2590
|
-
};
|
|
2591
2619
|
}
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2620
|
+
return;
|
|
2621
|
+
}
|
|
2622
|
+
function normalizeAgentEvent(input, parsed, options) {
|
|
2623
|
+
const source = parseAgentEventSource(parsed.options.source);
|
|
2624
|
+
const event = parsed.options.event ?? "stop";
|
|
2625
|
+
const base = normalizeAgentEventBase(input, source, event, options);
|
|
2626
|
+
switch (source) {
|
|
2627
|
+
case "codex":
|
|
2628
|
+
return normalizeCodexAgentEvent(input, base);
|
|
2629
|
+
case "claude-code":
|
|
2630
|
+
case "opencode":
|
|
2631
|
+
case "openrouter":
|
|
2632
|
+
case "unknown":
|
|
2633
|
+
return normalizeGenericAgentEvent(input, base);
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
function normalizeAgentEventBase(input, source, event, options) {
|
|
2637
|
+
const threadId = firstString(input, [["thread_id"], ["threadId"], ["conversation_id"], ["conversationId"]]);
|
|
2638
|
+
const turnId = firstString(input, [["turn_id"], ["turnId"], ["id"]]);
|
|
2639
|
+
const lastAssistantMessage = firstString(input, [
|
|
2640
|
+
["last_assistant_message"],
|
|
2641
|
+
["lastAssistantMessage"],
|
|
2642
|
+
["assistant_response"],
|
|
2643
|
+
["assistantResponse"],
|
|
2644
|
+
["message", "content"],
|
|
2645
|
+
["lastMessage", "content"]
|
|
2646
|
+
]);
|
|
2595
2647
|
return {
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
`Global config: ${config.globalConfigPath}`
|
|
2605
|
-
].join(`
|
|
2606
|
-
`) + `
|
|
2607
|
-
`,
|
|
2608
|
-
stderr: "",
|
|
2609
|
-
exitCode: 0
|
|
2648
|
+
source,
|
|
2649
|
+
event,
|
|
2650
|
+
workspacePath: options.cwd ?? process.cwd(),
|
|
2651
|
+
occurredAt: new Date().toISOString(),
|
|
2652
|
+
...threadId ? { threadId } : {},
|
|
2653
|
+
...turnId ? { turnId } : {},
|
|
2654
|
+
...lastAssistantMessage ? { lastAssistantMessage: truncateText(lastAssistantMessage, 6000) } : {},
|
|
2655
|
+
payloadKeys: Object.keys(input).sort()
|
|
2610
2656
|
};
|
|
2611
2657
|
}
|
|
2612
|
-
|
|
2613
|
-
const
|
|
2614
|
-
|
|
2615
|
-
const config = await resolveCliConfig(parsed.flags, {
|
|
2616
|
-
...options,
|
|
2617
|
-
cwd
|
|
2618
|
-
});
|
|
2619
|
-
const client = await createClient(config, options);
|
|
2620
|
-
const project = await selectProject(parsed, client, options);
|
|
2621
|
-
const projectId = project.id;
|
|
2622
|
-
const updates = {
|
|
2623
|
-
projectId,
|
|
2624
|
-
...parsed.flags.server ? { server: parsed.flags.server } : {}
|
|
2625
|
-
};
|
|
2626
|
-
await mergeConfigFile(repoConfigPath, updates);
|
|
2627
|
-
const data = {
|
|
2628
|
-
repoConfigPath,
|
|
2629
|
-
projectId,
|
|
2630
|
-
project,
|
|
2631
|
-
...parsed.flags.server ? { server: parsed.flags.server } : {}
|
|
2632
|
-
};
|
|
2633
|
-
const meta = commandMeta(parsed, {
|
|
2634
|
-
...config,
|
|
2635
|
-
projectId
|
|
2636
|
-
});
|
|
2637
|
-
if (parsed.flags.json) {
|
|
2658
|
+
function normalizeCodexAgentEvent(input, base) {
|
|
2659
|
+
const agentSessionId = firstString(input, [["session_id"], ["sessionId"], ["session", "id"]]);
|
|
2660
|
+
if (!agentSessionId) {
|
|
2638
2661
|
return {
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
exitCode: 0
|
|
2662
|
+
...base,
|
|
2663
|
+
diagnosticReason: "missing_agent_session_id"
|
|
2642
2664
|
};
|
|
2643
2665
|
}
|
|
2644
2666
|
return {
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
exitCode: 0
|
|
2667
|
+
...base,
|
|
2668
|
+
agentSessionId,
|
|
2669
|
+
agentSessionKey: agentSessionKey(base.source, agentSessionId)
|
|
2649
2670
|
};
|
|
2650
2671
|
}
|
|
2651
|
-
function
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2672
|
+
function normalizeGenericAgentEvent(input, base) {
|
|
2673
|
+
const agentSessionId = firstString(input, [
|
|
2674
|
+
["agent_session_id"],
|
|
2675
|
+
["agentSessionId"],
|
|
2676
|
+
["session_id"],
|
|
2677
|
+
["sessionId"],
|
|
2678
|
+
["session", "id"]
|
|
2679
|
+
]);
|
|
2680
|
+
if (!agentSessionId) {
|
|
2656
2681
|
return {
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
projectSlug: null,
|
|
2660
|
-
name: null
|
|
2682
|
+
...base,
|
|
2683
|
+
diagnosticReason: "missing_agent_session_id"
|
|
2661
2684
|
};
|
|
2662
2685
|
}
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
orgSlug,
|
|
2669
|
-
projectSlug
|
|
2670
|
-
});
|
|
2686
|
+
return {
|
|
2687
|
+
...base,
|
|
2688
|
+
agentSessionId,
|
|
2689
|
+
agentSessionKey: agentSessionKey(base.source, agentSessionId)
|
|
2690
|
+
};
|
|
2671
2691
|
}
|
|
2672
|
-
|
|
2673
|
-
return
|
|
2692
|
+
function agentSessionKey(source, agentSessionId) {
|
|
2693
|
+
return `${source}:${stableHash(agentSessionId)}`;
|
|
2674
2694
|
}
|
|
2675
|
-
function
|
|
2695
|
+
function defaultAgentLoopState() {
|
|
2676
2696
|
return {
|
|
2677
|
-
|
|
2678
|
-
|
|
2697
|
+
version: 2,
|
|
2698
|
+
sessions: {}
|
|
2679
2699
|
};
|
|
2680
2700
|
}
|
|
2681
|
-
function
|
|
2682
|
-
|
|
2683
|
-
const name = project.name ? ` ${project.name}` : "";
|
|
2684
|
-
return `${index + 1}. ${slug}${name}`;
|
|
2701
|
+
function randomCheckpointThreshold() {
|
|
2702
|
+
return defaultCheckpointThresholdMin + Math.floor(Math.random() * (defaultCheckpointThresholdMax - defaultCheckpointThresholdMin + 1));
|
|
2685
2703
|
}
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2704
|
+
function agentLoopDirectory(parsed, options) {
|
|
2705
|
+
return parsed.options["state-dir"] ? isAbsolute(parsed.options["state-dir"]) ? parsed.options["state-dir"] : join(options.cwd ?? process.cwd(), parsed.options["state-dir"]) : join(options.cwd ?? process.cwd(), ".context", "cadence-agent-loop");
|
|
2706
|
+
}
|
|
2707
|
+
function agentLoopStatePath(parsed, options) {
|
|
2708
|
+
return join(agentLoopDirectory(parsed, options), "state.json");
|
|
2709
|
+
}
|
|
2710
|
+
function agentLoopLockPath(parsed, options, agentSessionKeyValue) {
|
|
2711
|
+
return join(agentLoopDirectory(parsed, options), agentSessionKeyValue ? `closeout-${stableHash(agentSessionKeyValue)}.lock` : "closeout.lock");
|
|
2712
|
+
}
|
|
2713
|
+
async function readAgentLoopState(parsed, options) {
|
|
2714
|
+
const filePath = agentLoopStatePath(parsed, options);
|
|
2715
|
+
const file = Bun.file(filePath);
|
|
2716
|
+
if (!await file.exists()) {
|
|
2717
|
+
return defaultAgentLoopState();
|
|
2689
2718
|
}
|
|
2690
|
-
const
|
|
2691
|
-
if (
|
|
2719
|
+
const parsedState = JSON.parse(await file.text());
|
|
2720
|
+
if (!parsedState || typeof parsedState !== "object" || Array.isArray(parsedState)) {
|
|
2721
|
+
return defaultAgentLoopState();
|
|
2722
|
+
}
|
|
2723
|
+
const record = parsedState;
|
|
2724
|
+
if (record.version === 2) {
|
|
2725
|
+
return {
|
|
2726
|
+
version: 2,
|
|
2727
|
+
sessions: readAgentLoopSessions(record.sessions),
|
|
2728
|
+
...record.diagnostics && typeof record.diagnostics === "object" && !Array.isArray(record.diagnostics) ? { diagnostics: readAgentLoopDiagnostics(record.diagnostics) } : {}
|
|
2729
|
+
};
|
|
2730
|
+
}
|
|
2731
|
+
return {
|
|
2732
|
+
version: 2,
|
|
2733
|
+
sessions: {},
|
|
2734
|
+
diagnostics: {
|
|
2735
|
+
migratedLegacyRepoCounter: "stopCount" in record
|
|
2736
|
+
}
|
|
2737
|
+
};
|
|
2738
|
+
}
|
|
2739
|
+
async function writeAgentLoopState(parsed, options, state) {
|
|
2740
|
+
const filePath = agentLoopStatePath(parsed, options);
|
|
2741
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
2742
|
+
await writeFile(filePath, `${JSON.stringify(state, null, 2)}
|
|
2743
|
+
`);
|
|
2744
|
+
}
|
|
2745
|
+
function readAgentLoopSessions(rawSessions) {
|
|
2746
|
+
const sessions = {};
|
|
2747
|
+
if (!rawSessions || typeof rawSessions !== "object" || Array.isArray(rawSessions)) {
|
|
2748
|
+
return sessions;
|
|
2749
|
+
}
|
|
2750
|
+
for (const [key, rawSession] of Object.entries(rawSessions)) {
|
|
2751
|
+
if (!rawSession || typeof rawSession !== "object" || Array.isArray(rawSession)) {
|
|
2752
|
+
continue;
|
|
2753
|
+
}
|
|
2754
|
+
const record = rawSession;
|
|
2755
|
+
const source = parseAgentEventSource(typeof record.source === "string" ? record.source : undefined);
|
|
2756
|
+
const stopCount = typeof record.stopCount === "number" && Number.isInteger(record.stopCount) && record.stopCount >= 0 ? record.stopCount : 0;
|
|
2757
|
+
const threshold = typeof record.threshold === "number" && Number.isInteger(record.threshold) && record.threshold > 0 ? record.threshold : randomCheckpointThreshold();
|
|
2758
|
+
sessions[key] = {
|
|
2759
|
+
source,
|
|
2760
|
+
stopCount,
|
|
2761
|
+
threshold,
|
|
2762
|
+
...typeof record.firstObservedAt === "string" ? { firstObservedAt: record.firstObservedAt } : {},
|
|
2763
|
+
...typeof record.lastObservedAt === "string" ? { lastObservedAt: record.lastObservedAt } : {},
|
|
2764
|
+
...typeof record.lastAction === "string" ? { lastAction: record.lastAction } : {},
|
|
2765
|
+
...typeof record.lastReason === "string" ? { lastReason: record.lastReason } : {},
|
|
2766
|
+
...typeof record.lastEventFile === "string" ? { lastEventFile: record.lastEventFile } : {},
|
|
2767
|
+
...typeof record.lastAssistantMessage === "string" ? { lastAssistantMessage: record.lastAssistantMessage } : {},
|
|
2768
|
+
...typeof record.lastCheckpointAt === "string" ? { lastCheckpointAt: record.lastCheckpointAt } : {},
|
|
2769
|
+
...typeof record.lastCheckpointFingerprint === "string" ? { lastCheckpointFingerprint: record.lastCheckpointFingerprint } : {},
|
|
2770
|
+
...typeof record.previousCheckpointSummary === "string" ? { previousCheckpointSummary: record.previousCheckpointSummary } : {}
|
|
2771
|
+
};
|
|
2772
|
+
}
|
|
2773
|
+
return sessions;
|
|
2774
|
+
}
|
|
2775
|
+
function readAgentLoopDiagnostics(record) {
|
|
2776
|
+
return {
|
|
2777
|
+
...typeof record.missingSessionIdCount === "number" && Number.isInteger(record.missingSessionIdCount) && record.missingSessionIdCount > 0 ? { missingSessionIdCount: record.missingSessionIdCount } : {},
|
|
2778
|
+
...typeof record.lastMissingSessionIdAt === "string" ? { lastMissingSessionIdAt: record.lastMissingSessionIdAt } : {},
|
|
2779
|
+
...typeof record.lastMissingSessionIdSource === "string" ? { lastMissingSessionIdSource: parseAgentEventSource(record.lastMissingSessionIdSource) } : {},
|
|
2780
|
+
...Array.isArray(record.lastMissingSessionIdPayloadKeys) ? { lastMissingSessionIdPayloadKeys: record.lastMissingSessionIdPayloadKeys.filter((key) => typeof key === "string") } : {},
|
|
2781
|
+
...typeof record.migratedLegacyRepoCounter === "boolean" ? { migratedLegacyRepoCounter: record.migratedLegacyRepoCounter } : {}
|
|
2782
|
+
};
|
|
2783
|
+
}
|
|
2784
|
+
function recordMissingAgentSessionId(state, event) {
|
|
2785
|
+
return {
|
|
2786
|
+
...state,
|
|
2787
|
+
diagnostics: {
|
|
2788
|
+
...state.diagnostics ?? {},
|
|
2789
|
+
missingSessionIdCount: (state.diagnostics?.missingSessionIdCount ?? 0) + 1,
|
|
2790
|
+
lastMissingSessionIdAt: new Date().toISOString(),
|
|
2791
|
+
lastMissingSessionIdSource: event.source,
|
|
2792
|
+
lastMissingSessionIdPayloadKeys: event.payloadKeys
|
|
2793
|
+
}
|
|
2794
|
+
};
|
|
2795
|
+
}
|
|
2796
|
+
function clearAgentSessionReason(state) {
|
|
2797
|
+
const { lastReason: _lastReason, ...stateWithoutReason } = state;
|
|
2798
|
+
return stateWithoutReason;
|
|
2799
|
+
}
|
|
2800
|
+
async function writeAgentEventFile(parsed, options, event) {
|
|
2801
|
+
const eventsDirectory = join(agentLoopDirectory(parsed, options), "events");
|
|
2802
|
+
const fileName = `${new Date().toISOString().replace(/[:.]/g, "-")}-${randomUUID()}.json`;
|
|
2803
|
+
const filePath = join(eventsDirectory, fileName);
|
|
2804
|
+
await mkdir(eventsDirectory, { recursive: true });
|
|
2805
|
+
await writeFile(filePath, `${JSON.stringify(event, null, 2)}
|
|
2806
|
+
`);
|
|
2807
|
+
return filePath;
|
|
2808
|
+
}
|
|
2809
|
+
async function removeStaleAgentLoopLock(lockPath) {
|
|
2810
|
+
try {
|
|
2811
|
+
const lockStats = await stat(lockPath);
|
|
2812
|
+
if (Date.now() - lockStats.mtimeMs < defaultCheckpointWorkerTimeoutMs) {
|
|
2813
|
+
return false;
|
|
2814
|
+
}
|
|
2815
|
+
await rm(lockPath, { recursive: true, force: true });
|
|
2816
|
+
return true;
|
|
2817
|
+
} catch (error) {
|
|
2818
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
2819
|
+
return false;
|
|
2820
|
+
}
|
|
2821
|
+
throw error;
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
async function acquireAgentLoopLock(parsed, options, agentSessionKeyValue) {
|
|
2825
|
+
const lockPath = agentLoopLockPath(parsed, options, agentSessionKeyValue);
|
|
2826
|
+
await mkdir(dirname(lockPath), { recursive: true });
|
|
2827
|
+
try {
|
|
2828
|
+
await mkdir(lockPath);
|
|
2829
|
+
await writeFile(join(lockPath, "holder.json"), `${JSON.stringify({ pid: process.pid, acquiredAt: new Date().toISOString() }, null, 2)}
|
|
2830
|
+
`);
|
|
2831
|
+
return { acquired: true, lockPath };
|
|
2832
|
+
} catch (error) {
|
|
2833
|
+
if (!error || typeof error !== "object" || !("code" in error) || error.code !== "EEXIST") {
|
|
2834
|
+
throw error;
|
|
2835
|
+
}
|
|
2836
|
+
if (await removeStaleAgentLoopLock(lockPath)) {
|
|
2837
|
+
await mkdir(lockPath);
|
|
2838
|
+
await writeFile(join(lockPath, "holder.json"), `${JSON.stringify({ pid: process.pid, acquiredAt: new Date().toISOString(), recoveredStale: true }, null, 2)}
|
|
2839
|
+
`);
|
|
2840
|
+
return { acquired: true, lockPath };
|
|
2841
|
+
}
|
|
2842
|
+
return { acquired: false, lockPath };
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
async function releaseAgentLoopLock(lockPath) {
|
|
2846
|
+
if (lockPath) {
|
|
2847
|
+
await rm(lockPath, { recursive: true, force: true });
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2850
|
+
function runLocalCommand(command, args, options, commandOptions = {}) {
|
|
2851
|
+
if (options.runCommand) {
|
|
2852
|
+
return options.runCommand(command, args, commandOptions);
|
|
2853
|
+
}
|
|
2854
|
+
const result = spawnSync(command, [...args], {
|
|
2855
|
+
cwd: commandOptions.cwd,
|
|
2856
|
+
env: {
|
|
2857
|
+
...process.env,
|
|
2858
|
+
...commandOptions.env ?? {}
|
|
2859
|
+
},
|
|
2860
|
+
encoding: "utf8",
|
|
2861
|
+
timeout: commandOptions.timeoutMs,
|
|
2862
|
+
maxBuffer: 1024 * 1024
|
|
2863
|
+
});
|
|
2864
|
+
return {
|
|
2865
|
+
status: result.status,
|
|
2866
|
+
stdout: result.stdout ?? "",
|
|
2867
|
+
stderr: result.stderr ?? "",
|
|
2868
|
+
...result.error ? { error: result.error } : {}
|
|
2869
|
+
};
|
|
2870
|
+
}
|
|
2871
|
+
function gitOutput(args, options) {
|
|
2872
|
+
const result = runLocalCommand("git", args, options, {
|
|
2873
|
+
cwd: options.cwd ?? process.cwd(),
|
|
2874
|
+
timeoutMs: 1e4
|
|
2875
|
+
});
|
|
2876
|
+
if (result.status !== 0) {
|
|
2877
|
+
return "";
|
|
2878
|
+
}
|
|
2879
|
+
return result.stdout.trim();
|
|
2880
|
+
}
|
|
2881
|
+
function currentCliWorkerInvocation() {
|
|
2882
|
+
const scriptPath = Bun.argv[1];
|
|
2883
|
+
if (scriptPath) {
|
|
2884
|
+
return {
|
|
2885
|
+
command: process.execPath,
|
|
2886
|
+
argsPrefix: [scriptPath]
|
|
2887
|
+
};
|
|
2888
|
+
}
|
|
2889
|
+
return {
|
|
2890
|
+
command: "cadence",
|
|
2891
|
+
argsPrefix: []
|
|
2892
|
+
};
|
|
2893
|
+
}
|
|
2894
|
+
async function spawnAgentRunCloseout(args, options) {
|
|
2895
|
+
const cwd = options.cwd ?? process.cwd();
|
|
2896
|
+
const invocation = currentCliWorkerInvocation();
|
|
2897
|
+
const env = {
|
|
2898
|
+
...process.env,
|
|
2899
|
+
[agentLoopSuppressEnv]: "1"
|
|
2900
|
+
};
|
|
2901
|
+
if (options.spawnDetached) {
|
|
2902
|
+
await options.spawnDetached(invocation.command, [...invocation.argsPrefix, ...args], { cwd, env });
|
|
2903
|
+
return;
|
|
2904
|
+
}
|
|
2905
|
+
const child = spawn(invocation.command, [...invocation.argsPrefix, ...args], {
|
|
2906
|
+
cwd,
|
|
2907
|
+
env,
|
|
2908
|
+
detached: true,
|
|
2909
|
+
stdio: "ignore"
|
|
2910
|
+
});
|
|
2911
|
+
child.unref();
|
|
2912
|
+
}
|
|
2913
|
+
function activeSessionFromCurrent(current) {
|
|
2914
|
+
const record = current && typeof current === "object" ? current : null;
|
|
2915
|
+
const sessions = Array.isArray(record?.sessions) ? record.sessions : [];
|
|
2916
|
+
const activeSession = sessions.find((session) => {
|
|
2917
|
+
if (!session || typeof session !== "object") {
|
|
2918
|
+
return false;
|
|
2919
|
+
}
|
|
2920
|
+
const sessionRecord2 = session;
|
|
2921
|
+
return sessionRecord2.status === "active" && typeof sessionRecord2.ticketId === "string";
|
|
2922
|
+
});
|
|
2923
|
+
if (activeSession && typeof activeSession === "object") {
|
|
2924
|
+
return activeSession;
|
|
2925
|
+
}
|
|
2926
|
+
const activeLeases = Array.isArray(record?.activeLeases) ? record.activeLeases : Array.isArray(record?.leases) ? record.leases.filter((lease) => lease && typeof lease === "object" && lease.status === "active") : [];
|
|
2927
|
+
const activeLease = activeLeases.find((lease) => {
|
|
2928
|
+
if (!lease || typeof lease !== "object") {
|
|
2929
|
+
return false;
|
|
2930
|
+
}
|
|
2931
|
+
const leaseRecord2 = lease;
|
|
2932
|
+
return typeof leaseRecord2.ticketId === "string";
|
|
2933
|
+
});
|
|
2934
|
+
if (!activeLease || typeof activeLease !== "object") {
|
|
2935
|
+
return null;
|
|
2936
|
+
}
|
|
2937
|
+
const leaseRecord = activeLease;
|
|
2938
|
+
const sessionId = typeof leaseRecord.sessionId === "string" ? leaseRecord.sessionId : undefined;
|
|
2939
|
+
const matchingSession = sessions.find((session) => {
|
|
2940
|
+
if (!session || typeof session !== "object") {
|
|
2941
|
+
return false;
|
|
2942
|
+
}
|
|
2943
|
+
return sessionId && session.id === sessionId;
|
|
2944
|
+
});
|
|
2945
|
+
const sessionRecord = matchingSession && typeof matchingSession === "object" ? matchingSession : {};
|
|
2946
|
+
return {
|
|
2947
|
+
...sessionRecord,
|
|
2948
|
+
...sessionId ? { id: sessionId } : {},
|
|
2949
|
+
ticketId: leaseRecord.ticketId,
|
|
2950
|
+
...typeof leaseRecord.changesetId === "string" ? { changesetId: leaseRecord.changesetId } : typeof sessionRecord.changesetId === "string" ? { changesetId: sessionRecord.changesetId } : {},
|
|
2951
|
+
status: "active"
|
|
2952
|
+
};
|
|
2953
|
+
}
|
|
2954
|
+
function fingerprintForCheckpoint(event, options) {
|
|
2955
|
+
const status = gitOutput(["status", "--short"], options);
|
|
2956
|
+
const changedFiles = gitOutput(["diff", "--name-only", "origin/dev..."], options);
|
|
2957
|
+
return stableHash(JSON.stringify({
|
|
2958
|
+
source: event.source,
|
|
2959
|
+
event: event.event,
|
|
2960
|
+
lastAssistantMessage: event.lastAssistantMessage ?? "",
|
|
2961
|
+
status,
|
|
2962
|
+
changedFiles
|
|
2963
|
+
}));
|
|
2964
|
+
}
|
|
2965
|
+
function shouldSkipForCooldown(state, cooldownSeconds) {
|
|
2966
|
+
if (!state.lastCheckpointAt) {
|
|
2967
|
+
return false;
|
|
2968
|
+
}
|
|
2969
|
+
const lastCheckpointMs = new Date(state.lastCheckpointAt).getTime();
|
|
2970
|
+
return !Number.isNaN(lastCheckpointMs) && Date.now() - lastCheckpointMs < cooldownSeconds * 1000;
|
|
2971
|
+
}
|
|
2972
|
+
function synthesizeAgentEventFromSession(agentSessionKeyValue, session, options) {
|
|
2973
|
+
return {
|
|
2974
|
+
source: session.source,
|
|
2975
|
+
event: "closeout",
|
|
2976
|
+
workspacePath: options.cwd ?? process.cwd(),
|
|
2977
|
+
occurredAt: new Date().toISOString(),
|
|
2978
|
+
agentSessionKey: agentSessionKeyValue,
|
|
2979
|
+
...session.lastAssistantMessage ? { lastAssistantMessage: session.lastAssistantMessage } : {},
|
|
2980
|
+
payloadKeys: []
|
|
2981
|
+
};
|
|
2982
|
+
}
|
|
2983
|
+
function parseCheckpointJson(raw) {
|
|
2984
|
+
const trimmed = raw.trim();
|
|
2985
|
+
try {
|
|
2986
|
+
const parsed = JSON.parse(trimmed);
|
|
2987
|
+
if (parsed && typeof parsed === "object") {
|
|
2988
|
+
const record = parsed;
|
|
2989
|
+
return {
|
|
2990
|
+
body: typeof record.body === "string" && record.body.trim() ? record.body.trim() : trimmed,
|
|
2991
|
+
summary: typeof record.summary === "string" && record.summary.trim() ? record.summary.trim() : undefined
|
|
2992
|
+
};
|
|
2993
|
+
}
|
|
2994
|
+
} catch {
|
|
2995
|
+
const start = trimmed.indexOf("{");
|
|
2996
|
+
const end = trimmed.lastIndexOf("}");
|
|
2997
|
+
if (start >= 0 && end > start) {
|
|
2998
|
+
try {
|
|
2999
|
+
const parsed = JSON.parse(trimmed.slice(start, end + 1));
|
|
3000
|
+
if (parsed && typeof parsed === "object") {
|
|
3001
|
+
const record = parsed;
|
|
3002
|
+
return {
|
|
3003
|
+
body: typeof record.body === "string" && record.body.trim() ? record.body.trim() : trimmed,
|
|
3004
|
+
summary: typeof record.summary === "string" && record.summary.trim() ? record.summary.trim() : undefined
|
|
3005
|
+
};
|
|
3006
|
+
}
|
|
3007
|
+
} catch {}
|
|
3008
|
+
}
|
|
3009
|
+
}
|
|
3010
|
+
return {
|
|
3011
|
+
body: trimmed,
|
|
3012
|
+
summary: undefined
|
|
3013
|
+
};
|
|
3014
|
+
}
|
|
3015
|
+
function buildCheckpointPrompt(input) {
|
|
3016
|
+
return [
|
|
3017
|
+
"You are generating a concise Cadence checkpoint for an active coding session.",
|
|
3018
|
+
"Do not include raw diffs, raw transcripts, terminal logs, tool streams, secrets, or model reasoning.",
|
|
3019
|
+
'Return JSON only with this shape: {"summary":"one short current work summary","body":"checkpoint body suitable for a Cadence ticket work log"}.',
|
|
3020
|
+
"The body should mention what changed, decisions made, verification if known, and the next risk or next step. Keep it under 1200 characters.",
|
|
3021
|
+
"",
|
|
3022
|
+
`Ticket: ${input.ticketId}`,
|
|
3023
|
+
input.sessionId ? `Session: ${input.sessionId}` : "",
|
|
3024
|
+
input.changesetId ? `ChangeSet: ${input.changesetId}` : "",
|
|
3025
|
+
`Agent event: ${input.event.source}/${input.event.event}`,
|
|
3026
|
+
input.previousCheckpointSummary ? `Previous checkpoint summary: ${input.previousCheckpointSummary}` : "",
|
|
3027
|
+
input.event.lastAssistantMessage ? `Last assistant message:
|
|
3028
|
+
${truncateText(input.event.lastAssistantMessage, 3000)}` : "Last assistant message: unavailable",
|
|
3029
|
+
input.gitStatus ? `Git status --short:
|
|
3030
|
+
${truncateText(input.gitStatus, 2000)}` : "Git status --short: clean or unavailable",
|
|
3031
|
+
input.gitDiffStat ? `Git diff --stat origin/dev...:
|
|
3032
|
+
${truncateText(input.gitDiffStat, 2000)}` : "Git diff stat: unavailable",
|
|
3033
|
+
input.changedFiles ? `Changed files:
|
|
3034
|
+
${truncateText(input.changedFiles, 2000)}` : "Changed files: unavailable"
|
|
3035
|
+
].filter(Boolean).join(`
|
|
3036
|
+
`);
|
|
3037
|
+
}
|
|
3038
|
+
async function findRepoRoot(cwd) {
|
|
3039
|
+
const result = spawnSync("git", ["rev-parse", "--show-toplevel"], {
|
|
3040
|
+
cwd,
|
|
3041
|
+
encoding: "utf8"
|
|
3042
|
+
});
|
|
3043
|
+
if (result.status === 0 && result.stdout.trim()) {
|
|
3044
|
+
return result.stdout.trim();
|
|
3045
|
+
}
|
|
3046
|
+
const cadenceDirectory = await findRepoCadenceDirectory(cwd);
|
|
3047
|
+
return cadenceDirectory ? dirname(cadenceDirectory) : cwd;
|
|
3048
|
+
}
|
|
3049
|
+
function codexHookEntry(command) {
|
|
3050
|
+
return {
|
|
3051
|
+
hooks: [
|
|
3052
|
+
{
|
|
3053
|
+
type: "command",
|
|
3054
|
+
command,
|
|
3055
|
+
timeout: 5,
|
|
3056
|
+
statusMessage: "Checking Cadence checkpoint"
|
|
3057
|
+
}
|
|
3058
|
+
]
|
|
3059
|
+
};
|
|
3060
|
+
}
|
|
3061
|
+
async function readJsonObjectFile(filePath) {
|
|
3062
|
+
const file = Bun.file(filePath);
|
|
3063
|
+
if (!await file.exists()) {
|
|
3064
|
+
return {};
|
|
3065
|
+
}
|
|
3066
|
+
return tryParseJsonObject(await file.text(), filePath);
|
|
3067
|
+
}
|
|
3068
|
+
async function installCodexHookFile(filePath, command) {
|
|
3069
|
+
const existing = await readJsonObjectFile(filePath);
|
|
3070
|
+
const hooksValue = existing.hooks;
|
|
3071
|
+
const hooks = hooksValue && typeof hooksValue === "object" && !Array.isArray(hooksValue) ? hooksValue : {};
|
|
3072
|
+
const stopValue = hooks.Stop;
|
|
3073
|
+
const stopHooks = Array.isArray(stopValue) ? stopValue : [];
|
|
3074
|
+
const alreadyInstalled = stopHooks.some((entry) => JSON.stringify(entry).includes(command));
|
|
3075
|
+
const nextStopHooks = alreadyInstalled ? stopHooks : [...stopHooks, codexHookEntry(command)];
|
|
3076
|
+
const nextConfig = {
|
|
3077
|
+
...existing,
|
|
3078
|
+
hooks: {
|
|
3079
|
+
...hooks,
|
|
3080
|
+
Stop: nextStopHooks
|
|
3081
|
+
}
|
|
3082
|
+
};
|
|
3083
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
3084
|
+
await writeFile(filePath, `${JSON.stringify(nextConfig, null, 2)}
|
|
3085
|
+
`);
|
|
3086
|
+
return {
|
|
3087
|
+
path: filePath,
|
|
3088
|
+
installed: !alreadyInstalled,
|
|
3089
|
+
alreadyInstalled
|
|
3090
|
+
};
|
|
3091
|
+
}
|
|
3092
|
+
async function codexHookPaths(scope, options) {
|
|
3093
|
+
const cwd = options.cwd ?? process.cwd();
|
|
3094
|
+
const paths = [];
|
|
3095
|
+
if (scope === "repo" || scope === "both") {
|
|
3096
|
+
paths.push(join(await findRepoRoot(cwd), ".codex", "hooks.json"));
|
|
3097
|
+
}
|
|
3098
|
+
if (scope === "global" || scope === "both") {
|
|
3099
|
+
const home = options.env?.HOME ?? process.env.HOME;
|
|
3100
|
+
if (!home) {
|
|
3101
|
+
throw new CliError("CONFIG_ERROR", "HOME is required for global hook installation.");
|
|
3102
|
+
}
|
|
3103
|
+
paths.push(join(home, ".codex", "hooks.json"));
|
|
3104
|
+
}
|
|
3105
|
+
return paths;
|
|
3106
|
+
}
|
|
3107
|
+
async function codexHookInstalled(filePath, command) {
|
|
3108
|
+
const existing = await readJsonObjectFile(filePath);
|
|
3109
|
+
const hooks = existing.hooks;
|
|
3110
|
+
const stopHooks = hooks && typeof hooks === "object" && !Array.isArray(hooks) && Array.isArray(hooks.Stop) ? hooks.Stop : [];
|
|
3111
|
+
return stopHooks.some((entry) => JSON.stringify(entry).includes(command));
|
|
3112
|
+
}
|
|
3113
|
+
async function runStatus(parsed, options) {
|
|
3114
|
+
const config = await resolveCliConfig(parsed.flags, options);
|
|
3115
|
+
const store = getCredentialStore(options);
|
|
3116
|
+
const credential = await readStoredCredential(store, config.server);
|
|
3117
|
+
const client = await createClient(config, options);
|
|
3118
|
+
const health = await client.health();
|
|
3119
|
+
const webBaseUrl = getCliWebBaseUrl(config, parsed, options);
|
|
3120
|
+
const data = {
|
|
3121
|
+
server: config.server,
|
|
3122
|
+
webBaseUrl,
|
|
3123
|
+
profile: config.profile,
|
|
3124
|
+
projectId: config.projectId,
|
|
3125
|
+
credentialConfigured: Boolean(credential),
|
|
3126
|
+
authTokenConfigured: Boolean(credential),
|
|
3127
|
+
health,
|
|
3128
|
+
config: {
|
|
3129
|
+
repoConfigPath: config.repoConfigPath,
|
|
3130
|
+
localConfigPath: config.localConfigPath,
|
|
3131
|
+
globalConfigPath: config.globalConfigPath
|
|
3132
|
+
}
|
|
3133
|
+
};
|
|
3134
|
+
const meta = commandMeta(parsed, config);
|
|
3135
|
+
if (parsed.flags.json) {
|
|
3136
|
+
return {
|
|
3137
|
+
stdout: formatJson(successEnvelope(data, meta)),
|
|
3138
|
+
stderr: "",
|
|
3139
|
+
exitCode: 0
|
|
3140
|
+
};
|
|
3141
|
+
}
|
|
3142
|
+
return {
|
|
3143
|
+
stdout: `Cadence API ${health.ok ? "ok" : "unavailable"} at ${config.server}
|
|
3144
|
+
`,
|
|
3145
|
+
stderr: "",
|
|
3146
|
+
exitCode: 0
|
|
3147
|
+
};
|
|
3148
|
+
}
|
|
3149
|
+
async function runInit(parsed, options) {
|
|
3150
|
+
const cwd = options.cwd ?? process.cwd();
|
|
3151
|
+
const repoConfigPath = join(cwd, ".cadence", "config.json");
|
|
3152
|
+
const config = await resolveCliConfig(parsed.flags, {
|
|
3153
|
+
...options,
|
|
3154
|
+
cwd
|
|
3155
|
+
});
|
|
3156
|
+
const client = await createClient(config, options);
|
|
3157
|
+
const project = await selectProject(parsed, client, options);
|
|
3158
|
+
const projectId = project.id;
|
|
3159
|
+
const updates = {
|
|
3160
|
+
projectId,
|
|
3161
|
+
...parsed.flags.server ? { server: parsed.flags.server } : {}
|
|
3162
|
+
};
|
|
3163
|
+
await mergeConfigFile(repoConfigPath, updates);
|
|
3164
|
+
const data = {
|
|
3165
|
+
repoConfigPath,
|
|
3166
|
+
projectId,
|
|
3167
|
+
project,
|
|
3168
|
+
...parsed.flags.server ? { server: parsed.flags.server } : {}
|
|
3169
|
+
};
|
|
3170
|
+
const meta = commandMeta(parsed, {
|
|
3171
|
+
...config,
|
|
3172
|
+
projectId
|
|
3173
|
+
});
|
|
3174
|
+
if (parsed.flags.json) {
|
|
3175
|
+
return {
|
|
3176
|
+
stdout: formatJson(successEnvelope(data, meta)),
|
|
3177
|
+
stderr: "",
|
|
3178
|
+
exitCode: 0
|
|
3179
|
+
};
|
|
3180
|
+
}
|
|
3181
|
+
return {
|
|
3182
|
+
stdout: `Initialized Cadence config at ${repoConfigPath}
|
|
3183
|
+
`,
|
|
3184
|
+
stderr: "",
|
|
3185
|
+
exitCode: 0
|
|
3186
|
+
};
|
|
3187
|
+
}
|
|
3188
|
+
function isUuid(value) {
|
|
3189
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
|
|
3190
|
+
}
|
|
3191
|
+
async function resolveProjectReference(projectReference, client) {
|
|
3192
|
+
if (isUuid(projectReference)) {
|
|
3193
|
+
return {
|
|
3194
|
+
id: projectReference,
|
|
3195
|
+
orgSlug: null,
|
|
3196
|
+
projectSlug: null,
|
|
3197
|
+
name: null
|
|
3198
|
+
};
|
|
3199
|
+
}
|
|
3200
|
+
const [orgSlug, projectSlug, extra] = projectReference.split("/");
|
|
3201
|
+
if (!orgSlug || !projectSlug || extra) {
|
|
3202
|
+
throw new CliError("CLI_USAGE", "--project must be a project ID or org/project slug.");
|
|
3203
|
+
}
|
|
3204
|
+
return await client.projects.resolve({
|
|
3205
|
+
orgSlug,
|
|
3206
|
+
projectSlug
|
|
3207
|
+
});
|
|
3208
|
+
}
|
|
3209
|
+
function formatProjectChoice(project, index) {
|
|
3210
|
+
const slug = project.orgSlug && project.projectSlug ? `${project.orgSlug}/${project.projectSlug}` : project.id;
|
|
3211
|
+
const name = project.name ? ` ${project.name}` : "";
|
|
3212
|
+
return `${index + 1}. ${slug}${name}`;
|
|
3213
|
+
}
|
|
3214
|
+
async function selectProject(parsed, client, options) {
|
|
3215
|
+
if (parsed.flags.project) {
|
|
3216
|
+
return await resolveProjectReference(parsed.flags.project, client);
|
|
3217
|
+
}
|
|
3218
|
+
const projects = await client.projects.list();
|
|
3219
|
+
if (projects.length === 1) {
|
|
2692
3220
|
return projects[0];
|
|
2693
3221
|
}
|
|
2694
3222
|
if (parsed.flags.json || !isInteractive(options)) {
|
|
@@ -2720,17 +3248,579 @@ async function selectProject(parsed, client, options) {
|
|
|
2720
3248
|
}
|
|
2721
3249
|
return matching;
|
|
2722
3250
|
}
|
|
3251
|
+
async function runAgentRunCommand(parsed, options) {
|
|
3252
|
+
const config = await resolveCliConfig(parsed.flags, options);
|
|
3253
|
+
const projectId = config.projectId;
|
|
3254
|
+
const meta = commandMeta(parsed, config);
|
|
3255
|
+
switch (parsed.command.name) {
|
|
3256
|
+
case "agent-run.ingest-stop":
|
|
3257
|
+
return await runAgentRunIngestStop(parsed, options, config, meta);
|
|
3258
|
+
case "agent-run.closeout":
|
|
3259
|
+
return await runAgentRunCloseout(parsed, options, config, meta);
|
|
3260
|
+
case "agent-run.sweep":
|
|
3261
|
+
return await runAgentRunSweep(parsed, options, meta);
|
|
3262
|
+
case "agent-run.doctor": {
|
|
3263
|
+
const state = await readAgentLoopState(parsed, options);
|
|
3264
|
+
const data = {
|
|
3265
|
+
action: "doctor",
|
|
3266
|
+
state,
|
|
3267
|
+
sessionCount: Object.keys(state.sessions).length,
|
|
3268
|
+
pendingSessions: Object.entries(state.sessions).filter(([, session]) => session.stopCount > 0).map(([agentSessionKeyValue, session]) => ({
|
|
3269
|
+
agentSessionKey: agentSessionKeyValue,
|
|
3270
|
+
source: session.source,
|
|
3271
|
+
stopCount: session.stopCount,
|
|
3272
|
+
threshold: session.threshold,
|
|
3273
|
+
lastObservedAt: session.lastObservedAt,
|
|
3274
|
+
lastAction: session.lastAction
|
|
3275
|
+
}))
|
|
3276
|
+
};
|
|
3277
|
+
return {
|
|
3278
|
+
stdout: parsed.flags.json ? formatJson(successEnvelope(data, meta)) : `${JSON.stringify(data, null, 2)}
|
|
3279
|
+
`,
|
|
3280
|
+
stderr: "",
|
|
3281
|
+
exitCode: 0
|
|
3282
|
+
};
|
|
3283
|
+
}
|
|
3284
|
+
default:
|
|
3285
|
+
throw new CliError("CLI_USAGE", `Unknown command: ${parsed.command.path.join(" ")}`);
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3288
|
+
async function runAgentRunIngestStop(parsed, options, config, meta) {
|
|
3289
|
+
const suppressed = options.env?.[agentLoopSuppressEnv] === "1" || options.env?.CADENCE_HOOK_SUPPRESS === "1" || options.env?.CADENCE_STOP_HOOK_SUPPRESS === "1" || process.env[agentLoopSuppressEnv] === "1" || process.env.CADENCE_HOOK_SUPPRESS === "1" || process.env.CADENCE_STOP_HOOK_SUPPRESS === "1";
|
|
3290
|
+
if (suppressed) {
|
|
3291
|
+
const data2 = {
|
|
3292
|
+
action: "skipped",
|
|
3293
|
+
reason: "suppressed"
|
|
3294
|
+
};
|
|
3295
|
+
return {
|
|
3296
|
+
stdout: parsed.flags.json ? formatJson(successEnvelope(data2, meta)) : `${JSON.stringify(data2, null, 2)}
|
|
3297
|
+
`,
|
|
3298
|
+
stderr: "",
|
|
3299
|
+
exitCode: 0
|
|
3300
|
+
};
|
|
3301
|
+
}
|
|
3302
|
+
const rawInput = await readStdin(options);
|
|
3303
|
+
const normalized = normalizeAgentEvent(tryParseJsonObject(rawInput, "agent event input"), parsed, options);
|
|
3304
|
+
const projectId = config.projectId;
|
|
3305
|
+
if (!projectId) {
|
|
3306
|
+
const data2 = {
|
|
3307
|
+
action: "skipped",
|
|
3308
|
+
reason: "no_project",
|
|
3309
|
+
event: normalized
|
|
3310
|
+
};
|
|
3311
|
+
return {
|
|
3312
|
+
stdout: parsed.flags.json ? formatJson(successEnvelope(data2, meta)) : `${JSON.stringify(data2, null, 2)}
|
|
3313
|
+
`,
|
|
3314
|
+
stderr: "",
|
|
3315
|
+
exitCode: 0
|
|
3316
|
+
};
|
|
3317
|
+
}
|
|
3318
|
+
const state = await readAgentLoopState(parsed, options);
|
|
3319
|
+
if (!normalized.agentSessionKey) {
|
|
3320
|
+
const nextState = recordMissingAgentSessionId(state, normalized);
|
|
3321
|
+
await writeAgentLoopState(parsed, options, nextState);
|
|
3322
|
+
const data2 = {
|
|
3323
|
+
action: "skipped",
|
|
3324
|
+
reason: normalized.diagnosticReason ?? "missing_agent_session_id",
|
|
3325
|
+
event: normalized,
|
|
3326
|
+
diagnostics: nextState.diagnostics
|
|
3327
|
+
};
|
|
3328
|
+
return {
|
|
3329
|
+
stdout: parsed.flags.json ? formatJson(successEnvelope(data2, meta)) : `${JSON.stringify(data2, null, 2)}
|
|
3330
|
+
`,
|
|
3331
|
+
stderr: "",
|
|
3332
|
+
exitCode: 0
|
|
3333
|
+
};
|
|
3334
|
+
}
|
|
3335
|
+
const forcedThreshold = parsePositiveInteger(parsed.options.threshold, "--threshold");
|
|
3336
|
+
const cooldownSeconds = parsePositiveInteger(parsed.options["cooldown-seconds"], "--cooldown-seconds") ?? defaultCheckpointCooldownSeconds;
|
|
3337
|
+
const dryRun = parseBooleanOption(parsed.options["dry-run"], false);
|
|
3338
|
+
const existingSession = state.sessions[normalized.agentSessionKey];
|
|
3339
|
+
const now = new Date().toISOString();
|
|
3340
|
+
const threshold = forcedThreshold ?? existingSession?.threshold ?? randomCheckpointThreshold();
|
|
3341
|
+
const nextCount = (existingSession?.stopCount ?? 0) + 1;
|
|
3342
|
+
const observedSession = {
|
|
3343
|
+
...clearAgentSessionReason(existingSession ?? {
|
|
3344
|
+
source: normalized.source,
|
|
3345
|
+
stopCount: 0,
|
|
3346
|
+
threshold
|
|
3347
|
+
}),
|
|
3348
|
+
source: normalized.source,
|
|
3349
|
+
stopCount: nextCount,
|
|
3350
|
+
threshold,
|
|
3351
|
+
firstObservedAt: existingSession?.firstObservedAt ?? now,
|
|
3352
|
+
lastObservedAt: now,
|
|
3353
|
+
lastAction: "counted",
|
|
3354
|
+
...normalized.lastAssistantMessage ? { lastAssistantMessage: normalized.lastAssistantMessage } : {}
|
|
3355
|
+
};
|
|
3356
|
+
const countedState = {
|
|
3357
|
+
...state,
|
|
3358
|
+
sessions: {
|
|
3359
|
+
...state.sessions,
|
|
3360
|
+
[normalized.agentSessionKey]: observedSession
|
|
3361
|
+
}
|
|
3362
|
+
};
|
|
3363
|
+
if (nextCount < threshold) {
|
|
3364
|
+
await writeAgentLoopState(parsed, options, countedState);
|
|
3365
|
+
const data2 = {
|
|
3366
|
+
action: "counted",
|
|
3367
|
+
agentSessionKey: normalized.agentSessionKey,
|
|
3368
|
+
stopCount: nextCount,
|
|
3369
|
+
threshold
|
|
3370
|
+
};
|
|
3371
|
+
return {
|
|
3372
|
+
stdout: parsed.flags.json ? formatJson(successEnvelope(data2, meta)) : `${JSON.stringify(data2, null, 2)}
|
|
3373
|
+
`,
|
|
3374
|
+
stderr: "",
|
|
3375
|
+
exitCode: 0
|
|
3376
|
+
};
|
|
3377
|
+
}
|
|
3378
|
+
if (shouldSkipForCooldown(observedSession, cooldownSeconds)) {
|
|
3379
|
+
await writeAgentLoopState(parsed, options, {
|
|
3380
|
+
...state,
|
|
3381
|
+
sessions: {
|
|
3382
|
+
...state.sessions,
|
|
3383
|
+
[normalized.agentSessionKey]: {
|
|
3384
|
+
...observedSession,
|
|
3385
|
+
lastAction: "skipped",
|
|
3386
|
+
lastReason: "cooldown"
|
|
3387
|
+
}
|
|
3388
|
+
}
|
|
3389
|
+
});
|
|
3390
|
+
const data2 = {
|
|
3391
|
+
action: "skipped",
|
|
3392
|
+
reason: "cooldown",
|
|
3393
|
+
agentSessionKey: normalized.agentSessionKey,
|
|
3394
|
+
stopCount: nextCount,
|
|
3395
|
+
threshold
|
|
3396
|
+
};
|
|
3397
|
+
return {
|
|
3398
|
+
stdout: parsed.flags.json ? formatJson(successEnvelope(data2, meta)) : `${JSON.stringify(data2, null, 2)}
|
|
3399
|
+
`,
|
|
3400
|
+
stderr: "",
|
|
3401
|
+
exitCode: 0
|
|
3402
|
+
};
|
|
3403
|
+
}
|
|
3404
|
+
const fingerprint = fingerprintForCheckpoint(normalized, options);
|
|
3405
|
+
if (observedSession.lastCheckpointFingerprint && observedSession.lastCheckpointFingerprint === fingerprint) {
|
|
3406
|
+
await writeAgentLoopState(parsed, options, {
|
|
3407
|
+
...state,
|
|
3408
|
+
sessions: {
|
|
3409
|
+
...state.sessions,
|
|
3410
|
+
[normalized.agentSessionKey]: {
|
|
3411
|
+
...observedSession,
|
|
3412
|
+
stopCount: 0,
|
|
3413
|
+
threshold: randomCheckpointThreshold(),
|
|
3414
|
+
lastAction: "skipped",
|
|
3415
|
+
lastReason: "unchanged"
|
|
3416
|
+
}
|
|
3417
|
+
}
|
|
3418
|
+
});
|
|
3419
|
+
const data2 = {
|
|
3420
|
+
action: "skipped",
|
|
3421
|
+
reason: "unchanged",
|
|
3422
|
+
agentSessionKey: normalized.agentSessionKey
|
|
3423
|
+
};
|
|
3424
|
+
return {
|
|
3425
|
+
stdout: parsed.flags.json ? formatJson(successEnvelope(data2, meta)) : `${JSON.stringify(data2, null, 2)}
|
|
3426
|
+
`,
|
|
3427
|
+
stderr: "",
|
|
3428
|
+
exitCode: 0
|
|
3429
|
+
};
|
|
3430
|
+
}
|
|
3431
|
+
const eventFile = await writeAgentEventFile(parsed, options, normalized);
|
|
3432
|
+
const lockPath = agentLoopLockPath(parsed, options, normalized.agentSessionKey);
|
|
3433
|
+
const workerArgs = [
|
|
3434
|
+
"agent-run",
|
|
3435
|
+
"closeout",
|
|
3436
|
+
"--agent-session-key",
|
|
3437
|
+
normalized.agentSessionKey,
|
|
3438
|
+
"--reason",
|
|
3439
|
+
"threshold",
|
|
3440
|
+
"--event-file",
|
|
3441
|
+
eventFile,
|
|
3442
|
+
"--lock",
|
|
3443
|
+
lockPath,
|
|
3444
|
+
...parsed.options["log-kind"] ? ["--log-kind", parsed.options["log-kind"]] : [],
|
|
3445
|
+
...parsed.options["update-summary"] ? ["--update-summary", parsed.options["update-summary"]] : [],
|
|
3446
|
+
...parsed.flags.project ? ["--project", parsed.flags.project] : [],
|
|
3447
|
+
...parsed.flags.server ? ["--server", parsed.flags.server] : []
|
|
3448
|
+
];
|
|
3449
|
+
if (dryRun) {
|
|
3450
|
+
await writeAgentLoopState(parsed, options, {
|
|
3451
|
+
...state,
|
|
3452
|
+
sessions: {
|
|
3453
|
+
...state.sessions,
|
|
3454
|
+
[normalized.agentSessionKey]: {
|
|
3455
|
+
...observedSession,
|
|
3456
|
+
lastAction: "would_spawn",
|
|
3457
|
+
lastEventFile: eventFile
|
|
3458
|
+
}
|
|
3459
|
+
}
|
|
3460
|
+
});
|
|
3461
|
+
const data2 = {
|
|
3462
|
+
action: "would_spawn",
|
|
3463
|
+
agentSessionKey: normalized.agentSessionKey,
|
|
3464
|
+
eventFile,
|
|
3465
|
+
workerArgs
|
|
3466
|
+
};
|
|
3467
|
+
return {
|
|
3468
|
+
stdout: parsed.flags.json ? formatJson(successEnvelope(data2, meta)) : `${JSON.stringify(data2, null, 2)}
|
|
3469
|
+
`,
|
|
3470
|
+
stderr: "",
|
|
3471
|
+
exitCode: 0
|
|
3472
|
+
};
|
|
3473
|
+
}
|
|
3474
|
+
const lock = await acquireAgentLoopLock(parsed, options, normalized.agentSessionKey);
|
|
3475
|
+
if (!lock.acquired) {
|
|
3476
|
+
await writeAgentLoopState(parsed, options, {
|
|
3477
|
+
...state,
|
|
3478
|
+
sessions: {
|
|
3479
|
+
...state.sessions,
|
|
3480
|
+
[normalized.agentSessionKey]: {
|
|
3481
|
+
...observedSession,
|
|
3482
|
+
lastAction: "skipped",
|
|
3483
|
+
lastReason: "lock_held"
|
|
3484
|
+
}
|
|
3485
|
+
}
|
|
3486
|
+
});
|
|
3487
|
+
const data2 = {
|
|
3488
|
+
action: "skipped",
|
|
3489
|
+
reason: "lock_held",
|
|
3490
|
+
lockPath: lock.lockPath,
|
|
3491
|
+
agentSessionKey: normalized.agentSessionKey
|
|
3492
|
+
};
|
|
3493
|
+
return {
|
|
3494
|
+
stdout: parsed.flags.json ? formatJson(successEnvelope(data2, meta)) : `${JSON.stringify(data2, null, 2)}
|
|
3495
|
+
`,
|
|
3496
|
+
stderr: "",
|
|
3497
|
+
exitCode: 0
|
|
3498
|
+
};
|
|
3499
|
+
}
|
|
3500
|
+
try {
|
|
3501
|
+
await spawnAgentRunCloseout(workerArgs, options);
|
|
3502
|
+
} catch (error) {
|
|
3503
|
+
await releaseAgentLoopLock(lock.lockPath);
|
|
3504
|
+
throw error;
|
|
3505
|
+
}
|
|
3506
|
+
await writeAgentLoopState(parsed, options, {
|
|
3507
|
+
...state,
|
|
3508
|
+
sessions: {
|
|
3509
|
+
...state.sessions,
|
|
3510
|
+
[normalized.agentSessionKey]: {
|
|
3511
|
+
...observedSession,
|
|
3512
|
+
stopCount: 0,
|
|
3513
|
+
threshold: randomCheckpointThreshold(),
|
|
3514
|
+
lastAction: "spawned",
|
|
3515
|
+
lastEventFile: eventFile,
|
|
3516
|
+
lastCheckpointAt: new Date().toISOString(),
|
|
3517
|
+
lastCheckpointFingerprint: fingerprint
|
|
3518
|
+
}
|
|
3519
|
+
}
|
|
3520
|
+
});
|
|
3521
|
+
const data = {
|
|
3522
|
+
action: "spawned",
|
|
3523
|
+
agentSessionKey: normalized.agentSessionKey,
|
|
3524
|
+
eventFile,
|
|
3525
|
+
lockPath: lock.lockPath
|
|
3526
|
+
};
|
|
3527
|
+
return {
|
|
3528
|
+
stdout: parsed.flags.json ? formatJson(successEnvelope(data, meta)) : `${JSON.stringify(data, null, 2)}
|
|
3529
|
+
`,
|
|
3530
|
+
stderr: "",
|
|
3531
|
+
exitCode: 0
|
|
3532
|
+
};
|
|
3533
|
+
}
|
|
3534
|
+
async function runAgentRunCloseout(parsed, options, config, meta) {
|
|
3535
|
+
const projectId = requireProjectId(config);
|
|
3536
|
+
const client = await createClient(config, options);
|
|
3537
|
+
const agentSessionKeyValue = requireOption(parsed, "agent-session-key");
|
|
3538
|
+
const state = await readAgentLoopState(parsed, options);
|
|
3539
|
+
const sessionState = state.sessions[agentSessionKeyValue];
|
|
3540
|
+
if (!sessionState) {
|
|
3541
|
+
throw new CliError("AGENT_RUN_SESSION_NOT_FOUND", "No local agent-run session state exists for --agent-session-key.", {
|
|
3542
|
+
agentSessionKey: agentSessionKeyValue
|
|
3543
|
+
});
|
|
3544
|
+
}
|
|
3545
|
+
const current = await client.sessions.current({
|
|
3546
|
+
projectId,
|
|
3547
|
+
filters: {
|
|
3548
|
+
limit: 100
|
|
3549
|
+
}
|
|
3550
|
+
});
|
|
3551
|
+
const currentSession = activeSessionFromCurrent(current);
|
|
3552
|
+
const ticketId = parsed.options.ticket ?? (currentSession && typeof currentSession.ticketId === "string" ? currentSession.ticketId : undefined);
|
|
3553
|
+
const sessionId = parsed.options.session ?? (currentSession && typeof currentSession.id === "string" ? currentSession.id : undefined);
|
|
3554
|
+
const changesetId = parsed.options.changeset ?? (currentSession && typeof currentSession.changesetId === "string" ? currentSession.changesetId : undefined);
|
|
3555
|
+
if (!ticketId) {
|
|
3556
|
+
await writeAgentLoopState(parsed, options, {
|
|
3557
|
+
...state,
|
|
3558
|
+
sessions: {
|
|
3559
|
+
...state.sessions,
|
|
3560
|
+
[agentSessionKeyValue]: {
|
|
3561
|
+
...sessionState,
|
|
3562
|
+
lastAction: "skipped",
|
|
3563
|
+
lastReason: "no_active_ticket"
|
|
3564
|
+
}
|
|
3565
|
+
}
|
|
3566
|
+
});
|
|
3567
|
+
const data = {
|
|
3568
|
+
action: "skipped",
|
|
3569
|
+
reason: "no_active_ticket",
|
|
3570
|
+
agentSessionKey: agentSessionKeyValue
|
|
3571
|
+
};
|
|
3572
|
+
return {
|
|
3573
|
+
stdout: parsed.flags.json ? formatJson(successEnvelope(data, meta)) : `${JSON.stringify(data, null, 2)}
|
|
3574
|
+
`,
|
|
3575
|
+
stderr: "",
|
|
3576
|
+
exitCode: 0
|
|
3577
|
+
};
|
|
3578
|
+
}
|
|
3579
|
+
const eventFile = parsed.options["event-file"] ?? sessionState.lastEventFile;
|
|
3580
|
+
const event = eventFile ? tryParseJsonObject(await readFile(eventFile, "utf8"), eventFile) : synthesizeAgentEventFromSession(agentSessionKeyValue, sessionState, options);
|
|
3581
|
+
const logKind = parseWorkLogEntryKind(parsed.options["log-kind"] ?? "note");
|
|
3582
|
+
const updateSummary = parseBooleanOption(parsed.options["update-summary"], false);
|
|
3583
|
+
const dryRun = parseBooleanOption(parsed.options["dry-run"], false);
|
|
3584
|
+
const lockPath = parsed.options.lock;
|
|
3585
|
+
const gitStatus = gitOutput(["status", "--short"], options);
|
|
3586
|
+
const gitDiffStat = gitOutput(["diff", "--stat", "origin/dev..."], options);
|
|
3587
|
+
const changedFiles = gitOutput(["diff", "--name-only", "origin/dev..."], options);
|
|
3588
|
+
const prompt = buildCheckpointPrompt({
|
|
3589
|
+
event,
|
|
3590
|
+
ticketId,
|
|
3591
|
+
...sessionId ? { sessionId } : {},
|
|
3592
|
+
...changesetId ? { changesetId } : {},
|
|
3593
|
+
gitStatus,
|
|
3594
|
+
gitDiffStat,
|
|
3595
|
+
changedFiles,
|
|
3596
|
+
...sessionState.previousCheckpointSummary ? { previousCheckpointSummary: sessionState.previousCheckpointSummary } : {}
|
|
3597
|
+
});
|
|
3598
|
+
if (dryRun) {
|
|
3599
|
+
const data = {
|
|
3600
|
+
action: "would_closeout",
|
|
3601
|
+
prompt,
|
|
3602
|
+
ticketId,
|
|
3603
|
+
agentSessionKey: agentSessionKeyValue,
|
|
3604
|
+
...sessionId ? { sessionId } : {},
|
|
3605
|
+
...changesetId ? { changesetId } : {}
|
|
3606
|
+
};
|
|
3607
|
+
return {
|
|
3608
|
+
stdout: parsed.flags.json ? formatJson(successEnvelope(data, meta)) : `${JSON.stringify(data, null, 2)}
|
|
3609
|
+
`,
|
|
3610
|
+
stderr: "",
|
|
3611
|
+
exitCode: 0
|
|
3612
|
+
};
|
|
3613
|
+
}
|
|
3614
|
+
try {
|
|
3615
|
+
const codexCommand = parsed.options["codex-command"] ?? "codex";
|
|
3616
|
+
const codex = runLocalCommand(codexCommand, ["exec", "--disable", "hooks", "--sandbox", "read-only", "-C", options.cwd ?? process.cwd(), prompt], options, {
|
|
3617
|
+
cwd: options.cwd ?? process.cwd(),
|
|
3618
|
+
env: {
|
|
3619
|
+
[agentLoopSuppressEnv]: "1",
|
|
3620
|
+
CADENCE_HOOK_SUPPRESS: "1"
|
|
3621
|
+
},
|
|
3622
|
+
timeoutMs: defaultCheckpointWorkerTimeoutMs
|
|
3623
|
+
});
|
|
3624
|
+
if (codex.status !== 0 || codex.error) {
|
|
3625
|
+
throw new CliError("AGENT_RUN_CLOSEOUT_FAILED", "Codex closeout generation failed.", {
|
|
3626
|
+
status: codex.status,
|
|
3627
|
+
stderr: truncateText(codex.stderr, 2000),
|
|
3628
|
+
error: codex.error?.message
|
|
3629
|
+
});
|
|
3630
|
+
}
|
|
3631
|
+
const checkpoint = parseCheckpointJson(codex.stdout);
|
|
3632
|
+
const body = checkpoint.body.trim();
|
|
3633
|
+
const summary = checkpoint.summary ?? truncateText(body.replace(/\s+/g, " "), 500);
|
|
3634
|
+
if (!body) {
|
|
3635
|
+
throw new CliError("AGENT_RUN_CLOSEOUT_FAILED", "Codex closeout generation returned an empty checkpoint.");
|
|
3636
|
+
}
|
|
3637
|
+
await client.tickets.log({
|
|
3638
|
+
projectId,
|
|
3639
|
+
ticketId,
|
|
3640
|
+
entry: {
|
|
3641
|
+
entryKind: logKind,
|
|
3642
|
+
body,
|
|
3643
|
+
summary,
|
|
3644
|
+
...sessionId ? { sessionId } : {},
|
|
3645
|
+
...changesetId ? { changesetId } : {},
|
|
3646
|
+
...commandMetadata()
|
|
3647
|
+
}
|
|
3648
|
+
});
|
|
3649
|
+
if (updateSummary) {
|
|
3650
|
+
const ticket = await client.tickets.get({ projectId, ticketId });
|
|
3651
|
+
if (typeof ticket.projectionVersion === "number") {
|
|
3652
|
+
await client.tickets.update({
|
|
3653
|
+
projectId,
|
|
3654
|
+
ticketId,
|
|
3655
|
+
ifVersion: ticket.projectionVersion,
|
|
3656
|
+
ticket: {
|
|
3657
|
+
currentSummary: summary,
|
|
3658
|
+
...commandMetadata()
|
|
3659
|
+
}
|
|
3660
|
+
});
|
|
3661
|
+
}
|
|
3662
|
+
}
|
|
3663
|
+
await writeAgentLoopState(parsed, options, {
|
|
3664
|
+
...state,
|
|
3665
|
+
sessions: {
|
|
3666
|
+
...state.sessions,
|
|
3667
|
+
[agentSessionKeyValue]: {
|
|
3668
|
+
...sessionState,
|
|
3669
|
+
previousCheckpointSummary: summary,
|
|
3670
|
+
lastAction: "closed_out",
|
|
3671
|
+
...eventFile ? { lastEventFile: eventFile } : {}
|
|
3672
|
+
}
|
|
3673
|
+
}
|
|
3674
|
+
});
|
|
3675
|
+
const data = {
|
|
3676
|
+
action: "closed_out",
|
|
3677
|
+
ticketId,
|
|
3678
|
+
summary,
|
|
3679
|
+
agentSessionKey: agentSessionKeyValue,
|
|
3680
|
+
...sessionId ? { sessionId } : {},
|
|
3681
|
+
...changesetId ? { changesetId } : {}
|
|
3682
|
+
};
|
|
3683
|
+
return {
|
|
3684
|
+
stdout: parsed.flags.json ? formatJson(successEnvelope(data, meta)) : `${JSON.stringify(data, null, 2)}
|
|
3685
|
+
`,
|
|
3686
|
+
stderr: "",
|
|
3687
|
+
exitCode: 0
|
|
3688
|
+
};
|
|
3689
|
+
} finally {
|
|
3690
|
+
await releaseAgentLoopLock(lockPath);
|
|
3691
|
+
}
|
|
3692
|
+
}
|
|
3693
|
+
async function runAgentRunSweep(parsed, options, meta) {
|
|
3694
|
+
const state = await readAgentLoopState(parsed, options);
|
|
3695
|
+
const idleAfterSeconds = parsePositiveInteger(parsed.options["idle-after-seconds"], "--idle-after-seconds") ?? 5 * 60;
|
|
3696
|
+
const dryRun = parseBooleanOption(parsed.options["dry-run"], false);
|
|
3697
|
+
const nowMs = Date.now();
|
|
3698
|
+
const staleSessions = Object.entries(state.sessions).filter(([, session]) => {
|
|
3699
|
+
if (session.stopCount <= 0 || !session.lastObservedAt) {
|
|
3700
|
+
return false;
|
|
3701
|
+
}
|
|
3702
|
+
const lastObservedMs = new Date(session.lastObservedAt).getTime();
|
|
3703
|
+
return !Number.isNaN(lastObservedMs) && nowMs - lastObservedMs >= idleAfterSeconds * 1000;
|
|
3704
|
+
}).map(([agentSessionKeyValue, session]) => ({
|
|
3705
|
+
agentSessionKey: agentSessionKeyValue,
|
|
3706
|
+
source: session.source,
|
|
3707
|
+
stopCount: session.stopCount,
|
|
3708
|
+
threshold: session.threshold,
|
|
3709
|
+
lastObservedAt: session.lastObservedAt
|
|
3710
|
+
}));
|
|
3711
|
+
if (!dryRun) {
|
|
3712
|
+
for (const staleSession of staleSessions) {
|
|
3713
|
+
await spawnAgentRunCloseout([
|
|
3714
|
+
"agent-run",
|
|
3715
|
+
"closeout",
|
|
3716
|
+
"--agent-session-key",
|
|
3717
|
+
staleSession.agentSessionKey,
|
|
3718
|
+
"--reason",
|
|
3719
|
+
"idle",
|
|
3720
|
+
"--lock",
|
|
3721
|
+
agentLoopLockPath(parsed, options, staleSession.agentSessionKey),
|
|
3722
|
+
...parsed.flags.project ? ["--project", parsed.flags.project] : [],
|
|
3723
|
+
...parsed.flags.server ? ["--server", parsed.flags.server] : []
|
|
3724
|
+
], options);
|
|
3725
|
+
}
|
|
3726
|
+
}
|
|
3727
|
+
const data = {
|
|
3728
|
+
action: dryRun ? "would_sweep" : "swept",
|
|
3729
|
+
idleAfterSeconds,
|
|
3730
|
+
staleSessions
|
|
3731
|
+
};
|
|
3732
|
+
return {
|
|
3733
|
+
stdout: parsed.flags.json ? formatJson(successEnvelope(data, meta)) : `${JSON.stringify(data, null, 2)}
|
|
3734
|
+
`,
|
|
3735
|
+
stderr: "",
|
|
3736
|
+
exitCode: 0
|
|
3737
|
+
};
|
|
3738
|
+
}
|
|
3739
|
+
async function runHooksCommand(parsed, options) {
|
|
3740
|
+
const config = await resolveCliConfig(parsed.flags, options);
|
|
3741
|
+
const meta = commandMeta(parsed, config);
|
|
3742
|
+
let data;
|
|
3743
|
+
switch (parsed.command.name) {
|
|
3744
|
+
case "hooks.install":
|
|
3745
|
+
{
|
|
3746
|
+
const provider = parsed.options.provider ?? "codex";
|
|
3747
|
+
if (provider !== "codex") {
|
|
3748
|
+
throw new CliError("CLI_USAGE", "Only --provider codex is supported for hook installation in this version.");
|
|
3749
|
+
}
|
|
3750
|
+
const scope = parseHookScope(parsed.options.scope);
|
|
3751
|
+
const command = parsed.options.command ?? defaultHookCommand;
|
|
3752
|
+
const paths = await codexHookPaths(scope, options);
|
|
3753
|
+
const results = [];
|
|
3754
|
+
for (const filePath of paths) {
|
|
3755
|
+
results.push(await installCodexHookFile(filePath, command));
|
|
3756
|
+
}
|
|
3757
|
+
data = {
|
|
3758
|
+
provider,
|
|
3759
|
+
scope,
|
|
3760
|
+
command,
|
|
3761
|
+
results,
|
|
3762
|
+
trustRequired: "Open Codex /hooks and trust the Cadence hook before it can run."
|
|
3763
|
+
};
|
|
3764
|
+
}
|
|
3765
|
+
break;
|
|
3766
|
+
case "hooks.doctor":
|
|
3767
|
+
{
|
|
3768
|
+
const provider = parsed.options.provider ?? "codex";
|
|
3769
|
+
if (provider !== "codex") {
|
|
3770
|
+
throw new CliError("CLI_USAGE", "Only --provider codex is supported for hook doctor in this version.");
|
|
3771
|
+
}
|
|
3772
|
+
const scope = parseHookScope(parsed.options.scope);
|
|
3773
|
+
const command = parsed.options.command ?? defaultHookCommand;
|
|
3774
|
+
const paths = await codexHookPaths(scope, options);
|
|
3775
|
+
const codexVersion = runLocalCommand("codex", ["--version"], options, {
|
|
3776
|
+
cwd: options.cwd ?? process.cwd(),
|
|
3777
|
+
timeoutMs: 1e4
|
|
3778
|
+
});
|
|
3779
|
+
data = {
|
|
3780
|
+
provider,
|
|
3781
|
+
scope,
|
|
3782
|
+
command,
|
|
3783
|
+
codexAvailable: codexVersion.status === 0,
|
|
3784
|
+
codexVersion: codexVersion.status === 0 ? codexVersion.stdout.trim() : null,
|
|
3785
|
+
hooks: await Promise.all(paths.map(async (filePath) => ({
|
|
3786
|
+
path: filePath,
|
|
3787
|
+
exists: await Bun.file(filePath).exists(),
|
|
3788
|
+
installed: await codexHookInstalled(filePath, command)
|
|
3789
|
+
})))
|
|
3790
|
+
};
|
|
3791
|
+
}
|
|
3792
|
+
break;
|
|
3793
|
+
default:
|
|
3794
|
+
throw new CliError("CLI_USAGE", `Unknown command: ${parsed.command.path.join(" ")}`);
|
|
3795
|
+
}
|
|
3796
|
+
if (parsed.flags.json) {
|
|
3797
|
+
return {
|
|
3798
|
+
stdout: formatJson(successEnvelope(data, meta)),
|
|
3799
|
+
stderr: "",
|
|
3800
|
+
exitCode: 0
|
|
3801
|
+
};
|
|
3802
|
+
}
|
|
3803
|
+
return {
|
|
3804
|
+
stdout: `${JSON.stringify(data, null, 2)}
|
|
3805
|
+
`,
|
|
3806
|
+
stderr: "",
|
|
3807
|
+
exitCode: 0
|
|
3808
|
+
};
|
|
3809
|
+
}
|
|
2723
3810
|
async function runAuthCommand(parsed, options) {
|
|
2724
3811
|
const config = await resolveCliConfig(parsed.flags, options);
|
|
2725
3812
|
const store = getCredentialStore(options);
|
|
2726
|
-
|
|
3813
|
+
let meta = commandMeta(parsed, config);
|
|
2727
3814
|
let data;
|
|
2728
3815
|
switch (parsed.command.name) {
|
|
2729
3816
|
case "auth.login":
|
|
2730
3817
|
{
|
|
2731
|
-
const
|
|
3818
|
+
const loginConfig = await resolveAuthLoginConfig(config, parsed, options);
|
|
3819
|
+
meta = commandMeta(parsed, loginConfig);
|
|
3820
|
+
const client = await createClient(loginConfig, options, false);
|
|
3821
|
+
const loginBaseUrl = loginConfig.webBaseUrl;
|
|
2732
3822
|
const challenge = await client.auth.cli.start({
|
|
2733
|
-
loginBaseUrl
|
|
3823
|
+
loginBaseUrl
|
|
2734
3824
|
});
|
|
2735
3825
|
if (parsed.flags.json || !isInteractive(options)) {
|
|
2736
3826
|
throw new CliError("HUMAN_AUTH_REQUIRED", "Cadence login must be completed by a human in the browser.", {
|
|
@@ -2762,20 +3852,15 @@ async function runAuthCommand(parsed, options) {
|
|
|
2762
3852
|
if (poll.status === "denied") {
|
|
2763
3853
|
throw new CliError("AUTH_LOGIN_DENIED", "Cadence browser login was denied.");
|
|
2764
3854
|
}
|
|
2765
|
-
await writeStoredCredential(store,
|
|
2766
|
-
await mergeConfigFile(
|
|
3855
|
+
await writeStoredCredential(store, loginConfig.server, poll.credential);
|
|
3856
|
+
await mergeConfigFile(loginConfig.globalConfigPath, { server: loginConfig.server, webBaseUrl: loginBaseUrl });
|
|
2767
3857
|
await writeInteractiveStatus("Cadence CLI login approved. Credential stored.", options);
|
|
2768
|
-
const projectConfigured = Boolean(config.projectId);
|
|
2769
|
-
if (!projectConfigured) {
|
|
2770
|
-
await writeInteractiveStatus("No Cadence project is configured for this repo. Run cadence init to choose one.", options);
|
|
2771
|
-
}
|
|
2772
3858
|
data = {
|
|
2773
|
-
server:
|
|
3859
|
+
server: loginConfig.server,
|
|
3860
|
+
webBaseUrl: loginBaseUrl,
|
|
2774
3861
|
credentialStored: true,
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
loginUrl: challenge.loginUrl,
|
|
2778
|
-
...!projectConfigured ? { nextCommand: "cadence init" } : {}
|
|
3862
|
+
globalConfigPath: loginConfig.globalConfigPath,
|
|
3863
|
+
loginUrl: challenge.loginUrl
|
|
2779
3864
|
};
|
|
2780
3865
|
}
|
|
2781
3866
|
break;
|
|
@@ -2813,26 +3898,18 @@ async function runAuthCommand(parsed, options) {
|
|
|
2813
3898
|
exitCode: 0
|
|
2814
3899
|
};
|
|
2815
3900
|
}
|
|
2816
|
-
if (parsed.command.name === "auth.login") {
|
|
2817
|
-
return {
|
|
2818
|
-
stdout: "",
|
|
2819
|
-
stderr: "",
|
|
2820
|
-
exitCode: 0
|
|
2821
|
-
};
|
|
2822
|
-
}
|
|
2823
3901
|
return {
|
|
2824
|
-
stdout:
|
|
3902
|
+
stdout: `${JSON.stringify(data, null, 2)}
|
|
3903
|
+
`,
|
|
2825
3904
|
stderr: "",
|
|
2826
3905
|
exitCode: 0
|
|
2827
3906
|
};
|
|
2828
3907
|
}
|
|
2829
3908
|
async function runReadCommand(parsed, options) {
|
|
2830
3909
|
const config = await resolveCliConfig(parsed.flags, options);
|
|
3910
|
+
const projectId = requireProjectId(config);
|
|
2831
3911
|
const client = await createClient(config, options);
|
|
2832
|
-
const
|
|
2833
|
-
const resolvedConfig = configWithProject(config, project);
|
|
2834
|
-
const projectId = project.id;
|
|
2835
|
-
const meta = commandMeta(parsed, resolvedConfig);
|
|
3912
|
+
const meta = commandMeta(parsed, config);
|
|
2836
3913
|
let data;
|
|
2837
3914
|
switch (parsed.command.name) {
|
|
2838
3915
|
case "events.list":
|
|
@@ -2925,7 +4002,8 @@ async function runReadCommand(parsed, options) {
|
|
|
2925
4002
|
};
|
|
2926
4003
|
}
|
|
2927
4004
|
return {
|
|
2928
|
-
stdout:
|
|
4005
|
+
stdout: `${JSON.stringify(data, null, 2)}
|
|
4006
|
+
`,
|
|
2929
4007
|
stderr: "",
|
|
2930
4008
|
exitCode: 0
|
|
2931
4009
|
};
|
|
@@ -2933,53 +4011,25 @@ async function runReadCommand(parsed, options) {
|
|
|
2933
4011
|
async function runProjectCommand(parsed, options) {
|
|
2934
4012
|
const config = await resolveCliConfig(parsed.flags, options);
|
|
2935
4013
|
const client = await createClient(config, options);
|
|
4014
|
+
const meta = commandMeta(parsed, config);
|
|
2936
4015
|
let data;
|
|
2937
|
-
let outputConfig = config;
|
|
2938
4016
|
switch (parsed.command.name) {
|
|
2939
4017
|
case "projects.list":
|
|
2940
4018
|
data = await client.projects.list();
|
|
2941
4019
|
break;
|
|
2942
|
-
case "projects.use":
|
|
2943
|
-
{
|
|
2944
|
-
const project = await resolveProjectReference(requireArg(parsed, 0, "<project-id|org/project>"), client);
|
|
2945
|
-
const repoConfigPath = await repoConfigPathForWrite(options);
|
|
2946
|
-
const updates = {
|
|
2947
|
-
projectId: project.id,
|
|
2948
|
-
...parsed.flags.server ? { server: parsed.flags.server } : {}
|
|
2949
|
-
};
|
|
2950
|
-
await mergeConfigFile(repoConfigPath, updates);
|
|
2951
|
-
outputConfig = configWithProject(config, project);
|
|
2952
|
-
data = {
|
|
2953
|
-
repoConfigPath,
|
|
2954
|
-
projectId: project.id,
|
|
2955
|
-
project,
|
|
2956
|
-
...parsed.flags.server ? { server: parsed.flags.server } : {}
|
|
2957
|
-
};
|
|
2958
|
-
}
|
|
2959
|
-
break;
|
|
2960
4020
|
default:
|
|
2961
4021
|
throw new CliError("CLI_USAGE", `Unknown command: ${parsed.command.path.join(" ")}`);
|
|
2962
4022
|
}
|
|
2963
4023
|
if (parsed.flags.json) {
|
|
2964
4024
|
return {
|
|
2965
|
-
stdout: formatJson(successEnvelope(data,
|
|
2966
|
-
stderr: "",
|
|
2967
|
-
exitCode: 0
|
|
2968
|
-
};
|
|
2969
|
-
}
|
|
2970
|
-
if (parsed.command.name === "projects.use") {
|
|
2971
|
-
const project = data;
|
|
2972
|
-
const slug = project.project?.orgSlug && project.project.projectSlug ? `${project.project.orgSlug}/${project.project.projectSlug}` : project.projectId;
|
|
2973
|
-
const name = project.project?.name ? ` (${project.project.name})` : "";
|
|
2974
|
-
return {
|
|
2975
|
-
stdout: `Using Cadence project ${slug}${name}.
|
|
2976
|
-
`,
|
|
4025
|
+
stdout: formatJson(successEnvelope(data, meta)),
|
|
2977
4026
|
stderr: "",
|
|
2978
4027
|
exitCode: 0
|
|
2979
4028
|
};
|
|
2980
4029
|
}
|
|
2981
4030
|
return {
|
|
2982
|
-
stdout:
|
|
4031
|
+
stdout: `${JSON.stringify(data, null, 2)}
|
|
4032
|
+
`,
|
|
2983
4033
|
stderr: "",
|
|
2984
4034
|
exitCode: 0
|
|
2985
4035
|
};
|
|
@@ -2987,13 +4037,11 @@ async function runProjectCommand(parsed, options) {
|
|
|
2987
4037
|
async function runActorCommand(parsed, options) {
|
|
2988
4038
|
const config = await resolveCliConfig(parsed.flags, options);
|
|
2989
4039
|
const client = await createClient(config, options);
|
|
2990
|
-
const
|
|
2991
|
-
const resolvedConfig = configWithProject(config, project);
|
|
2992
|
-
const meta = commandMeta(parsed, resolvedConfig);
|
|
4040
|
+
const meta = commandMeta(parsed, config);
|
|
2993
4041
|
let data;
|
|
2994
4042
|
switch (parsed.command.name) {
|
|
2995
4043
|
case "actors.ensure-workspace-agent":
|
|
2996
|
-
data = await ensureWorkspaceAgentIdentity(parsed,
|
|
4044
|
+
data = await ensureWorkspaceAgentIdentity(parsed, config, client, options);
|
|
2997
4045
|
break;
|
|
2998
4046
|
default:
|
|
2999
4047
|
throw new CliError("CLI_USAGE", `Unknown command: ${parsed.command.path.join(" ")}`);
|
|
@@ -3006,18 +4054,17 @@ async function runActorCommand(parsed, options) {
|
|
|
3006
4054
|
};
|
|
3007
4055
|
}
|
|
3008
4056
|
return {
|
|
3009
|
-
stdout:
|
|
4057
|
+
stdout: `${JSON.stringify(data, null, 2)}
|
|
4058
|
+
`,
|
|
3010
4059
|
stderr: "",
|
|
3011
4060
|
exitCode: 0
|
|
3012
4061
|
};
|
|
3013
4062
|
}
|
|
3014
4063
|
async function runIntakeCommand(parsed, options) {
|
|
3015
4064
|
const config = await resolveCliConfig(parsed.flags, options);
|
|
4065
|
+
const projectId = requireProjectId(config);
|
|
3016
4066
|
const client = await createClient(config, options);
|
|
3017
|
-
const
|
|
3018
|
-
const resolvedConfig = configWithProject(config, project);
|
|
3019
|
-
const projectId = project.id;
|
|
3020
|
-
const meta = commandMeta(parsed, resolvedConfig);
|
|
4067
|
+
const meta = commandMeta(parsed, config);
|
|
3021
4068
|
let data;
|
|
3022
4069
|
const ifVersion = () => parseRequiredPositiveInteger(requireOption(parsed, "if-version"), "--if-version");
|
|
3023
4070
|
switch (parsed.command.name) {
|
|
@@ -3249,7 +4296,8 @@ async function runIntakeCommand(parsed, options) {
|
|
|
3249
4296
|
};
|
|
3250
4297
|
}
|
|
3251
4298
|
return {
|
|
3252
|
-
stdout:
|
|
4299
|
+
stdout: `${JSON.stringify(data, null, 2)}
|
|
4300
|
+
`,
|
|
3253
4301
|
stderr: "",
|
|
3254
4302
|
exitCode: 0
|
|
3255
4303
|
};
|
|
@@ -3281,8 +4329,27 @@ async function runCli(argv, options = {}) {
|
|
|
3281
4329
|
try {
|
|
3282
4330
|
const parsed = parseCliArgs(argv);
|
|
3283
4331
|
const meta = {
|
|
3284
|
-
command: parsed.command.name
|
|
4332
|
+
command: parsed.flags.version ? "version" : parsed.command.name
|
|
3285
4333
|
};
|
|
4334
|
+
if (parsed.flags.version || parsed.command.name === "version") {
|
|
4335
|
+
const data = {
|
|
4336
|
+
name: package_default.name,
|
|
4337
|
+
version: cliVersion
|
|
4338
|
+
};
|
|
4339
|
+
if (parsed.flags.json) {
|
|
4340
|
+
return {
|
|
4341
|
+
stdout: formatJson(successEnvelope(data, meta)),
|
|
4342
|
+
stderr: "",
|
|
4343
|
+
exitCode: 0
|
|
4344
|
+
};
|
|
4345
|
+
}
|
|
4346
|
+
return {
|
|
4347
|
+
stdout: `cadence ${cliVersion}
|
|
4348
|
+
`,
|
|
4349
|
+
stderr: "",
|
|
4350
|
+
exitCode: 0
|
|
4351
|
+
};
|
|
4352
|
+
}
|
|
3286
4353
|
if (parsed.flags.help || parsed.command.name === "help") {
|
|
3287
4354
|
if (parsed.flags.json) {
|
|
3288
4355
|
return {
|
|
@@ -3313,9 +4380,15 @@ async function runCli(argv, options = {}) {
|
|
|
3313
4380
|
if (parsed.command.name === "events.list" || parsed.command.name === "work.overview" || parsed.command.name === "tickets.get" || parsed.command.name === "tickets.list" || parsed.command.name === "sessions.current" || parsed.command.name === "changesets.get" || parsed.command.name === "changesets.context" || parsed.command.name === "changesets.current" || parsed.command.name === "changesets.list" || parsed.command.name === "changesets.notes.get") {
|
|
3314
4381
|
return await runReadCommand(parsed, options);
|
|
3315
4382
|
}
|
|
3316
|
-
if (parsed.command.name === "projects.list"
|
|
4383
|
+
if (parsed.command.name === "projects.list") {
|
|
3317
4384
|
return await runProjectCommand(parsed, options);
|
|
3318
4385
|
}
|
|
4386
|
+
if (parsed.command.name === "agent-run.ingest-stop" || parsed.command.name === "agent-run.closeout" || parsed.command.name === "agent-run.sweep" || parsed.command.name === "agent-run.doctor") {
|
|
4387
|
+
return await runAgentRunCommand(parsed, options);
|
|
4388
|
+
}
|
|
4389
|
+
if (parsed.command.name === "hooks.install" || parsed.command.name === "hooks.doctor") {
|
|
4390
|
+
return await runHooksCommand(parsed, options);
|
|
4391
|
+
}
|
|
3319
4392
|
if (parsed.command.name === "intake" || parsed.command.name === "intake.dismiss" || parsed.command.name === "tickets.attach" || parsed.command.name === "tickets.create" || parsed.command.name === "tickets.update" || parsed.command.name === "tickets.claim" || parsed.command.name === "tickets.release" || parsed.command.name === "tickets.log" || parsed.command.name === "tickets.complete" || parsed.command.name === "sessions.start" || parsed.command.name === "sessions.end" || parsed.command.name === "sessions.files" || parsed.command.name === "changesets.create" || parsed.command.name === "changesets.notes.put" || parsed.command.name === "changesets.notes.apply") {
|
|
3320
4393
|
return await runIntakeCommand(parsed, options);
|
|
3321
4394
|
}
|