@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.
- package/dashboard/README.md +3 -1
- package/dashboard/dashboard-data.mjs +13 -2
- package/dashboard/dashboard.html +95 -2
- package/dashboard/lib/gate-exit-tickets.d.mts +45 -0
- package/dashboard/lib/gate-exit-tickets.mjs +233 -0
- package/dashboard/lib/semantic-model.d.mts +21 -0
- package/dashboard/lib/semantic-model.mjs +47 -0
- package/dist/{add-YTCDLPUS.js → add-VA2W7I6P.js} +3 -3
- package/dist/{chunk-VTOUQAVR.js → chunk-25J2SMJQ.js} +9 -1
- package/dist/{chunk-2FQX2O4I.js → chunk-6GYKOHIL.js} +2 -2
- package/dist/{chunk-DBPNGZNH.js → chunk-6KUSG7BJ.js} +15 -2
- package/dist/{chunk-C26I6HTO.js → chunk-C4Q77GGJ.js} +0 -1
- package/dist/{chunk-U5F6RJN4.js → chunk-GO3K752H.js} +4 -1
- package/dist/chunk-GUQYNUV4.js +61 -0
- package/dist/{chunk-ESNAFDYT.js → chunk-JEI6HACW.js} +44 -1
- package/dist/chunk-KVY425NM.js +519 -0
- package/dist/{chunk-WUWRGOUV.js → chunk-L5V3W3DY.js} +13 -1
- package/dist/chunk-MO63KZBP.js +516 -0
- package/dist/chunk-MUTTNRX4.js +53 -0
- package/dist/{chunk-PPKX55GI.js → chunk-NNKOWUWM.js} +1 -1
- package/dist/{chunk-DRND2KYV.js → chunk-OVOXRH5W.js} +1 -1
- package/dist/{chunk-GN4SAZF2.js → chunk-OWWFKGKS.js} +2 -2
- package/dist/chunk-QTBTKEXK.js +77 -0
- package/dist/{chunk-TMFOLRU2.js → chunk-SV2BR2LZ.js} +44 -9
- package/dist/{chunk-3DGTPONS.js → chunk-U26MRIWN.js} +1 -1
- package/dist/{chunk-BBVBYQJO.js → chunk-VQBI3JVS.js} +2 -2
- package/dist/{chunk-YP4OOK4T.js → chunk-WMVMBPCR.js} +2 -2
- package/dist/{chunk-OZYJUNAQ.js → chunk-YRFSTHR5.js} +29 -525
- package/dist/{chunk-3XKVQS2K.js → chunk-Z7ABX5QI.js} +13 -9
- package/dist/{chunk-DBXDV5O7.js → chunk-Z7RYIHHX.js} +90 -5
- package/dist/{contribute-A7NCWTUM.js → contribute-NLM2BN6Z.js} +4 -4
- package/dist/countdown-QXJD6TDB.js +479 -0
- package/dist/{diff-4MDU5SLE.js → diff-HHG4JEDE.js} +5 -5
- package/dist/{doctor-SS6Y2T6H.js → doctor-TWF2M3S3.js} +9 -9
- package/dist/ground-control-NMYYIVCV.js +434 -0
- package/dist/{handoff-MK5JG3J5.js → handoff-5AGE2D3T.js} +3 -2
- package/dist/{hook-D2WHBKRN.js → hook-VP7FHRSZ.js} +7 -6
- package/dist/index.js +30 -20
- package/dist/{init-GVVBA2BM.js → init-UOTV4ASJ.js} +12 -12
- package/dist/{install-R4HPI7VK.js → install-VCHPXQV3.js} +12 -12
- package/dist/{plan-index-V7T5MKL6.js → plan-index-BNLUU6T5.js} +3 -2
- package/dist/{run-3B26CVIL.js → run-R66G276Q.js} +58 -12
- package/dist/{run-plan-APTBO2OT.js → run-plan-24Z5K4P3.js} +12 -7
- package/dist/{scan-K6TQGV4S.js → scan-UX4HARMJ.js} +1 -1
- package/dist/{setup-global-AT4DD3MG.js → setup-global-M6LKGJHQ.js} +6 -6
- package/dist/{status-MPJHI5RP.js → status-5UKVIKOI.js} +8 -8
- package/dist/{update-P62637ZI.js → update-VQBGEUWS.js} +33 -43
- 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-
|
|
16
|
+
} from "./chunk-JEI6HACW.js";
|
|
15
17
|
import {
|
|
16
18
|
ensureLoopLogsDir,
|
|
17
19
|
runnerStatePaths
|
|
18
|
-
} from "./chunk-
|
|
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
|
|
156
|
-
|
|
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-
|
|
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-
|
|
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
|
|
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 =
|
|
111
|
+
const legacyPath = path2.join(projectRoot, LEGACY_ONBOARD_PATH);
|
|
31
112
|
if (!await fileExists(legacyPath)) return "absent";
|
|
32
|
-
const namespacedPath =
|
|
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-
|
|
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-
|
|
10
|
+
} from "./chunk-25J2SMJQ.js";
|
|
11
11
|
import {
|
|
12
12
|
loadAgentKitManifest,
|
|
13
13
|
normalizeRelPath,
|
|
14
14
|
resolveContained
|
|
15
|
-
} from "./chunk-
|
|
16
|
-
import "./chunk-
|
|
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
|
+
};
|