@trycadence/cli 0.1.7 → 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 +1247 -119
  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);
@@ -2163,7 +2214,6 @@ function helpText() {
2163
2214
  " cadence actors ensure-workspace-agent --agent-kind <kind> [--workspace-name <name>] [--workspace-ref <ref>] [--display-name <name>] [--project <project-id>] [--json]",
2164
2215
  " cadence status [--project <project-id>] [--json]",
2165
2216
  " cadence projects list [--json]",
2166
- " cadence projects use <project-id|org/project> [--server <url>] [--json]",
2167
2217
  " cadence work overview [--project <project-id>] [--json]",
2168
2218
  " cadence tickets get <ticket-id> [--project <project-id>] [--json]",
2169
2219
  " cadence tickets list [--project <project-id>] [--status <status>] [--json]",
@@ -2188,10 +2238,16 @@ function helpText() {
2188
2238
  " cadence changesets notes get [--changeset <id>|--branch current|<branch>] [--project <project-id>] [--json]",
2189
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]",
2190
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]",
2191
2247
  " cadence events list [--project <project-id>] [--ticket <ticket-id>] [--changeset <changeset-id>] [--session <session-id>] [--json]",
2192
2248
  "",
2193
2249
  "Global flags:",
2194
- " --project <id|org/project> Cadence project ID or org/project slug",
2250
+ " --project <id> Cadence project ID or org/project slug",
2195
2251
  " --server <url> Cadence API server override",
2196
2252
  " --json Print stable JSON envelope",
2197
2253
  "",
@@ -2448,6 +2504,572 @@ async function readBodyFile(path, options) {
2448
2504
  const resolvedPath = isAbsolute(path) ? path : join(options.cwd ?? process.cwd(), path);
2449
2505
  return readFile(resolvedPath, "utf8");
2450
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
3050
+ };
3051
+ }
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));
3072
+ }
2451
3073
  async function runStatus(parsed, options) {
2452
3074
  const config = await resolveCliConfig(parsed.flags, options);
2453
3075
  const store = getCredentialStore(options);
@@ -2477,21 +3099,8 @@ async function runStatus(parsed, options) {
2477
3099
  exitCode: 0
2478
3100
  };
2479
3101
  }
