@dadado/agent-kit-cli 6.1.1 → 6.3.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 (48) hide show
  1. package/dashboard/README.md +3 -1
  2. package/dashboard/dashboard-data.mjs +13 -2
  3. package/dashboard/dashboard.html +95 -2
  4. package/dashboard/lib/gate-exit-tickets.d.mts +45 -0
  5. package/dashboard/lib/gate-exit-tickets.mjs +233 -0
  6. package/dashboard/lib/semantic-model.d.mts +21 -0
  7. package/dashboard/lib/semantic-model.mjs +47 -0
  8. package/dist/{add-YTCDLPUS.js → add-VA2W7I6P.js} +3 -3
  9. package/dist/{chunk-VTOUQAVR.js → chunk-25J2SMJQ.js} +9 -1
  10. package/dist/{chunk-2FQX2O4I.js → chunk-6GYKOHIL.js} +2 -2
  11. package/dist/{chunk-DBPNGZNH.js → chunk-6KUSG7BJ.js} +15 -2
  12. package/dist/{chunk-C26I6HTO.js → chunk-C4Q77GGJ.js} +0 -1
  13. package/dist/{chunk-U5F6RJN4.js → chunk-GO3K752H.js} +4 -1
  14. package/dist/chunk-GUQYNUV4.js +61 -0
  15. package/dist/{chunk-ESNAFDYT.js → chunk-JEI6HACW.js} +44 -1
  16. package/dist/chunk-KVY425NM.js +519 -0
  17. package/dist/{chunk-WUWRGOUV.js → chunk-L5V3W3DY.js} +13 -1
  18. package/dist/chunk-MO63KZBP.js +516 -0
  19. package/dist/chunk-MUTTNRX4.js +53 -0
  20. package/dist/{chunk-PPKX55GI.js → chunk-NNKOWUWM.js} +1 -1
  21. package/dist/{chunk-DRND2KYV.js → chunk-OVOXRH5W.js} +1 -1
  22. package/dist/{chunk-GN4SAZF2.js → chunk-OWWFKGKS.js} +2 -2
  23. package/dist/chunk-QTBTKEXK.js +77 -0
  24. package/dist/{chunk-TMFOLRU2.js → chunk-SV2BR2LZ.js} +44 -9
  25. package/dist/{chunk-3DGTPONS.js → chunk-U26MRIWN.js} +1 -1
  26. package/dist/{chunk-BBVBYQJO.js → chunk-VQBI3JVS.js} +2 -2
  27. package/dist/{chunk-YP4OOK4T.js → chunk-WMVMBPCR.js} +2 -2
  28. package/dist/{chunk-OZYJUNAQ.js → chunk-YRFSTHR5.js} +29 -525
  29. package/dist/{chunk-3XKVQS2K.js → chunk-Z7ABX5QI.js} +13 -9
  30. package/dist/{chunk-DBXDV5O7.js → chunk-Z7RYIHHX.js} +90 -5
  31. package/dist/{contribute-A7NCWTUM.js → contribute-NLM2BN6Z.js} +4 -4
  32. package/dist/countdown-QXJD6TDB.js +479 -0
  33. package/dist/{diff-4MDU5SLE.js → diff-HHG4JEDE.js} +5 -5
  34. package/dist/{doctor-SS6Y2T6H.js → doctor-TWF2M3S3.js} +9 -9
  35. package/dist/ground-control-NMYYIVCV.js +434 -0
  36. package/dist/{handoff-MK5JG3J5.js → handoff-5AGE2D3T.js} +3 -2
  37. package/dist/{hook-D2WHBKRN.js → hook-VP7FHRSZ.js} +7 -6
  38. package/dist/index.js +30 -20
  39. package/dist/{init-GVVBA2BM.js → init-UOTV4ASJ.js} +12 -12
  40. package/dist/{install-R4HPI7VK.js → install-VCHPXQV3.js} +12 -12
  41. package/dist/{plan-index-V7T5MKL6.js → plan-index-BNLUU6T5.js} +3 -2
  42. package/dist/{run-3B26CVIL.js → run-R66G276Q.js} +58 -12
  43. package/dist/{run-plan-APTBO2OT.js → run-plan-24Z5K4P3.js} +12 -7
  44. package/dist/{scan-K6TQGV4S.js → scan-UX4HARMJ.js} +1 -1
  45. package/dist/{setup-global-AT4DD3MG.js → setup-global-M6LKGJHQ.js} +6 -6
  46. package/dist/{status-MPJHI5RP.js → status-5UKVIKOI.js} +8 -8
  47. package/dist/{update-P62637ZI.js → update-VQBGEUWS.js} +33 -43
  48. package/package.json +1 -1
