@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.
Files changed (2) hide show
  1. package/dist/cadence +1401 -328
  2. 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|org/project> Cadence project ID or org/project slug",
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
- async function runStatus(parsed, options) {
2564
- const config = await resolveCliConfig(parsed.flags, options);
2565
- const store = getCredentialStore(options);
2566
- const credential = await readStoredCredential(store, config.server);
2567
- const client = await createClient(config, options);
2568
- const health = await client.health();
2569
- const webBaseUrl = getCliWebBaseUrl(config, parsed, options);
2570
- const data = {
2571
- server: config.server,
2572
- webBaseUrl,
2573
- profile: config.profile,
2574
- projectId: config.projectId,
2575
- credentialConfigured: Boolean(credential),
2576
- authTokenConfigured: Boolean(credential),
2577
- health,
2578
- config: {
2579
- repoConfigPath: config.repoConfigPath,
2580
- localConfigPath: config.localConfigPath,
2581
- globalConfigPath: config.globalConfigPath
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
- const projectLabel = config.projectId ?? "not configured";
2593
- const profileLabel = config.profile ?? "default";
2594
- const credentialLabel = credential ? "configured" : "not configured";
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
- stdout: [
2597
- `Cadence API: ${health.ok ? "ok" : "unavailable"} at ${config.server}`,
2598
- `Cadence web: ${webBaseUrl}`,
2599
- `Profile: ${profileLabel}`,
2600
- `Project: ${projectLabel}`,
2601
- `Credential: ${credentialLabel}`,
2602
- `Repo config: ${config.repoConfigPath ?? "not found"}`,
2603
- `Local config: ${config.localConfigPath ?? "not found"}`,
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
- async function runInit(parsed, options) {
2613
- const cwd = options.cwd ?? process.cwd();
2614
- const repoConfigPath = await repoConfigPathForWrite(options);
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
- stdout: formatJson(successEnvelope(data, meta)),
2640
- stderr: "",
2641
- exitCode: 0
2662
+ ...base,
2663
+ diagnosticReason: "missing_agent_session_id"
2642
2664
  };
2643
2665
  }
2644
2666
  return {
2645
- stdout: `Initialized Cadence config at ${repoConfigPath}
2646
- `,
2647
- stderr: "",
2648
- exitCode: 0
2667
+ ...base,
2668
+ agentSessionId,
2669
+ agentSessionKey: agentSessionKey(base.source, agentSessionId)
2649
2670
  };
2650
2671
  }
2651
- function isUuid(value) {
2652
- 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);
2653
- }
2654
- async function resolveProjectReference(projectReference, client) {
2655
- if (isUuid(projectReference)) {
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
- id: projectReference,
2658
- orgSlug: null,
2659
- projectSlug: null,
2660
- name: null
2682
+ ...base,
2683
+ diagnosticReason: "missing_agent_session_id"
2661
2684
  };
2662
2685
  }
2663
- const [orgSlug, projectSlug, extra] = projectReference.split("/");
2664
- if (!orgSlug || !projectSlug || extra) {
2665
- throw new CliError("CLI_USAGE", "--project must be a project ID or org/project slug.");
2666
- }
2667
- return await client.projects.resolve({
2668
- orgSlug,
2669
- projectSlug
2670
- });
2686
+ return {
2687
+ ...base,
2688
+ agentSessionId,
2689
+ agentSessionKey: agentSessionKey(base.source, agentSessionId)
2690
+ };
2671
2691
  }
2672
- async function resolveRequiredProject(config, client) {
2673
- return await resolveProjectReference(requireProjectId(config), client);
2692
+ function agentSessionKey(source, agentSessionId) {
2693
+ return `${source}:${stableHash(agentSessionId)}`;
2674
2694
  }
2675
- function configWithProject(config, project) {
2695
+ function defaultAgentLoopState() {
2676
2696
  return {
2677
- ...config,
2678
- projectId: project.id
2697
+ version: 2,
2698
+ sessions: {}
2679
2699
  };
2680
2700
  }
2681
- function formatProjectChoice(project, index) {
2682
- const slug = project.orgSlug && project.projectSlug ? `${project.orgSlug}/${project.projectSlug}` : project.id;
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
- async function selectProject(parsed, client, options) {
2687
- if (parsed.flags.project) {
2688
- return await resolveProjectReference(parsed.flags.project, client);
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 projects = await client.projects.list();
2691
- if (projects.length === 1) {
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
- const meta = commandMeta(parsed, config);
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 client = await createClient(config, options, false);
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: getCliWebBaseUrl(config, parsed, options)
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, config.server, poll.credential);
2766
- await mergeConfigFile(config.globalConfigPath, { server: config.server });
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: config.server,
3859
+ server: loginConfig.server,
3860
+ webBaseUrl: loginBaseUrl,
2774
3861
  credentialStored: true,
2775
- projectConfigured,
2776
- globalConfigPath: config.globalConfigPath,
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: humanCommandOutput(parsed.command.name, data),
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 project = await resolveRequiredProject(config, client);
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: humanCommandOutput(parsed.command.name, data),
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, commandMeta(parsed, outputConfig))),
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: humanCommandOutput(parsed.command.name, data),
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 project = await resolveRequiredProject(config, client);
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, resolvedConfig, client, options);
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: humanCommandOutput(parsed.command.name, data),
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 project = await resolveRequiredProject(config, client);
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: humanCommandOutput(parsed.command.name, data),
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" || parsed.command.name === "projects.use") {
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
  }