@dadado/agent-kit-cli 5.15.1 → 5.17.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/dashboard-data.mjs +69 -65
- package/dashboard/lib/guards.d.mts +5 -0
- package/dashboard/lib/guards.mjs +31 -0
- package/dashboard/lib/identity-probe.mjs +56 -0
- package/dashboard/lib/pipeline-cache.mjs +58 -0
- package/dashboard/lib/processes.mjs +71 -0
- package/dashboard/serve.mjs +43 -1
- package/dashboard/start-broadcast.mjs +2 -12
- package/dashboard/start.mjs +2 -13
- package/dist/add-HTYIFKMN.js +150 -0
- package/dist/chunk-2IPXQFJE.js +253 -0
- package/dist/chunk-2RDRMGD3.js +82 -0
- package/dist/chunk-2YU2H7I6.js +42 -0
- package/dist/chunk-4IWG5CVE.js +214 -0
- package/dist/chunk-5VCOWTTP.js +6 -0
- package/dist/chunk-77V75A7O.js +39 -0
- package/dist/chunk-7HDQEK6T.js +26 -0
- package/dist/chunk-AW74INUW.js +1456 -0
- package/dist/chunk-B73KQ5KX.js +972 -0
- package/dist/chunk-CBCW5R5J.js +484 -0
- package/dist/chunk-DE3LQFSJ.js +18 -0
- package/dist/chunk-DQA434Y4.js +106 -0
- package/dist/chunk-F4VMYR7U.js +318 -0
- package/dist/chunk-F6C3GFKV.js +57 -0
- package/dist/chunk-HFUH3HVS.js +282 -0
- package/dist/chunk-HS7LVVLO.js +74 -0
- package/dist/chunk-IHROBTNM.js +135 -0
- package/dist/chunk-KD2MYBB6.js +213 -0
- package/dist/chunk-MAPOQEA2.js +43 -0
- package/dist/chunk-N3HTJVBR.js +341 -0
- package/dist/chunk-NQ7IQ5XT.js +26 -0
- package/dist/chunk-OUQZC744.js +1315 -0
- package/dist/chunk-PCCQUWE5.js +817 -0
- package/dist/chunk-PNFF6OU3.js +273 -0
- package/dist/chunk-PNG7LO2F.js +53 -0
- package/dist/chunk-QTON2GQX.js +203 -0
- package/dist/chunk-S6GMFDKV.js +50 -0
- package/dist/chunk-SMGFN3Z4.js +503 -0
- package/dist/chunk-STGFBAYT.js +641 -0
- package/dist/chunk-TFZ2JUXH.js +1081 -0
- package/dist/chunk-VY7MTPJ2.js +205 -0
- package/dist/chunk-W24CMQ32.js +525 -0
- package/dist/chunk-WW3YZKHW.js +165 -0
- package/dist/chunk-ZB43YMHW.js +20 -0
- package/dist/chunk-ZCTSUQRR.js +874 -0
- package/dist/chunk-ZXKZBTXE.js +17 -0
- package/dist/contribute-C6AW2P6J.js +367 -0
- package/dist/cursor-awareness-KTL2RGKD.js +77 -0
- package/dist/dashboard-YSNUGBFY.js +21 -0
- package/dist/dashboard-broadcast-M4CMW7RX.js +119 -0
- package/dist/diff-VHNR62SI.js +74 -0
- package/dist/doctor-PET2HRRJ.js +236 -0
- package/dist/dogfood-KKFVRWT6.js +366 -0
- package/dist/guard-EHXF76ID.js +530 -0
- package/dist/handoff-3UI6VKVG.js +302 -0
- package/dist/hook-DYIMKJYL.js +1074 -0
- package/dist/index.js +62 -14068
- package/dist/init-Q4LBM6KT.js +127 -0
- package/dist/install-RHOS3OYZ.js +34 -0
- package/dist/mission-control-LJACEVVN.js +89 -0
- package/dist/monitors-QV2IS3MP.js +176 -0
- package/dist/plan-index-JMC7Z4DW.js +44 -0
- package/dist/public-inbound-radar-WP3XF7GS.js +68 -0
- package/dist/remote-issues-SZ4HDQL5.js +44 -0
- package/dist/run-I3BFGULT.js +243 -0
- package/dist/run-plan-FO7CL4M3.js +118 -0
- package/dist/scan-NI2SWJ2E.js +33 -0
- package/dist/setup-global-M42GEODI.js +340 -0
- package/dist/status-XPVQVQMX.js +150 -0
- package/dist/update-FJ2EBZVO.js +283 -0
- package/dist/validate-V2CW7J4B.js +201 -0
- package/package.json +3 -3
|
@@ -0,0 +1,641 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CLAUDE_COMMANDS_DIR_REL,
|
|
3
|
+
CLAUDE_SETTINGS_REL,
|
|
4
|
+
GUARD_SHELL_HOOK_MARKER,
|
|
5
|
+
RESOLVE_AGENT_KIT_REL,
|
|
6
|
+
SESSION_START_HOOK_MARKER,
|
|
7
|
+
discoverInstalledCommands
|
|
8
|
+
} from "./chunk-W24CMQ32.js";
|
|
9
|
+
import {
|
|
10
|
+
createReadinessReport
|
|
11
|
+
} from "./chunk-PNFF6OU3.js";
|
|
12
|
+
import {
|
|
13
|
+
KIT_OWNED_IGNORE_PATTERNS,
|
|
14
|
+
REPOSITORY_PROFILE_REL,
|
|
15
|
+
REQUIRED_SECRET_PATTERNS,
|
|
16
|
+
detectGitDirty,
|
|
17
|
+
runScanner
|
|
18
|
+
} from "./chunk-ZCTSUQRR.js";
|
|
19
|
+
import {
|
|
20
|
+
CONTEXT_CONFIG_REL,
|
|
21
|
+
contextConfigPath
|
|
22
|
+
} from "./chunk-NQ7IQ5XT.js";
|
|
23
|
+
import {
|
|
24
|
+
ensureDir,
|
|
25
|
+
fileExists,
|
|
26
|
+
readJson,
|
|
27
|
+
writeJson
|
|
28
|
+
} from "./chunk-77V75A7O.js";
|
|
29
|
+
|
|
30
|
+
// src/invariants/hooks-health.ts
|
|
31
|
+
import { execFile } from "child_process";
|
|
32
|
+
import { constants, access, readFile, readdir, stat } from "fs/promises";
|
|
33
|
+
import path from "path";
|
|
34
|
+
import { promisify } from "util";
|
|
35
|
+
var execFileAsync = promisify(execFile);
|
|
36
|
+
var GIT_HOOK_CANONICAL_NAMES = ["pre-commit", "pre-push", "prepare-commit-msg"];
|
|
37
|
+
var EXPECTED_EVENTS = [
|
|
38
|
+
"sessionStart",
|
|
39
|
+
"preCompact",
|
|
40
|
+
"beforeShellExecution",
|
|
41
|
+
"afterFileEdit",
|
|
42
|
+
"beforeSubmitPrompt"
|
|
43
|
+
];
|
|
44
|
+
async function exists(p) {
|
|
45
|
+
try {
|
|
46
|
+
await access(p);
|
|
47
|
+
return true;
|
|
48
|
+
} catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async function isExecutable(p) {
|
|
53
|
+
try {
|
|
54
|
+
await access(p, constants.X_OK);
|
|
55
|
+
return true;
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async function resolveAgentKitCli(rootDir) {
|
|
61
|
+
const root = path.resolve(rootDir);
|
|
62
|
+
const candidates = [
|
|
63
|
+
path.join(root, "node_modules", ".bin", "agent-kit"),
|
|
64
|
+
path.join(root, "packages", "cli", "dist", "index.js")
|
|
65
|
+
];
|
|
66
|
+
for (const c of candidates) {
|
|
67
|
+
if (await exists(c)) return c;
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
const { stdout } = await execFileAsync("which", ["agent-kit"], { encoding: "utf8" });
|
|
71
|
+
const hit = stdout.trim().split("\n")[0]?.trim();
|
|
72
|
+
if (hit) return hit;
|
|
73
|
+
} catch {
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
function commandLooksLikeAdapter(command) {
|
|
78
|
+
const trimmed = command.trim();
|
|
79
|
+
if (!trimmed) return null;
|
|
80
|
+
const m = trimmed.match(/(\.cursor\/hooks\/agent\/[A-Za-z0-9._-]+\.sh)\b/);
|
|
81
|
+
return m?.[1] ?? null;
|
|
82
|
+
}
|
|
83
|
+
async function assessGitHooksInstallDrift(rootDir) {
|
|
84
|
+
const root = path.resolve(rootDir);
|
|
85
|
+
const canonicalDir = path.join(root, "git-hooks");
|
|
86
|
+
if (!await exists(canonicalDir)) return [];
|
|
87
|
+
let hooksPathCfg = "";
|
|
88
|
+
try {
|
|
89
|
+
const { stdout } = await execFileAsync(
|
|
90
|
+
"git",
|
|
91
|
+
["-C", root, "config", "--get", "core.hooksPath"],
|
|
92
|
+
{ encoding: "utf8" }
|
|
93
|
+
);
|
|
94
|
+
hooksPathCfg = stdout.trim();
|
|
95
|
+
} catch {
|
|
96
|
+
}
|
|
97
|
+
if (hooksPathCfg) {
|
|
98
|
+
const resolved = path.isAbsolute(hooksPathCfg) ? path.normalize(hooksPathCfg) : path.normalize(path.join(root, hooksPathCfg));
|
|
99
|
+
if (resolved === path.normalize(canonicalDir)) {
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const gitDir = path.join(root, ".git");
|
|
104
|
+
if (!await exists(gitDir)) return [];
|
|
105
|
+
try {
|
|
106
|
+
const st = await stat(gitDir);
|
|
107
|
+
if (!st.isDirectory()) return [];
|
|
108
|
+
} catch {
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
const installedDir = path.join(gitDir, "hooks");
|
|
112
|
+
const advisories = [];
|
|
113
|
+
const installHint = "Install or refresh with: `cp git-hooks/<name> .git/hooks/<name> && chmod +x .git/hooks/<name>` (see git-hooks/README.md)";
|
|
114
|
+
let names = [...GIT_HOOK_CANONICAL_NAMES];
|
|
115
|
+
try {
|
|
116
|
+
const listed = await readdir(canonicalDir);
|
|
117
|
+
const fromDisk = listed.filter(
|
|
118
|
+
(n) => GIT_HOOK_CANONICAL_NAMES.includes(n)
|
|
119
|
+
);
|
|
120
|
+
if (fromDisk.length > 0) names = fromDisk;
|
|
121
|
+
} catch {
|
|
122
|
+
}
|
|
123
|
+
for (const name of names) {
|
|
124
|
+
const canonical = path.join(canonicalDir, name);
|
|
125
|
+
const installed = path.join(installedDir, name);
|
|
126
|
+
if (!await exists(canonical)) continue;
|
|
127
|
+
if (!await exists(installed)) {
|
|
128
|
+
advisories.push(
|
|
129
|
+
`git-hooks drift: \`.git/hooks/${name}\` missing (canonical \`git-hooks/${name}\` present). ${installHint}`
|
|
130
|
+
);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
const [a, b] = await Promise.all([readFile(canonical, "utf8"), readFile(installed, "utf8")]);
|
|
135
|
+
if (a !== b) {
|
|
136
|
+
advisories.push(
|
|
137
|
+
`git-hooks drift: \`.git/hooks/${name}\` differs from \`git-hooks/${name}\`. ${installHint}`
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
} catch {
|
|
141
|
+
advisories.push(
|
|
142
|
+
`git-hooks drift: could not compare \`git-hooks/${name}\` with \`.git/hooks/${name}\`. ${installHint}`
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return advisories;
|
|
147
|
+
}
|
|
148
|
+
async function assessSignatureGateHook(rootDir) {
|
|
149
|
+
const root = path.resolve(rootDir);
|
|
150
|
+
if (!await exists(path.join(root, "autogit", "gitupdate.md"))) return [];
|
|
151
|
+
if (await exists(path.join(root, "git-hooks", "prepare-commit-msg"))) return [];
|
|
152
|
+
return [
|
|
153
|
+
"signature gate hook missing: `git-hooks/prepare-commit-msg` (/git-staging runs `sh git-hooks/prepare-commit-msg --check -`; a missing file exits 127 and reads as red). Run `agent-kit update` to restore it."
|
|
154
|
+
];
|
|
155
|
+
}
|
|
156
|
+
async function assessClaudeCommandAdapters(rootDir) {
|
|
157
|
+
const root = path.resolve(rootDir);
|
|
158
|
+
if (!await exists(path.join(root, ".claude"))) return [];
|
|
159
|
+
const sources = await discoverInstalledCommands(root);
|
|
160
|
+
if (sources.length === 0) return [];
|
|
161
|
+
const missing = [];
|
|
162
|
+
for (const { name } of sources) {
|
|
163
|
+
if (!await exists(path.join(root, CLAUDE_COMMANDS_DIR_REL, `${name}.md`))) missing.push(name);
|
|
164
|
+
}
|
|
165
|
+
if (missing.length === 0) return [];
|
|
166
|
+
const shown = missing.slice(0, 3).map((n) => `/${n}`);
|
|
167
|
+
const more = missing.length > shown.length ? `, +${missing.length - shown.length} more` : "";
|
|
168
|
+
return [
|
|
169
|
+
`Claude Code command adapters missing: ${missing.length} of ${sources.length} installed kit commands have no \`.claude/commands/<name>.md\` (${shown.join(", ")}${more}), so they are not recognized as slash commands. Run \`agent-kit update --claude\` to generate them (writes only \`.claude/commands/*\`).`
|
|
170
|
+
];
|
|
171
|
+
}
|
|
172
|
+
async function resolveGuardProbeArgv(root) {
|
|
173
|
+
const hookBin = process.env.AGENT_KIT_HOOK_BIN;
|
|
174
|
+
if (hookBin && await isExecutable(hookBin)) return [hookBin];
|
|
175
|
+
const local = path.join(root, "node_modules", ".bin", "agent-kit");
|
|
176
|
+
if (await isExecutable(local)) return [local];
|
|
177
|
+
const dist = path.join(root, "packages", "cli", "dist", "index.js");
|
|
178
|
+
if (await exists(dist)) return [process.execPath, dist];
|
|
179
|
+
try {
|
|
180
|
+
const { stdout } = await execFileAsync("which", ["agent-kit"], { encoding: "utf8" });
|
|
181
|
+
const hit = stdout.trim().split("\n")[0]?.trim();
|
|
182
|
+
if (hit) return [hit];
|
|
183
|
+
} catch {
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
async function assessClaudeGuardProbe(rootDir) {
|
|
188
|
+
const root = path.resolve(rootDir);
|
|
189
|
+
let text;
|
|
190
|
+
try {
|
|
191
|
+
text = await readFile(path.join(root, CLAUDE_SETTINGS_REL), "utf8");
|
|
192
|
+
} catch {
|
|
193
|
+
return [];
|
|
194
|
+
}
|
|
195
|
+
if (!text.includes(GUARD_SHELL_HOOK_MARKER)) {
|
|
196
|
+
return text.includes(SESSION_START_HOOK_MARKER) ? [
|
|
197
|
+
"Claude SessionStart hook is installed but the PreToolUse guard hook is absent from `.claude/settings.json`. Run `agent-kit install --claude`."
|
|
198
|
+
] : [];
|
|
199
|
+
}
|
|
200
|
+
const stale = "Claude guard hook active but resolved CLI is too old or stale dist; update the CLI / rebuild.";
|
|
201
|
+
const argv = await resolveGuardProbeArgv(root);
|
|
202
|
+
if (!argv) return [`${stale} (agent-kit CLI not resolvable)`];
|
|
203
|
+
const [bin, ...lead] = argv;
|
|
204
|
+
try {
|
|
205
|
+
const { stdout } = await execFileAsync(
|
|
206
|
+
bin,
|
|
207
|
+
[
|
|
208
|
+
...lead,
|
|
209
|
+
"guard",
|
|
210
|
+
"shell",
|
|
211
|
+
"--format",
|
|
212
|
+
"claude",
|
|
213
|
+
"--command",
|
|
214
|
+
"git push --force origin main"
|
|
215
|
+
],
|
|
216
|
+
{
|
|
217
|
+
cwd: root,
|
|
218
|
+
encoding: "utf8",
|
|
219
|
+
timeout: 1e4,
|
|
220
|
+
env: { ...process.env, ALLOW_MAIN_PUSH: "" }
|
|
221
|
+
}
|
|
222
|
+
);
|
|
223
|
+
const decision = JSON.parse(stdout).hookSpecificOutput?.permissionDecision;
|
|
224
|
+
return decision === "deny" ? [] : [stale];
|
|
225
|
+
} catch {
|
|
226
|
+
return [stale];
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
async function assessHooksHealth(rootDir) {
|
|
230
|
+
const root = path.resolve(rootDir);
|
|
231
|
+
const hooksJsonPath = ".cursor/hooks.json";
|
|
232
|
+
const hooksJsonAbs = path.join(root, hooksJsonPath);
|
|
233
|
+
const reasons = [];
|
|
234
|
+
const wiredEvents = [];
|
|
235
|
+
const advisories = [
|
|
236
|
+
...await assessGitHooksInstallDrift(root),
|
|
237
|
+
...await assessSignatureGateHook(root),
|
|
238
|
+
...await assessClaudeGuardProbe(root),
|
|
239
|
+
...await assessClaudeCommandAdapters(root)
|
|
240
|
+
];
|
|
241
|
+
if (!await exists(hooksJsonAbs)) {
|
|
242
|
+
return {
|
|
243
|
+
status: "missing",
|
|
244
|
+
reasons: ["`.cursor/hooks.json` not found"],
|
|
245
|
+
advisories,
|
|
246
|
+
hooksJsonPath,
|
|
247
|
+
expectedEvents: [...EXPECTED_EVENTS],
|
|
248
|
+
wiredEvents
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
let parsed;
|
|
252
|
+
try {
|
|
253
|
+
parsed = JSON.parse(await readFile(hooksJsonAbs, "utf8"));
|
|
254
|
+
} catch {
|
|
255
|
+
return {
|
|
256
|
+
status: "degraded",
|
|
257
|
+
reasons: ["`.cursor/hooks.json` is not valid JSON"],
|
|
258
|
+
advisories,
|
|
259
|
+
hooksJsonPath,
|
|
260
|
+
expectedEvents: [...EXPECTED_EVENTS],
|
|
261
|
+
wiredEvents
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
const hooks = parsed.hooks ?? {};
|
|
265
|
+
const adapterRels = /* @__PURE__ */ new Set();
|
|
266
|
+
for (const event of EXPECTED_EVENTS) {
|
|
267
|
+
const list = hooks[event];
|
|
268
|
+
if (Array.isArray(list) && list.length > 0) {
|
|
269
|
+
wiredEvents.push(event);
|
|
270
|
+
for (const entry of list) {
|
|
271
|
+
if (!entry || typeof entry !== "object") continue;
|
|
272
|
+
const command = String(entry.command ?? "");
|
|
273
|
+
if (command.endsWith(".py") || command.includes("python")) {
|
|
274
|
+
reasons.push(`${event} still points at a Python script (${command})`);
|
|
275
|
+
}
|
|
276
|
+
const rel = commandLooksLikeAdapter(command);
|
|
277
|
+
if (rel) adapterRels.add(rel);
|
|
278
|
+
}
|
|
279
|
+
} else {
|
|
280
|
+
reasons.push(`missing hook event: ${event}`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
const resolveLib = path.join(root, RESOLVE_AGENT_KIT_REL);
|
|
284
|
+
if (!await exists(resolveLib)) {
|
|
285
|
+
reasons.push(`missing \`${RESOLVE_AGENT_KIT_REL}\` (thin adapter resolver)`);
|
|
286
|
+
} else if (!await isExecutable(resolveLib)) {
|
|
287
|
+
reasons.push(`\`${RESOLVE_AGENT_KIT_REL}\` is not executable (chmod +x)`);
|
|
288
|
+
}
|
|
289
|
+
for (const rel of adapterRels) {
|
|
290
|
+
const abs = path.join(root, rel);
|
|
291
|
+
if (!await exists(abs)) {
|
|
292
|
+
reasons.push(`missing adapter script: \`${rel}\``);
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
try {
|
|
296
|
+
const st = await stat(abs);
|
|
297
|
+
if (!st.isFile()) {
|
|
298
|
+
reasons.push(`adapter path is not a file: \`${rel}\``);
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
} catch {
|
|
302
|
+
reasons.push(`unreadable adapter script: \`${rel}\``);
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
if (!await isExecutable(abs)) {
|
|
306
|
+
reasons.push(`adapter not executable: \`${rel}\` (chmod +x)`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
const cli = await resolveAgentKitCli(root);
|
|
310
|
+
if (!cli) {
|
|
311
|
+
reasons.push(
|
|
312
|
+
"agent-kit CLI not resolvable (node_modules/.bin/agent-kit, packages/cli/dist, or PATH)"
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
if (Array.isArray(hooks.stop) && hooks.stop.length > 0) {
|
|
316
|
+
reasons.push("`stop` hook is registered (forbidden; remove it)");
|
|
317
|
+
}
|
|
318
|
+
const status = reasons.length === 0 && wiredEvents.length === EXPECTED_EVENTS.length ? "active" : "degraded";
|
|
319
|
+
return {
|
|
320
|
+
status,
|
|
321
|
+
reasons,
|
|
322
|
+
advisories,
|
|
323
|
+
hooksJsonPath,
|
|
324
|
+
expectedEvents: [...EXPECTED_EVENTS],
|
|
325
|
+
wiredEvents
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// src/scanner/safe-fixes.ts
|
|
330
|
+
import { readFile as readFile2, writeFile } from "fs/promises";
|
|
331
|
+
import path2 from "path";
|
|
332
|
+
var CONTEXT_CONFIG_REL_POSIX = CONTEXT_CONFIG_REL.replace(/\\/g, "/");
|
|
333
|
+
var ESSENTIAL_DIRECTORIES = [
|
|
334
|
+
".cursor",
|
|
335
|
+
".cursor/context",
|
|
336
|
+
".cursor/context/current",
|
|
337
|
+
".cursor/context/backups",
|
|
338
|
+
".cursor/plans",
|
|
339
|
+
".cursor/memory"
|
|
340
|
+
];
|
|
341
|
+
function isObject(value) {
|
|
342
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
343
|
+
}
|
|
344
|
+
function mergeMissing(existing, defaults) {
|
|
345
|
+
if (!isObject(existing) || !isObject(defaults)) return existing ?? defaults;
|
|
346
|
+
const merged = { ...existing };
|
|
347
|
+
for (const [key, value] of Object.entries(defaults)) {
|
|
348
|
+
merged[key] = key in existing ? mergeMissing(existing[key], value) : value;
|
|
349
|
+
}
|
|
350
|
+
return merged;
|
|
351
|
+
}
|
|
352
|
+
function jsonEqual(left, right) {
|
|
353
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
354
|
+
}
|
|
355
|
+
function relativeEvidence(relativePath, detail) {
|
|
356
|
+
return [`${relativePath}: ${detail}`];
|
|
357
|
+
}
|
|
358
|
+
function createProfile(scan, report, generatedAt) {
|
|
359
|
+
const git = {
|
|
360
|
+
mode: scan.git.mode,
|
|
361
|
+
workflow: scan.git.workflow,
|
|
362
|
+
remotes: scan.git.remotes,
|
|
363
|
+
remoteUrl: scan.git.remoteUrl,
|
|
364
|
+
remoteName: scan.git.remoteName,
|
|
365
|
+
currentBranch: scan.git.currentBranch,
|
|
366
|
+
defaultBranch: scan.git.defaultBranch,
|
|
367
|
+
isDirty: scan.git.isDirty,
|
|
368
|
+
hasLocalStaging: scan.git.hasLocalStaging,
|
|
369
|
+
hasRemoteStaging: scan.git.hasRemoteStaging
|
|
370
|
+
};
|
|
371
|
+
if (scan.git.providerConfidence === "high") {
|
|
372
|
+
git.provider = scan.git.provider;
|
|
373
|
+
git.providerKind = scan.git.providerKind;
|
|
374
|
+
git.providerConfidence = scan.git.providerConfidence;
|
|
375
|
+
git.providerEvidence = scan.git.providerEvidence;
|
|
376
|
+
}
|
|
377
|
+
return {
|
|
378
|
+
schemaVersion: 1,
|
|
379
|
+
contractVersion: 1,
|
|
380
|
+
purpose: scan.purpose,
|
|
381
|
+
stack: scan.stack,
|
|
382
|
+
git,
|
|
383
|
+
infra: scan.infra,
|
|
384
|
+
services: scan.services,
|
|
385
|
+
context: scan.context,
|
|
386
|
+
detection: {
|
|
387
|
+
generatedAt,
|
|
388
|
+
repositoryFingerprint: report.repositoryFingerprint,
|
|
389
|
+
providerConfidence: scan.git.providerConfidence,
|
|
390
|
+
providerEvidence: scan.git.providerEvidence
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
function createOnboardingState(report, generatedAt, deferredItems = report.deferredChecks) {
|
|
395
|
+
const validDeferredCheckIds = new Set(
|
|
396
|
+
deferredItems.filter((item) => item.reason.trim().length > 0).map((item) => item.checkId)
|
|
397
|
+
);
|
|
398
|
+
const unresolvedEssential = report.pillars.flatMap((pillar) => pillar.checks).some((check) => {
|
|
399
|
+
if (!check.essential || check.status === "ready") return false;
|
|
400
|
+
if (check.status === "blocked") return true;
|
|
401
|
+
if (!validDeferredCheckIds.has(check.id)) return true;
|
|
402
|
+
return check.actions.length > 0;
|
|
403
|
+
});
|
|
404
|
+
return {
|
|
405
|
+
contractVersion: 1,
|
|
406
|
+
status: unresolvedEssential ? "in_progress" : "completed",
|
|
407
|
+
updatedAt: generatedAt,
|
|
408
|
+
checks: Object.fromEntries(
|
|
409
|
+
report.pillars.flatMap((pillar) => pillar.checks).map((check) => [
|
|
410
|
+
check.id,
|
|
411
|
+
{
|
|
412
|
+
status: check.status,
|
|
413
|
+
essential: check.essential,
|
|
414
|
+
evidence: check.evidence
|
|
415
|
+
}
|
|
416
|
+
])
|
|
417
|
+
),
|
|
418
|
+
deferredItems
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
function validDeferredItems(value, fallback) {
|
|
422
|
+
if (!Array.isArray(value)) return fallback;
|
|
423
|
+
return value.filter(
|
|
424
|
+
(item) => isObject(item) && typeof item.checkId === "string" && typeof item.reason === "string" && (item.recoveryCommand === void 0 || typeof item.recoveryCommand === "string")
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
function reconcileOnboardingState(report, existingConfig, generatedAt) {
|
|
428
|
+
const existing = isObject(existingConfig.onboarding) ? existingConfig.onboarding : {};
|
|
429
|
+
const deferredItems = validDeferredItems(existing.deferredItems, report.deferredChecks);
|
|
430
|
+
const derived = createOnboardingState(report, generatedAt, deferredItems);
|
|
431
|
+
const candidate = {
|
|
432
|
+
...existing,
|
|
433
|
+
...derived
|
|
434
|
+
};
|
|
435
|
+
const { updatedAt: _candidateUpdatedAt, ...candidateState } = candidate;
|
|
436
|
+
const { updatedAt: existingUpdatedAt, ...existingState } = existing;
|
|
437
|
+
candidate.updatedAt = jsonEqual(candidateState, existingState) && typeof existingUpdatedAt === "string" ? existingUpdatedAt : generatedAt;
|
|
438
|
+
return candidate;
|
|
439
|
+
}
|
|
440
|
+
function preferenceDefaults(onboarding, onboarded) {
|
|
441
|
+
return {
|
|
442
|
+
onboarded: onboarded === true,
|
|
443
|
+
onboarding,
|
|
444
|
+
externalPlanReview: {
|
|
445
|
+
enabled: false,
|
|
446
|
+
backend: "claude",
|
|
447
|
+
autoRemediate: false,
|
|
448
|
+
offerOnExhausted: true,
|
|
449
|
+
mode: "paste",
|
|
450
|
+
midBatchAudits: false,
|
|
451
|
+
preflight: "off"
|
|
452
|
+
},
|
|
453
|
+
autoHandoff: false,
|
|
454
|
+
agentPersona: {
|
|
455
|
+
default: "autopilot",
|
|
456
|
+
modes: {
|
|
457
|
+
"continue-plan": "autopilot",
|
|
458
|
+
"run-plan": "night-shift",
|
|
459
|
+
"cli-run-plan": "ghost-runner"
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
function mergeIgnorePatterns(existing, patterns) {
|
|
465
|
+
const activeLines = new Set(
|
|
466
|
+
existing.split(/\r?\n/).map((line) => line.trim()).filter((line) => line && !line.startsWith("#"))
|
|
467
|
+
);
|
|
468
|
+
const missing = patterns.filter((pattern) => !activeLines.has(pattern));
|
|
469
|
+
if (missing.length === 0) return existing;
|
|
470
|
+
const prefix = existing.length === 0 ? "" : existing.endsWith("\n") ? existing : `${existing}
|
|
471
|
+
`;
|
|
472
|
+
return `${prefix}${missing.join("\n")}
|
|
473
|
+
`;
|
|
474
|
+
}
|
|
475
|
+
function mergeSecretIgnores(existing) {
|
|
476
|
+
return mergeIgnorePatterns(existing, REQUIRED_SECRET_PATTERNS);
|
|
477
|
+
}
|
|
478
|
+
function mergeKitOwnedIgnores(existing) {
|
|
479
|
+
return mergeIgnorePatterns(existing, KIT_OWNED_IGNORE_PATTERNS);
|
|
480
|
+
}
|
|
481
|
+
function recordChange(changes, id, relativePath, changed, dryRun, evidence) {
|
|
482
|
+
changes.push({
|
|
483
|
+
id,
|
|
484
|
+
path: relativePath,
|
|
485
|
+
status: changed ? dryRun ? "planned" : "applied" : "skipped",
|
|
486
|
+
evidence
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
function appliedActions(changes) {
|
|
490
|
+
return changes.filter((change) => change.status === "applied").map((change) => ({
|
|
491
|
+
id: change.id,
|
|
492
|
+
status: "ready",
|
|
493
|
+
recommendation: `Applied safe local change to ${change.path}`,
|
|
494
|
+
owner: "system"
|
|
495
|
+
}));
|
|
496
|
+
}
|
|
497
|
+
function withoutGeneratedAtForComparison(profile) {
|
|
498
|
+
const { generatedAt: _generatedAt, ...detection } = isObject(profile.detection) ? profile.detection : {};
|
|
499
|
+
return { ...profile, detection };
|
|
500
|
+
}
|
|
501
|
+
function reconcileFreshOverExisting(desired, existing) {
|
|
502
|
+
const merged = { ...existing };
|
|
503
|
+
for (const [key, value] of Object.entries(desired)) {
|
|
504
|
+
const existingValue = existing[key];
|
|
505
|
+
merged[key] = isObject(value) && isObject(existingValue) ? reconcileFreshOverExisting(value, existingValue) : value;
|
|
506
|
+
}
|
|
507
|
+
return merged;
|
|
508
|
+
}
|
|
509
|
+
async function refreshRepositoryProfile(rootDir, options) {
|
|
510
|
+
const generatedAt = options.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
511
|
+
const scan = await runScanner(rootDir);
|
|
512
|
+
const report = createReadinessReport(scan, {
|
|
513
|
+
generatorVersion: options.generatorVersion,
|
|
514
|
+
generatedAt
|
|
515
|
+
});
|
|
516
|
+
const desiredProfile = createProfile(scan, report, generatedAt);
|
|
517
|
+
const profilePath = path2.join(scan.rootDir, REPOSITORY_PROFILE_REL);
|
|
518
|
+
const existingProfile = await readJson(profilePath) ?? {};
|
|
519
|
+
const reconciled = reconcileFreshOverExisting(desiredProfile, existingProfile);
|
|
520
|
+
const meaningfulChange = !jsonEqual(
|
|
521
|
+
withoutGeneratedAtForComparison(existingProfile),
|
|
522
|
+
withoutGeneratedAtForComparison(reconciled)
|
|
523
|
+
);
|
|
524
|
+
if (!meaningfulChange) {
|
|
525
|
+
return { changed: false, profile: existingProfile };
|
|
526
|
+
}
|
|
527
|
+
await writeJson(profilePath, reconciled);
|
|
528
|
+
return { changed: true, profile: reconciled };
|
|
529
|
+
}
|
|
530
|
+
async function executeSafeReadinessFixes(rootDir, options) {
|
|
531
|
+
const dryRun = options.dryRun ?? false;
|
|
532
|
+
const generatedAt = options.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
533
|
+
const beforeScan = await runScanner(rootDir);
|
|
534
|
+
const before = createReadinessReport(beforeScan, {
|
|
535
|
+
generatorVersion: options.generatorVersion,
|
|
536
|
+
generatedAt
|
|
537
|
+
});
|
|
538
|
+
const changes = [];
|
|
539
|
+
for (const relativePath of ESSENTIAL_DIRECTORIES) {
|
|
540
|
+
const absolutePath = path2.join(beforeScan.rootDir, relativePath);
|
|
541
|
+
const exists2 = await fileExists(absolutePath);
|
|
542
|
+
if (!exists2 && !dryRun) await ensureDir(absolutePath);
|
|
543
|
+
recordChange(
|
|
544
|
+
changes,
|
|
545
|
+
"ensure-agent-kit-directory",
|
|
546
|
+
relativePath,
|
|
547
|
+
!exists2,
|
|
548
|
+
dryRun,
|
|
549
|
+
relativeEvidence(relativePath, exists2 ? "already exists" : "missing directory")
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
const gitignoreRelativePath = ".gitignore";
|
|
553
|
+
const gitignorePath = path2.join(beforeScan.rootDir, gitignoreRelativePath);
|
|
554
|
+
const existingGitignore = await fileExists(gitignorePath) ? await readFile2(gitignorePath, "utf8") : "";
|
|
555
|
+
const gitignoreAfterSecrets = mergeSecretIgnores(existingGitignore);
|
|
556
|
+
const secretsChanged = gitignoreAfterSecrets !== existingGitignore;
|
|
557
|
+
const mergedGitignore = mergeKitOwnedIgnores(gitignoreAfterSecrets);
|
|
558
|
+
const gitignoreChanged = mergedGitignore !== existingGitignore;
|
|
559
|
+
const kitOwnedChanged = mergedGitignore !== gitignoreAfterSecrets;
|
|
560
|
+
if (gitignoreChanged && !dryRun) await writeFile(gitignorePath, mergedGitignore, "utf8");
|
|
561
|
+
recordChange(
|
|
562
|
+
changes,
|
|
563
|
+
"merge-secret-ignores",
|
|
564
|
+
gitignoreRelativePath,
|
|
565
|
+
secretsChanged,
|
|
566
|
+
dryRun,
|
|
567
|
+
relativeEvidence(
|
|
568
|
+
gitignoreRelativePath,
|
|
569
|
+
secretsChanged ? "required secret patterns are missing" : "required patterns are present"
|
|
570
|
+
)
|
|
571
|
+
);
|
|
572
|
+
recordChange(
|
|
573
|
+
changes,
|
|
574
|
+
"merge-kit-owned-ignores",
|
|
575
|
+
gitignoreRelativePath,
|
|
576
|
+
kitOwnedChanged,
|
|
577
|
+
dryRun,
|
|
578
|
+
relativeEvidence(
|
|
579
|
+
gitignoreRelativePath,
|
|
580
|
+
kitOwnedChanged ? "kit-owned session/derived paths are missing" : "kit-owned session/derived paths are present"
|
|
581
|
+
)
|
|
582
|
+
);
|
|
583
|
+
const profilePath = path2.join(beforeScan.rootDir, REPOSITORY_PROFILE_REL);
|
|
584
|
+
const existingProfile = await readJson(profilePath) ?? {};
|
|
585
|
+
const desiredProfile = createProfile(beforeScan, before, generatedAt);
|
|
586
|
+
const mergedProfile = mergeMissing(existingProfile, desiredProfile);
|
|
587
|
+
const profileChanged = !jsonEqual(existingProfile, mergedProfile);
|
|
588
|
+
if (profileChanged && !dryRun) await writeJson(profilePath, mergedProfile);
|
|
589
|
+
recordChange(
|
|
590
|
+
changes,
|
|
591
|
+
"merge-repository-profile",
|
|
592
|
+
REPOSITORY_PROFILE_REL,
|
|
593
|
+
profileChanged,
|
|
594
|
+
dryRun,
|
|
595
|
+
relativeEvidence(
|
|
596
|
+
REPOSITORY_PROFILE_REL,
|
|
597
|
+
profileChanged ? "missing scanner-derived facts" : "existing facts preserved"
|
|
598
|
+
)
|
|
599
|
+
);
|
|
600
|
+
const evidenceScan = dryRun ? beforeScan : await runScanner(beforeScan.rootDir);
|
|
601
|
+
const evidenceReport = createReadinessReport(evidenceScan, {
|
|
602
|
+
generatorVersion: options.generatorVersion,
|
|
603
|
+
generatedAt
|
|
604
|
+
});
|
|
605
|
+
const configPath = contextConfigPath(beforeScan.rootDir);
|
|
606
|
+
const existingContextConfig = await readJson(configPath) ?? {};
|
|
607
|
+
const onboarding = reconcileOnboardingState(evidenceReport, existingContextConfig, generatedAt);
|
|
608
|
+
const defaults = preferenceDefaults(onboarding, existingContextConfig.onboarded);
|
|
609
|
+
const mergedContextConfig = mergeMissing(existingContextConfig, defaults);
|
|
610
|
+
mergedContextConfig.onboarding = onboarding;
|
|
611
|
+
const contextConfigChanged = !jsonEqual(existingContextConfig, mergedContextConfig);
|
|
612
|
+
if (contextConfigChanged && !dryRun) await writeJson(configPath, mergedContextConfig);
|
|
613
|
+
recordChange(
|
|
614
|
+
changes,
|
|
615
|
+
"merge-onboarding-state",
|
|
616
|
+
CONTEXT_CONFIG_REL_POSIX,
|
|
617
|
+
contextConfigChanged,
|
|
618
|
+
dryRun,
|
|
619
|
+
relativeEvidence(
|
|
620
|
+
CONTEXT_CONFIG_REL_POSIX,
|
|
621
|
+
contextConfigChanged ? "missing preferences or onboarding state" : "state already merged"
|
|
622
|
+
)
|
|
623
|
+
);
|
|
624
|
+
const afterScan = contextConfigChanged && !dryRun && evidenceScan.git.mode !== "none" ? {
|
|
625
|
+
...evidenceScan,
|
|
626
|
+
git: { ...evidenceScan.git, isDirty: await detectGitDirty(evidenceScan.rootDir) }
|
|
627
|
+
} : evidenceScan;
|
|
628
|
+
const after = createReadinessReport(afterScan, {
|
|
629
|
+
generatorVersion: options.generatorVersion,
|
|
630
|
+
generatedAt
|
|
631
|
+
});
|
|
632
|
+
after.appliedSafeFixes = appliedActions(changes);
|
|
633
|
+
return { dryRun, changes, before, after };
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
export {
|
|
637
|
+
assessClaudeCommandAdapters,
|
|
638
|
+
assessHooksHealth,
|
|
639
|
+
refreshRepositoryProfile,
|
|
640
|
+
executeSafeReadinessFixes
|
|
641
|
+
};
|