@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
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ensurePathIgnored
|
|
3
|
+
} from "./chunk-MUTTNRX4.js";
|
|
4
|
+
import {
|
|
5
|
+
claudeRedactions,
|
|
6
|
+
findHitlGateInText,
|
|
7
|
+
redactSecrets
|
|
8
|
+
} from "./chunk-JEI6HACW.js";
|
|
9
|
+
import {
|
|
10
|
+
findOnPath
|
|
11
|
+
} from "./chunk-WGLPZXNU.js";
|
|
12
|
+
|
|
13
|
+
// src/plan-loop/gate-exit.ts
|
|
14
|
+
import { execFile } from "child_process";
|
|
15
|
+
import { mkdir, mkdtemp, readFile, rename, rm, writeFile } from "fs/promises";
|
|
16
|
+
import { tmpdir } from "os";
|
|
17
|
+
import path from "path";
|
|
18
|
+
var GATE_EXIT_BRANCH_PREFIX = "gate-exit/";
|
|
19
|
+
var GATE_EXIT_MARKER_RELPATH = ".cursor/context/gate-exit.json";
|
|
20
|
+
var GATE_EXIT_NOTE_DIR = ".cursor/gate-exit";
|
|
21
|
+
var GATE_EXIT_DEFAULT_BASE = "staging";
|
|
22
|
+
var GATE_EXIT_MARKER_SCHEMA = 1;
|
|
23
|
+
var GATE_EXIT_STAMP_RE = /^\d{8}T\d{6}Z$/;
|
|
24
|
+
var GATE_EXIT_SUMMARY_MAX_CHARS = 8 * 1024;
|
|
25
|
+
var GATE_EXIT_FINDINGS_MAX_CHARS = 32 * 1024;
|
|
26
|
+
var SAFE_REF_RE = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/;
|
|
27
|
+
var FALLBACK_GIT_IDENTITY = [
|
|
28
|
+
"-c",
|
|
29
|
+
"user.name=agent-kit",
|
|
30
|
+
"-c",
|
|
31
|
+
"user.email=agent-kit@noreply.invalid"
|
|
32
|
+
];
|
|
33
|
+
var LAND_ON_GATE_DESCRIPTION = "Gate exit: when the run stops at an unanswered gate, is stopped by the operator, or fails, push the outcome note to a gate-exit/<UTC stamp> branch from a temporary worktree and open a PR/MR when gh/glab is authenticated (root tree untouched; off by default)";
|
|
34
|
+
function gateExitStamp(now = /* @__PURE__ */ new Date()) {
|
|
35
|
+
return now.toISOString().replace(/\.\d{3}Z$/, "Z").replace(/[-:]/g, "");
|
|
36
|
+
}
|
|
37
|
+
function isGateExitStamp(value) {
|
|
38
|
+
return GATE_EXIT_STAMP_RE.test(value);
|
|
39
|
+
}
|
|
40
|
+
function gateExitBranch(stamp) {
|
|
41
|
+
if (!isGateExitStamp(stamp)) throw new Error(`invalid gate-exit stamp: ${stamp}`);
|
|
42
|
+
return `${GATE_EXIT_BRANCH_PREFIX}${stamp}`;
|
|
43
|
+
}
|
|
44
|
+
function isSafeBaseRef(ref) {
|
|
45
|
+
return SAFE_REF_RE.test(ref) && !ref.includes("..") && !ref.endsWith("/") && !ref.endsWith(".lock");
|
|
46
|
+
}
|
|
47
|
+
function classifyGateExit(input) {
|
|
48
|
+
if (input.stop) {
|
|
49
|
+
return input.stop.message.startsWith("stopped-by-operator") ? "stopped-by-operator" : "gate";
|
|
50
|
+
}
|
|
51
|
+
return input.failed ? "failure" : null;
|
|
52
|
+
}
|
|
53
|
+
function gateExitMarkerPath(root) {
|
|
54
|
+
return path.join(root, GATE_EXIT_MARKER_RELPATH);
|
|
55
|
+
}
|
|
56
|
+
async function readGateExitMarker(root) {
|
|
57
|
+
let raw;
|
|
58
|
+
try {
|
|
59
|
+
raw = await readFile(gateExitMarkerPath(root), "utf8");
|
|
60
|
+
} catch (err) {
|
|
61
|
+
if (err.code === "ENOENT") return { kind: "absent" };
|
|
62
|
+
return { kind: "invalid", detail: String(err) };
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const parsed = JSON.parse(raw);
|
|
66
|
+
if (parsed?.schema !== GATE_EXIT_MARKER_SCHEMA || !Array.isArray(parsed.pending) || !Array.isArray(parsed.skipped)) {
|
|
67
|
+
return { kind: "invalid", detail: "unexpected marker shape" };
|
|
68
|
+
}
|
|
69
|
+
const pending = parsed.pending.filter(
|
|
70
|
+
(e) => typeof e?.stamp === "string" && isGateExitStamp(e.stamp)
|
|
71
|
+
);
|
|
72
|
+
return {
|
|
73
|
+
kind: "ok",
|
|
74
|
+
marker: { schema: GATE_EXIT_MARKER_SCHEMA, pending, skipped: parsed.skipped.map(String) }
|
|
75
|
+
};
|
|
76
|
+
} catch (err) {
|
|
77
|
+
return { kind: "invalid", detail: String(err) };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async function writeGateExitMarker(root, marker) {
|
|
81
|
+
const file = gateExitMarkerPath(root);
|
|
82
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
83
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
84
|
+
await writeFile(tmp, `${JSON.stringify(marker, null, 2)}
|
|
85
|
+
`, "utf8");
|
|
86
|
+
await rename(tmp, file);
|
|
87
|
+
}
|
|
88
|
+
function gateExitRedactions(env) {
|
|
89
|
+
return claudeRedactions(env);
|
|
90
|
+
}
|
|
91
|
+
function gateExitHeadline(stamp, outcome) {
|
|
92
|
+
const what = outcome.trigger === "failure" ? "run failed" : outcome.trigger === "review" ? "review findings" : outcome.trigger === "stopped-by-operator" ? `stopped by operator at gate \`${outcome.askId ?? "none"}\`` : `unanswered gate \`${outcome.askId ?? "none"}\``;
|
|
93
|
+
return `# Gate exit ${stamp}: ${what}`;
|
|
94
|
+
}
|
|
95
|
+
function buildGateExitNote(input) {
|
|
96
|
+
const { outcome } = input;
|
|
97
|
+
const gateLine = outcome.askId && input.labels.length > 0 ? `\`HITL_GATE: ${outcome.askId} | ${input.labels.join(" | ")}\`` : "(no `HITL_GATE:` line recorded)";
|
|
98
|
+
const plan = outcome.plan !== void 0 ? `${outcome.plan} (ticks: ${outcome.ticks ?? 0}, pending to-dos: ${outcome.pending ?? "unknown"})` : "(none)";
|
|
99
|
+
const review = outcome.trigger === "review";
|
|
100
|
+
const lines = [
|
|
101
|
+
gateExitHeadline(input.stamp, outcome),
|
|
102
|
+
"",
|
|
103
|
+
review ? "- **Provenance:** agent-inferred. Written by `agent-kit` at the end of a headless review-only run (no tools, no edits, no commits to the reviewed branches); the findings below are the reviewer's output, not an operator decision." : "- **Provenance:** agent-inferred. Written by `agent-kit` at the end of a headless run; no operator reply exists for this gate and none was inferred.",
|
|
104
|
+
`- **Trigger:** ${outcome.trigger}`,
|
|
105
|
+
`- **Ask id:** ${outcome.askId ? `\`${outcome.askId}\`` : "(none)"}`,
|
|
106
|
+
`- **Gate:** ${gateLine}`,
|
|
107
|
+
`- **Stop:** ${outcome.reason}`,
|
|
108
|
+
`- **Exit code:** ${outcome.exitCode}`,
|
|
109
|
+
`- **Command:** \`agent-kit ${outcome.command}\` (backend ${outcome.backend})`,
|
|
110
|
+
`- **Plan:** ${plan}`,
|
|
111
|
+
`- **Log:** ${input.logRel ? `\`${input.logRel}\` (on the machine that ran; not pushed)` : "(none)"}`,
|
|
112
|
+
`- **Root tree:** ${input.dirtyPaths > 0 ? `dirty (${input.dirtyPaths} path(s)), reported and not cleaned` : "clean"}`,
|
|
113
|
+
`- **Base:** \`${input.base}\``,
|
|
114
|
+
"",
|
|
115
|
+
review ? "Next: read the findings, act on any of them in a live session, then accept or discard this ticket (`agent-kit ground-control`). Never `/git-prod` from a headless run." : "Next: answer the gate in a live session (`/continue-plan` or `/run-plan`), then accept or discard this ticket (`agent-kit ground-control`). Never `/git-prod` from a headless run."
|
|
116
|
+
];
|
|
117
|
+
const summary = outcome.summary?.trim();
|
|
118
|
+
if (summary) {
|
|
119
|
+
const max = review ? GATE_EXIT_FINDINGS_MAX_CHARS : GATE_EXIT_SUMMARY_MAX_CHARS;
|
|
120
|
+
const what = review ? "findings" : "summary";
|
|
121
|
+
const capped = summary.length > max ? `${summary.slice(0, max)}
|
|
122
|
+
|
|
123
|
+
(${what} truncated at ${max} characters)` : summary;
|
|
124
|
+
lines.push(
|
|
125
|
+
"",
|
|
126
|
+
review ? "## Findings" : "## Run summary",
|
|
127
|
+
"",
|
|
128
|
+
"```text",
|
|
129
|
+
capped.replaceAll("```", "'''"),
|
|
130
|
+
"```"
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
return `${lines.join("\n")}
|
|
134
|
+
`;
|
|
135
|
+
}
|
|
136
|
+
function collectStrings(value, out, depth) {
|
|
137
|
+
if (depth > 8) return;
|
|
138
|
+
if (typeof value === "string") out.push(value);
|
|
139
|
+
else if (Array.isArray(value)) for (const v of value) collectStrings(v, out, depth + 1);
|
|
140
|
+
else if (value && typeof value === "object") {
|
|
141
|
+
for (const v of Object.values(value)) collectStrings(v, out, depth + 1);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function decodeRunLog(raw) {
|
|
145
|
+
const out = [];
|
|
146
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
147
|
+
out.push(line);
|
|
148
|
+
if (!line.startsWith("{")) continue;
|
|
149
|
+
try {
|
|
150
|
+
collectStrings(JSON.parse(line), out, 0);
|
|
151
|
+
} catch {
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return out.join("\n");
|
|
155
|
+
}
|
|
156
|
+
async function findGateLabels(askId, sources) {
|
|
157
|
+
const fromText = sources.gateText ? findHitlGateInText(sources.gateText) : null;
|
|
158
|
+
if (fromText?.askId === askId) return fromText.labels;
|
|
159
|
+
if (!sources.logPath) return [];
|
|
160
|
+
try {
|
|
161
|
+
const gate = findHitlGateInText(decodeRunLog(await readFile(sources.logPath, "utf8")));
|
|
162
|
+
return gate?.askId === askId ? gate.labels : [];
|
|
163
|
+
} catch {
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
var defaultExec = (bin, args, opts) => new Promise((resolve) => {
|
|
168
|
+
execFile(
|
|
169
|
+
bin,
|
|
170
|
+
[...args],
|
|
171
|
+
{
|
|
172
|
+
cwd: opts.cwd,
|
|
173
|
+
env: { ...opts.env ?? process.env, GIT_TERMINAL_PROMPT: "0" },
|
|
174
|
+
encoding: "utf8",
|
|
175
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
176
|
+
timeout: 12e4
|
|
177
|
+
},
|
|
178
|
+
(err, stdout, stderr) => {
|
|
179
|
+
const code = err ? typeof err.code === "number" ? err.code : 1 : 0;
|
|
180
|
+
resolve({
|
|
181
|
+
code,
|
|
182
|
+
stdout: String(stdout ?? ""),
|
|
183
|
+
stderr: String(stderr ?? err?.message ?? "")
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
);
|
|
187
|
+
});
|
|
188
|
+
async function resolveGateExitRefs(git, hasOrigin, requestedBase = GATE_EXIT_DEFAULT_BASE) {
|
|
189
|
+
let base = isSafeBaseRef(requestedBase) ? requestedBase : GATE_EXIT_DEFAULT_BASE;
|
|
190
|
+
const verify = (ref) => git(["rev-parse", "--verify", "--quiet", `${ref}^{commit}`]);
|
|
191
|
+
if (hasOrigin) {
|
|
192
|
+
const scope2 = `refs/remotes/origin/${GATE_EXIT_BRANCH_PREFIX}`;
|
|
193
|
+
if ((await verify(`refs/remotes/origin/${base}`)).code === 0) {
|
|
194
|
+
return { base, baseRef: `refs/remotes/origin/${base}`, scope: scope2 };
|
|
195
|
+
}
|
|
196
|
+
const head = await git(["symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD"]);
|
|
197
|
+
const fallback = head.stdout.trim().replace(/^origin\//, "");
|
|
198
|
+
if (head.code === 0 && isSafeBaseRef(fallback)) {
|
|
199
|
+
base = fallback;
|
|
200
|
+
return { base, baseRef: `refs/remotes/origin/${fallback}`, scope: scope2 };
|
|
201
|
+
}
|
|
202
|
+
return { base, baseRef: "HEAD", scope: scope2 };
|
|
203
|
+
}
|
|
204
|
+
const scope = `refs/heads/${GATE_EXIT_BRANCH_PREFIX}`;
|
|
205
|
+
if ((await verify(`refs/heads/${base}`)).code === 0) {
|
|
206
|
+
return { base, baseRef: `refs/heads/${base}`, scope };
|
|
207
|
+
}
|
|
208
|
+
return { base, baseRef: "HEAD", scope };
|
|
209
|
+
}
|
|
210
|
+
async function listGateExitBranches(git, refs, opts = {}) {
|
|
211
|
+
const listed = await git([
|
|
212
|
+
"for-each-ref",
|
|
213
|
+
"--format=%(refname:short)",
|
|
214
|
+
...opts.unmergedOnly ? [`--no-merged=${refs.baseRef}`] : [],
|
|
215
|
+
refs.scope
|
|
216
|
+
]);
|
|
217
|
+
return {
|
|
218
|
+
ok: listed.code === 0,
|
|
219
|
+
branches: listed.stdout.split("\n").filter((l) => l.trim())
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
function gateExitNotePath(stamp) {
|
|
223
|
+
if (!isGateExitStamp(stamp)) throw new Error(`invalid gate-exit stamp: ${stamp}`);
|
|
224
|
+
return `${GATE_EXIT_NOTE_DIR}/${stamp}.md`;
|
|
225
|
+
}
|
|
226
|
+
function isOutside(parent, child) {
|
|
227
|
+
const rel = path.relative(parent, child);
|
|
228
|
+
return rel.startsWith("..") || path.isAbsolute(rel);
|
|
229
|
+
}
|
|
230
|
+
async function landGateExit(opts) {
|
|
231
|
+
const env = opts.env ?? process.env;
|
|
232
|
+
const exec = opts.exec ?? defaultExec;
|
|
233
|
+
const which = opts.which ?? ((bin) => findOnPath(bin, { env }));
|
|
234
|
+
const redactions = gateExitRedactions(env);
|
|
235
|
+
const red = (text) => redactSecrets(text, redactions);
|
|
236
|
+
const log = (line) => opts.log?.(red(`gate-exit: ${line}`));
|
|
237
|
+
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
238
|
+
const stamp = gateExitStamp(now);
|
|
239
|
+
const branch = gateExitBranch(stamp);
|
|
240
|
+
const outcome = opts.outcome;
|
|
241
|
+
const requestedBase = opts.base ?? GATE_EXIT_DEFAULT_BASE;
|
|
242
|
+
const result = {
|
|
243
|
+
stamp,
|
|
244
|
+
branch: null,
|
|
245
|
+
committed: false,
|
|
246
|
+
pushed: false,
|
|
247
|
+
url: null,
|
|
248
|
+
markerWritten: false,
|
|
249
|
+
note: ""
|
|
250
|
+
};
|
|
251
|
+
const git = (args, cwd) => exec("git", args, { cwd, env });
|
|
252
|
+
const labels = outcome.askId ? await findGateLabels(outcome.askId, { gateText: outcome.gateText, logPath: outcome.logPath }) : [];
|
|
253
|
+
const top = await git(["rev-parse", "--show-toplevel"], opts.root);
|
|
254
|
+
const repo = top.code === 0 ? top.stdout.trim() : null;
|
|
255
|
+
let dirtyPaths = 0;
|
|
256
|
+
if (repo) {
|
|
257
|
+
const status = await git(["status", "--porcelain"], repo);
|
|
258
|
+
dirtyPaths = status.stdout.split("\n").filter((l) => l.trim()).length;
|
|
259
|
+
if (dirtyPaths > 0) {
|
|
260
|
+
log(`root tree is dirty (${dirtyPaths} path(s)); reported, not cleaned`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
let base = isSafeBaseRef(requestedBase) ? requestedBase : GATE_EXIT_DEFAULT_BASE;
|
|
264
|
+
const logRel = outcome.logPath ? path.relative(opts.root, outcome.logPath) : null;
|
|
265
|
+
const buildNote = () => red(buildGateExitNote({ stamp, outcome, labels, base, logRel, dirtyPaths }));
|
|
266
|
+
const writeMarker = async () => {
|
|
267
|
+
const entry = {
|
|
268
|
+
stamp,
|
|
269
|
+
openedAt: now.toISOString(),
|
|
270
|
+
trigger: outcome.trigger,
|
|
271
|
+
askId: outcome.askId ?? null,
|
|
272
|
+
labels: labels.map(red),
|
|
273
|
+
base,
|
|
274
|
+
branch: result.branch,
|
|
275
|
+
pushed: result.pushed,
|
|
276
|
+
url: result.url
|
|
277
|
+
};
|
|
278
|
+
const current = await readGateExitMarker(opts.root);
|
|
279
|
+
if (current.kind === "invalid") {
|
|
280
|
+
log(
|
|
281
|
+
`${GATE_EXIT_MARKER_RELPATH} is unreadable (${current.detail}); left as is, entry ${stamp} not recorded`
|
|
282
|
+
);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
const marker = current.kind === "ok" ? { ...current.marker, pending: [...current.marker.pending, entry] } : { schema: GATE_EXIT_MARKER_SCHEMA, pending: [entry], skipped: [] };
|
|
286
|
+
if (repo && !await ensurePathIgnored(
|
|
287
|
+
(args) => git(args, opts.root),
|
|
288
|
+
opts.root,
|
|
289
|
+
GATE_EXIT_MARKER_RELPATH
|
|
290
|
+
)) {
|
|
291
|
+
log(
|
|
292
|
+
`${GATE_EXIT_MARKER_RELPATH} is not gitignored here; add it to .gitignore (agent-kit update adds it)`
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
try {
|
|
296
|
+
await writeGateExitMarker(opts.root, marker);
|
|
297
|
+
result.markerWritten = true;
|
|
298
|
+
} catch (err) {
|
|
299
|
+
log(`could not write ${GATE_EXIT_MARKER_RELPATH}: ${String(err)}`);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
if (!repo) {
|
|
304
|
+
log("not a git repository; no branch landed");
|
|
305
|
+
result.note = buildNote();
|
|
306
|
+
await writeMarker();
|
|
307
|
+
return result;
|
|
308
|
+
}
|
|
309
|
+
const origin = await git(["remote", "get-url", "origin"], repo);
|
|
310
|
+
const hasOrigin = origin.code === 0 && origin.stdout.trim().length > 0;
|
|
311
|
+
const originUrl = hasOrigin ? origin.stdout.trim() : "";
|
|
312
|
+
let startRef = null;
|
|
313
|
+
if (hasOrigin) {
|
|
314
|
+
const fetched = await git(["fetch", "--quiet", "origin", base], repo);
|
|
315
|
+
if (fetched.code !== 0)
|
|
316
|
+
log(`fetch origin ${base} failed: ${fetched.stderr.trim().split("\n")[0] ?? ""}`);
|
|
317
|
+
const verify = await git(
|
|
318
|
+
["rev-parse", "--verify", "--quiet", `refs/remotes/origin/${base}^{commit}`],
|
|
319
|
+
repo
|
|
320
|
+
);
|
|
321
|
+
if (verify.code === 0) {
|
|
322
|
+
startRef = `refs/remotes/origin/${base}`;
|
|
323
|
+
} else {
|
|
324
|
+
const head = await git(
|
|
325
|
+
["symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD"],
|
|
326
|
+
repo
|
|
327
|
+
);
|
|
328
|
+
const fallback = head.stdout.trim().replace(/^origin\//, "");
|
|
329
|
+
if (head.code === 0 && isSafeBaseRef(fallback)) {
|
|
330
|
+
log(`origin/${base} not found; using the remote default branch ${fallback}`);
|
|
331
|
+
base = fallback;
|
|
332
|
+
startRef = `refs/remotes/origin/${fallback}`;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
} else {
|
|
336
|
+
log("no `origin` remote: the branch stays local, nothing is pushed");
|
|
337
|
+
const local = await git(
|
|
338
|
+
["rev-parse", "--verify", "--quiet", `refs/heads/${base}^{commit}`],
|
|
339
|
+
repo
|
|
340
|
+
);
|
|
341
|
+
startRef = local.code === 0 ? `refs/heads/${base}` : "HEAD";
|
|
342
|
+
}
|
|
343
|
+
result.note = buildNote();
|
|
344
|
+
if (!startRef) {
|
|
345
|
+
log(`no base to branch from (origin/${base} missing); note not landed`);
|
|
346
|
+
await writeMarker();
|
|
347
|
+
return result;
|
|
348
|
+
}
|
|
349
|
+
let tmp = null;
|
|
350
|
+
let wt = null;
|
|
351
|
+
try {
|
|
352
|
+
tmp = await mkdtemp(path.join(opts.tmpRoot ?? tmpdir(), "agent-kit-gate-exit-"));
|
|
353
|
+
if (!isOutside(repo, tmp)) {
|
|
354
|
+
log("temporary directory resolves inside the repository; refusing to land there");
|
|
355
|
+
return result;
|
|
356
|
+
}
|
|
357
|
+
wt = path.join(tmp, "wt");
|
|
358
|
+
const added = await git(["worktree", "add", "--detach", "--quiet", wt, startRef], repo);
|
|
359
|
+
if (added.code !== 0) {
|
|
360
|
+
wt = null;
|
|
361
|
+
log(`worktree add failed: ${added.stderr.trim().split("\n")[0] ?? ""}`);
|
|
362
|
+
return result;
|
|
363
|
+
}
|
|
364
|
+
const noteRel = gateExitNotePath(stamp);
|
|
365
|
+
await mkdir(path.join(wt, GATE_EXIT_NOTE_DIR), { recursive: true });
|
|
366
|
+
await writeFile(path.join(wt, noteRel), result.note, "utf8");
|
|
367
|
+
const add = await git(["add", "-f", "--", noteRel], wt);
|
|
368
|
+
if (add.code !== 0) {
|
|
369
|
+
log(`git add failed: ${add.stderr.trim().split("\n")[0] ?? ""}`);
|
|
370
|
+
return result;
|
|
371
|
+
}
|
|
372
|
+
const email = await git(["config", "user.email"], wt);
|
|
373
|
+
const identity = email.code === 0 && email.stdout.trim() ? [] : FALLBACK_GIT_IDENTITY;
|
|
374
|
+
const subject = `docs(gate-exit): record the ${outcome.trigger} of ${stamp}`;
|
|
375
|
+
const body = `Ask id: ${outcome.askId ?? "none"}. Provenance: agent-inferred.`;
|
|
376
|
+
const commit = await git([...identity, "commit", "--quiet", "-m", subject, "-m", body], wt);
|
|
377
|
+
if (commit.code !== 0) {
|
|
378
|
+
log(`commit failed: ${commit.stderr.trim().split("\n")[0] ?? ""}`);
|
|
379
|
+
return result;
|
|
380
|
+
}
|
|
381
|
+
result.committed = true;
|
|
382
|
+
if (hasOrigin) {
|
|
383
|
+
const push = await git(["push", "--quiet", "origin", `HEAD:refs/heads/${branch}`], wt);
|
|
384
|
+
if (push.code === 0) {
|
|
385
|
+
result.pushed = true;
|
|
386
|
+
result.branch = branch;
|
|
387
|
+
log(`pushed ${branch}`);
|
|
388
|
+
} else {
|
|
389
|
+
log(`push failed: ${push.stderr.trim().split("\n")[0] ?? ""}`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
if (!result.pushed) {
|
|
393
|
+
const keep = await git(["branch", branch, "HEAD"], wt);
|
|
394
|
+
if (keep.code === 0) {
|
|
395
|
+
result.branch = branch;
|
|
396
|
+
log(`branch ${branch} kept locally; push it with: git push origin ${branch}`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
if (result.pushed) {
|
|
400
|
+
result.url = await openReview({
|
|
401
|
+
branch,
|
|
402
|
+
base,
|
|
403
|
+
title: `${outcome.trigger === "failure" ? "Gate exit: run failed" : outcome.trigger === "review" ? "Gate exit: review findings" : `Gate exit: ${outcome.askId ?? "gate"}`} (${stamp})`,
|
|
404
|
+
body: result.note,
|
|
405
|
+
cwd: wt,
|
|
406
|
+
tmp,
|
|
407
|
+
originUrl,
|
|
408
|
+
exec,
|
|
409
|
+
which,
|
|
410
|
+
env,
|
|
411
|
+
log
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
} catch (err) {
|
|
415
|
+
log(`landing failed: ${String(err)}`);
|
|
416
|
+
} finally {
|
|
417
|
+
if (wt) await git(["worktree", "remove", "--force", wt], repo);
|
|
418
|
+
if (tmp) await rm(tmp, { recursive: true, force: true }).catch(() => void 0);
|
|
419
|
+
await git(["worktree", "prune"], repo);
|
|
420
|
+
await writeMarker();
|
|
421
|
+
}
|
|
422
|
+
return result;
|
|
423
|
+
}
|
|
424
|
+
function lastUrl(text) {
|
|
425
|
+
const urls = text.match(/https?:\/\/\S+/g);
|
|
426
|
+
return urls?.at(-1) ?? null;
|
|
427
|
+
}
|
|
428
|
+
async function findAuthenticatedForge(input) {
|
|
429
|
+
const forges = /gitlab/i.test(input.originUrl) ? ["glab", "gh"] : ["gh", "glab"];
|
|
430
|
+
for (const forge of forges) {
|
|
431
|
+
const bin = await input.which(forge);
|
|
432
|
+
if (!bin) continue;
|
|
433
|
+
const auth = await input.exec(bin, ["auth", "status"], { cwd: input.cwd, env: input.env });
|
|
434
|
+
if (auth.code !== 0) {
|
|
435
|
+
input.log(`${forge} is not authenticated; skipping the ${forge === "gh" ? "PR" : "MR"}`);
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
return { forge, bin };
|
|
439
|
+
}
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
async function openReview(input) {
|
|
443
|
+
const found = await findAuthenticatedForge(input);
|
|
444
|
+
if (found) {
|
|
445
|
+
const { forge, bin } = found;
|
|
446
|
+
let created;
|
|
447
|
+
if (forge === "gh") {
|
|
448
|
+
const bodyFile = path.join(input.tmp, "pr-body.md");
|
|
449
|
+
await writeFile(bodyFile, input.body, "utf8");
|
|
450
|
+
created = await input.exec(
|
|
451
|
+
bin,
|
|
452
|
+
[
|
|
453
|
+
"pr",
|
|
454
|
+
"create",
|
|
455
|
+
"--base",
|
|
456
|
+
input.base,
|
|
457
|
+
"--head",
|
|
458
|
+
input.branch,
|
|
459
|
+
"--title",
|
|
460
|
+
input.title,
|
|
461
|
+
"--body-file",
|
|
462
|
+
bodyFile
|
|
463
|
+
],
|
|
464
|
+
{ cwd: input.cwd, env: input.env }
|
|
465
|
+
);
|
|
466
|
+
} else {
|
|
467
|
+
created = await input.exec(
|
|
468
|
+
bin,
|
|
469
|
+
[
|
|
470
|
+
"mr",
|
|
471
|
+
"create",
|
|
472
|
+
"--source-branch",
|
|
473
|
+
input.branch,
|
|
474
|
+
"--target-branch",
|
|
475
|
+
input.base,
|
|
476
|
+
"--title",
|
|
477
|
+
input.title,
|
|
478
|
+
"--description",
|
|
479
|
+
input.body,
|
|
480
|
+
"--yes"
|
|
481
|
+
],
|
|
482
|
+
{ cwd: input.cwd, env: input.env }
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
if (created.code === 0) {
|
|
486
|
+
const url = lastUrl(created.stdout);
|
|
487
|
+
input.log(`${forge === "gh" ? "PR" : "MR"} opened${url ? `: ${url}` : ""}`);
|
|
488
|
+
return url;
|
|
489
|
+
}
|
|
490
|
+
input.log(`${forge} could not open the review: ${created.stderr.trim().split("\n")[0] ?? ""}`);
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
input.log(
|
|
494
|
+
`no authenticated gh/glab; open the review by hand: gh pr create --base ${input.base} --head ${input.branch} (or glab mr create --source-branch ${input.branch} --target-branch ${input.base})`
|
|
495
|
+
);
|
|
496
|
+
return null;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
export {
|
|
500
|
+
GATE_EXIT_BRANCH_PREFIX,
|
|
501
|
+
GATE_EXIT_MARKER_RELPATH,
|
|
502
|
+
GATE_EXIT_NOTE_DIR,
|
|
503
|
+
LAND_ON_GATE_DESCRIPTION,
|
|
504
|
+
gateExitStamp,
|
|
505
|
+
isGateExitStamp,
|
|
506
|
+
gateExitBranch,
|
|
507
|
+
isSafeBaseRef,
|
|
508
|
+
classifyGateExit,
|
|
509
|
+
readGateExitMarker,
|
|
510
|
+
writeGateExitMarker,
|
|
511
|
+
gateExitRedactions,
|
|
512
|
+
defaultExec,
|
|
513
|
+
resolveGateExitRefs,
|
|
514
|
+
listGateExitBranches,
|
|
515
|
+
gateExitNotePath,
|
|
516
|
+
isOutside,
|
|
517
|
+
landGateExit,
|
|
518
|
+
findAuthenticatedForge
|
|
519
|
+
};
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ensurePathIgnored,
|
|
3
|
+
gitAt
|
|
4
|
+
} from "./chunk-MUTTNRX4.js";
|
|
1
5
|
import {
|
|
2
6
|
HANDOFF_REL
|
|
3
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-C4Q77GGJ.js";
|
|
4
8
|
import {
|
|
5
9
|
ensureDir
|
|
6
10
|
} from "./chunk-5B4DVO6C.js";
|
|
@@ -24,6 +28,9 @@ function defaultPlanIndexIo() {
|
|
|
24
28
|
async writeFile(absPath, body) {
|
|
25
29
|
await ensureDir(path.dirname(absPath));
|
|
26
30
|
await writeFile(absPath, body, "utf8");
|
|
31
|
+
},
|
|
32
|
+
ensureIgnored(root, relPath) {
|
|
33
|
+
return ensurePathIgnored(gitAt(root), root, relPath);
|
|
27
34
|
}
|
|
28
35
|
};
|
|
29
36
|
}
|
|
@@ -127,7 +134,12 @@ async function writePlanIndex(root, io = defaultPlanIndexIo(), now = () => /* @_
|
|
|
127
134
|
const index = await buildPlanIndex(root, io, now);
|
|
128
135
|
const dest = path.join(root, PLAN_INDEX_REL);
|
|
129
136
|
const write = io.writeFile ?? defaultPlanIndexIo().writeFile;
|
|
137
|
+
const ensureIgnored = io.ensureIgnored ?? (io.writeFile ? void 0 : defaultPlanIndexIo().ensureIgnored);
|
|
130
138
|
if (write) {
|
|
139
|
+
try {
|
|
140
|
+
await ensureIgnored?.(root, PLAN_INDEX_REL);
|
|
141
|
+
} catch {
|
|
142
|
+
}
|
|
131
143
|
await write(dest, `${JSON.stringify(index, null, 2)}
|
|
132
144
|
`);
|
|
133
145
|
}
|