@@ -4,18 +4,20 @@ import {
4
4
  claudeChildEnv,
5
5
  claudeHeadlessArgs,
6
6
  claudeRedactions,
7
+ claudeReviewArgs,
7
8
  claudeRunCaps,
8
9
  codexBackend,
9
10
  cursorAcpBackend,
10
11
  cursorAgentHeadlessArgs,
12
+ cursorAgentReviewArgs,
11
13
  redactSecrets,
12
14
  resolveHitlRunOptions,
13
15
  spawnLogged
14
- } from "./chunk-ESNAFDYT.js";
16
+ } from "./chunk-JEI6HACW.js";
15
17
  import {
16
18
  ensureLoopLogsDir,
17
19
  runnerStatePaths
18
- } from "./chunk-C26I6HTO.js";
20
+ } from "./chunk-C4Q77GGJ.js";
19
21
  import {
20
22
  fileExists
21
23
  } from "./chunk-5B4DVO6C.js";
@@ -32,7 +34,8 @@ var RUN_CATALOG = [
32
34
  "backlog-edit",
33
35
  "backlog-delete",
34
36
  "backlog-cancel",
35
- "git-staging"
37
+ "git-staging",
38
+ "review-pack"
36
39
  ];
37
40
  var FIRST_CLASS_SLASH_COMMANDS = ["run-plan", "run-plan-all"];
38
41
  var RUN_PROMOTE_BLOCKED = RESERVED_GATE_IDS;
@@ -105,6 +108,9 @@ function claudeDispatchArgs(opts = {}) {
105
108
  return claudeHeadlessArgs(opts);
106
109
  }
