knodin 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +590 -0
  3. package/dist/bin/cli.js +1704 -0
  4. package/dist/src/agent-integration.js +250 -0
  5. package/dist/src/artifact-refresh.js +81 -0
  6. package/dist/src/cli-args.js +267 -0
  7. package/dist/src/cli-model.js +324 -0
  8. package/dist/src/compact-structural.js +96 -0
  9. package/dist/src/competitive-constraints.js +20 -0
  10. package/dist/src/competitive-manifest.js +330 -0
  11. package/dist/src/competitive-measurement.js +183 -0
  12. package/dist/src/competitive-runner.js +453 -0
  13. package/dist/src/competitive-sandbox.js +108 -0
  14. package/dist/src/context-export.js +422 -0
  15. package/dist/src/context.js +102 -0
  16. package/dist/src/docs-sections.js +141 -0
  17. package/dist/src/doctor.js +380 -0
  18. package/dist/src/engine/ann-hnsw.js +271 -0
  19. package/dist/src/engine/embeddings.js +193 -0
  20. package/dist/src/engine/file-walker.js +43 -0
  21. package/dist/src/engine/index.js +13030 -0
  22. package/dist/src/engine/perf.js +115 -0
  23. package/dist/src/engine/prune.js +112 -0
  24. package/dist/src/engine/source-policy.js +69 -0
  25. package/dist/src/engine/sqlite.js +71 -0
  26. package/dist/src/engine/symbol-delete.js +58 -0
  27. package/dist/src/failure-diagnosis.js +590 -0
  28. package/dist/src/fleet.js +7 -0
  29. package/dist/src/git-executable.js +31 -0
  30. package/dist/src/graph-query-health.js +115 -0
  31. package/dist/src/index-activity.js +125 -0
  32. package/dist/src/init-progress-worker.js +107 -0
  33. package/dist/src/init-progress.js +155 -0
  34. package/dist/src/init.js +985 -0
  35. package/dist/src/lifecycle-health.js +213 -0
  36. package/dist/src/lsp-readonly.js +217 -0
  37. package/dist/src/output-compression.js +629 -0
  38. package/dist/src/output-telemetry.js +359 -0
  39. package/dist/src/pr-triage.js +638 -0
  40. package/dist/src/relationship-adapters.js +370 -0
  41. package/dist/src/release-attestation.js +533 -0
  42. package/dist/src/repair-progress-worker.js +121 -0
  43. package/dist/src/repair-progress.js +262 -0
  44. package/dist/src/repository-init-process.js +173 -0
  45. package/dist/src/repository-management.js +1089 -0
  46. package/dist/src/response-budget.js +184 -0
  47. package/dist/src/server.js +53 -0
  48. package/dist/src/system-config.js +615 -0
  49. package/dist/src/terminal-help.js +83 -0
  50. package/dist/src/tools/knodin-tools.js +1438 -0
  51. package/dist/src/tools/reckon-tools.js +5 -0
  52. package/dist/src/update-policy.js +944 -0
  53. package/dist/src/update-trust.js +503 -0
  54. package/dist/src/version.js +13 -0
  55. package/dist/src/visualization.js +162 -0
  56. package/dist/src/wait-for-fresh.js +98 -0
  57. package/dist/src/worktree-lifecycle.js +231 -0
  58. package/docs/CLI.md +39 -0
  59. package/docs/COMMAND-OUTPUT-COMPRESSION.md +194 -0
  60. package/docs/DEAD-CODE-AND-IMPACT.md +27 -0
  61. package/docs/DOCTOR-AND-UPDATES.md +84 -0
  62. package/docs/INDEXING-POLICY-AND-PROVENANCE.md +37 -0
  63. package/docs/INSTALLATION.md +208 -0
  64. package/docs/MCP.md +100 -0
  65. package/docs/PT-ACCESS-RECOMMENDATION.md +91 -0
  66. package/docs/RELEASE-0.3-EVIDENCE.md +73 -0
  67. package/docs/REPOSITORIES-AND-WORKTREES.md +81 -0
  68. package/docs/SIGNED-UPDATES.md +146 -0
  69. package/docs/SYSTEMS-AND-RELATIONSHIPS.md +45 -0
  70. package/docs/TELEMETRY.md +42 -0
  71. package/docs/releases/0.3.0.md +46 -0
  72. package/docs/releases/0.4.0.md +68 -0
  73. package/docs/releases/0.4.1.md +28 -0
  74. package/docs/releases/0.4.2.md +27 -0
  75. package/docs/releases/0.4.3.md +23 -0
  76. package/docs/releases/0.5.0.md +29 -0
  77. package/package.json +110 -0
  78. package/schemas/release-attestation-v1.schema.json +210 -0
  79. package/tree-sitter-prisma.wasm +0 -0
  80. package/tree-sitter-sql.wasm +0 -0
  81. package/tree-sitter-xml.wasm +0 -0