2480
- const projectLabel = config.projectId ?? "not configured";
2481
- const profileLabel = config.profile ?? "default";
2482
- const credentialLabel = credential ? "configured" : "not configured";
2483
3102
  return {
2484
- stdout: [
2485
- `Cadence API: ${health.ok ? "ok" : "unavailable"} at ${config.server}`,
2486
- `Cadence web: ${webBaseUrl}`,
2487
- `Profile: ${profileLabel}`,
2488
- `Project: ${projectLabel}`,
2489
- `Credential: ${credentialLabel}`,
2490
- `Repo config: ${config.repoConfigPath ?? "not found"}`,
2491
- `Local config: ${config.localConfigPath ?? "not found"}`,
2492
- `Global config: ${config.globalConfigPath}`
2493
- ].join(`
2494
- `) + `
3103
+ stdout: `Cadence API ${health.ok ? "ok" : "unavailable"} at ${config.server}
2495
3104
  `,
2496
3105
  stderr: "",
2497
3106
  exitCode: 0
@@ -2499,7 +3108,7 @@ async function runStatus(parsed, options) {
2499
3108
  }
2500
3109
  async function runInit(parsed, options) {
2501
3110
  const cwd = options.cwd ?? process.cwd();
2502
- const repoConfigPath = await repoConfigPathForWrite(options);
3111
+ const repoConfigPath = join(cwd, ".cadence", "config.json");
2503
3112
  const config = await resolveCliConfig(parsed.flags, {
2504
3113
  ...options,
2505
3114
  cwd
@@ -2557,15 +3166,6 @@ async function resolveProjectReference(projectReference, client) {
2557
3166
  projectSlug
2558
3167
  });
2559
3168
  }
2560
- async function resolveRequiredProject(config, client) {
2561
- return await resolveProjectReference(requireProjectId(config), client);
2562
- }
2563
- function configWithProject(config, project) {
2564
- return {
2565
- ...config,
2566
- projectId: project.id
2567
- };
2568
- }
2569
3169
  function formatProjectChoice(project, index) {
2570
3170
  const slug = project.orgSlug && project.projectSlug ? `${project.orgSlug}/${project.projectSlug}` : project.id;
2571
3171
  const name = project.name ? ` ${project.name}` : "";
@@ -2608,17 +3208,579 @@ async function selectProject(parsed, client, options) {
2608
3208
  }
2609
3209
  return matching;
2610
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
+ }
2611
3770
  async function runAuthCommand(parsed, options) {
2612
3771
  const config = await resolveCliConfig(parsed.flags, options);
2613
3772
  const store = getCredentialStore(options);
2614
- const meta = commandMeta(parsed, config);
3773
+ let meta = commandMeta(parsed, config);
2615
3774
  let data;
2616
3775
  switch (parsed.command.name) {
2617
3776
  case "auth.login":
2618
3777
  {
2619
- 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;
2620
3782
  const challenge = await client.auth.cli.start({
2621
- loginBaseUrl: getCliWebBaseUrl(config, parsed, options)
3783
+ loginBaseUrl
2622
3784
  });
2623
3785
  if (parsed.flags.json || !isInteractive(options)) {
2624
3786
  throw new CliError("HUMAN_AUTH_REQUIRED", "Cadence login must be completed by a human in the browser.", {
@@ -2650,20 +3812,15 @@ async function runAuthCommand(parsed, options) {
2650
3812
  if (poll.status === "denied") {
2651
3813
  throw new CliError("AUTH_LOGIN_DENIED", "Cadence browser login was denied.");
2652
3814
  }
2653
- await writeStoredCredential(store, config.server, poll.credential);
2654
- 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 });
2655
3817
  await writeInteractiveStatus("Cadence CLI login approved. Credential stored.", options);
2656
- const projectConfigured = Boolean(config.projectId);
2657
- if (!projectConfigured) {
2658
- await writeInteractiveStatus("No Cadence project is configured for this repo. Run cadence init to choose one.", options);
2659
- }
2660
3818
  data = {
2661
- server: config.server,
3819
+ server: loginConfig.server,
3820
+ webBaseUrl: loginBaseUrl,
2662
3821
  credentialStored: true,
2663
- projectConfigured,
2664
- globalConfigPath: config.globalConfigPath,
2665
- loginUrl: challenge.loginUrl,
2666
- ...!projectConfigured ? { nextCommand: "cadence init" } : {}
3822
+ globalConfigPath: loginConfig.globalConfigPath,
3823
+ loginUrl: challenge.loginUrl
2667
3824
  };
2668
3825
  }
2669
3826
  break;
@@ -2710,11 +3867,9 @@ async function runAuthCommand(parsed, options) {
2710
3867
  }
2711
3868
  async function runReadCommand(parsed, options) {
2712
3869
  const config = await resolveCliConfig(parsed.flags, options);
3870
+ const projectId = requireProjectId(config);
2713
3871
  const client = await createClient(config, options);
2714
- const project = await resolveRequiredProject(config, client);
2715
- const resolvedConfig = configWithProject(config, project);
2716
- const projectId = project.id;
2717
- const meta = commandMeta(parsed, resolvedConfig);
3872
+ const meta = commandMeta(parsed, config);
2718
3873
  let data;
2719
3874
  switch (parsed.command.name) {
2720
3875
  case "events.list":
@@ -2816,47 +3971,18 @@ async function runReadCommand(parsed, options) {
2816
3971
  async function runProjectCommand(parsed, options) {
2817
3972
  const config = await resolveCliConfig(parsed.flags, options);
2818
3973
  const client = await createClient(config, options);
3974
+ const meta = commandMeta(parsed, config);
2819
3975
  let data;
2820
- let outputConfig = config;
2821
3976
  switch (parsed.command.name) {
2822
3977
  case "projects.list":
2823
3978
  data = await client.projects.list();
2824
3979
  break;
2825
- case "projects.use":
2826
- {
2827
- const project = await resolveProjectReference(requireArg(parsed, 0, "<project-id|org/project>"), client);
2828
- const repoConfigPath = await repoConfigPathForWrite(options);
2829
- const updates = {
2830
- projectId: project.id,
2831
- ...parsed.flags.server ? { server: parsed.flags.server } : {}
2832
- };
2833
- await mergeConfigFile(repoConfigPath, updates);
2834
- outputConfig = configWithProject(config, project);
2835
- data = {
2836
- repoConfigPath,
2837
- projectId: project.id,
2838
- project,
2839
- ...parsed.flags.server ? { server: parsed.flags.server } : {}
2840
- };
2841
- }
2842
- break;
2843
3980
  default:
2844
3981
  throw new CliError("CLI_USAGE", `Unknown command: ${parsed.command.path.join(" ")}`);
2845
3982
  }
2846
3983
  if (parsed.flags.json) {
2847
3984
  return {
2848
- stdout: formatJson(successEnvelope(data, commandMeta(parsed, outputConfig))),
2849
- stderr: "",
2850
- exitCode: 0
2851
- };
2852
- }
2853
- if (parsed.command.name === "projects.use") {
2854
- const project = data;
2855
- const slug = project.project?.orgSlug && project.project.projectSlug ? `${project.project.orgSlug}/${project.project.projectSlug}` : project.projectId;
2856
- const name = project.project?.name ? ` (${project.project.name})` : "";
2857
- return {
2858
- stdout: `Using Cadence project ${slug}${name}.
2859
- `,
3985
+ stdout: formatJson(successEnvelope(data, meta)),
2860
3986
  stderr: "",
2861
3987
  exitCode: 0
2862
3988
  };
@@ -2871,13 +3997,11 @@ async function runProjectCommand(parsed, options) {
2871
3997
  async function runActorCommand(parsed, options) {
2872
3998
  const config = await resolveCliConfig(parsed.flags, options);
2873
3999
  const client = await createClient(config, options);
2874
- const project = await resolveRequiredProject(config, client);
2875
- const resolvedConfig = configWithProject(config, project);
2876
- const meta = commandMeta(parsed, resolvedConfig);
4000
+ const meta = commandMeta(parsed, config);
2877
4001
  let data;
2878
4002
  switch (parsed.command.name) {
2879
4003
  case "actors.ensure-workspace-agent":
2880
- data = await ensureWorkspaceAgentIdentity(parsed, resolvedConfig, client, options);
4004
+ data = await ensureWorkspaceAgentIdentity(parsed, config, client, options);
2881
4005
  break;
2882
4006
  default:
2883
4007
  throw new CliError("CLI_USAGE", `Unknown command: ${parsed.command.path.join(" ")}`);
@@ -2898,11 +4022,9 @@ async function runActorCommand(parsed, options) {
2898
4022
  }
2899
4023
  async function runIntakeCommand(parsed, options) {
2900
4024
  const config = await resolveCliConfig(parsed.flags, options);
4025
+ const projectId = requireProjectId(config);
2901
4026
  const client = await createClient(config, options);
2902
- const project = await resolveRequiredProject(config, client);
2903
- const resolvedConfig = configWithProject(config, project);
2904
- const projectId = project.id;
2905
- const meta = commandMeta(parsed, resolvedConfig);
4027
+ const meta = commandMeta(parsed, config);
2906
4028
  let data;
2907
4029
  const ifVersion = () => parseRequiredPositiveInteger(requireOption(parsed, "if-version"), "--if-version");
2908
4030
  switch (parsed.command.name) {
@@ -3199,9 +4321,15 @@ async function runCli(argv, options = {}) {
3199
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") {
3200
4322
  return await runReadCommand(parsed, options);
3201
4323
  }
3202
- if (parsed.command.name === "projects.list" || parsed.command.name === "projects.use") {
4324
+ if (parsed.command.name === "projects.list") {
3203
4325
  return await runProjectCommand(parsed, options);
3204
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
+ }
3205
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") {
3206
4334
  return await runIntakeCommand(parsed, options);
3207
4335
  }