107
110
  async function runHeadlessDispatch(opts) {
111
+ if (opts.readOnly && opts.backendId !== "claude" && opts.backendId !== "cursor-agent") {
112
+ throw new Error(`${opts.backendId}: a review-only session runs on claude or cursor-agent only`);
113
+ }
108
114
  if (opts.backendId === "codex" || opts.backendId === "cursor-acp") {
109
115
  const backend = opts.backendId === "codex" ? codexBackend : cursorAcpBackend;
110
116
  return backend.run({
@@ -132,7 +138,7 @@ async function runHeadlessDispatch(opts) {
132
138
  try {
133
139
  return await spawnLogged(
134
140
  opts.bin,
135
- claudeDispatchArgs({ model: opts.model, ...caps }),
141
+ opts.readOnly ? claudeReviewArgs({ model: opts.model, ...caps }) : claudeDispatchArgs({ model: opts.model, ...caps }),
136
142
  opts.logPath,
137
143
  {
138
144
  cwd: opts.workspace,
@@ -152,13 +158,11 @@ async function runHeadlessDispatch(opts) {
152
158
  throw new Error(`claude dispatch failed to start: ${redactSecrets(String(err), redact)}`);
153
159
  }
154
160
  }
155
- const args = cursorAgentDispatchArgs({
156
- workspace: opts.workspace,
157
- prompt: opts.prompt,
158
- model: opts.model
159
- });
161
+ const argvInput = { workspace: opts.workspace, prompt: opts.prompt, model: opts.model };
162
+ const args = opts.readOnly ? cursorAgentReviewArgs(argvInput) : cursorAgentDispatchArgs(argvInput);
160
163
  return spawnLogged(opts.bin, args, opts.logPath, {
161
164
  spawnFn: opts.spawnFn,
165
+ ...opts.readOnly ? { cwd: opts.workspace } : {},
162
166
  redact: claudeRedactions(process.env),
163
167
  hitl: resolveHitlRunOptions(opts.hitl, { kind: "none", backend: "cursor-agent" }),
164
168
  render: opts.render,
@@ -1,6 +1,11 @@
1
+ import {
2
+ compareSemver,
3
+ normalizeSemver,
4
+ readLocalKitVersion
5
+ } from "./chunk-6GYKOHIL.js";
1
6
  import {
2
7
  L0_ARTIFACTS
3
- } from "./chunk-VTOUQAVR.js";
8
+ } from "./chunk-25J2SMJQ.js";
4
9
  import {
5
10
  copyRegistryFile,
6
11
  emptyStats,
@@ -12,24 +17,100 @@ import {
12
17
  recordOutcome,
13
18
  resolveProtectedGlobs,
14
19
  saveManagedHashLedger
15
- } from "./chunk-DBPNGZNH.js";
20
+ } from "./chunk-6KUSG7BJ.js";
21
+ import {
22
+ KIT_PACKAGE_SPEC,
23
+ KIT_VERSION
24
+ } from "./chunk-7HDQEK6T.js";
16
25
  import {
17
26
  fileExists
18
27
  } from "./chunk-5B4DVO6C.js";
19
28
 
29
+ // src/lifecycle/stale-cli.ts
30
+ import path from "path";
31
+ async function detectStaleCli(registryRoot, options) {
32
+ const registryVersionRaw = await readLocalKitVersion(registryRoot);
33
+ const registryVersion = registryVersionRaw ? normalizeSemver(registryVersionRaw) : null;
34
+ const cliVersion = normalizeSemver(options.cliVersion ?? KIT_VERSION);
35
+ if (!registryVersion) {
36
+ options.warn(
37
+ "Could not read a version from the registry checkout - skipping the CLI freshness check."
38
+ );
39
+ return null;
40
+ }
41
+ if (cliVersion && compareSemver(cliVersion, registryVersion) < 0) {
42
+ return { cliVersion, registryVersion };
43
+ }
44
+ return null;
45
+ }
46
+ var SHELL_SAFE = /^[A-Za-z0-9_./:@=+,-]+$/;
47
+ function shellQuote(value) {
48
+ return SHELL_SAFE.test(value) ? value : `'${value.replaceAll("'", `'\\''`)}'`;
49
+ }
50
+ function formatRerunFlags(args, flags, currentDir = process.cwd()) {
51
+ const out = [];
52
+ for (const flag of flags) {
53
+ const value = args[flag];
54
+ if (value === true) {
55
+ out.push(flag === "yes" ? "-y" : `--${flag}`);
56
+ } else if (typeof value === "string" && value.length > 0) {
57
+ if (flag === "cwd" && path.resolve(value) === path.resolve(currentDir)) continue;
58
+ out.push(`--${flag}`, shellQuote(value));
59
+ }
60
+ }
61
+ return out.join(" ");
62
+ }
63
+ function formatStaleCliRefusal(input) {
64
+ const { command, stale, rerun } = input;
65
+ const { cliVersion, registryVersion } = stale;
66
+ const tail = rerun ? ` ${rerun}` : "";
67
+ return {
68
+ error: `This CLI is v${cliVersion}, older than the registry it would apply (v${registryVersion}).`,
69
+ recovery: [
70
+ "",
71
+ `\`agent-kit ${command}\` syncs registry content but stamps .cursor/agent-kit.json with the`,
72
+ `version of the CLI running it, so applying now would write v${cliVersion} again, not v${registryVersion}.`,
73
+ "",
74
+ "Upgrade the binary first, then re-run:",
75
+ ` npm i -g ${KIT_PACKAGE_SPEC}@${registryVersion}`,
76
+ ` agent-kit ${command}${tail}`,
77
+ "",
78
+ "Or run that version once without a global install:",
79
+ ` npx -y ${KIT_PACKAGE_SPEC}@${registryVersion} ${command}${tail}`,
80
+ "",
81
+ `If npm has no ${registryVersion} yet, the release is still mid-publish`,
82
+ "(content sync and npm publish run in parallel) - retry shortly.",
83
+ "",
84
+ "Factory/dev checkouts that intend to apply newer content with an older",
85
+ "binary can pass --allow-stale-cli.",
86
+ ""
87
+ ].join("\n")
88
+ };
89
+ }
90
+ var StaleCliError = class extends Error {
91
+ constructor(stale) {
92
+ super(
93
+ `This CLI is v${stale.cliVersion}, older than the registry it would apply (v${stale.registryVersion}).`
94
+ );
95
+ this.stale = stale;
96
+ this.name = "StaleCliError";
97
+ }
98
+ stale;
99
+ };
100
+
20
101
  // src/lifecycle/onboard-migration.ts
21
102
  import { createHash } from "crypto";
22
103
  import { readFile, unlink } from "fs/promises";
23
- import path from "path";
104
+ import path2 from "path";
24
105
  var LEGACY_ONBOARD_PATH = ".cursor/commands/onboard.md";
25
106
  var NAMESPACED_ONBOARD_PATH = ".cursor/commands/agent-kit-onboard.md";
26
107
  var MANAGED_LEGACY_HASHES = /* @__PURE__ */ new Set([
27
108
  "b274a68941813f19b185893cb7c5561dff027f53270890029992f208e24992fe"
28
109
  ]);
29
110
  async function migrateLegacyOnboardCommand(projectRoot, managedHashes = MANAGED_LEGACY_HASHES) {
30
- const legacyPath = path.join(projectRoot, LEGACY_ONBOARD_PATH);
111
+ const legacyPath = path2.join(projectRoot, LEGACY_ONBOARD_PATH);
31
112
  if (!await fileExists(legacyPath)) return "absent";
32
- const namespacedPath = path.join(projectRoot, NAMESPACED_ONBOARD_PATH);
113
+ const namespacedPath = path2.join(projectRoot, NAMESPACED_ONBOARD_PATH);
33
114
  if (!await fileExists(namespacedPath)) return "preserved-customized";
34
115
  const content = await readFile(legacyPath);
35
116
  const hash = createHash("sha256").update(content).digest("hex");
@@ -100,6 +181,10 @@ var NON_INTERACTIVE_ROOT_ARGS = {
100
181
  };
101
182
 
102
183
  export {
184
+ detectStaleCli,
185
+ formatRerunFlags,
186
+ formatStaleCliRefusal,
187
+ StaleCliError,
103
188
  installL0,
104
189
  syncFromManifest,
105
190
  NON_INTERACTIVE_ROOT_ARGS
@@ -1,19 +1,19 @@
1
1
  import {
2
2
  REGISTRY_CLI_ARGS,
3
3
  resolveRegistryFromCli
4
- } from "./chunk-3DGTPONS.js";
4
+ } from "./chunk-U26MRIWN.js";
5
5
  import {
6
6
  logger
7
7
  } from "./chunk-ZB43YMHW.js";
8
8
  import {
9
9
  managedPairs
10
- } from "./chunk-VTOUQAVR.js";
10
+ } from "./chunk-25J2SMJQ.js";
11
11
  import {
12
12
  loadAgentKitManifest,
13
13
  normalizeRelPath,
14
14
  resolveContained
15
- } from "./chunk-DBPNGZNH.js";
16
- import "./chunk-C26I6HTO.js";
15
+ } from "./chunk-6KUSG7BJ.js";
16
+ import "./chunk-C4Q77GGJ.js";
17
17
  import "./chunk-5B4DVO6C.js";
18
18
 
19
19
  // src/commands/contribute.ts
@@ -0,0 +1,479 @@
1
+ import {
2
+ REVIEW_BACKENDS,
3
+ REVIEW_PACK_DEFAULT_MAX_BYTES,
4
+ REVIEW_PACK_SLASH,
5
+ parseReviewPackMaxBytes,
6
+ reviewBackendMessage
7
+ } from "./chunk-MO63KZBP.js";
8
+ import {
9
+ detectAgentBackend,
10
+ listDetectBackendIds
11
+ } from "./chunk-GUQYNUV4.js";
12
+ import "./chunk-QTBTKEXK.js";
13
+ import {
14
+ GATE_EXIT_MARKER_RELPATH,
15
+ defaultExec,
16
+ gateExitRedactions,
17
+ gateExitStamp,
18
+ landGateExit,
19
+ listGateExitBranches,
20
+ readGateExitMarker,
21
+ resolveGateExitRefs,
22
+ writeGateExitMarker
23
+ } from "./chunk-KVY425NM.js";
24
+ import "./chunk-MUTTNRX4.js";
25
+ import {
26
+ classifySlash,
27
+ normalizeSlashName,
28
+ promoteBlockedMessage,
29
+ unknownSlashMessage
30
+ } from "./chunk-Z7ABX5QI.js";
31
+ import {
32
+ CLAUDE_MAX_BUDGET_USD_ENV,
33
+ CLAUDE_MAX_TURNS_ENV,
34
+ HITL_UNANSWERED_EXIT_CODE,
35
+ RedactingStreamBuffer,
36
+ redactSecrets
37
+ } from "./chunk-JEI6HACW.js";
38
+ import "./chunk-WGLPZXNU.js";
39
+ import "./chunk-MAPOQEA2.js";
40
+ import "./chunk-QTON2GQX.js";
41
+ import {
42
+ logger
43
+ } from "./chunk-ZB43YMHW.js";
44
+ import {
45
+ ensureLoopLogsDir,
46
+ runnerStatePaths
47
+ } from "./chunk-C4Q77GGJ.js";
48
+ import "./chunk-5B4DVO6C.js";
49
+
50
+ // src/commands/countdown.ts
51
+ import { spawn } from "child_process";
52
+ import { appendFile } from "fs/promises";
53
+ import path from "path";
54
+ import { defineCommand } from "citty";
55
+ var COUNTDOWN_EXIT = {
56
+ completed: 0,
57
+ usage: 1,
58
+ /** A gate exit is pending, or the run reached a gate. */
59
+ pending: 2,
60
+ /** Circuit's Dead: failed, stalled, over the window, dirty tree, fetch failed. */
61
+ circuitsDead: 3
62
+ };
63
+ var COUNTDOWN_JOBS = ["tick", "dispatch", "review"];
64
+ var COUNTDOWN_DEFAULT_MAX_TICKS = 1;
65
+ var COUNTDOWN_DEFAULT_WINDOW_MIN = 120;
66
+ var COUNTDOWN_DEFAULT_STALL_MIN = 20;
67
+ var COUNTDOWN_KILL_GRACE_MS = 3e4;
68
+ var defaultSpawnJob = (args, opts) => {
69
+ const entry = process.argv[1] ?? "";
70
+ const group = process.platform !== "win32";
71
+ const child = spawn(process.execPath, [entry, ...args], {
72
+ cwd: opts.cwd,
73
+ env: opts.env,
74
+ detached: group,
75
+ stdio: ["ignore", "pipe", "pipe"]
76
+ });
77
+ const listeners = [];
78
+ const emit = (text) => {
79
+ for (const l of listeners) l(text);
80
+ };
81
+ child.stdout?.setEncoding("utf8");
82
+ child.stderr?.setEncoding("utf8");
83
+ child.stdout?.on("data", (t) => emit(t));
84
+ child.stderr?.on("data", (t) => emit(t));
85
+ const exited = new Promise((resolve) => {
86
+ child.once("error", (err) => {
87
+ emit(`countdown: could not start the job: ${String(err)}
88
+ `);
89
+ resolve({ code: 1, signal: null });
90
+ });
91
+ child.once("close", (code, signal) => resolve({ code, signal }));
92
+ });
93
+ return {
94
+ onData: (listener) => listeners.push(listener),
95
+ kill: (signal) => {
96
+ try {
97
+ if (group && child.pid) process.kill(-child.pid, signal);
98
+ else child.kill(signal);
99
+ } catch {
100
+ child.kill(signal);
101
+ }
102
+ },
103
+ exited
104
+ };
105
+ };
106
+ async function superviseJob(handle, opts) {
107
+ let dead = null;
108
+ let killTimer;
109
+ const stop = (cause) => {
110
+ if (dead) return;
111
+ dead = cause;
112
+ opts.onCircuitsDead?.(cause);
113
+ handle.kill("SIGTERM");
114
+ killTimer = setTimeout(() => handle.kill("SIGKILL"), opts.killGraceMs);
115
+ };
116
+ const windowTimer = setTimeout(() => stop("window"), opts.windowMs);
117
+ let stallTimer = setTimeout(() => stop("stall"), opts.stallMs);
118
+ handle.onData((text) => {
119
+ opts.onData?.(text);
120
+ if (dead) return;
121
+ clearTimeout(stallTimer);
122
+ stallTimer = setTimeout(() => stop("stall"), opts.stallMs);
123
+ });
124
+ try {
125
+ const exit = await handle.exited;
126
+ return { ...exit, circuitsDead: dead };
127
+ } finally {
128
+ clearTimeout(windowTimer);
129
+ clearTimeout(stallTimer);
130
+ if (killTimer) clearTimeout(killTimer);
131
+ }
132
+ }
133
+ function countdownExitCode(input) {
134
+ if (input.circuitsDead) return COUNTDOWN_EXIT.circuitsDead;
135
+ if (input.code === 0) return input.pendingGrew ? COUNTDOWN_EXIT.pending : 0;
136
+ if (input.code === HITL_UNANSWERED_EXIT_CODE) return COUNTDOWN_EXIT.pending;
137
+ return COUNTDOWN_EXIT.circuitsDead;
138
+ }
139
+ function countdownChildArgs(input) {
140
+ const head = input.job === "tick" ? ["run-plan"] : input.job === "review" ? ["run", REVIEW_PACK_SLASH] : ["run", normalizeSlashName(input.slash ?? "")];
141
+ return [
142
+ ...head,
143
+ "--backend",
144
+ input.backend,
145
+ ...input.job === "review" ? [
146
+ "--max-pack-bytes",
147
+ String(input.maxPackBytes ?? REVIEW_PACK_DEFAULT_MAX_BYTES),
148
+ ...input.includeReviewed ? ["--include-reviewed"] : []
149
+ ] : ["--max-ticks", String(input.maxTicks)],
150
+ "--no-hitl",
151
+ "--land-on-gate",
152
+ "--plain",
153
+ ...input.model ? ["--model", input.model] : []
154
+ ];
155
+ }
156
+ async function countdownPreflight(opts) {
157
+ const git = (args) => opts.exec("git", args, { cwd: opts.root, env: opts.env });
158
+ const first = (text) => text.trim().split("\n")[0] ?? "";
159
+ const top = await git(["rev-parse", "--show-toplevel"]);
160
+ if (top.code !== 0) {
161
+ return { ok: false, exitCode: COUNTDOWN_EXIT.usage, reason: "not a git repository" };
162
+ }
163
+ const origin = await git(["remote", "get-url", "origin"]);
164
+ const hasOrigin = origin.code === 0 && origin.stdout.trim().length > 0;
165
+ if (hasOrigin) {
166
+ const fetched = await git(["fetch", "--prune", "--quiet", "origin"]);
167
+ if (fetched.code !== 0) {
168
+ return {
169
+ ok: false,
170
+ exitCode: COUNTDOWN_EXIT.circuitsDead,
171
+ reason: `Circuit's Dead: git fetch --prune failed (${first(fetched.stderr)}); pending gate-exit branches cannot be checked`
172
+ };
173
+ }
174
+ opts.log("fetched origin (--prune)");
175
+ } else {
176
+ opts.log("no `origin` remote: nothing fetched; checking local gate-exit branches");
177
+ }
178
+ const marker = await readGateExitMarker(opts.root);
179
+ if (marker.kind === "invalid") {
180
+ return {
181
+ ok: false,
182
+ exitCode: COUNTDOWN_EXIT.pending,
183
+ reason: `${GATE_EXIT_MARKER_RELPATH} is unreadable (${marker.detail}); not starting into an unknown gate state`
184
+ };
185
+ }
186
+ const pending = marker.kind === "ok" ? marker.marker.pending : [];
187
+ if (marker.kind === "ok" && pending.length > 0) {
188
+ const skipped = `skipped ${gateExitStamp(opts.now)}`;
189
+ try {
190
+ await writeGateExitMarker(opts.root, {
191
+ ...marker.marker,
192
+ skipped: [...marker.marker.skipped, skipped]
193
+ });
194
+ opts.log(`recorded \`${skipped}\` in ${GATE_EXIT_MARKER_RELPATH}`);
195
+ } catch (err) {
196
+ opts.log(`could not record the skipped run in ${GATE_EXIT_MARKER_RELPATH}: ${String(err)}`);
197
+ }
198
+ const stamps = pending.map((e) => e.stamp).join(", ");
199
+ return {
200
+ ok: false,
201
+ exitCode: COUNTDOWN_EXIT.pending,
202
+ reason: `gate exit pending (${stamps}); triage it before the next run`
203
+ };
204
+ }
205
+ const status = await git(["status", "--porcelain"]);
206
+ const dirty = status.stdout.split("\n").filter((l) => l.trim()).length;
207
+ if (status.code !== 0 || dirty > 0) {
208
+ return {
209
+ ok: false,
210
+ exitCode: COUNTDOWN_EXIT.circuitsDead,
211
+ reason: status.code !== 0 ? `Circuit's Dead: git status failed (${first(status.stderr)})` : `Circuit's Dead: the working tree is dirty (${dirty} path(s)); reported, not cleaned`
212
+ };
213
+ }
214
+ const refs = await resolveGateExitRefs(git, hasOrigin);
215
+ const unmerged = await listGateExitBranches(git, refs, { unmergedOnly: true });
216
+ if (unmerged.ok && unmerged.branches.length > 0) {
217
+ return {
218
+ ok: false,
219
+ exitCode: COUNTDOWN_EXIT.pending,
220
+ reason: `unmerged gate-exit branch(es) with no marker entry: ${unmerged.branches.join(", ")}; merge or discard them first`
221
+ };
222
+ }
223
+ return { ok: true, pendingBefore: pending.length, base: refs.base };
224
+ }
225
+ function positive(raw, flag, integer) {
226
+ const value = integer ? Number.parseInt(String(raw), 10) : Number.parseFloat(String(raw));
227
+ if (!Number.isFinite(value) || value <= 0 || integer && String(value) !== String(raw).trim()) {
228
+ return `${flag} must be a positive ${integer ? "integer" : "number"}`;
229
+ }
230
+ return value;
231
+ }
232
+ async function executeCountdown(input) {
233
+ const root = path.resolve(input.cwd);
234
+ const env = input.env ?? process.env;
235
+ const exec = input.exec ?? defaultExec;
236
+ const now = input.now ?? /* @__PURE__ */ new Date();
237
+ const write = input.write ?? ((text) => process.stdout.write(text));
238
+ const usage = (error) => ({
239
+ exitCode: COUNTDOWN_EXIT.usage,
240
+ error
241
+ });
242
+ const slash = normalizeSlashName(input.slash ?? "");
243
+ const job = String(input.job).trim();
244
+ for (const name of [slash, job]) {
245
+ if (name && classifySlash(name) === "promote-blocked")
246
+ return usage(promoteBlockedMessage(name));
247
+ }
248
+ if (!COUNTDOWN_JOBS.includes(job)) {
249
+ return usage(`--job must be one of: ${COUNTDOWN_JOBS.join(", ")}`);
250
+ }
251
+ if (job === "dispatch") {
252
+ if (!slash) return usage("--job dispatch needs --slash <name> (e.g. --slash backlog-add)");
253
+ if (classifySlash(slash) === "unknown") return usage(unknownSlashMessage(slash));
254
+ } else if (slash) {
255
+ return usage("--slash only applies to --job dispatch");
256
+ }
257
+ const maxTicks = positive(input.maxTicks, "--max-ticks", true);
258
+ const windowMin = positive(input.windowMin, "--window-min", false);
259
+ const stallMin = positive(input.stallMin, "--stall-min", false);
260
+ for (const v of [maxTicks, windowMin, stallMin]) if (typeof v === "string") return usage(v);
261
+ const packRaw = input.maxPackBytes === void 0 ? "" : String(input.maxPackBytes).trim();
262
+ if (job !== "review" && packRaw) return usage("--max-pack-bytes only applies to --job review");
263
+ if (job !== "review" && input.includeReviewed) {
264
+ return usage("--include-reviewed only applies to --job review");
265
+ }
266
+ const detected = await detectAgentBackend(input.backend, input.whichFn);
267
+ if (!detected.ok) return usage(detected.message);
268
+ let maxPackBytes;
269
+ if (job === "review") {
270
+ if (!REVIEW_BACKENDS.includes(detected.id)) {
271
+ return usage(reviewBackendMessage(detected.id));
272
+ }
273
+ const parsed = parseReviewPackMaxBytes(packRaw, detected.id);
274
+ if (typeof parsed === "string") return usage(parsed);
275
+ maxPackBytes = parsed;
276
+ }
277
+ const statePaths = runnerStatePaths(root, env);
278
+ await ensureLoopLogsDir(statePaths).catch(() => void 0);
279
+ const redactions = gateExitRedactions(env);
280
+ const red = (text) => redactSecrets(text, redactions);
281
+ const buffered = [];
282
+ let logPath = null;
283
+ let logChain = Promise.resolve();
284
+ const toLog = (text) => {
285
+ if (!logPath) {
286
+ buffered.push(text);
287
+ return;
288
+ }
289
+ const file = logPath;
290
+ logChain = logChain.then(() => appendFile(file, text, "utf8").catch(() => void 0));
291
+ };
292
+ const say = (line) => {
293
+ const text = `${red(`countdown: ${line}`)}
294
+ `;
295
+ write(text);
296
+ toLog(text);
297
+ };
298
+ const openLog = async () => {
299
+ await ensureLoopLogsDir(statePaths);
300
+ const file = path.join(statePaths.loopLogsDir, `countdown-${gateExitStamp(now)}.log`);
301
+ await appendFile(file, buffered.join(""), "utf8");
302
+ buffered.length = 0;
303
+ logPath = file;
304
+ };
305
+ const jobLabel = job === "tick" ? "run-plan" : job === "review" ? `run ${REVIEW_PACK_SLASH}` : `run ${slash}`;
306
+ say(`fire ${gateExitStamp(now)}: job ${job} (${jobLabel}) on ${detected.id}`);
307
+ const preflight = await countdownPreflight({ root, env, exec, now, log: say });
308
+ if (!preflight.ok) {
309
+ say(`refused: ${preflight.reason} (exit ${preflight.exitCode})`);
310
+ await openLog().catch(() => void 0);
311
+ return { exitCode: preflight.exitCode, ...logPath ? { logPath } : {} };
312
+ }
313
+ await openLog();
314
+ const capLines = [CLAUDE_MAX_TURNS_ENV, CLAUDE_MAX_BUDGET_USD_ENV].filter((k) => env[k]).map((k) => `${k}=${env[k]}`);
315
+ const sizeCap = job === "review" ? `pack ceiling ${maxPackBytes} bytes${input.includeReviewed ? ", including branches already reviewed" : ""}` : `max ticks ${maxTicks}`;
316
+ say(
317
+ `caps: window ${windowMin} min, stall ${stallMin} min, ${sizeCap}${capLines.length ? `, ${capLines.join(", ")}` : ""}`
318
+ );
319
+ const args = countdownChildArgs({
320
+ job,
321
+ slash,
322
+ backend: detected.id,
323
+ maxTicks,
324
+ ...input.model ? { model: input.model } : {},
325
+ ...maxPackBytes !== void 0 ? { maxPackBytes } : {},
326
+ ...job === "review" && input.includeReviewed ? { includeReviewed: true } : {}
327
+ });
328
+ say(`starting: agent-kit ${args.join(" ")}`);
329
+ const handle = (input.spawnJob ?? defaultSpawnJob)(args, { cwd: root, env });
330
+ const forward = (signal) => handle.kill(signal);
331
+ const onSigint = () => forward("SIGINT");
332
+ const onSigterm = () => forward("SIGTERM");
333
+ if (input.forwardSignals) {
334
+ process.on("SIGINT", onSigint);
335
+ process.on("SIGTERM", onSigterm);
336
+ }
337
+ const jobOut = new RedactingStreamBuffer(redactions);
338
+ const emitJob = (out) => {
339
+ if (!out) return;
340
+ write(out);
341
+ toLog(out);
342
+ };
343
+ let end;
344
+ try {
345
+ end = await superviseJob(handle, {
346
+ windowMs: windowMin * 6e4,
347
+ stallMs: stallMin * 6e4,
348
+ killGraceMs: input.killGraceMs ?? COUNTDOWN_KILL_GRACE_MS,
349
+ onData: (text) => emitJob(jobOut.push(text)),
350
+ onCircuitsDead: (cause) => say(
351
+ cause === "window" ? `Circuit's Dead: the ${windowMin} min window is over; stopping the job (SIGTERM, then SIGKILL)` : `Circuit's Dead: no stream event for ${stallMin} min; stopping the job (SIGTERM, then SIGKILL)`
352
+ )
353
+ });
354
+ } finally {
355
+ if (input.forwardSignals) {
356
+ process.off("SIGINT", onSigint);
357
+ process.off("SIGTERM", onSigterm);
358
+ }
359
+ emitJob(jobOut.flush());
360
+ }
361
+ const after = await readGateExitMarker(root);
362
+ const pendingAfter = after.kind === "ok" ? after.marker.pending.length : preflight.pendingBefore;
363
+ const pendingGrew = pendingAfter > preflight.pendingBefore;
364
+ const exitCode = countdownExitCode({
365
+ code: end.code,
366
+ circuitsDead: end.circuitsDead,
367
+ pendingGrew
368
+ });
369
+ if (exitCode === COUNTDOWN_EXIT.circuitsDead && !pendingGrew) {
370
+ const reason = end.circuitsDead ? `Circuit's Dead: ${end.circuitsDead === "window" ? `window of ${windowMin} min exceeded` : `no stream event for ${stallMin} min`}` : `Circuit's Dead: job exited with ${end.code === null ? `signal ${end.signal ?? "unknown"}` : `code ${end.code}`}`;
371
+ const outcome = {
372
+ trigger: "failure",
373
+ exitCode,
374
+ reason,
375
+ command: `countdown --job ${job}${job === "dispatch" ? ` --slash ${slash}` : ""}`,
376
+ backend: detected.id,
377
+ ...logPath ? { logPath } : {}
378
+ };
379
+ try {
380
+ await (input.land ?? landGateExit)({ root, outcome, env, log: (line) => say(line) });
381
+ } catch (err) {
382
+ say(`gate-exit: landing failed: ${String(err)}`);
383
+ }
384
+ }
385
+ const meaning = exitCode === 0 ? "completed" : exitCode === COUNTDOWN_EXIT.pending ? job === "review" && end.code === 0 ? "review findings landed; gate exit pending" : "gate reached; gate exit pending" : "Circuit's Dead";
386
+ say(`exit ${exitCode} (${meaning})`);
387
+ await logChain;
388
+ return { exitCode, ...logPath ? { logPath } : {} };
389
+ }
390
+ var countdownCommand = defineCommand({
391
+ meta: {
392
+ name: "countdown",
393
+ description: "Countdown: one-shot launcher for a cron/systemd timer. Runs one headless job (tick, dispatch or review) with --no-hitl --land-on-gate; refused while a gate exit is pending. Exit 0 done, 2 gate or findings pending, 3 Circuit's Dead. Never git-prod."
394
+ },
395
+ args: {
396
+ cwd: {
397
+ type: "string",
398
+ description: "Project root (default: current directory)",
399
+ default: process.cwd()
400
+ },
401
+ job: {
402
+ type: "string",
403
+ description: "tick (one run-plan tick, default) | dispatch (agent-kit run --slash <name>) | review (review-only pass over a bounded diff pack of unmerged branches; findings land as a gate exit)",
404
+ default: "tick"
405
+ },
406
+ slash: {
407
+ type: "string",
408
+ description: "For --job dispatch: the L0 slash to run (git-prod and kit-prod are refused)",
409
+ default: ""
410
+ },
411
+ backend: {
412
+ type: "string",
413
+ description: `Agent backend (${listDetectBackendIds().join(" | ")}; default: auto)`,
414
+ default: "auto"
415
+ },
416
+ model: {
417
+ type: "string",
418
+ description: "Optional model id passed to the agent backend",
419
+ default: ""
420
+ },
421
+ "max-ticks": {
422
+ type: "string",
423
+ description: `Maximum ticks for this fire (default: ${COUNTDOWN_DEFAULT_MAX_TICKS})`,
424
+ default: String(COUNTDOWN_DEFAULT_MAX_TICKS)
425
+ },
426
+ "max-pack-bytes": {
427
+ type: "string",
428
+ description: `For --job review: diff pack ceiling in bytes (default: ${REVIEW_PACK_DEFAULT_MAX_BYTES}; a branch whose section does not fit is listed as not reviewed, never cut)`,
429
+ default: ""
430
+ },
431
+ "include-reviewed": {
432
+ type: "boolean",
433
+ description: "For --job review: also review branches whose tip is unchanged since the last completed review",
434
+ default: false
435
+ },
436
+ "window-min": {
437
+ type: "string",
438
+ description: `Wall-clock cap in minutes; past it the job is stopped as Circuit's Dead (default: ${COUNTDOWN_DEFAULT_WINDOW_MIN})`,
439
+ default: String(COUNTDOWN_DEFAULT_WINDOW_MIN)
440
+ },
441
+ "stall-min": {
442
+ type: "string",
443
+ description: `Minutes with no stream event before the job is stopped as Circuit's Dead (default: ${COUNTDOWN_DEFAULT_STALL_MIN})`,
444
+ default: String(COUNTDOWN_DEFAULT_STALL_MIN)
445
+ }
446
+ },
447
+ async run({ args }) {
448
+ const result = await executeCountdown({
449
+ cwd: String(args.cwd),
450
+ job: String(args.job),
451
+ slash: String(args.slash ?? ""),
452
+ backend: String(args.backend),
453
+ ...args.model ? { model: String(args.model) } : {},
454
+ maxTicks: String(args["max-ticks"]),
455
+ ...args["max-pack-bytes"] ? { maxPackBytes: String(args["max-pack-bytes"]) } : {},
456
+ ...args["include-reviewed"] ? { includeReviewed: true } : {},
457
+ windowMin: String(args["window-min"]),
458
+ stallMin: String(args["stall-min"]),
459
+ forwardSignals: true
460
+ });
461
+ if (result.error) logger.error(result.error);
462
+ process.exitCode = result.exitCode;
463
+ }
464
+ });
465
+ export {
466
+ COUNTDOWN_DEFAULT_MAX_TICKS,
467
+ COUNTDOWN_DEFAULT_STALL_MIN,
468
+ COUNTDOWN_DEFAULT_WINDOW_MIN,
469
+ COUNTDOWN_EXIT,
470
+ COUNTDOWN_JOBS,
471
+ COUNTDOWN_KILL_GRACE_MS,
472
+ countdownChildArgs,
473
+ countdownCommand,
474
+ countdownExitCode,
475
+ countdownPreflight,
476
+ defaultSpawnJob,
477
+ executeCountdown,
478
+ superviseJob
479
+ };