@@ -0,0 +1,1089 @@
1
+ import child_process from "node:child_process";
2
+ import crypto from "node:crypto";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { gitExecutable } from "./git-executable.js";
6
+ import { initializeRepository, isRepositoryInitializationCurrent, readRepositoryIntegrationConfig, } from "./init.js";
7
+ import { applyResponseBudget } from "./response-budget.js";
8
+ const DEFAULT_DEPTH = 4;
9
+ const MAX_DEPTH = 32;
10
+ const MAX_GIT_ERROR_BYTES = 16 * 1024;
11
+ const MAX_TRACKED_PATH_BYTES = 1024 * 1024;
12
+ const MAX_DOCUMENT_CANDIDATES = 20;
13
+ const SKIPPED_DIRECTORIES = new Set([".git", ".reckon", "node_modules"]);
14
+ function gitTopLevel(directory) {
15
+ return child_process
16
+ .execFileSync(gitExecutable(), ["rev-parse", "--show-toplevel"], {
17
+ cwd: directory,
18
+ encoding: "utf-8",
19
+ stdio: ["ignore", "pipe", "ignore"],
20
+ })
21
+ .trim();
22
+ }
23
+ function errorMessage(error) {
24
+ return error instanceof Error ? error.message : String(error);
25
+ }
26
+ function validateDepth(depth) {
27
+ if (!Number.isInteger(depth) || depth < 0 || depth > MAX_DEPTH) {
28
+ throw new Error(`knodin repos: --depth must be an integer between 0 and ${MAX_DEPTH}`);
29
+ }
30
+ }
31
+ function worktreeRecords(repository) {
32
+ const listing = child_process.execFileSync(gitExecutable(), ["worktree", "list", "--porcelain", "-z"], {
33
+ cwd: repository,
34
+ encoding: "utf-8",
35
+ stdio: ["ignore", "pipe", "ignore"],
36
+ });
37
+ const records = [];
38
+ for (const field of listing.split("\0")) {
39
+ if (field.startsWith("worktree ")) {
40
+ records.push({
41
+ path: path.resolve(field.slice("worktree ".length)),
42
+ main: records.length === 0,
43
+ });
44
+ }
45
+ else if (field.startsWith("prunable ")) {
46
+ const current = records.at(-1);
47
+ if (current)
48
+ current.stale = field.slice("prunable ".length);
49
+ }
50
+ }
51
+ return records;
52
+ }
53
+ /**
54
+ * Discover repository checkouts without conflating independent repositories
55
+ * with Git linked worktrees. Classification is relative to the supplied roots:
56
+ * a repository below another repository is nested; other additional Git roots
57
+ * are unrelated unless Git's own worktree metadata links them.
58
+ */
59
+ export async function discoverRepositories(roots, options = {}) {
60
+ const depth = options.depth ?? DEFAULT_DEPTH;
61
+ const linkedWorktrees = options.linkedWorktrees ?? "skip";
62
+ const legacy = await discoverGitRepositoryPaths(roots, depth, linkedWorktrees);
63
+ const canonicalRoots = (await Promise.all(roots.map(async (root) => {
64
+ try {
65
+ return await fs.promises.realpath(path.resolve(root));
66
+ }
67
+ catch {
68
+ return path.resolve(root);
69
+ }
70
+ }))).sort((left, right) => left.localeCompare(right));
71
+ const mainByWorktree = new Map();
72
+ for (const repository of legacy.repositories) {
73
+ try {
74
+ const records = worktreeRecords(repository);
75
+ const main = records.find(({ main }) => main)?.path ?? repository;
76
+ for (const record of records)
77
+ mainByWorktree.set(record.path, main);
78
+ }
79
+ catch {
80
+ mainByWorktree.set(repository, repository);
81
+ }
82
+ }
83
+ const repositorySet = new Set(legacy.repositories);
84
+ const mainRepositories = legacy.repositories.filter((repository) => (mainByWorktree.get(repository) ?? repository) === repository);
85
+ const topLevelRepositories = mainRepositories
86
+ .filter((repository) => !mainRepositories.some((candidate) => candidate !== repository && repository.startsWith(`${candidate}${path.sep}`)))
87
+ .sort((left, right) => left.localeCompare(right));
88
+ const repositories = legacy.repositories
89
+ .map((repository) => {
90
+ const mainWorktree = mainByWorktree.get(repository) ?? repository;
91
+ if (mainWorktree !== repository) {
92
+ return { path: repository, classification: "linked-worktree", mainWorktree };
93
+ }
94
+ const hasParentRepository = mainRepositories.some((candidate) => candidate !== repository &&
95
+ repository.startsWith(`${candidate}${path.sep}`) &&
96
+ repositorySet.has(candidate));
97
+ if (hasParentRepository) {
98
+ return {
99
+ path: repository,
100
+ classification: "nested-repository",
101
+ mainWorktree: repository,
102
+ };
103
+ }
104
+ const isPrimaryForRoot = canonicalRoots.some((root) => topLevelRepositories.find((candidate) => candidate === root || candidate.startsWith(`${root}${path.sep}`)) === repository);
105
+ return {
106
+ path: repository,
107
+ classification: isPrimaryForRoot ? "main-worktree" : "unrelated-repository",
108
+ mainWorktree: repository,
109
+ };
110
+ })
111
+ .filter(({ classification }) => linkedWorktrees === "include" || classification !== "linked-worktree");
112
+ // Deterministic UX ordering groups linked worktrees before their main
113
+ // checkout, followed by nested and then unrelated repositories.
114
+ const classificationOrder = {
115
+ "linked-worktree": 0,
116
+ "main-worktree": 1,
117
+ "nested-repository": 2,
118
+ "unrelated-repository": 3,
119
+ };
120
+ repositories.sort((left, right) => classificationOrder[left.classification] - classificationOrder[right.classification] ||
121
+ left.path.localeCompare(right.path));
122
+ return {
123
+ repositories,
124
+ issues: legacy.issues,
125
+ emptyRoots: legacy.emptyRoots,
126
+ staleLinkedWorktrees: legacy.staleWorktrees,
127
+ };
128
+ }
129
+ function requiredArgument(args, index, option) {
130
+ const value = args[index + 1];
131
+ if (value === undefined)
132
+ throw new Error(`knodin repos: ${option} requires a value`);
133
+ return value;
134
+ }
135
+ function setLinkedWorktreePolicy(state, value) {
136
+ if (value !== "skip" && value !== "include")
137
+ throw new Error("knodin repos: --linked-worktrees must be skip or include");
138
+ state.linkedWorktrees = value;
139
+ }
140
+ function parseRepositoryPrefixedOrPositional(argument, index, cwd, command, state) {
141
+ if (argument.startsWith("--include=") || argument.startsWith("--exclude=")) {
142
+ const target = argument.startsWith("--include=") ? state.include : state.exclude;
143
+ target.push(...argument
144
+ .slice(argument.indexOf("=") + 1)
145
+ .split(",")
146
+ .filter(Boolean));
147
+ return index;
148
+ }
149
+ if (argument.startsWith("--depth=")) {
150
+ state.depth = Number(argument.slice("--depth=".length));
151
+ return index;
152
+ }
153
+ if (argument.startsWith("--linked-worktrees=")) {
154
+ setLinkedWorktreePolicy(state, argument.slice("--linked-worktrees=".length));
155
+ return index;
156
+ }
157
+ if (argument.startsWith("--manifest=")) {
158
+ state.manifestPath = path.resolve(cwd, argument.slice("--manifest=".length));
159
+ return index;
160
+ }
161
+ if (argument.startsWith("--"))
162
+ throw new Error(`knodin repos: unknown option ${argument}`);
163
+ if (command === "search" && state.query === undefined)
164
+ state.query = argument;
165
+ else
166
+ state.roots.push(path.resolve(cwd, argument));
167
+ return index;
168
+ }
169
+ function parseRepositoryOption(args, index, cwd, command, state) {
170
+ const argument = args[index] ?? "";
171
+ switch (argument) {
172
+ case "--dry-run":
173
+ state.dryRun = true;
174
+ return index;
175
+ case "--json":
176
+ state.json = true;
177
+ return index;
178
+ case "--allow-partial":
179
+ state.allowPartial = true;
180
+ return index;
181
+ case "--root":
182
+ state.roots.push(path.resolve(cwd, requiredArgument(args, index, argument)));
183
+ return index + 1;
184
+ case "--include":
185
+ case "--exclude": {
186
+ const target = argument === "--include" ? state.include : state.exclude;
187
+ target.push(...requiredArgument(args, index, argument).split(",").filter(Boolean));
188
+ return index + 1;
189
+ }
190
+ case "--items":
191
+ case "--bytes":
192
+ case "--tokens": {
193
+ const value = Number(requiredArgument(args, index, argument));
194
+ if (!Number.isInteger(value) || value < 1)
195
+ throw new Error(`knodin repos: ${argument} requires a positive integer`);
196
+ if (argument === "--items")
197
+ state.itemBudget = value;
198
+ if (argument === "--bytes")
199
+ state.byteBudget = value;
200
+ if (argument === "--tokens")
201
+ state.tokenBudget = value;
202
+ return index + 1;
203
+ }
204
+ case "--cursor":
205
+ state.cursor = requiredArgument(args, index, argument);
206
+ return index + 1;
207
+ case "--manifest":
208
+ state.manifestPath = path.resolve(cwd, requiredArgument(args, index, argument));
209
+ return index + 1;
210
+ case "--depth":
211
+ state.depth = Number(requiredArgument(args, index, argument));
212
+ return index + 1;
213
+ case "--linked-worktrees":
214
+ setLinkedWorktreePolicy(state, requiredArgument(args, index, argument));
215
+ return index + 1;
216
+ }
217
+ return parseRepositoryPrefixedOrPositional(argument, index, cwd, command, state);
218
+ }
219
+ function parseRepositoryCommand(value) {
220
+ if (!value || !["discover", "init", "status", "doctor", "search"].includes(value))
221
+ throw new Error("knodin repos requires discover, init, status, doctor, or search");
222
+ return value;
223
+ }
224
+ export function parseRepositoryCommandArgs(args, cwd) {
225
+ const command = parseRepositoryCommand(args[0]);
226
+ const state = {
227
+ depth: DEFAULT_DEPTH,
228
+ dryRun: false,
229
+ json: false,
230
+ linkedWorktrees: "skip",
231
+ roots: [],
232
+ include: [],
233
+ exclude: [],
234
+ allowPartial: false,
235
+ };
236
+ let index = 1;
237
+ while (index < args.length) {
238
+ index = parseRepositoryOption(args, index, cwd, command, state) + 1;
239
+ }
240
+ validateDepth(state.depth);
241
+ if (command === "search" && !state.query?.trim())
242
+ throw new Error("knodin repos search requires a non-empty <query>");
243
+ if (state.roots.length === 0) {
244
+ if (command === "search")
245
+ state.roots.push(cwd);
246
+ else
247
+ throw new Error(`knodin repos ${command} requires at least one <root>`);
248
+ }
249
+ if (state.dryRun && command !== "init")
250
+ throw new Error(`knodin repos ${command}: --dry-run applies only to init`);
251
+ if (state.manifestPath && command !== "init")
252
+ throw new Error(`knodin repos ${command}: --manifest applies only to init`);
253
+ if (command !== "search" &&
254
+ command !== "init" &&
255
+ (state.include.length > 0 || state.exclude.length > 0))
256
+ throw new Error(`knodin repos ${command}: repository selectors apply only to init or search`);
257
+ if (command !== "search" &&
258
+ (state.allowPartial ||
259
+ state.itemBudget ||
260
+ state.byteBudget ||
261
+ state.tokenBudget ||
262
+ state.cursor))
263
+ throw new Error(`knodin repos ${command}: search options apply only to search`);
264
+ return { command, ...state };
265
+ }
266
+ /**
267
+ * Find real Git worktree roots without following child symlinks. Every supplied
268
+ * root is resolved through realpath, so aliases are accepted but cannot create
269
+ * duplicate work. Nested repositories and linked worktrees are independent
270
+ * targets; traversal continues below a repository until the requested depth.
271
+ */
272
+ async function gitRepositoryAt(directory) {
273
+ const gitEntry = path.join(directory, ".git");
274
+ try {
275
+ await fs.promises.lstat(gitEntry);
276
+ }
277
+ catch (error) {
278
+ if (error.code === "ENOENT")
279
+ return undefined;
280
+ throw error;
281
+ }
282
+ const realTopLevel = await fs.promises.realpath(gitTopLevel(directory));
283
+ return realTopLevel === directory ? realTopLevel : undefined;
284
+ }
285
+ async function childDirectories(current, depth, worktrees) {
286
+ if (current.level >= depth)
287
+ return [];
288
+ const entries = await fs.promises.readdir(current.directory, { withFileTypes: true });
289
+ entries.sort((left, right) => left.name.localeCompare(right.name));
290
+ return entries.flatMap((entry) => {
291
+ const skipped = !entry.isDirectory() ||
292
+ SKIPPED_DIRECTORIES.has(entry.name) ||
293
+ (entry.name === ".worktrees" && worktrees === "skip");
294
+ return skipped
295
+ ? []
296
+ : [{ directory: path.join(current.directory, entry.name), level: current.level + 1 }];
297
+ });
298
+ }
299
+ async function scanDiscoveryRoot(root, depth, worktrees) {
300
+ const repositories = new Set();
301
+ const issues = [];
302
+ const staleWorktrees = new Map();
303
+ let found = false;
304
+ const queue = [{ directory: root, level: 0 }];
305
+ for (const current of queue) {
306
+ try {
307
+ const repository = await gitRepositoryAt(current.directory);
308
+ if (repository) {
309
+ repositories.add(repository);
310
+ found = true;
311
+ for (const record of worktreeRecords(repository)) {
312
+ if (record.stale || !fs.existsSync(record.path)) {
313
+ staleWorktrees.set(record.path, record.stale ?? "registered worktree path no longer exists");
314
+ }
315
+ }
316
+ }
317
+ queue.push(...(await childDirectories(current, depth, worktrees)));
318
+ }
319
+ catch (error) {
320
+ issues.push({ path: current.directory, message: errorMessage(error) });
321
+ }
322
+ }
323
+ return {
324
+ repositories: [...repositories],
325
+ issues,
326
+ staleWorktrees: [...staleWorktrees],
327
+ found,
328
+ };
329
+ }
330
+ export async function discoverGitRepositoryPaths(roots, depth = DEFAULT_DEPTH, worktrees = "skip") {
331
+ validateDepth(depth);
332
+ const repositories = new Set();
333
+ const issues = [];
334
+ const emptyRoots = [];
335
+ const seenRoots = new Set();
336
+ const staleWorktrees = new Map();
337
+ for (const suppliedRoot of [...roots].sort((left, right) => left.localeCompare(right))) {
338
+ let root;
339
+ try {
340
+ root = await fs.promises.realpath(path.resolve(suppliedRoot));
341
+ const stat = await fs.promises.stat(root);
342
+ if (!stat.isDirectory())
343
+ throw new Error("not a directory");
344
+ }
345
+ catch (error) {
346
+ issues.push({
347
+ path: path.resolve(suppliedRoot),
348
+ message: errorMessage(error),
349
+ });
350
+ continue;
351
+ }
352
+ if (seenRoots.has(root))
353
+ continue;
354
+ seenRoots.add(root);
355
+ const scanned = await scanDiscoveryRoot(root, depth, worktrees);
356
+ for (const repository of scanned.repositories)
357
+ repositories.add(repository);
358
+ issues.push(...scanned.issues);
359
+ for (const [worktree, message] of scanned.staleWorktrees)
360
+ staleWorktrees.set(worktree, message);
361
+ if (!scanned.found && scanned.issues.length === 0)
362
+ emptyRoots.push(root);
363
+ }
364
+ const orderedRepositories = [...repositories];
365
+ orderedRepositories.sort((left, right) => left.localeCompare(right));
366
+ const orderedIssues = [...issues];
367
+ orderedIssues.sort((left, right) => left.path.localeCompare(right.path));
368
+ const orderedEmptyRoots = [...emptyRoots];
369
+ orderedEmptyRoots.sort((left, right) => left.localeCompare(right));
370
+ return {
371
+ repositories: orderedRepositories,
372
+ issues: orderedIssues,
373
+ emptyRoots: orderedEmptyRoots,
374
+ staleWorktrees: [...staleWorktrees]
375
+ .map(([stalePath, message]) => ({ path: stalePath, message }))
376
+ .sort((left, right) => left.path.localeCompare(right.path)),
377
+ };
378
+ }
379
+ /** Parse the deliberately small, bounded `knodin fleet init` argument surface. */
380
+ function parseFleetOption(args, index, cwd, plan) {
381
+ const argument = args[index] ?? "";
382
+ switch (argument) {
383
+ case "--dry-run":
384
+ plan.dryRun = true;
385
+ return index;
386
+ case "--json":
387
+ plan.json = true;
388
+ return index;
389
+ case "--depth": {
390
+ const value = args[index + 1];
391
+ if (value === undefined)
392
+ throw new Error("knodin fleet init: --depth requires a value");
393
+ plan.depth = Number(value);
394
+ return index + 1;
395
+ }
396
+ }
397
+ if (argument.startsWith("--depth=")) {
398
+ plan.depth = Number(argument.slice("--depth=".length));
399
+ return index;
400
+ }
401
+ if (argument === "--worktrees" || argument.startsWith("--worktrees=")) {
402
+ const value = argument === "--worktrees" ? args[index + 1] : argument.slice("--worktrees=".length);
403
+ if (value !== "skip" && value !== "include")
404
+ throw new Error("knodin fleet init: --worktrees must be skip or include");
405
+ plan.worktrees = value;
406
+ return argument === "--worktrees" ? index + 1 : index;
407
+ }
408
+ if (argument.startsWith("--"))
409
+ throw new Error(`knodin fleet init: unknown option ${argument}`);
410
+ plan.roots.push(path.resolve(cwd, argument));
411
+ return index;
412
+ }
413
+ export function parseFleetInitArgs(args, cwd) {
414
+ if (args[0] !== "init")
415
+ throw new Error("knodin fleet requires `init`");
416
+ const plan = {
417
+ depth: DEFAULT_DEPTH,
418
+ dryRun: false,
419
+ json: false,
420
+ worktrees: "skip",
421
+ roots: [],
422
+ };
423
+ let index = 1;
424
+ while (index < args.length) {
425
+ index = parseFleetOption(args, index, cwd, plan) + 1;
426
+ }
427
+ validateDepth(plan.depth);
428
+ if (plan.roots.length === 0)
429
+ throw new Error("knodin fleet init requires at least one <root>");
430
+ return plan;
431
+ }
432
+ function gitValue(repository, args) {
433
+ try {
434
+ return child_process
435
+ .execFileSync(gitExecutable(), args, {
436
+ cwd: repository,
437
+ encoding: "utf-8",
438
+ stdio: ["ignore", "pipe", "ignore"],
439
+ })
440
+ .trim();
441
+ }
442
+ catch {
443
+ return "";
444
+ }
445
+ }
446
+ function portableRemote(remote) {
447
+ try {
448
+ const parsed = new URL(remote);
449
+ parsed.username = "";
450
+ parsed.password = "";
451
+ parsed.search = "";
452
+ parsed.hash = "";
453
+ return parsed.toString().replace(/\/$/, "");
454
+ }
455
+ catch {
456
+ return remote.replace(/^[^@/\s]+@(?=[^:/\s]+[:/])/, "").replace(/\.git$/, "");
457
+ }
458
+ }
459
+ /** Stable, path-independent identity where Git origin or root history exists. */
460
+ export function repositoryIdentity(repository) {
461
+ const remote = portableRemote(gitValue(repository, ["remote", "get-url", "origin"]));
462
+ const rootCommit = gitValue(repository, ["rev-list", "--max-parents=0", "HEAD"]).split("\n")[0] ?? "";
463
+ const seed = remote || `${rootCommit}:${path.basename(repository)}`;
464
+ return `repo_${crypto.createHash("sha256").update(seed).digest("hex").slice(0, 20)}`;
465
+ }
466
+ const LANGUAGE_BY_EXTENSION = {
467
+ ".c": "c",
468
+ ".cc": "cpp",
469
+ ".cpp": "cpp",
470
+ ".cs": "csharp",
471
+ ".go": "go",
472
+ ".java": "java",
473
+ ".js": "javascript",
474
+ ".jsx": "javascript",
475
+ ".kt": "kotlin",
476
+ ".php": "php",
477
+ ".py": "python",
478
+ ".rb": "ruby",
479
+ ".rs": "rust",
480
+ ".swift": "swift",
481
+ ".ts": "typescript",
482
+ ".tsx": "typescript",
483
+ };
484
+ function insertBoundedCandidate(candidates, file) {
485
+ const insertion = candidates.findIndex((candidate) => candidate.localeCompare(file) > 0);
486
+ if (insertion === -1)
487
+ candidates.push(file);
488
+ else
489
+ candidates.splice(insertion, 0, file);
490
+ if (candidates.length > MAX_DOCUMENT_CANDIDATES)
491
+ candidates.pop();
492
+ }
493
+ function classifyTrackedFile(inventory, file) {
494
+ inventory.count += 1;
495
+ const language = LANGUAGE_BY_EXTENSION[path.extname(file).toLowerCase()];
496
+ if (language && !inventory.languages.includes(language))
497
+ inventory.languages.push(language);
498
+ if (["package.json", "pyproject.toml", "Cargo.toml", "go.mod"].includes(file))
499
+ inventory.manifests.add(file);
500
+ const normalized = file.replaceAll("\\", "/");
501
+ const basename = path.posix.basename(normalized).toLowerCase();
502
+ const documentationLike = /\.(?:md|mdx|txt|rst|adoc|asciidoc)$/i.test(basename);
503
+ if (/^readme(?:\.|$)/.test(basename))
504
+ insertBoundedCandidate(inventory.documentCandidates.readme, file);
505
+ if (documentationLike &&
506
+ (/^(?:architecture|design)(?:\.|$)/.test(basename) ||
507
+ /^adr[-_.]?\d/i.test(basename) ||
508
+ normalized
509
+ .toLowerCase()
510
+ .split("/")
511
+ .some((part) => part === "architecture" || part === "adr" || part === "adrs")))
512
+ insertBoundedCandidate(inventory.documentCandidates.architecture, file);
513
+ if (documentationLike && /^(?:roadmap|backlog)(?:\.|$)/.test(basename))
514
+ insertBoundedCandidate(inventory.documentCandidates.roadmap, file);
515
+ }
516
+ function trackedFileInventory(repository) {
517
+ return new Promise((resolve, reject) => {
518
+ const inventory = {
519
+ count: 0,
520
+ languages: [],
521
+ manifests: new Set(),
522
+ documentCandidates: { readme: [], architecture: [], roadmap: [] },
523
+ };
524
+ const child = child_process.spawn(gitExecutable(), ["ls-files", "-z"], {
525
+ cwd: repository,
526
+ stdio: ["ignore", "pipe", "pipe"],
527
+ });
528
+ let pending = Buffer.alloc(0);
529
+ let stderr = "";
530
+ let settled = false;
531
+ const fail = (error) => {
532
+ if (settled)
533
+ return;
534
+ settled = true;
535
+ child.kill();
536
+ reject(error);
537
+ };
538
+ child.stdout.on("data", (chunk) => {
539
+ if (settled)
540
+ return;
541
+ pending = Buffer.concat([pending, chunk]);
542
+ let separator = pending.indexOf(0);
543
+ while (separator >= 0) {
544
+ classifyTrackedFile(inventory, pending.subarray(0, separator).toString("utf-8"));
545
+ pending = pending.subarray(separator + 1);
546
+ separator = pending.indexOf(0);
547
+ }
548
+ if (pending.length > MAX_TRACKED_PATH_BYTES) {
549
+ fail(new Error(`git ls-files returned a path over ${MAX_TRACKED_PATH_BYTES} bytes`));
550
+ }
551
+ });
552
+ child.stderr.on("data", (chunk) => {
553
+ if (stderr.length < MAX_GIT_ERROR_BYTES)
554
+ stderr += chunk.toString("utf-8").slice(0, MAX_GIT_ERROR_BYTES - stderr.length);
555
+ });
556
+ child.on("error", fail);
557
+ child.on("close", (code, signal) => {
558
+ if (settled)
559
+ return;
560
+ settled = true;
561
+ if (code !== 0) {
562
+ const outcome = signal ? `signal ${signal}` : `exit ${code ?? "unknown"}`;
563
+ const diagnostic = stderr.trim() || "no diagnostic";
564
+ reject(new Error(`git ls-files failed (${outcome}): ${diagnostic}`));
565
+ return;
566
+ }
567
+ if (pending.length > 0)
568
+ classifyTrackedFile(inventory, pending.toString("utf-8"));
569
+ inventory.languages.sort((left, right) => left.localeCompare(right));
570
+ resolve(inventory);
571
+ });
572
+ });
573
+ }
574
+ function packageIdentities(repository, manifestFiles) {
575
+ const manifests = [
576
+ ["package.json", "name"],
577
+ ["pyproject.toml", "name"],
578
+ ["Cargo.toml", "name"],
579
+ ["go.mod", "module"],
580
+ ];
581
+ const identities = [];
582
+ for (const [manifest, field] of manifests) {
583
+ if (!manifestFiles.has(manifest))
584
+ continue;
585
+ try {
586
+ const content = fs.readFileSync(path.join(repository, manifest), "utf-8");
587
+ if (manifest === "package.json") {
588
+ const name = JSON.parse(content).name;
589
+ if (typeof name === "string")
590
+ identities.push(name);
591
+ }
592
+ else {
593
+ const pattern = new RegExp(String.raw `^${field}[ \t]*(?:=[ \t]*)?["']?([^"'\s]+)`, "m");
594
+ const match = pattern.exec(content);
595
+ if (match?.[1])
596
+ identities.push(match[1]);
597
+ }
598
+ }
599
+ catch {
600
+ // Malformed manifests remain visible through the repository's health error.
601
+ }
602
+ }
603
+ return [...new Set(identities)].sort((left, right) => left.localeCompare(right));
604
+ }
605
+ function canonicalRepositorySelector(value) {
606
+ try {
607
+ return fs.realpathSync(path.resolve(value));
608
+ }
609
+ catch {
610
+ return path.resolve(value);
611
+ }
612
+ }
613
+ async function inspectInventoryGraph(repository, options) {
614
+ try {
615
+ const graph = await options.status(repository);
616
+ const lastIndexedHead = (await options.lastIndexedHead?.(repository)) ?? graph.lastIndexedHead ?? "";
617
+ const indexGeneration = graph.indexGeneration === undefined
618
+ ? graph.verification.verifiedAt
619
+ : String(graph.indexGeneration);
620
+ let health = "healthy";
621
+ if (graph.status !== "healthy")
622
+ health = graph.status;
623
+ else if (graph.coverage.sourceFiles > 0 && graph.coverage.indexedFiles === 0)
624
+ health = "empty-index";
625
+ else if (graph.lifecycle && graph.lifecycle.status !== "healthy")
626
+ health = "lifecycle-degraded";
627
+ return {
628
+ health,
629
+ lastIndexedHead,
630
+ indexGeneration,
631
+ errors: [],
632
+ remediation: health === "healthy" ? [] : graph.repairSteps,
633
+ };
634
+ }
635
+ catch (error) {
636
+ return {
637
+ health: "unknown",
638
+ lastIndexedHead: "",
639
+ indexGeneration: null,
640
+ errors: [errorMessage(error)],
641
+ remediation: [],
642
+ };
643
+ }
644
+ }
645
+ function inventoryFreshness(lastIndexedHead, head) {
646
+ if (lastIndexedHead === "")
647
+ return "unknown";
648
+ return lastIndexedHead === head ? "fresh" : "stale";
649
+ }
650
+ export async function inventoryRepository(record, options) {
651
+ const id = repositoryIdentity(record.path);
652
+ const mainWorktreeId = repositoryIdentity(record.mainWorktree);
653
+ const databaseExists = fs.existsSync(path.join(record.path, ".reckon", "db.sqlite"));
654
+ const head = gitValue(record.path, ["rev-parse", "HEAD"]);
655
+ let tracked;
656
+ try {
657
+ tracked = await trackedFileInventory(record.path);
658
+ }
659
+ catch (error) {
660
+ const message = `tracked-file inventory failed: ${errorMessage(error)}`;
661
+ return {
662
+ schemaVersion: 1,
663
+ id,
664
+ ...(options.exposePaths === false ? {} : { path: record.path }),
665
+ classification: record.classification,
666
+ mainWorktreeId,
667
+ initialization: databaseExists ? "initialized" : "not-initialized",
668
+ health: "unknown",
669
+ head,
670
+ lastIndexedHead: "",
671
+ freshness: "unknown",
672
+ indexGeneration: null,
673
+ languages: [],
674
+ packageIdentities: [],
675
+ systemMemberships: [...(options.systemMemberships?.(record.path) ?? [])].sort((left, right) => left.localeCompare(right)),
676
+ documentCandidates: { readme: [], architecture: [], roadmap: [] },
677
+ errors: [message],
678
+ remediation: [`verify git ls-files in ${record.path}`, "retry repository inventory"],
679
+ };
680
+ }
681
+ const baselineHealth = tracked.count === 0 ? "empty-repository" : "not-initialized";
682
+ const graph = databaseExists && tracked.count > 0
683
+ ? await inspectInventoryGraph(record.path, options)
684
+ : {
685
+ health: baselineHealth,
686
+ lastIndexedHead: "",
687
+ indexGeneration: null,
688
+ errors: [],
689
+ remediation: [],
690
+ };
691
+ const remediation = [...graph.remediation];
692
+ const { health, lastIndexedHead, indexGeneration, errors } = graph;
693
+ if (health === "not-initialized")
694
+ remediation.push(`knodin init --repo ${record.path}`);
695
+ if (health === "empty-index" || health === "repair-needed")
696
+ remediation.push("knodin repair");
697
+ return {
698
+ schemaVersion: 1,
699
+ id,
700
+ ...(options.exposePaths === false ? {} : { path: record.path }),
701
+ classification: record.classification,
702
+ mainWorktreeId,
703
+ initialization: databaseExists ? "initialized" : "not-initialized",
704
+ health,
705
+ head,
706
+ lastIndexedHead,
707
+ freshness: inventoryFreshness(lastIndexedHead, head),
708
+ indexGeneration,
709
+ languages: tracked.languages,
710
+ packageIdentities: packageIdentities(record.path, tracked.manifests),
711
+ systemMemberships: [...(options.systemMemberships?.(record.path) ?? [])].sort((left, right) => left.localeCompare(right)),
712
+ documentCandidates: tracked.documentCandidates,
713
+ errors,
714
+ remediation: [...new Set(remediation)],
715
+ };
716
+ }
717
+ function encodeCursor(value) {
718
+ return Buffer.from(JSON.stringify({ version: 1, ...value })).toString("base64url");
719
+ }
720
+ function decodeCursor(cursor, hash) {
721
+ if (!cursor)
722
+ return { repository: 0, offset: 0 };
723
+ try {
724
+ const value = JSON.parse(Buffer.from(cursor, "base64url").toString("utf-8"));
725
+ if (value.version !== 1 ||
726
+ value.hash !== hash ||
727
+ !Number.isInteger(value.repository) ||
728
+ !Number.isInteger(value.offset) ||
729
+ (value.repository ?? -1) < 0 ||
730
+ (value.offset ?? -1) < 0)
731
+ throw new Error("invalid cursor");
732
+ return { repository: value.repository ?? 0, offset: value.offset ?? 0 };
733
+ }
734
+ catch {
735
+ throw new Error("knodin repos search: cursor does not match this query and selection");
736
+ }
737
+ }
738
+ function searchStatus(omissionCount, resultCount, selectedCount) {
739
+ if (omissionCount > 0)
740
+ return "partial";
741
+ if (resultCount > 0)
742
+ return "ok";
743
+ return selectedCount > 0 ? "no-match" : "unavailable";
744
+ }
745
+ function finalizeRepositorySearch(value, byteLimit, tokenLimit, itemLimit) {
746
+ return applyResponseBudget(value, "repositories:search", { bytes: byteLimit, tokens: tokenLimit, items: itemLimit }, { bytes: byteLimit, tokens: tokenLimit, items: itemLimit });
747
+ }
748
+ function searchCandidate(inventory, match, exposePaths) {
749
+ return {
750
+ repositoryId: inventory.id,
751
+ ...(exposePaths === false ? {} : { repositoryPath: inventory.path }),
752
+ identity: match.identity,
753
+ symbol: match.symbol,
754
+ kind: match.kind,
755
+ file: match.filePath,
756
+ sourceEvidence: match.source.slice(0, 4000),
757
+ sourceTruncated: match.source.length > 4000,
758
+ confidence: match.rrfScore || match.similarity,
759
+ freshness: inventory.freshness,
760
+ health: "healthy",
761
+ };
762
+ }
763
+ async function searchHealthyRepository(inventory, repositoryIndex, offset, context) {
764
+ const { query, itemLimit, effectiveBytes, results, omissions, selected, options } = context;
765
+ const page = await options.search(query, inventory.path ?? "", itemLimit - results.length + 1, offset);
766
+ for (const [matchIndex, match] of page.results.entries()) {
767
+ if (results.length >= itemLimit) {
768
+ return { repository: repositoryIndex, offset: offset + matchIndex };
769
+ }
770
+ const candidate = searchCandidate(inventory, match, options.exposePaths);
771
+ const probe = JSON.stringify({
772
+ results: [...results, candidate],
773
+ omissions,
774
+ repositories: selected,
775
+ });
776
+ if (Buffer.byteLength(probe) > effectiveBytes) {
777
+ return { repository: repositoryIndex, offset: offset + matchIndex };
778
+ }
779
+ results.push(candidate);
780
+ }
781
+ if (page.hasMore)
782
+ return { repository: repositoryIndex, offset: offset + page.results.length };
783
+ return undefined;
784
+ }
785
+ async function collectRepositorySearchResults(selected, start, query, itemLimit, effectiveBytes, options) {
786
+ const results = [];
787
+ const omissions = [];
788
+ for (const [index, inventory] of selected.entries()) {
789
+ if (inventory.health !== "healthy") {
790
+ omissions.push({
791
+ repositoryId: inventory.id,
792
+ state: inventory.health === "no-match" ? "unknown" : inventory.health,
793
+ reason: inventory.errors[0] ?? `repository graph is ${inventory.health}`,
794
+ remediation: inventory.remediation,
795
+ });
796
+ continue;
797
+ }
798
+ if (index < start.repository)
799
+ continue;
800
+ const offset = index === start.repository ? start.offset : 0;
801
+ const next = await searchHealthyRepository(inventory, index, offset, {
802
+ query,
803
+ itemLimit,
804
+ effectiveBytes,
805
+ results,
806
+ omissions,
807
+ selected,
808
+ options,
809
+ });
810
+ if (next)
811
+ return { results, omissions, next };
812
+ }
813
+ return { results, omissions };
814
+ }
815
+ /** Search independent repositories sequentially with explicit omissions and hard response budgets. */
816
+ export async function searchRepositories(roots, query, options) {
817
+ const discovery = await discoverRepositories(roots, options);
818
+ const selectors = new Set((options.include ?? []).flatMap((value) => [value, canonicalRepositorySelector(value)]));
819
+ const excluded = new Set((options.exclude ?? []).flatMap((value) => [value, canonicalRepositorySelector(value)]));
820
+ const identities = new Map(discovery.repositories.map((record) => [record.path, repositoryIdentity(record.path)]));
821
+ const matchesSelector = (record, selector) => selector === record.path ||
822
+ canonicalRepositorySelector(selector) === record.path ||
823
+ selector === identities.get(record.path);
824
+ const unknownSelectors = (options.include ?? []).filter((selector) => !discovery.repositories.some((record) => matchesSelector(record, selector)));
825
+ if (unknownSelectors.length > 0) {
826
+ throw new Error(`knodin repos search: include selector(s) matched no discovered repository: ${unknownSelectors.join(", ")}`);
827
+ }
828
+ const selectedRecords = discovery.repositories.filter((record) => {
829
+ const id = identities.get(record.path) ?? repositoryIdentity(record.path);
830
+ return ((selectors.size === 0 || selectors.has(id) || selectors.has(record.path)) &&
831
+ !excluded.has(id) &&
832
+ !excluded.has(record.path));
833
+ });
834
+ const selected = [];
835
+ for (const record of selectedRecords) {
836
+ selected.push(await inventoryRepository(record, options));
837
+ }
838
+ const hash = crypto
839
+ .createHash("sha256")
840
+ .update(JSON.stringify([query, selected.map(({ id }) => id)]))
841
+ .digest("hex")
842
+ .slice(0, 20);
843
+ const start = decodeCursor(options.cursor, hash);
844
+ const itemLimit = Math.min(Math.max(options.itemBudget ?? 25, 1), 1000);
845
+ if (options.byteBudget !== undefined && options.byteBudget < 1024)
846
+ throw new Error("knodin repos search: byte budget must be at least 1024");
847
+ if (options.tokenBudget !== undefined && options.tokenBudget < 256)
848
+ throw new Error("knodin repos search: token budget must be at least 256");
849
+ const byteLimit = options.byteBudget ?? 65_536;
850
+ const tokenLimit = options.tokenBudget ?? 16_384;
851
+ const effectiveBytes = Math.min(byteLimit, tokenLimit * 4);
852
+ const { results, omissions, next } = await collectRepositorySearchResults(selected, start, query, itemLimit, effectiveBytes, options);
853
+ if (omissions.length > 0 && options.allowPartial !== true) {
854
+ return finalizeRepositorySearch({
855
+ schemaVersion: 1,
856
+ status: "unavailable",
857
+ query,
858
+ results: [],
859
+ repositories: selected,
860
+ omissions,
861
+ }, byteLimit, tokenLimit, itemLimit);
862
+ }
863
+ const status = searchStatus(omissions.length, results.length, selected.length);
864
+ const base = {
865
+ schemaVersion: 1,
866
+ status,
867
+ query,
868
+ results,
869
+ repositories: selected,
870
+ omissions,
871
+ ...(next ? { cursor: encodeCursor({ hash, ...next }) } : {}),
872
+ };
873
+ return finalizeRepositorySearch(base, byteLimit, tokenLimit, itemLimit);
874
+ }
875
+ /**
876
+ * Initialize repositories sequentially. A healthy graph is never fully indexed
877
+ * merely to refresh stale hooks; a no-op index callback lets the initializer
878
+ * update only its managed files. Failures are isolated to their repository.
879
+ */
880
+ function initializationMessage(databaseExists, healthy) {
881
+ if (healthy)
882
+ return "managed installation updated; healthy graph preserved";
883
+ return databaseExists
884
+ ? "managed installation and graph updated"
885
+ : "managed installation and graph initialized";
886
+ }
887
+ function dryRunInitialization(repository, databaseExists, current) {
888
+ if (databaseExists && current) {
889
+ return {
890
+ repository,
891
+ status: "skipped",
892
+ message: "dry-run: would verify graph health before deciding whether to update",
893
+ };
894
+ }
895
+ return {
896
+ repository,
897
+ status: "skipped",
898
+ plannedStatus: databaseExists ? "updated" : "initialized",
899
+ message: databaseExists
900
+ ? "dry-run: would update managed installation"
901
+ : "dry-run: would initialize",
902
+ };
903
+ }
904
+ function initializationSignature(repositories, depth, worktrees) {
905
+ return crypto
906
+ .createHash("sha256")
907
+ .update(JSON.stringify({ repositories, depth, worktrees }))
908
+ .digest("hex");
909
+ }
910
+ function readInitializationManifest(manifestPath, signature) {
911
+ if (!manifestPath || !fs.existsSync(manifestPath))
912
+ return null;
913
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
914
+ if (manifest.schemaVersion !== 1 ||
915
+ manifest.signature !== signature ||
916
+ !Array.isArray(manifest.completed)) {
917
+ throw new Error(`knodin repos init: ${manifestPath} does not match this repository selection and policy`);
918
+ }
919
+ return manifest;
920
+ }
921
+ function writeInitializationManifest(manifestPath, signature, completed) {
922
+ fs.mkdirSync(path.dirname(manifestPath), { recursive: true, mode: 0o700 });
923
+ const temporary = `${manifestPath}.${process.pid}.tmp`;
924
+ const manifest = {
925
+ schemaVersion: 1,
926
+ signature,
927
+ completed,
928
+ updatedAt: new Date().toISOString(),
929
+ };
930
+ fs.writeFileSync(temporary, `${JSON.stringify(manifest, null, 2)}\n`, {
931
+ encoding: "utf-8",
932
+ mode: 0o600,
933
+ flag: "wx",
934
+ });
935
+ fs.renameSync(temporary, manifestPath);
936
+ }
937
+ async function initializeOneRepository(repository, options) {
938
+ try {
939
+ if (options.indexMode?.(repository) === "provenance-only") {
940
+ return {
941
+ repository,
942
+ status: "skipped",
943
+ message: "provenance-only policy: graph indexing and lifecycle hooks require an explicit index: full override",
944
+ };
945
+ }
946
+ if (!options.dryRun && options.isolatedInitialize) {
947
+ return await options.isolatedInitialize(repository);
948
+ }
949
+ const databaseExists = fs.existsSync(path.join(repository, ".reckon", "db.sqlite"));
950
+ const integration = readRepositoryIntegrationConfig(repository);
951
+ const scope = integration?.scope ?? "personal";
952
+ const agents = [...new Set([...(options.agents ?? []), ...(integration?.agents ?? [])])];
953
+ const current = await isRepositoryInitializationCurrent(repository, options.command, scope);
954
+ if (options.dryRun)
955
+ return dryRunInitialization(repository, databaseExists, current);
956
+ const health = databaseExists ? await options.status(repository) : null;
957
+ if (current && health?.status === "healthy") {
958
+ return {
959
+ repository,
960
+ status: "already-current",
961
+ message: "managed installation and graph are healthy",
962
+ };
963
+ }
964
+ const healthy = health?.status === "healthy";
965
+ await initializeRepository(repository, {
966
+ command: options.command,
967
+ index: healthy ? async () => undefined : options.index,
968
+ scope,
969
+ agents,
970
+ });
971
+ return {
972
+ repository,
973
+ status: databaseExists ? "updated" : "initialized",
974
+ message: initializationMessage(databaseExists, healthy),
975
+ };
976
+ }
977
+ catch (error) {
978
+ return {
979
+ repository,
980
+ status: "failed",
981
+ message: errorMessage(error),
982
+ };
983
+ }
984
+ }
985
+ export async function initializeRepositories(roots, options) {
986
+ const depth = options.depth ?? DEFAULT_DEPTH;
987
+ const worktrees = options.worktrees ?? "skip";
988
+ const discovery = await discoverGitRepositoryPaths(roots, depth, worktrees);
989
+ const identities = new Map(discovery.repositories.map((repository) => [repository, repositoryIdentity(repository)]));
990
+ const selectorMatches = (repository, selector) => selector === repository ||
991
+ canonicalRepositorySelector(selector) === repository ||
992
+ selector === identities.get(repository);
993
+ const unknownSelectors = (options.include ?? []).filter((selector) => !discovery.repositories.some((repository) => selectorMatches(repository, selector)));
994
+ if (unknownSelectors.length > 0) {
995
+ throw new Error(`knodin repos init: include selector(s) matched no discovered repository: ${unknownSelectors.join(", ")}`);
996
+ }
997
+ const repositories = discovery.repositories.filter((repository) => ((options.include?.length ?? 0) === 0 ||
998
+ (options.include ?? []).some((selector) => selectorMatches(repository, selector))) &&
999
+ !(options.exclude ?? []).some((selector) => selectorMatches(repository, selector)));
1000
+ const signature = initializationSignature(repositories, depth, worktrees);
1001
+ const manifestPath = options.manifestPath ? path.resolve(options.manifestPath) : undefined;
1002
+ const previousManifest = readInitializationManifest(manifestPath, signature);
1003
+ const completedByRepository = new Map((previousManifest?.completed ?? []).map((result) => [result.repository, result]));
1004
+ const results = discovery.issues.map((issue) => ({
1005
+ repository: issue.path,
1006
+ status: "failed",
1007
+ message: `discovery failed: ${issue.message}`,
1008
+ }));
1009
+ for (const emptyRoot of discovery.emptyRoots) {
1010
+ results.push({
1011
+ repository: emptyRoot,
1012
+ status: "skipped",
1013
+ message: `no Git worktree found within depth ${depth}`,
1014
+ });
1015
+ }
1016
+ for (const stale of discovery.staleWorktrees) {
1017
+ results.push({
1018
+ repository: stale.path,
1019
+ status: "skipped",
1020
+ message: `stale worktree metadata: ${stale.message}`,
1021
+ });
1022
+ }
1023
+ for (const repository of repositories) {
1024
+ const completed = completedByRepository.get(repository);
1025
+ if (completed) {
1026
+ results.push(completed);
1027
+ continue;
1028
+ }
1029
+ const result = await initializeOneRepository(repository, options);
1030
+ results.push(result);
1031
+ if (manifestPath &&
1032
+ !options.dryRun &&
1033
+ (result.status === "initialized" ||
1034
+ result.status === "updated" ||
1035
+ result.status === "already-current")) {
1036
+ completedByRepository.set(repository, result);
1037
+ writeInitializationManifest(manifestPath, signature, [...completedByRepository.values()]);
1038
+ }
1039
+ }
1040
+ results.sort((left, right) => left.repository.localeCompare(right.repository));
1041
+ const measured = results.filter(({ peakRssBytes }) => peakRssBytes !== undefined);
1042
+ return {
1043
+ exitCode: results.some(({ status }) => status === "failed") ? 1 : 0,
1044
+ worktrees,
1045
+ results,
1046
+ resources: {
1047
+ isolated: options.isolatedInitialize !== undefined && !options.dryRun,
1048
+ peakRepositoryRssBytes: measured.length > 0
1049
+ ? Math.max(...measured.map(({ peakRssBytes }) => peakRssBytes ?? 0))
1050
+ : null,
1051
+ memoryLimitBytes: measured.find(({ memoryLimitBytes }) => memoryLimitBytes !== undefined)?.memoryLimitBytes ??
1052
+ null,
1053
+ memoryLimitFailures: results.filter(({ message }) => message.includes("memory ceiling exceeded")).length,
1054
+ },
1055
+ ...(manifestPath
1056
+ ? {
1057
+ manifest: {
1058
+ path: manifestPath,
1059
+ resumed: previousManifest !== null,
1060
+ completed: completedByRepository.size,
1061
+ total: repositories.length,
1062
+ },
1063
+ }
1064
+ : {}),
1065
+ };
1066
+ }
1067
+ /** Stable line-oriented output for humans and logs that are not JSON consumers. */
1068
+ export function formatRepositoryHuman(summary) {
1069
+ const width = Math.max("already-current".length, ...summary.results.map(({ status }) => status.length));
1070
+ const lines = summary.results.map((result) => {
1071
+ const detail = result.message ? ` — ${result.message}` : "";
1072
+ return `${result.status.padEnd(width)} ${result.repository}${detail}`;
1073
+ });
1074
+ const counts = new Map();
1075
+ for (const result of summary.results)
1076
+ counts.set(result.status, (counts.get(result.status) ?? 0) + 1);
1077
+ const order = [
1078
+ "initialized",
1079
+ "updated",
1080
+ "already-current",
1081
+ "skipped",
1082
+ "failed",
1083
+ ];
1084
+ const totals = order
1085
+ .filter((status) => counts.has(status))
1086
+ .map((status) => `${counts.get(status)} ${status}`);
1087
+ lines.push(`${summary.results.length} entries: ${totals.join(", ")}; .worktrees policy=${summary.worktrees}`);
1088
+ return `${lines.join("\n")}\n`;
1089
+ }