@trycadence/cli 0.1.8 → 0.1.10-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 +1261 -247
  2. package/package.json +1 -1
package/dist/cadence CHANGED
@@ -1511,7 +1511,7 @@ 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";
@@ -1522,9 +1522,17 @@ var sessionFileChangeKinds = ["added", "modified", "deleted", "renamed", "unknow
1522
1522
  var workLogEntryKinds = ["intent", "decision", "rationale", "action", "verification", "blocker", "correction", "note"];
1523
1523
  var workLogParentSelectors = ["last", "ticket-last", "session-last", "last-decision", "last-correction", "last-action"];
1524
1524
  var changesetPrNoteSources = ["agent", "human", "system"];
1525
+ var hookScopes = ["repo", "global", "both"];
1526
+ var agentEventSources = ["codex", "claude-code", "opencode", "openrouter", "unknown"];
1525
1527
  var defaultLeaseTtlSeconds = 5 * 60 * 60;
1526
1528
  var defaultCliApiBaseUrl = "https://cadenceapi.deploy.lvl8studios.com";
1527
1529
  var defaultCliWebBaseUrl = "https://cadence.deploy.lvl8studios.com";
1530
+ var defaultCheckpointThresholdMin = 3;
1531
+ var defaultCheckpointThresholdMax = 5;
1532
+ var defaultCheckpointCooldownSeconds = 10 * 60;
1533
+ var defaultCheckpointWorkerTimeoutMs = 10 * 60 * 1000;
1534
+ var defaultHookCommand = "cadence agent-run ingest-stop --source codex --event stop";
1535
+ var agentLoopSuppressEnv = "CADENCE_AGENT_EVENT_SUPPRESS";
1528
1536
  var credentialRefreshSkewMs = 60 * 1000;
1529
1537
  var credentialRefreshLockTimeoutMs = 10 * 1000;
1530
1538
  var credentialRefreshLockStaleMs = 60 * 1000;
@@ -1560,6 +1568,12 @@ var knownCommandPaths = [
1560
1568
  ["changesets", "notes", "get"],
1561
1569
  ["changesets", "notes", "put"],
1562
1570
  ["changesets", "notes", "apply"],
1571
+ ["agent-run", "ingest-stop"],
1572
+ ["agent-run", "closeout"],
1573
+ ["agent-run", "sweep"],
1574
+ ["agent-run", "doctor"],
1575
+ ["hooks", "doctor"],
1576
+ ["hooks", "install"],
1563
1577
  ["sessions", "start"],
1564
1578
  ["sessions", "end"],
1565
1579
  ["sessions", "files"],
@@ -1578,7 +1592,6 @@ var knownCommandPaths = [
1578
1592
  ["events", "list"],
1579
1593
  ["work", "overview"],
1580
1594
  ["projects", "list"],
1581
- ["projects", "use"],
1582
1595
  ["init"],
1583
1596
  ["status"],
1584
1597
  ["help"]
@@ -1827,39 +1840,6 @@ async function findRepoCadenceDirectory(cwd) {
1827
1840
  current = parent;
1828
1841
  }
1829
1842
  }
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
1843
  function getConfigHome(env) {
1864
1844
  return env.CADENCE_CONFIG_HOME ?? (env.HOME ? join(env.HOME, ".config", "cadence") : join(parse(process.cwd()).root, ".config", "cadence"));
1865
1845
  }
@@ -1944,6 +1924,77 @@ function isInteractive(options) {
1944
1924
  function getCliWebBaseUrl(config, parsed, options) {
1945
1925
  return parsed.options["web-base-url"] ?? options.env?.CADENCE_WEB_BASE_URL ?? process.env.CADENCE_WEB_BASE_URL ?? config.webBaseUrl;
1946
1926
  }
1927
+ function normalizeBaseUrl(value, label) {
1928
+ try {
1929
+ return new URL(value).toString().replace(/\/$/, "");
1930
+ } catch {
1931
+ throw new CliError("CLI_USAGE", `${label} must be a valid URL.`);
1932
+ }
1933
+ }
1934
+ function cliConfigDiscoveryUrl(webBaseUrl) {
1935
+ return new URL("/cli/config", webBaseUrl).toString();
1936
+ }
1937
+ function hasExplicitWebBaseUrl(parsed, options) {
1938
+ return Boolean(parsed.options["web-base-url"] ?? options.env?.CADENCE_WEB_BASE_URL ?? process.env.CADENCE_WEB_BASE_URL);
1939
+ }
1940
+ async function discoverApiBaseUrlFromWeb(webBaseUrl, options) {
1941
+ const configUrl = cliConfigDiscoveryUrl(webBaseUrl);
1942
+ const requestFetch = options.fetch ?? fetch;
1943
+ let response;
1944
+ try {
1945
+ response = await requestFetch(configUrl, {
1946
+ headers: {
1947
+ accept: "application/json"
1948
+ }
1949
+ });
1950
+ } catch (error) {
1951
+ throw new CliError("WEB_CONFIG_DISCOVERY_FAILED", "Could not discover Cadence API URL from the web app.", {
1952
+ webBaseUrl,
1953
+ configUrl,
1954
+ cause: error instanceof Error ? error.message : String(error)
1955
+ });
1956
+ }
1957
+ if (!response.ok) {
1958
+ throw new CliError("WEB_CONFIG_DISCOVERY_FAILED", "Could not discover Cadence API URL from the web app.", {
1959
+ webBaseUrl,
1960
+ configUrl,
1961
+ status: response.status
1962
+ });
1963
+ }
1964
+ const parsed = await response.json();
1965
+ const apiBaseUrl = parsed && typeof parsed === "object" ? parsed.apiBaseUrl : undefined;
1966
+ if (typeof apiBaseUrl !== "string") {
1967
+ throw new CliError("WEB_CONFIG_DISCOVERY_FAILED", "Cadence web app returned invalid CLI configuration.", {
1968
+ webBaseUrl,
1969
+ configUrl
1970
+ });
1971
+ }
1972
+ return normalizeBaseUrl(apiBaseUrl, "Discovered API base URL");
1973
+ }
1974
+ async function resolveAuthLoginConfig(config, parsed, options) {
1975
+ const webBaseUrl = normalizeBaseUrl(getCliWebBaseUrl(config, parsed, options), "--web-base-url");
1976
+ if (parsed.flags.server && !hasExplicitWebBaseUrl(parsed, options)) {
1977
+ return {
1978
+ ...config,
1979
+ webBaseUrl
1980
+ };
1981
+ }
1982
+ try {
1983
+ return {
1984
+ ...config,
1985
+ server: await discoverApiBaseUrlFromWeb(webBaseUrl, options),
1986
+ webBaseUrl
1987
+ };
1988
+ } catch (error) {
1989
+ if (hasExplicitWebBaseUrl(parsed, options)) {
1990
+ throw error;
1991
+ }
1992
+ return {
1993
+ ...config,
1994
+ webBaseUrl
1995
+ };
1996
+ }
1997
+ }
1947
1998
  function deriveWebBaseUrl(server) {
1948
1999
  try {
1949
2000
  const url = new URL(server);
@@ -2151,118 +2202,6 @@ function formatJson(envelope) {
2151
2202
  return `${JSON.stringify(envelope, null, 2)}
2152
2203
  `;
2153
2204
  }
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
2205
  function helpText() {
2267
2206
  return [
2268
2207
  "Cadence CLI",
@@ -2275,7 +2214,6 @@ function helpText() {
2275
2214
  " cadence actors ensure-workspace-agent --agent-kind <kind> [--workspace-name <name>] [--workspace-ref <ref>] [--display-name <name>] [--project <project-id>] [--json]",
2276
2215
  " cadence status [--project <project-id>] [--json]",
2277
2216
  " cadence projects list [--json]",
2278
- " cadence projects use <project-id|org/project> [--server <url>] [--json]",
2279
2217
  " cadence work overview [--project <project-id>] [--json]",
2280
2218
  " cadence tickets get <ticket-id> [--project <project-id>] [--json]",
2281
2219
  " cadence tickets list [--project <project-id>] [--status <status>] [--json]",
@@ -2300,10 +2238,16 @@ function helpText() {
2300
2238
  " cadence changesets notes get [--changeset <id>|--branch current|<branch>] [--project <project-id>] [--json]",
2301
2239
  " 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
2240
  " cadence changesets notes apply [--changeset <id>|--branch current|<branch>] --provider github --pr-number <n> --pr-url <url> [--project <project-id>] [--json]",
2241
+ " cadence agent-run ingest-stop --source <codex|claude-code|opencode|openrouter> [--event <event>] [--threshold <n>] [--dry-run true|false] [--project <project-id>] [--json]",
2242
+ " cadence agent-run closeout --agent-session-key <key> [--reason <threshold|idle|manual>] [--event-file <path>] [--log-kind <kind>] [--update-summary true|false] [--json]",
2243
+ " cadence agent-run sweep [--idle-after-seconds <n>] [--dry-run true|false] [--json]",
2244
+ " cadence agent-run doctor [--json]",
2245
+ " cadence hooks install --provider codex --scope <repo|global|both> [--command <command>] [--json]",
2246
+ " cadence hooks doctor --provider codex --scope <repo|global|both> [--json]",
2303
2247
  " cadence events list [--project <project-id>] [--ticket <ticket-id>] [--changeset <changeset-id>] [--session <session-id>] [--json]",
2304
2248
  "",
2305
2249
  "Global flags:",
2306
- " --project <id|org/project> Cadence project ID or org/project slug",
2250
+ " --project <id> Cadence project ID or org/project slug",
2307
2251
  " --server <url> Cadence API server override",
2308
2252
  " --json Print stable JSON envelope",
2309
2253
  "",
@@ -2553,12 +2497,578 @@ async function changesetLookupFromOptions(parsed, options) {
2553
2497
  const branchOption = parsed.options.branch ?? "current";
2554
2498
  const branchName = branchOption === "current" ? await resolveCurrentBranch(options) : branchOption;
2555
2499
  return {
2556
- branchName
2500
+ branchName
2501
+ };
2502
+ }
2503
+ async function readBodyFile(path, options) {
2504
+ const resolvedPath = isAbsolute(path) ? path : join(options.cwd ?? process.cwd(), path);
2505
+ return readFile(resolvedPath, "utf8");
2506
+ }
2507
+ function parseHookScope(value) {
2508
+ if (!value) {
2509
+ return "repo";
2510
+ }
2511
+ if (hookScopes.includes(value)) {
2512
+ return value;
2513
+ }
2514
+ throw new CliError("CLI_USAGE", "--scope must be one of repo, global, or both.");
2515
+ }
2516
+ function parseAgentEventSource(value) {
2517
+ if (!value) {
2518
+ return "unknown";
2519
+ }
2520
+ if (agentEventSources.includes(value)) {
2521
+ return value;
2522
+ }
2523
+ throw new CliError("CLI_USAGE", "--source must be one of codex, claude-code, opencode, openrouter, or unknown.");
2524
+ }
2525
+ function parseBooleanOption(value, defaultValue) {
2526
+ if (!value) {
2527
+ return defaultValue;
2528
+ }
2529
+ if (value === "true") {
2530
+ return true;
2531
+ }
2532
+ if (value === "false") {
2533
+ return false;
2534
+ }
2535
+ throw new CliError("CLI_USAGE", "Boolean options must be true or false.");
2536
+ }
2537
+ function truncateText(value, maxLength) {
2538
+ if (value.length <= maxLength) {
2539
+ return value;
2540
+ }
2541
+ return `${value.slice(0, maxLength - 15)}
2542
+ [truncated]`;
2543
+ }
2544
+ function stableHash(value) {
2545
+ return createHash("sha256").update(value).digest("hex");
2546
+ }
2547
+ async function readStdin(options) {
2548
+ if (options.readStdin) {
2549
+ return await options.readStdin();
2550
+ }
2551
+ return await Bun.stdin.text();
2552
+ }
2553
+ function tryParseJsonObject(source, label) {
2554
+ if (!source.trim()) {
2555
+ return {};
2556
+ }
2557
+ const parsed = JSON.parse(source);
2558
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
2559
+ throw new CliError("CLI_USAGE", `${label} must be a JSON object.`);
2560
+ }
2561
+ return parsed;
2562
+ }
2563
+ function readNestedString(record, path) {
2564
+ let value = record;
2565
+ for (const key of path) {
2566
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
2567
+ return;
2568
+ }
2569
+ value = value[key];
2570
+ }
2571
+ return typeof value === "string" ? value : undefined;
2572
+ }
2573
+ function firstString(record, paths) {
2574
+ for (const path of paths) {
2575
+ const value = readNestedString(record, path);
2576
+ if (value?.trim()) {
2577
+ return value.trim();
2578
+ }
2579
+ }
2580
+ return;
2581
+ }
2582
+ function normalizeAgentEvent(input, parsed, options) {
2583
+ const source = parseAgentEventSource(parsed.options.source);
2584
+ const event = parsed.options.event ?? "stop";
2585
+ const base = normalizeAgentEventBase(input, source, event, options);
2586
+ switch (source) {
2587
+ case "codex":
2588
+ return normalizeCodexAgentEvent(input, base);
2589
+ case "claude-code":
2590
+ case "opencode":
2591
+ case "openrouter":
2592
+ case "unknown":
2593
+ return normalizeGenericAgentEvent(input, base);
2594
+ }
2595
+ }
2596
+ function normalizeAgentEventBase(input, source, event, options) {
2597
+ const threadId = firstString(input, [["thread_id"], ["threadId"], ["conversation_id"], ["conversationId"]]);
2598
+ const turnId = firstString(input, [["turn_id"], ["turnId"], ["id"]]);
2599
+ const lastAssistantMessage = firstString(input, [
2600
+ ["last_assistant_message"],
2601
+ ["lastAssistantMessage"],
2602
+ ["assistant_response"],
2603
+ ["assistantResponse"],
2604
+ ["message", "content"],
2605
+ ["lastMessage", "content"]
2606
+ ]);
2607
+ return {
2608
+ source,
2609
+ event,
2610
+ workspacePath: options.cwd ?? process.cwd(),
2611
+ occurredAt: new Date().toISOString(),
2612
+ ...threadId ? { threadId } : {},
2613
+ ...turnId ? { turnId } : {},
2614
+ ...lastAssistantMessage ? { lastAssistantMessage: truncateText(lastAssistantMessage, 6000) } : {},
2615
+ payloadKeys: Object.keys(input).sort()
2616
+ };
2617
+ }
2618
+ function normalizeCodexAgentEvent(input, base) {
2619
+ const agentSessionId = firstString(input, [["session_id"], ["sessionId"], ["session", "id"]]);
2620
+ if (!agentSessionId) {
2621
+ return {
2622
+ ...base,
2623
+ diagnosticReason: "missing_agent_session_id"
2624
+ };
2625
+ }
2626
+ return {
2627
+ ...base,
2628
+ agentSessionId,
2629
+ agentSessionKey: agentSessionKey(base.source, agentSessionId)
2630
+ };
2631
+ }
2632
+ function normalizeGenericAgentEvent(input, base) {
2633
+ const agentSessionId = firstString(input, [
2634
+ ["agent_session_id"],
2635
+ ["agentSessionId"],
2636
+ ["session_id"],
2637
+ ["sessionId"],
2638
+ ["session", "id"]
2639
+ ]);
2640
+ if (!agentSessionId) {
2641
+ return {
2642
+ ...base,
2643
+ diagnosticReason: "missing_agent_session_id"
2644
+ };
2645
+ }
2646
+ return {
2647
+ ...base,
2648
+ agentSessionId,
2649
+ agentSessionKey: agentSessionKey(base.source, agentSessionId)
2650
+ };
2651
+ }
2652
+ function agentSessionKey(source, agentSessionId) {
2653
+ return `${source}:${stableHash(agentSessionId)}`;
2654
+ }
2655
+ function defaultAgentLoopState() {
2656
+ return {
2657
+ version: 2,
2658
+ sessions: {}
2659
+ };
2660
+ }
2661
+ function randomCheckpointThreshold() {
2662
+ return defaultCheckpointThresholdMin + Math.floor(Math.random() * (defaultCheckpointThresholdMax - defaultCheckpointThresholdMin + 1));
2663
+ }
2664
+ function agentLoopDirectory(parsed, options) {
2665
+ 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");
2666
+ }
2667
+ function agentLoopStatePath(parsed, options) {
2668
+ return join(agentLoopDirectory(parsed, options), "state.json");
2669
+ }
2670
+ function agentLoopLockPath(parsed, options, agentSessionKeyValue) {
2671
+ return join(agentLoopDirectory(parsed, options), agentSessionKeyValue ? `closeout-${stableHash(agentSessionKeyValue)}.lock` : "closeout.lock");
2672
+ }
2673
+ async function readAgentLoopState(parsed, options) {
2674
+ const filePath = agentLoopStatePath(parsed, options);
2675
+ const file = Bun.file(filePath);
2676
+ if (!await file.exists()) {
2677
+ return defaultAgentLoopState();
2678
+ }
2679
+ const parsedState = JSON.parse(await file.text());
2680
+ if (!parsedState || typeof parsedState !== "object" || Array.isArray(parsedState)) {
2681
+ return defaultAgentLoopState();
2682
+ }
2683
+ const record = parsedState;
2684
+ if (record.version === 2) {
2685
+ return {
2686
+ version: 2,
2687
+ sessions: readAgentLoopSessions(record.sessions),
2688
+ ...record.diagnostics && typeof record.diagnostics === "object" && !Array.isArray(record.diagnostics) ? { diagnostics: readAgentLoopDiagnostics(record.diagnostics) } : {}
2689
+ };
2690
+ }
2691
+ return {
2692
+ version: 2,
2693
+ sessions: {},
2694
+ diagnostics: {
2695
+ migratedLegacyRepoCounter: "stopCount" in record
2696
+ }
2697
+ };
2698
+ }
2699
+ async function writeAgentLoopState(parsed, options, state) {
2700
+ const filePath = agentLoopStatePath(parsed, options);
2701
+ await mkdir(dirname(filePath), { recursive: true });
2702
+ await writeFile(filePath, `${JSON.stringify(state, null, 2)}
2703
+ `);
2704
+ }
2705
+ function readAgentLoopSessions(rawSessions) {
2706
+ const sessions = {};
2707
+ if (!rawSessions || typeof rawSessions !== "object" || Array.isArray(rawSessions)) {
2708
+ return sessions;
2709
+ }
2710
+ for (const [key, rawSession] of Object.entries(rawSessions)) {
2711
+ if (!rawSession || typeof rawSession !== "object" || Array.isArray(rawSession)) {
2712
+ continue;
2713
+ }
2714
+ const record = rawSession;
2715
+ const source = parseAgentEventSource(typeof record.source === "string" ? record.source : undefined);
2716
+ const stopCount = typeof record.stopCount === "number" && Number.isInteger(record.stopCount) && record.stopCount >= 0 ? record.stopCount : 0;
2717
+ const threshold = typeof record.threshold === "number" && Number.isInteger(record.threshold) && record.threshold > 0 ? record.threshold : randomCheckpointThreshold();
2718
+ sessions[key] = {
2719
+ source,
2720
+ stopCount,
2721
+ threshold,
2722
+ ...typeof record.firstObservedAt === "string" ? { firstObservedAt: record.firstObservedAt } : {},
2723
+ ...typeof record.lastObservedAt === "string" ? { lastObservedAt: record.lastObservedAt } : {},
2724
+ ...typeof record.lastAction === "string" ? { lastAction: record.lastAction } : {},
2725
+ ...typeof record.lastReason === "string" ? { lastReason: record.lastReason } : {},
2726
+ ...typeof record.lastEventFile === "string" ? { lastEventFile: record.lastEventFile } : {},
2727
+ ...typeof record.lastAssistantMessage === "string" ? { lastAssistantMessage: record.lastAssistantMessage } : {},
2728
+ ...typeof record.lastCheckpointAt === "string" ? { lastCheckpointAt: record.lastCheckpointAt } : {},
2729
+ ...typeof record.lastCheckpointFingerprint === "string" ? { lastCheckpointFingerprint: record.lastCheckpointFingerprint } : {},
2730
+ ...typeof record.previousCheckpointSummary === "string" ? { previousCheckpointSummary: record.previousCheckpointSummary } : {}
2731
+ };
2732
+ }
2733
+ return sessions;
2734
+ }
2735
+ function readAgentLoopDiagnostics(record) {
2736
+ return {
2737
+ ...typeof record.missingSessionIdCount === "number" && Number.isInteger(record.missingSessionIdCount) && record.missingSessionIdCount > 0 ? { missingSessionIdCount: record.missingSessionIdCount } : {},
2738
+ ...typeof record.lastMissingSessionIdAt === "string" ? { lastMissingSessionIdAt: record.lastMissingSessionIdAt } : {},
2739
+ ...typeof record.lastMissingSessionIdSource === "string" ? { lastMissingSessionIdSource: parseAgentEventSource(record.lastMissingSessionIdSource) } : {},
2740
+ ...Array.isArray(record.lastMissingSessionIdPayloadKeys) ? { lastMissingSessionIdPayloadKeys: record.lastMissingSessionIdPayloadKeys.filter((key) => typeof key === "string") } : {},
2741
+ ...typeof record.migratedLegacyRepoCounter === "boolean" ? { migratedLegacyRepoCounter: record.migratedLegacyRepoCounter } : {}
2742
+ };
2743
+ }
2744
+ function recordMissingAgentSessionId(state, event) {
2745
+ return {
2746
+ ...state,
2747
+ diagnostics: {
2748
+ ...state.diagnostics ?? {},
2749
+ missingSessionIdCount: (state.diagnostics?.missingSessionIdCount ?? 0) + 1,
2750
+ lastMissingSessionIdAt: new Date().toISOString(),
2751
+ lastMissingSessionIdSource: event.source,
2752
+ lastMissingSessionIdPayloadKeys: event.payloadKeys
2753
+ }
2754
+ };
2755
+ }
2756
+ function clearAgentSessionReason(state) {
2757
+ const { lastReason: _lastReason, ...stateWithoutReason } = state;
2758
+ return stateWithoutReason;
2759
+ }
2760
+ async function writeAgentEventFile(parsed, options, event) {
2761
+ const eventsDirectory = join(agentLoopDirectory(parsed, options), "events");
2762
+ const fileName = `${new Date().toISOString().replace(/[:.]/g, "-")}-${randomUUID()}.json`;
2763
+ const filePath = join(eventsDirectory, fileName);
2764
+ await mkdir(eventsDirectory, { recursive: true });
2765
+ await writeFile(filePath, `${JSON.stringify(event, null, 2)}
2766
+ `);
2767
+ return filePath;
2768
+ }
2769
+ async function removeStaleAgentLoopLock(lockPath) {
2770
+ try {
2771
+ const lockStats = await stat(lockPath);
2772
+ if (Date.now() - lockStats.mtimeMs < defaultCheckpointWorkerTimeoutMs) {
2773
+ return false;
2774
+ }
2775
+ await rm(lockPath, { recursive: true, force: true });
2776
+ return true;
2777
+ } catch (error) {
2778
+ if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
2779
+ return false;
2780
+ }
2781
+ throw error;
2782
+ }
2783
+ }
2784
+ async function acquireAgentLoopLock(parsed, options, agentSessionKeyValue) {
2785
+ const lockPath = agentLoopLockPath(parsed, options, agentSessionKeyValue);
2786
+ await mkdir(dirname(lockPath), { recursive: true });
2787
+ try {
2788
+ await mkdir(lockPath);
2789
+ await writeFile(join(lockPath, "holder.json"), `${JSON.stringify({ pid: process.pid, acquiredAt: new Date().toISOString() }, null, 2)}
2790
+ `);
2791
+ return { acquired: true, lockPath };
2792
+ } catch (error) {
2793
+ if (!error || typeof error !== "object" || !("code" in error) || error.code !== "EEXIST") {
2794
+ throw error;
2795
+ }
2796
+ if (await removeStaleAgentLoopLock(lockPath)) {
2797
+ await mkdir(lockPath);
2798
+ await writeFile(join(lockPath, "holder.json"), `${JSON.stringify({ pid: process.pid, acquiredAt: new Date().toISOString(), recoveredStale: true }, null, 2)}
2799
+ `);
2800
+ return { acquired: true, lockPath };
2801
+ }
2802
+ return { acquired: false, lockPath };
2803
+ }
2804
+ }
2805
+ async function releaseAgentLoopLock(lockPath) {
2806
+ if (lockPath) {
2807
+ await rm(lockPath, { recursive: true, force: true });
2808
+ }
2809
+ }
2810
+ function runLocalCommand(command, args, options, commandOptions = {}) {
2811
+ if (options.runCommand) {
2812
+ return options.runCommand(command, args, commandOptions);
2813
+ }
2814
+ const result = spawnSync(command, [...args], {
2815
+ cwd: commandOptions.cwd,
2816
+ env: {
2817
+ ...process.env,
2818
+ ...commandOptions.env ?? {}
2819
+ },
2820
+ encoding: "utf8",
2821
+ timeout: commandOptions.timeoutMs,
2822
+ maxBuffer: 1024 * 1024
2823
+ });
2824
+ return {
2825
+ status: result.status,
2826
+ stdout: result.stdout ?? "",
2827
+ stderr: result.stderr ?? "",
2828
+ ...result.error ? { error: result.error } : {}
2829
+ };
2830
+ }
2831
+ function gitOutput(args, options) {
2832
+ const result = runLocalCommand("git", args, options, {
2833
+ cwd: options.cwd ?? process.cwd(),
2834
+ timeoutMs: 1e4
2835
+ });
2836
+ if (result.status !== 0) {
2837
+ return "";
2838
+ }
2839
+ return result.stdout.trim();
2840
+ }
2841
+ function currentCliWorkerInvocation() {
2842
+ const scriptPath = Bun.argv[1];
2843
+ if (scriptPath) {
2844
+ return {
2845
+ command: process.execPath,
2846
+ argsPrefix: [scriptPath]
2847
+ };
2848
+ }
2849
+ return {
2850
+ command: "cadence",
2851
+ argsPrefix: []
2852
+ };
2853
+ }
2854
+ async function spawnAgentRunCloseout(args, options) {
2855
+ const cwd = options.cwd ?? process.cwd();
2856
+ const invocation = currentCliWorkerInvocation();
2857
+ const env = {
2858
+ ...process.env,
2859
+ [agentLoopSuppressEnv]: "1"
2860
+ };
2861
+ if (options.spawnDetached) {
2862
+ await options.spawnDetached(invocation.command, [...invocation.argsPrefix, ...args], { cwd, env });
2863
+ return;
2864
+ }
2865
+ const child = spawn(invocation.command, [...invocation.argsPrefix, ...args], {
2866
+ cwd,
2867
+ env,
2868
+ detached: true,
2869
+ stdio: "ignore"
2870
+ });
2871
+ child.unref();
2872
+ }
2873
+ function activeSessionFromCurrent(current) {
2874
+ const record = current && typeof current === "object" ? current : null;
2875
+ const sessions = Array.isArray(record?.sessions) ? record.sessions : [];
2876
+ const activeSession = sessions.find((session) => {
2877
+ if (!session || typeof session !== "object") {
2878
+ return false;
2879
+ }
2880
+ const sessionRecord2 = session;
2881
+ return sessionRecord2.status === "active" && typeof sessionRecord2.ticketId === "string";
2882
+ });
2883
+ if (activeSession && typeof activeSession === "object") {
2884
+ return activeSession;
2885
+ }
2886
+ const activeLeases = Array.isArray(record?.activeLeases) ? record.activeLeases : Array.isArray(record?.leases) ? record.leases.filter((lease) => lease && typeof lease === "object" && lease.status === "active") : [];
2887
+ const activeLease = activeLeases.find((lease) => {
2888
+ if (!lease || typeof lease !== "object") {
2889
+ return false;
2890
+ }
2891
+ const leaseRecord2 = lease;
2892
+ return typeof leaseRecord2.ticketId === "string";
2893
+ });
2894
+ if (!activeLease || typeof activeLease !== "object") {
2895
+ return null;
2896
+ }
2897
+ const leaseRecord = activeLease;
2898
+ const sessionId = typeof leaseRecord.sessionId === "string" ? leaseRecord.sessionId : undefined;
2899
+ const matchingSession = sessions.find((session) => {
2900
+ if (!session || typeof session !== "object") {
2901
+ return false;
2902
+ }
2903
+ return sessionId && session.id === sessionId;
2904
+ });
2905
+ const sessionRecord = matchingSession && typeof matchingSession === "object" ? matchingSession : {};
2906
+ return {
2907
+ ...sessionRecord,
2908
+ ...sessionId ? { id: sessionId } : {},
2909
+ ticketId: leaseRecord.ticketId,
2910
+ ...typeof leaseRecord.changesetId === "string" ? { changesetId: leaseRecord.changesetId } : typeof sessionRecord.changesetId === "string" ? { changesetId: sessionRecord.changesetId } : {},
2911
+ status: "active"
2912
+ };
2913
+ }
2914
+ function fingerprintForCheckpoint(event, options) {
2915
+ const status = gitOutput(["status", "--short"], options);
2916
+ const changedFiles = gitOutput(["diff", "--name-only", "origin/dev..."], options);
2917
+ return stableHash(JSON.stringify({
2918
+ source: event.source,
2919
+ event: event.event,
2920
+ lastAssistantMessage: event.lastAssistantMessage ?? "",
2921
+ status,
2922
+ changedFiles
2923
+ }));
2924
+ }
2925
+ function shouldSkipForCooldown(state, cooldownSeconds) {
2926
+ if (!state.lastCheckpointAt) {
2927
+ return false;
2928
+ }
2929
+ const lastCheckpointMs = new Date(state.lastCheckpointAt).getTime();
2930
+ return !Number.isNaN(lastCheckpointMs) && Date.now() - lastCheckpointMs < cooldownSeconds * 1000;
2931
+ }
2932
+ function synthesizeAgentEventFromSession(agentSessionKeyValue, session, options) {
2933
+ return {
2934
+ source: session.source,
2935
+ event: "closeout",
2936
+ workspacePath: options.cwd ?? process.cwd(),
2937
+ occurredAt: new Date().toISOString(),
2938
+ agentSessionKey: agentSessionKeyValue,
2939
+ ...session.lastAssistantMessage ? { lastAssistantMessage: session.lastAssistantMessage } : {},
2940
+ payloadKeys: []
2941
+ };
2942
+ }
2943
+ function parseCheckpointJson(raw) {
2944
+ const trimmed = raw.trim();
2945
+ try {
2946
+ const parsed = JSON.parse(trimmed);
2947
+ if (parsed && typeof parsed === "object") {
2948
+ const record = parsed;
2949
+ return {
2950
+ body: typeof record.body === "string" && record.body.trim() ? record.body.trim() : trimmed,
2951
+ summary: typeof record.summary === "string" && record.summary.trim() ? record.summary.trim() : undefined
2952
+ };
2953
+ }
2954
+ } catch {
2955
+ const start = trimmed.indexOf("{");
2956
+ const end = trimmed.lastIndexOf("}");
2957
+ if (start >= 0 && end > start) {
2958
+ try {
2959
+ const parsed = JSON.parse(trimmed.slice(start, end + 1));
2960
+ if (parsed && typeof parsed === "object") {
2961
+ const record = parsed;
2962
+ return {
2963
+ body: typeof record.body === "string" && record.body.trim() ? record.body.trim() : trimmed,
2964
+ summary: typeof record.summary === "string" && record.summary.trim() ? record.summary.trim() : undefined
2965
+ };
2966
+ }
2967
+ } catch {}
2968
+ }
2969
+ }
2970
+ return {
2971
+ body: trimmed,
2972
+ summary: undefined
2973
+ };
2974
+ }
2975
+ function buildCheckpointPrompt(input) {
2976
+ return [
2977
+ "You are generating a concise Cadence checkpoint for an active coding session.",
2978
+ "Do not include raw diffs, raw transcripts, terminal logs, tool streams, secrets, or model reasoning.",
2979
+ 'Return JSON only with this shape: {"summary":"one short current work summary","body":"checkpoint body suitable for a Cadence ticket work log"}.',
2980
+ "The body should mention what changed, decisions made, verification if known, and the next risk or next step. Keep it under 1200 characters.",
2981
+ "",
2982
+ `Ticket: ${input.ticketId}`,
2983
+ input.sessionId ? `Session: ${input.sessionId}` : "",
2984
+ input.changesetId ? `ChangeSet: ${input.changesetId}` : "",
2985
+ `Agent event: ${input.event.source}/${input.event.event}`,
2986
+ input.previousCheckpointSummary ? `Previous checkpoint summary: ${input.previousCheckpointSummary}` : "",
2987
+ input.event.lastAssistantMessage ? `Last assistant message:
2988
+ ${truncateText(input.event.lastAssistantMessage, 3000)}` : "Last assistant message: unavailable",
2989
+ input.gitStatus ? `Git status --short:
2990
+ ${truncateText(input.gitStatus, 2000)}` : "Git status --short: clean or unavailable",
2991
+ input.gitDiffStat ? `Git diff --stat origin/dev...:
2992
+ ${truncateText(input.gitDiffStat, 2000)}` : "Git diff stat: unavailable",
2993
+ input.changedFiles ? `Changed files:
2994
+ ${truncateText(input.changedFiles, 2000)}` : "Changed files: unavailable"
2995
+ ].filter(Boolean).join(`
2996
+ `);
2997
+ }
2998
+ async function findRepoRoot(cwd) {
2999
+ const result = spawnSync("git", ["rev-parse", "--show-toplevel"], {
3000
+ cwd,
3001
+ encoding: "utf8"
3002
+ });
3003
+ if (result.status === 0 && result.stdout.trim()) {
3004
+ return result.stdout.trim();
3005
+ }
3006
+ const cadenceDirectory = await findRepoCadenceDirectory(cwd);
3007
+ return cadenceDirectory ? dirname(cadenceDirectory) : cwd;
3008
+ }
3009
+ function codexHookEntry(command) {
3010
+ return {
3011
+ hooks: [
3012
+ {
3013
+ type: "command",
3014
+ command,
3015
+ timeout: 5,
3016
+ statusMessage: "Checking Cadence checkpoint"
3017
+ }
3018
+ ]
3019
+ };
3020
+ }
3021
+ async function readJsonObjectFile(filePath) {
3022
+ const file = Bun.file(filePath);
3023
+ if (!await file.exists()) {
3024
+ return {};
3025
+ }
3026
+ return tryParseJsonObject(await file.text(), filePath);
3027
+ }
3028
+ async function installCodexHookFile(filePath, command) {
3029
+ const existing = await readJsonObjectFile(filePath);
3030
+ const hooksValue = existing.hooks;
3031
+ const hooks = hooksValue && typeof hooksValue === "object" && !Array.isArray(hooksValue) ? hooksValue : {};
3032
+ const stopValue = hooks.Stop;
3033
+ const stopHooks = Array.isArray(stopValue) ? stopValue : [];
3034
+ const alreadyInstalled = stopHooks.some((entry) => JSON.stringify(entry).includes(command));
3035
+ const nextStopHooks = alreadyInstalled ? stopHooks : [...stopHooks, codexHookEntry(command)];
3036
+ const nextConfig = {
3037
+ ...existing,
3038
+ hooks: {
3039
+ ...hooks,
3040
+ Stop: nextStopHooks
3041
+ }
3042
+ };
3043
+ await mkdir(dirname(filePath), { recursive: true });
3044
+ await writeFile(filePath, `${JSON.stringify(nextConfig, null, 2)}
3045
+ `);
3046
+ return {
3047
+ path: filePath,
3048
+ installed: !alreadyInstalled,
3049
+ alreadyInstalled
2557
3050
  };
2558
3051
  }
2559
- async function readBodyFile(path, options) {
2560
- const resolvedPath = isAbsolute(path) ? path : join(options.cwd ?? process.cwd(), path);
2561
- return readFile(resolvedPath, "utf8");
3052
+ async function codexHookPaths(scope, options) {
3053
+ const cwd = options.cwd ?? process.cwd();
3054
+ const paths = [];
3055
+ if (scope === "repo" || scope === "both") {
3056
+ paths.push(join(await findRepoRoot(cwd), ".codex", "hooks.json"));
3057
+ }
3058
+ if (scope === "global" || scope === "both") {
3059
+ const home = options.env?.HOME ?? process.env.HOME;
3060
+ if (!home) {
3061
+ throw new CliError("CONFIG_ERROR", "HOME is required for global hook installation.");
3062
+ }
3063
+ paths.push(join(home, ".codex", "hooks.json"));
3064
+ }
3065
+ return paths;
3066
+ }
3067
+ async function codexHookInstalled(filePath, command) {
3068
+ const existing = await readJsonObjectFile(filePath);
3069
+ const hooks = existing.hooks;
3070
+ const stopHooks = hooks && typeof hooks === "object" && !Array.isArray(hooks) && Array.isArray(hooks.Stop) ? hooks.Stop : [];
3071
+ return stopHooks.some((entry) => JSON.stringify(entry).includes(command));
2562
3072
  }
2563
3073
  async function runStatus(parsed, options) {
2564
3074
  const config = await resolveCliConfig(parsed.flags, options);
@@ -2589,21 +3099,8 @@ async function runStatus(parsed, options) {
2589
3099
  exitCode: 0
2590
3100
  };
2591
3101
  }
2592
- const projectLabel = config.projectId ?? "not configured";
2593
- const profileLabel = config.profile ?? "default";
2594
- const credentialLabel = credential ? "configured" : "not configured";
2595
3102
  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
- `) + `
3103
+ stdout: `Cadence API ${health.ok ? "ok" : "unavailable"} at ${config.server}
2607
3104
  `,
2608
3105
  stderr: "",
2609
3106
  exitCode: 0
@@ -2611,7 +3108,7 @@ async function runStatus(parsed, options) {
2611
3108
  }
2612
3109
  async function runInit(parsed, options) {
2613
3110
  const cwd = options.cwd ?? process.cwd();
2614
- const repoConfigPath = await repoConfigPathForWrite(options);
3111
+ const repoConfigPath = join(cwd, ".cadence", "config.json");
2615
3112
  const config = await resolveCliConfig(parsed.flags, {
2616
3113
  ...options,
2617
3114
  cwd
@@ -2669,15 +3166,6 @@ async function resolveProjectReference(projectReference, client) {
2669
3166
  projectSlug
2670
3167
  });
2671
3168
  }
2672
- async function resolveRequiredProject(config, client) {
2673
- return await resolveProjectReference(requireProjectId(config), client);
2674
- }
2675
- function configWithProject(config, project) {
2676
- return {
2677
- ...config,
2678
- projectId: project.id
2679
- };
2680
- }
2681
3169
  function formatProjectChoice(project, index) {
2682
3170
  const slug = project.orgSlug && project.projectSlug ? `${project.orgSlug}/${project.projectSlug}` : project.id;
2683
3171
  const name = project.name ? ` ${project.name}` : "";
@@ -2720,17 +3208,579 @@ async function selectProject(parsed, client, options) {
2720
3208
  }
2721
3209
  return matching;
2722
3210
  }
3211
+ async function runAgentRunCommand(parsed, options) {
3212
+ const config = await resolveCliConfig(parsed.flags, options);
3213
+ const projectId = config.projectId;
3214
+ const meta = commandMeta(parsed, config);
3215
+ switch (parsed.command.name) {
3216
+ case "agent-run.ingest-stop":
3217
+ return await runAgentRunIngestStop(parsed, options, config, meta);
3218
+ case "agent-run.closeout":
3219
+ return await runAgentRunCloseout(parsed, options, config, meta);
3220
+ case "agent-run.sweep":
3221
+ return await runAgentRunSweep(parsed, options, meta);
3222
+ case "agent-run.doctor": {
3223
+ const state = await readAgentLoopState(parsed, options);
3224
+ const data = {
3225
+ action: "doctor",
3226
+ state,
3227
+ sessionCount: Object.keys(state.sessions).length,
3228
+ pendingSessions: Object.entries(state.sessions).filter(([, session]) => session.stopCount > 0).map(([agentSessionKeyValue, session]) => ({
3229
+ agentSessionKey: agentSessionKeyValue,
3230
+ source: session.source,
3231
+ stopCount: session.stopCount,
3232
+ threshold: session.threshold,
3233
+ lastObservedAt: session.lastObservedAt,
3234
+ lastAction: session.lastAction
3235
+ }))
3236
+ };
3237
+ return {
3238
+ stdout: parsed.flags.json ? formatJson(successEnvelope(data, meta)) : `${JSON.stringify(data, null, 2)}
3239
+ `,
3240
+ stderr: "",
3241
+ exitCode: 0
3242
+ };
3243
+ }
3244
+ default:
3245
+ throw new CliError("CLI_USAGE", `Unknown command: ${parsed.command.path.join(" ")}`);
3246
+ }
3247
+ }
3248
+ async function runAgentRunIngestStop(parsed, options, config, meta) {
3249
+ 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";
3250
+ if (suppressed) {
3251
+ const data2 = {
3252
+ action: "skipped",
3253
+ reason: "suppressed"
3254
+ };
3255
+ return {
3256
+ stdout: parsed.flags.json ? formatJson(successEnvelope(data2, meta)) : `${JSON.stringify(data2, null, 2)}
3257
+ `,
3258
+ stderr: "",
3259
+ exitCode: 0
3260
+ };
3261
+ }
3262
+ const rawInput = await readStdin(options);
3263
+ const normalized = normalizeAgentEvent(tryParseJsonObject(rawInput, "agent event input"), parsed, options);
3264
+ const projectId = config.projectId;
3265
+ if (!projectId) {
3266
+ const data2 = {
3267
+ action: "skipped",
3268
+ reason: "no_project",
3269
+ event: normalized
3270
+ };
3271
+ return {
3272
+ stdout: parsed.flags.json ? formatJson(successEnvelope(data2, meta)) : `${JSON.stringify(data2, null, 2)}
3273
+ `,
3274
+ stderr: "",
3275
+ exitCode: 0
3276
+ };
3277
+ }
3278
+ const state = await readAgentLoopState(parsed, options);
3279
+ if (!normalized.agentSessionKey) {
3280
+ const nextState = recordMissingAgentSessionId(state, normalized);
3281
+ await writeAgentLoopState(parsed, options, nextState);
3282
+ const data2 = {
3283
+ action: "skipped",
3284
+ reason: normalized.diagnosticReason ?? "missing_agent_session_id",
3285
+ event: normalized,
3286
+ diagnostics: nextState.diagnostics
3287
+ };
3288
+ return {
3289
+ stdout: parsed.flags.json ? formatJson(successEnvelope(data2, meta)) : `${JSON.stringify(data2, null, 2)}
3290
+ `,
3291
+ stderr: "",
3292
+ exitCode: 0
3293
+ };
3294
+ }
3295
+ const forcedThreshold = parsePositiveInteger(parsed.options.threshold, "--threshold");
3296
+ const cooldownSeconds = parsePositiveInteger(parsed.options["cooldown-seconds"], "--cooldown-seconds") ?? defaultCheckpointCooldownSeconds;
3297
+ const dryRun = parseBooleanOption(parsed.options["dry-run"], false);
3298
+ const existingSession = state.sessions[normalized.agentSessionKey];
3299
+ const now = new Date().toISOString();
3300
+ const threshold = forcedThreshold ?? existingSession?.threshold ?? randomCheckpointThreshold();
3301
+ const nextCount = (existingSession?.stopCount ?? 0) + 1;
3302
+ const observedSession = {
3303
+ ...clearAgentSessionReason(existingSession ?? {
3304
+ source: normalized.source,
3305
+ stopCount: 0,
3306
+ threshold
3307
+ }),
3308
+ source: normalized.source,
3309
+ stopCount: nextCount,
3310
+ threshold,
3311
+ firstObservedAt: existingSession?.firstObservedAt ?? now,
3312
+ lastObservedAt: now,
3313
+ lastAction: "counted",
3314
+ ...normalized.lastAssistantMessage ? { lastAssistantMessage: normalized.lastAssistantMessage } : {}
3315
+ };
3316
+ const countedState = {
3317
+ ...state,
3318
+ sessions: {
3319
+ ...state.sessions,
3320
+ [normalized.agentSessionKey]: observedSession
3321
+ }
3322
+ };
3323
+ if (nextCount < threshold) {
3324
+ await writeAgentLoopState(parsed, options, countedState);
3325
+ const data2 = {
3326
+ action: "counted",
3327
+ agentSessionKey: normalized.agentSessionKey,
3328
+ stopCount: nextCount,
3329
+ threshold
3330
+ };
3331
+ return {
3332
+ stdout: parsed.flags.json ? formatJson(successEnvelope(data2, meta)) : `${JSON.stringify(data2, null, 2)}
3333
+ `,
3334
+ stderr: "",
3335
+ exitCode: 0
3336
+ };
3337
+ }
3338
+ if (shouldSkipForCooldown(observedSession, cooldownSeconds)) {
3339
+ await writeAgentLoopState(parsed, options, {
3340
+ ...state,
3341
+ sessions: {
3342
+ ...state.sessions,
3343
+ [normalized.agentSessionKey]: {
3344
+ ...observedSession,
3345
+ lastAction: "skipped",
3346
+ lastReason: "cooldown"
3347
+ }
3348
+ }
3349
+ });
3350
+ const data2 = {
3351
+ action: "skipped",
3352
+ reason: "cooldown",
3353
+ agentSessionKey: normalized.agentSessionKey,
3354
+ stopCount: nextCount,
3355
+ threshold
3356
+ };
3357
+ return {
3358
+ stdout: parsed.flags.json ? formatJson(successEnvelope(data2, meta)) : `${JSON.stringify(data2, null, 2)}
3359
+ `,
3360
+ stderr: "",
3361
+ exitCode: 0
3362
+ };
3363
+ }
3364
+ const fingerprint = fingerprintForCheckpoint(normalized, options);
3365
+ if (observedSession.lastCheckpointFingerprint && observedSession.lastCheckpointFingerprint === fingerprint) {
3366
+ await writeAgentLoopState(parsed, options, {
3367
+ ...state,
3368
+ sessions: {
3369
+ ...state.sessions,
3370
+ [normalized.agentSessionKey]: {
3371
+ ...observedSession,
3372
+ stopCount: 0,
3373
+ threshold: randomCheckpointThreshold(),
3374
+ lastAction: "skipped",
3375
+ lastReason: "unchanged"
3376
+ }
3377
+ }
3378
+ });
3379
+ const data2 = {
3380
+ action: "skipped",
3381
+ reason: "unchanged",
3382
+ agentSessionKey: normalized.agentSessionKey
3383
+ };
3384
+ return {
3385
+ stdout: parsed.flags.json ? formatJson(successEnvelope(data2, meta)) : `${JSON.stringify(data2, null, 2)}
3386
+ `,
3387
+ stderr: "",
3388
+ exitCode: 0
3389
+ };
3390
+ }
3391
+ const eventFile = await writeAgentEventFile(parsed, options, normalized);
3392
+ const lockPath = agentLoopLockPath(parsed, options, normalized.agentSessionKey);
3393
+ const workerArgs = [
3394
+ "agent-run",
3395
+ "closeout",
3396
+ "--agent-session-key",
3397
+ normalized.agentSessionKey,
3398
+ "--reason",
3399
+ "threshold",
3400
+ "--event-file",
3401
+ eventFile,
3402
+ "--lock",
3403
+ lockPath,
3404
+ ...parsed.options["log-kind"] ? ["--log-kind", parsed.options["log-kind"]] : [],
3405
+ ...parsed.options["update-summary"] ? ["--update-summary", parsed.options["update-summary"]] : [],
3406
+ ...parsed.flags.project ? ["--project", parsed.flags.project] : [],
3407
+ ...parsed.flags.server ? ["--server", parsed.flags.server] : []
3408
+ ];
3409
+ if (dryRun) {
3410
+ await writeAgentLoopState(parsed, options, {
3411
+ ...state,
3412
+ sessions: {
3413
+ ...state.sessions,
3414
+ [normalized.agentSessionKey]: {
3415
+ ...observedSession,
3416
+ lastAction: "would_spawn",
3417
+ lastEventFile: eventFile
3418
+ }
3419
+ }
3420
+ });
3421
+ const data2 = {
3422
+ action: "would_spawn",
3423
+ agentSessionKey: normalized.agentSessionKey,
3424
+ eventFile,
3425
+ workerArgs
3426
+ };
3427
+ return {
3428
+ stdout: parsed.flags.json ? formatJson(successEnvelope(data2, meta)) : `${JSON.stringify(data2, null, 2)}
3429
+ `,
3430
+ stderr: "",
3431
+ exitCode: 0
3432
+ };
3433
+ }
3434
+ const lock = await acquireAgentLoopLock(parsed, options, normalized.agentSessionKey);
3435
+ if (!lock.acquired) {
3436
+ await writeAgentLoopState(parsed, options, {
3437
+ ...state,
3438
+ sessions: {
3439
+ ...state.sessions,
3440
+ [normalized.agentSessionKey]: {
3441
+ ...observedSession,
3442
+ lastAction: "skipped",
3443
+ lastReason: "lock_held"
3444
+ }
3445
+ }
3446
+ });
3447
+ const data2 = {
3448
+ action: "skipped",
3449
+ reason: "lock_held",
3450
+ lockPath: lock.lockPath,
3451
+ agentSessionKey: normalized.agentSessionKey
3452
+ };
3453
+ return {
3454
+ stdout: parsed.flags.json ? formatJson(successEnvelope(data2, meta)) : `${JSON.stringify(data2, null, 2)}
3455
+ `,
3456
+ stderr: "",
3457
+ exitCode: 0
3458
+ };
3459
+ }
3460
+ try {
3461
+ await spawnAgentRunCloseout(workerArgs, options);
3462
+ } catch (error) {
3463
+ await releaseAgentLoopLock(lock.lockPath);
3464
+ throw error;
3465
+ }
3466
+ await writeAgentLoopState(parsed, options, {
3467
+ ...state,
3468
+ sessions: {
3469
+ ...state.sessions,
3470
+ [normalized.agentSessionKey]: {
3471
+ ...observedSession,
3472
+ stopCount: 0,
3473
+ threshold: randomCheckpointThreshold(),
3474
+ lastAction: "spawned",
3475
+ lastEventFile: eventFile,
3476
+ lastCheckpointAt: new Date().toISOString(),
3477
+ lastCheckpointFingerprint: fingerprint
3478
+ }
3479
+ }
3480
+ });
3481
+ const data = {
3482
+ action: "spawned",
3483
+ agentSessionKey: normalized.agentSessionKey,
3484
+ eventFile,
3485
+ lockPath: lock.lockPath
3486
+ };
3487
+ return {
3488
+ stdout: parsed.flags.json ? formatJson(successEnvelope(data, meta)) : `${JSON.stringify(data, null, 2)}
3489
+ `,
3490
+ stderr: "",
3491
+ exitCode: 0
3492
+ };
3493
+ }
3494
+ async function runAgentRunCloseout(parsed, options, config, meta) {
3495
+ const projectId = requireProjectId(config);
3496
+ const client = await createClient(config, options);
3497
+ const agentSessionKeyValue = requireOption(parsed, "agent-session-key");
3498
+ const state = await readAgentLoopState(parsed, options);
3499
+ const sessionState = state.sessions[agentSessionKeyValue];
3500
+ if (!sessionState) {
3501
+ throw new CliError("AGENT_RUN_SESSION_NOT_FOUND", "No local agent-run session state exists for --agent-session-key.", {
3502
+ agentSessionKey: agentSessionKeyValue
3503
+ });
3504
+ }
3505
+ const current = await client.sessions.current({
3506
+ projectId,
3507
+ filters: {
3508
+ limit: 100
3509
+ }
3510
+ });
3511
+ const currentSession = activeSessionFromCurrent(current);
3512
+ const ticketId = parsed.options.ticket ?? (currentSession && typeof currentSession.ticketId === "string" ? currentSession.ticketId : undefined);
3513
+ const sessionId = parsed.options.session ?? (currentSession && typeof currentSession.id === "string" ? currentSession.id : undefined);
3514
+ const changesetId = parsed.options.changeset ?? (currentSession && typeof currentSession.changesetId === "string" ? currentSession.changesetId : undefined);
3515
+ if (!ticketId) {
3516
+ await writeAgentLoopState(parsed, options, {
3517
+ ...state,
3518
+ sessions: {
3519
+ ...state.sessions,
3520
+ [agentSessionKeyValue]: {
3521
+ ...sessionState,
3522
+ lastAction: "skipped",
3523
+ lastReason: "no_active_ticket"
3524
+ }
3525
+ }
3526
+ });
3527
+ const data = {
3528
+ action: "skipped",
3529
+ reason: "no_active_ticket",
3530
+ agentSessionKey: agentSessionKeyValue
3531
+ };
3532
+ return {
3533
+ stdout: parsed.flags.json ? formatJson(successEnvelope(data, meta)) : `${JSON.stringify(data, null, 2)}
3534
+ `,
3535
+ stderr: "",
3536
+ exitCode: 0
3537
+ };
3538
+ }
3539
+ const eventFile = parsed.options["event-file"] ?? sessionState.lastEventFile;
3540
+ const event = eventFile ? tryParseJsonObject(await readFile(eventFile, "utf8"), eventFile) : synthesizeAgentEventFromSession(agentSessionKeyValue, sessionState, options);
3541
+ const logKind = parseWorkLogEntryKind(parsed.options["log-kind"] ?? "note");
3542
+ const updateSummary = parseBooleanOption(parsed.options["update-summary"], false);
3543
+ const dryRun = parseBooleanOption(parsed.options["dry-run"], false);
3544
+ const lockPath = parsed.options.lock;
3545
+ const gitStatus = gitOutput(["status", "--short"], options);
3546
+ const gitDiffStat = gitOutput(["diff", "--stat", "origin/dev..."], options);
3547
+ const changedFiles = gitOutput(["diff", "--name-only", "origin/dev..."], options);
3548
+ const prompt = buildCheckpointPrompt({
3549
+ event,
3550
+ ticketId,
3551
+ ...sessionId ? { sessionId } : {},
3552
+ ...changesetId ? { changesetId } : {},
3553
+ gitStatus,
3554
+ gitDiffStat,
3555
+ changedFiles,
3556
+ ...sessionState.previousCheckpointSummary ? { previousCheckpointSummary: sessionState.previousCheckpointSummary } : {}
3557
+ });
3558
+ if (dryRun) {
3559
+ const data = {
3560
+ action: "would_closeout",
3561
+ prompt,
3562
+ ticketId,
3563
+ agentSessionKey: agentSessionKeyValue,
3564
+ ...sessionId ? { sessionId } : {},
3565
+ ...changesetId ? { changesetId } : {}
3566
+ };
3567
+ return {
3568
+ stdout: parsed.flags.json ? formatJson(successEnvelope(data, meta)) : `${JSON.stringify(data, null, 2)}
3569
+ `,
3570
+ stderr: "",
3571
+ exitCode: 0
3572
+ };
3573
+ }
3574
+ try {
3575
+ const codexCommand = parsed.options["codex-command"] ?? "codex";
3576
+ const codex = runLocalCommand(codexCommand, ["exec", "--disable", "hooks", "--sandbox", "read-only", "-C", options.cwd ?? process.cwd(), prompt], options, {
3577
+ cwd: options.cwd ?? process.cwd(),
3578
+ env: {
3579
+ [agentLoopSuppressEnv]: "1",
3580
+ CADENCE_HOOK_SUPPRESS: "1"
3581
+ },
3582
+ timeoutMs: defaultCheckpointWorkerTimeoutMs
3583
+ });
3584
+ if (codex.status !== 0 || codex.error) {
3585
+ throw new CliError("AGENT_RUN_CLOSEOUT_FAILED", "Codex closeout generation failed.", {
3586
+ status: codex.status,
3587
+ stderr: truncateText(codex.stderr, 2000),
3588
+ error: codex.error?.message
3589
+ });
3590
+ }
3591
+ const checkpoint = parseCheckpointJson(codex.stdout);
3592
+ const body = checkpoint.body.trim();
3593
+ const summary = checkpoint.summary ?? truncateText(body.replace(/\s+/g, " "), 500);
3594
+ if (!body) {
3595
+ throw new CliError("AGENT_RUN_CLOSEOUT_FAILED", "Codex closeout generation returned an empty checkpoint.");
3596
+ }
3597
+ await client.tickets.log({
3598
+ projectId,
3599
+ ticketId,
3600
+ entry: {
3601
+ entryKind: logKind,
3602
+ body,
3603
+ summary,
3604
+ ...sessionId ? { sessionId } : {},
3605
+ ...changesetId ? { changesetId } : {},
3606
+ ...commandMetadata()
3607
+ }
3608
+ });
3609
+ if (updateSummary) {
3610
+ const ticket = await client.tickets.get({ projectId, ticketId });
3611
+ if (typeof ticket.projectionVersion === "number") {
3612
+ await client.tickets.update({
3613
+ projectId,
3614
+ ticketId,
3615
+ ifVersion: ticket.projectionVersion,
3616
+ ticket: {
3617
+ currentSummary: summary,
3618
+ ...commandMetadata()
3619
+ }
3620
+ });
3621
+ }
3622
+ }
3623
+ await writeAgentLoopState(parsed, options, {
3624
+ ...state,
3625
+ sessions: {
3626
+ ...state.sessions,
3627
+ [agentSessionKeyValue]: {
3628
+ ...sessionState,
3629
+ previousCheckpointSummary: summary,
3630
+ lastAction: "closed_out",
3631
+ ...eventFile ? { lastEventFile: eventFile } : {}
3632
+ }
3633
+ }
3634
+ });
3635
+ const data = {
3636
+ action: "closed_out",
3637
+ ticketId,
3638
+ summary,
3639
+ agentSessionKey: agentSessionKeyValue,
3640
+ ...sessionId ? { sessionId } : {},
3641
+ ...changesetId ? { changesetId } : {}
3642
+ };
3643
+ return {
3644
+ stdout: parsed.flags.json ? formatJson(successEnvelope(data, meta)) : `${JSON.stringify(data, null, 2)}
3645
+ `,
3646
+ stderr: "",
3647
+ exitCode: 0
3648
+ };
3649
+ } finally {
3650
+ await releaseAgentLoopLock(lockPath);
3651
+ }
3652
+ }
3653
+ async function runAgentRunSweep(parsed, options, meta) {
3654
+ const state = await readAgentLoopState(parsed, options);
3655
+ const idleAfterSeconds = parsePositiveInteger(parsed.options["idle-after-seconds"], "--idle-after-seconds") ?? 5 * 60;
3656
+ const dryRun = parseBooleanOption(parsed.options["dry-run"], false);
3657
+ const nowMs = Date.now();
3658
+ const staleSessions = Object.entries(state.sessions).filter(([, session]) => {
3659
+ if (session.stopCount <= 0 || !session.lastObservedAt) {
3660
+ return false;
3661
+ }
3662
+ const lastObservedMs = new Date(session.lastObservedAt).getTime();
3663
+ return !Number.isNaN(lastObservedMs) && nowMs - lastObservedMs >= idleAfterSeconds * 1000;
3664
+ }).map(([agentSessionKeyValue, session]) => ({
3665
+ agentSessionKey: agentSessionKeyValue,
3666
+ source: session.source,
3667
+ stopCount: session.stopCount,
3668
+ threshold: session.threshold,
3669
+ lastObservedAt: session.lastObservedAt
3670
+ }));
3671
+ if (!dryRun) {
3672
+ for (const staleSession of staleSessions) {
3673
+ await spawnAgentRunCloseout([
3674
+ "agent-run",
3675
+ "closeout",
3676
+ "--agent-session-key",
3677
+ staleSession.agentSessionKey,
3678
+ "--reason",
3679
+ "idle",
3680
+ "--lock",
3681
+ agentLoopLockPath(parsed, options, staleSession.agentSessionKey),
3682
+ ...parsed.flags.project ? ["--project", parsed.flags.project] : [],
3683
+ ...parsed.flags.server ? ["--server", parsed.flags.server] : []
3684
+ ], options);
3685
+ }
3686
+ }
3687
+ const data = {
3688
+ action: dryRun ? "would_sweep" : "swept",
3689
+ idleAfterSeconds,
3690
+ staleSessions
3691
+ };
3692
+ return {
3693
+ stdout: parsed.flags.json ? formatJson(successEnvelope(data, meta)) : `${JSON.stringify(data, null, 2)}
3694
+ `,
3695
+ stderr: "",
3696
+ exitCode: 0
3697
+ };
3698
+ }
3699
+ async function runHooksCommand(parsed, options) {
3700
+ const config = await resolveCliConfig(parsed.flags, options);
3701
+ const meta = commandMeta(parsed, config);
3702
+ let data;
3703
+ switch (parsed.command.name) {
3704
+ case "hooks.install":
3705
+ {
3706
+ const provider = parsed.options.provider ?? "codex";
3707
+ if (provider !== "codex") {
3708
+ throw new CliError("CLI_USAGE", "Only --provider codex is supported for hook installation in this version.");
3709
+ }
3710
+ const scope = parseHookScope(parsed.options.scope);
3711
+ const command = parsed.options.command ?? defaultHookCommand;
3712
+ const paths = await codexHookPaths(scope, options);
3713
+ const results = [];
3714
+ for (const filePath of paths) {
3715
+ results.push(await installCodexHookFile(filePath, command));
3716
+ }
3717
+ data = {
3718
+ provider,
3719
+ scope,
3720
+ command,
3721
+ results,
3722
+ trustRequired: "Open Codex /hooks and trust the Cadence hook before it can run."
3723
+ };
3724
+ }
3725
+ break;
3726
+ case "hooks.doctor":
3727
+ {
3728
+ const provider = parsed.options.provider ?? "codex";
3729
+ if (provider !== "codex") {
3730
+ throw new CliError("CLI_USAGE", "Only --provider codex is supported for hook doctor in this version.");
3731
+ }
3732
+ const scope = parseHookScope(parsed.options.scope);
3733
+ const command = parsed.options.command ?? defaultHookCommand;
3734
+ const paths = await codexHookPaths(scope, options);
3735
+ const codexVersion = runLocalCommand("codex", ["--version"], options, {
3736
+ cwd: options.cwd ?? process.cwd(),
3737
+ timeoutMs: 1e4
3738
+ });
3739
+ data = {
3740
+ provider,
3741
+ scope,
3742
+ command,
3743
+ codexAvailable: codexVersion.status === 0,
3744
+ codexVersion: codexVersion.status === 0 ? codexVersion.stdout.trim() : null,
3745
+ hooks: await Promise.all(paths.map(async (filePath) => ({
3746
+ path: filePath,
3747
+ exists: await Bun.file(filePath).exists(),
3748
+ installed: await codexHookInstalled(filePath, command)
3749
+ })))
3750
+ };
3751
+ }
3752
+ break;
3753
+ default:
3754
+ throw new CliError("CLI_USAGE", `Unknown command: ${parsed.command.path.join(" ")}`);
3755
+ }
3756
+ if (parsed.flags.json) {
3757
+ return {
3758
+ stdout: formatJson(successEnvelope(data, meta)),
3759
+ stderr: "",
3760
+ exitCode: 0
3761
+ };
3762
+ }
3763
+ return {
3764
+ stdout: `${JSON.stringify(data, null, 2)}
3765
+ `,
3766
+ stderr: "",
3767
+ exitCode: 0
3768
+ };
3769
+ }
2723
3770
  async function runAuthCommand(parsed, options) {
2724
3771
  const config = await resolveCliConfig(parsed.flags, options);
2725
3772
  const store = getCredentialStore(options);
2726
- const meta = commandMeta(parsed, config);
3773
+ let meta = commandMeta(parsed, config);
2727
3774
  let data;
2728
3775
  switch (parsed.command.name) {
2729
3776
  case "auth.login":
2730
3777
  {
2731
- const client = await createClient(config, options, false);
3778
+ const loginConfig = await resolveAuthLoginConfig(config, parsed, options);
3779
+ meta = commandMeta(parsed, loginConfig);
3780
+ const client = await createClient(loginConfig, options, false);
3781
+ const loginBaseUrl = loginConfig.webBaseUrl;
2732
3782
  const challenge = await client.auth.cli.start({
2733
- loginBaseUrl: getCliWebBaseUrl(config, parsed, options)
3783
+ loginBaseUrl
2734
3784
  });
2735
3785
  if (parsed.flags.json || !isInteractive(options)) {
2736
3786
  throw new CliError("HUMAN_AUTH_REQUIRED", "Cadence login must be completed by a human in the browser.", {
@@ -2762,20 +3812,15 @@ async function runAuthCommand(parsed, options) {
2762
3812
  if (poll.status === "denied") {
2763
3813
  throw new CliError("AUTH_LOGIN_DENIED", "Cadence browser login was denied.");
2764
3814
  }
2765
- await writeStoredCredential(store, config.server, poll.credential);
2766
- await mergeConfigFile(config.globalConfigPath, { server: config.server });
3815
+ await writeStoredCredential(store, loginConfig.server, poll.credential);
3816
+ await mergeConfigFile(loginConfig.globalConfigPath, { server: loginConfig.server, webBaseUrl: loginBaseUrl });
2767
3817
  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
3818
  data = {
2773
- server: config.server,
3819
+ server: loginConfig.server,
3820
+ webBaseUrl: loginBaseUrl,
2774
3821
  credentialStored: true,
2775
- projectConfigured,
2776
- globalConfigPath: config.globalConfigPath,
2777
- loginUrl: challenge.loginUrl,
2778
- ...!projectConfigured ? { nextCommand: "cadence init" } : {}
3822
+ globalConfigPath: loginConfig.globalConfigPath,
3823
+ loginUrl: challenge.loginUrl
2779
3824
  };
2780
3825
  }
2781
3826
  break;
@@ -2813,26 +3858,18 @@ async function runAuthCommand(parsed, options) {
2813
3858
  exitCode: 0
2814
3859
  };
2815
3860
  }
2816
- if (parsed.command.name === "auth.login") {
2817
- return {
2818
- stdout: "",
2819
- stderr: "",
2820
- exitCode: 0
2821
- };
2822
- }
2823
3861
  return {
2824
- stdout: humanCommandOutput(parsed.command.name, data),
3862
+ stdout: `${JSON.stringify(data, null, 2)}
3863
+ `,
2825
3864
  stderr: "",
2826
3865
  exitCode: 0
2827
3866
  };
2828
3867
  }
2829
3868
  async function runReadCommand(parsed, options) {
2830
3869
  const config = await resolveCliConfig(parsed.flags, options);
3870
+ const projectId = requireProjectId(config);
2831
3871
  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);
3872
+ const meta = commandMeta(parsed, config);
2836
3873
  let data;
2837
3874
  switch (parsed.command.name) {
2838
3875
  case "events.list":
@@ -2925,7 +3962,8 @@ async function runReadCommand(parsed, options) {
2925
3962
  };
2926
3963
  }
2927
3964
  return {
2928
- stdout: humanCommandOutput(parsed.command.name, data),
3965
+ stdout: `${JSON.stringify(data, null, 2)}
3966
+ `,
2929
3967
  stderr: "",
2930
3968
  exitCode: 0
2931
3969
  };
@@ -2933,53 +3971,25 @@ async function runReadCommand(parsed, options) {
2933
3971
  async function runProjectCommand(parsed, options) {
2934
3972
  const config = await resolveCliConfig(parsed.flags, options);
2935
3973
  const client = await createClient(config, options);
3974
+ const meta = commandMeta(parsed, config);
2936
3975
  let data;
2937
- let outputConfig = config;
2938
3976
  switch (parsed.command.name) {
2939
3977
  case "projects.list":
2940
3978
  data = await client.projects.list();
2941
3979
  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
3980
  default:
2961
3981
  throw new CliError("CLI_USAGE", `Unknown command: ${parsed.command.path.join(" ")}`);
2962
3982
  }
2963
3983
  if (parsed.flags.json) {
2964
3984
  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
- `,
3985
+ stdout: formatJson(successEnvelope(data, meta)),
2977
3986
  stderr: "",
2978
3987
  exitCode: 0
2979
3988
  };
2980
3989
  }
2981
3990
  return {
2982
- stdout: humanCommandOutput(parsed.command.name, data),
3991
+ stdout: `${JSON.stringify(data, null, 2)}
3992
+ `,
2983
3993
  stderr: "",
2984
3994
  exitCode: 0
2985
3995
  };
@@ -2987,13 +3997,11 @@ async function runProjectCommand(parsed, options) {
2987
3997
  async function runActorCommand(parsed, options) {
2988
3998
  const config = await resolveCliConfig(parsed.flags, options);
2989
3999
  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);
4000
+ const meta = commandMeta(parsed, config);
2993
4001
  let data;
2994
4002
  switch (parsed.command.name) {
2995
4003
  case "actors.ensure-workspace-agent":
2996
- data = await ensureWorkspaceAgentIdentity(parsed, resolvedConfig, client, options);
4004
+ data = await ensureWorkspaceAgentIdentity(parsed, config, client, options);
2997
4005
  break;
2998
4006
  default:
2999
4007
  throw new CliError("CLI_USAGE", `Unknown command: ${parsed.command.path.join(" ")}`);
@@ -3006,18 +4014,17 @@ async function runActorCommand(parsed, options) {
3006
4014
  };
3007
4015
  }
3008
4016
  return {
3009
- stdout: humanCommandOutput(parsed.command.name, data),
4017
+ stdout: `${JSON.stringify(data, null, 2)}
4018
+ `,
3010
4019
  stderr: "",
3011
4020
  exitCode: 0
3012
4021
  };
3013
4022
  }
3014
4023
  async function runIntakeCommand(parsed, options) {
3015
4024
  const config = await resolveCliConfig(parsed.flags, options);
4025
+ const projectId = requireProjectId(config);
3016
4026
  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);
4027
+ const meta = commandMeta(parsed, config);
3021
4028
  let data;
3022
4029
  const ifVersion = () => parseRequiredPositiveInteger(requireOption(parsed, "if-version"), "--if-version");
3023
4030
  switch (parsed.command.name) {
@@ -3249,7 +4256,8 @@ async function runIntakeCommand(parsed, options) {
3249
4256
  };
3250
4257
  }
3251
4258
  return {
3252
- stdout: humanCommandOutput(parsed.command.name, data),
4259
+ stdout: `${JSON.stringify(data, null, 2)}
4260
+ `,
3253
4261
  stderr: "",
3254
4262
  exitCode: 0
3255
4263
  };
@@ -3313,9 +4321,15 @@ async function runCli(argv, options = {}) {
3313
4321
  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
4322
  return await runReadCommand(parsed, options);
3315
4323
  }
3316
- if (parsed.command.name === "projects.list" || parsed.command.name === "projects.use") {
4324
+ if (parsed.command.name === "projects.list") {
3317
4325
  return await runProjectCommand(parsed, options);
3318
4326
  }
4327
+ 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") {
4328
+ return await runAgentRunCommand(parsed, options);
4329
+ }
4330
+ if (parsed.command.name === "hooks.install" || parsed.command.name === "hooks.doctor") {
4331
+ return await runHooksCommand(parsed, options);
4332
+ }
3319
4333
  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
4334
  return await runIntakeCommand(parsed, options);
3321
4335
  }