knodin 0.5.1 → 0.7.3
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/README.md +190 -535
- package/dist/bin/cli.js +173 -17
- package/dist/bin/launcher.js +11 -0
- package/dist/src/agent-integration.js +4 -16
- package/dist/src/artifact-refresh.js +1 -1
- package/dist/src/cli-args.js +8 -1
- package/dist/src/cli-model.js +35 -1
- package/dist/src/codeflow-replay.js +80 -0
- package/dist/src/competitive-cold-mcp.js +40 -0
- package/dist/src/competitive-manifest.js +106 -25
- package/dist/src/competitive-runner.js +37 -3
- package/dist/src/competitive-sandbox.js +4 -4
- package/dist/src/context-export.js +1 -1
- package/dist/src/diagnostics.js +449 -0
- package/dist/src/docs-sections.js +5 -5
- package/dist/src/engine/ann-hnsw.js +5 -5
- package/dist/src/engine/embeddings.js +4 -4
- package/dist/src/engine/git-history.js +289 -0
- package/dist/src/engine/index.js +450 -103
- package/dist/src/engine/prune.js +2 -2
- package/dist/src/engine/scip-import.js +408 -0
- package/dist/src/engine/symbol-delete.js +1 -1
- package/dist/src/execution-profile.js +203 -0
- package/dist/src/failure-diagnosis.js +69 -10
- package/dist/src/hook-manager-integration.js +156 -0
- package/dist/src/index-activity.js +1 -1
- package/dist/src/init-progress-worker.js +1 -1
- package/dist/src/init.js +361 -92
- package/dist/src/lifecycle-health.js +51 -14
- package/dist/src/lsp-readonly.js +1 -1
- package/dist/src/output-compression.js +1 -1
- package/dist/src/output-telemetry.js +8 -4
- package/dist/src/pr-triage.js +2 -2
- package/dist/src/progressive-evidence.js +473 -0
- package/dist/src/pure-compression-cli.js +101 -0
- package/dist/src/release-preflight.js +510 -0
- package/dist/src/repair-progress-worker.js +1 -1
- package/dist/src/repository-init-process.js +1 -1
- package/dist/src/repository-management.js +145 -3
- package/dist/src/response-budget.js +11 -1
- package/dist/src/server.js +23 -3
- package/dist/src/structural-fast-path.js +303 -0
- package/dist/src/structural-snapshot.js +33 -0
- package/dist/src/system-config.js +7 -7
- package/dist/src/tools/knodin-tools.js +106 -15
- package/dist/src/update-ceremony.js +158 -0
- package/dist/src/update-policy.js +4 -4
- package/dist/src/wait-for-fresh.js +2 -2
- package/dist/src/worktree-lifecycle.js +2 -2
- package/docs/CLI.md +22 -0
- package/docs/COMMAND-OUTPUT-COMPRESSION.md +32 -16
- package/docs/CONTAINED-EXECUTION.md +77 -0
- package/docs/DIAGNOSTICS.md +45 -0
- package/docs/DOCTOR-AND-UPDATES.md +7 -4
- package/docs/GIT-HISTORY-REVIEW.md +39 -0
- package/docs/INSTALLATION.md +1 -1
- package/docs/MCP.md +15 -0
- package/docs/PROGRESSIVE-EVIDENCE.md +37 -0
- package/docs/REPOSITORIES-AND-WORKTREES.md +31 -1
- package/docs/SCIP-IMPORT.md +57 -0
- package/docs/SIGNED-UPDATES.md +5 -0
- package/docs/SYSTEMS-AND-RELATIONSHIPS.md +2 -2
- package/docs/TELEMETRY.md +6 -2
- package/docs/releases/0.6.0.md +18 -0
- package/docs/releases/0.7.0.md +24 -0
- package/docs/releases/0.7.1.md +21 -0
- package/docs/releases/0.7.2.md +21 -0
- package/docs/releases/0.7.3.md +23 -0
- package/package.json +34 -3
- package/dist/src/tools/reckon-tools.js +0 -5
package/dist/src/init.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import child_process from "node:child_process";
|
|
2
|
+
import crypto from "node:crypto";
|
|
2
3
|
import fs from "node:fs";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { configureProjectAgents, } from "./agent-integration.js";
|
|
5
6
|
import { isIndexableSourcePath } from "./engine/source-policy.js";
|
|
7
|
+
import { inspectLefthookIntegration, installHookManagerIntegration, isActiveLefthookHook, } from "./hook-manager-integration.js";
|
|
6
8
|
import { registerInitializedWorktree } from "./worktree-lifecycle.js";
|
|
7
9
|
const MANAGED_MARKER = "KNODIN MANAGED HOOK";
|
|
8
|
-
const LEGACY_MANAGED_MARKER = "RECKON-GRAPH MANAGED HOOK";
|
|
9
10
|
const HOOK_NAMES = ["post-commit", "post-checkout", "post-merge", "post-rewrite"];
|
|
10
11
|
const HOOK_LOG_MAX_BYTES = 1_048_576;
|
|
11
12
|
const HOOK_FAILURE_FILE = "last-refresh-failure";
|
|
@@ -40,6 +41,203 @@ function runGit(repo, args, encoding = "utf-8") {
|
|
|
40
41
|
stdio: ["ignore", "pipe", "ignore"],
|
|
41
42
|
});
|
|
42
43
|
}
|
|
44
|
+
const MUTATION_AUDIT_MAX_ENTRIES = 2_048;
|
|
45
|
+
const MUTATION_AUDIT_MAX_FILE_BYTES = 8 * 1_024 * 1_024;
|
|
46
|
+
function boundedDirectoryEntries(directory) {
|
|
47
|
+
try {
|
|
48
|
+
const entries = fs.readdirSync(directory, { withFileTypes: true });
|
|
49
|
+
if (entries.length > MUTATION_AUDIT_MAX_ENTRIES)
|
|
50
|
+
throw new Error(`${directory}: configure mutation audit exceeds ${MUTATION_AUDIT_MAX_ENTRIES} managed entries`);
|
|
51
|
+
return entries;
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
if (error.code === "ENOENT")
|
|
55
|
+
return [];
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function configureMutationAuditTargets(repo) {
|
|
60
|
+
const commonRaw = runGit(repo, [
|
|
61
|
+
"rev-parse",
|
|
62
|
+
"--path-format=absolute",
|
|
63
|
+
"--git-common-dir",
|
|
64
|
+
]).trim();
|
|
65
|
+
const common = path.isAbsolute(commonRaw) ? commonRaw : path.resolve(repo, commonRaw);
|
|
66
|
+
const excludeRaw = runGit(repo, [
|
|
67
|
+
"rev-parse",
|
|
68
|
+
"--path-format=absolute",
|
|
69
|
+
"--git-path",
|
|
70
|
+
"info/exclude",
|
|
71
|
+
]).trim();
|
|
72
|
+
const exclude = path.isAbsolute(excludeRaw) ? excludeRaw : path.resolve(repo, excludeRaw);
|
|
73
|
+
const effectiveHooks = effectiveHooksDirectory(repo);
|
|
74
|
+
const targets = new Map();
|
|
75
|
+
const add = (target) => {
|
|
76
|
+
const key = path.resolve(target.absolute);
|
|
77
|
+
if (!targets.has(key))
|
|
78
|
+
targets.set(key, { ...target, absolute: key });
|
|
79
|
+
};
|
|
80
|
+
for (const relative of [
|
|
81
|
+
"AGENTS.md",
|
|
82
|
+
"GEMINI.md",
|
|
83
|
+
".mcp.json",
|
|
84
|
+
".codex",
|
|
85
|
+
".codex/config.toml",
|
|
86
|
+
".gemini",
|
|
87
|
+
".gemini/settings.json",
|
|
88
|
+
".agents",
|
|
89
|
+
".agents/mcp_config.json",
|
|
90
|
+
".gitignore",
|
|
91
|
+
".claude",
|
|
92
|
+
".claude/hooks",
|
|
93
|
+
".claude/hooks/nudge.sh",
|
|
94
|
+
".claude/settings.example.json",
|
|
95
|
+
".knodin",
|
|
96
|
+
".knodin/integration.json",
|
|
97
|
+
".knodin/indexer.log",
|
|
98
|
+
".knodin/indexer.log.1",
|
|
99
|
+
"lefthook-local.yml",
|
|
100
|
+
"lefthook-local.yaml",
|
|
101
|
+
".lefthook-local.yml",
|
|
102
|
+
".lefthook-local.yaml",
|
|
103
|
+
".config/lefthook-local.yml",
|
|
104
|
+
".config/lefthook-local.yaml",
|
|
105
|
+
])
|
|
106
|
+
add({ absolute: path.join(repo, relative), label: relative });
|
|
107
|
+
for (const relative of [
|
|
108
|
+
".knodin/hooks",
|
|
109
|
+
".knodin/hooks/background-index.sh",
|
|
110
|
+
".knodin/hooks/manager-dispatch.sh",
|
|
111
|
+
".knodin/hooks/events",
|
|
112
|
+
".knodin/hooks/last-refresh-event",
|
|
113
|
+
".knodin/hooks/last-refresh-failure",
|
|
114
|
+
".knodin/hooks/refresh.lock",
|
|
115
|
+
".knodin/hooks/refresh.lock/pid",
|
|
116
|
+
])
|
|
117
|
+
add({ absolute: path.join(repo, relative), label: relative });
|
|
118
|
+
for (const entry of boundedDirectoryEntries(path.join(repo, ".knodin", "hooks", "events")))
|
|
119
|
+
add({
|
|
120
|
+
absolute: path.join(repo, ".knodin", "hooks", "events", entry.name),
|
|
121
|
+
label: `.knodin/hooks/events/${entry.name}`,
|
|
122
|
+
});
|
|
123
|
+
add({ absolute: path.join(common, "config"), label: ".git/config" });
|
|
124
|
+
add({ absolute: exclude, label: ".git/info/exclude" });
|
|
125
|
+
add({ absolute: path.join(common, "knodin"), label: ".git/knodin" });
|
|
126
|
+
add({
|
|
127
|
+
absolute: path.join(common, "knodin", "worktrees.json"),
|
|
128
|
+
label: ".git/knodin/worktrees.json",
|
|
129
|
+
});
|
|
130
|
+
const defaultHooks = path.join(common, "hooks");
|
|
131
|
+
const managedHooks = path.join(common, "knodin-hooks");
|
|
132
|
+
add({ absolute: defaultHooks, label: ".git/hooks" });
|
|
133
|
+
add({ absolute: managedHooks, label: ".git/knodin-hooks" });
|
|
134
|
+
const effectiveLabel = path.resolve(effectiveHooks) === path.resolve(defaultHooks)
|
|
135
|
+
? ".git/hooks"
|
|
136
|
+
: path.resolve(effectiveHooks) === path.resolve(managedHooks)
|
|
137
|
+
? ".git/knodin-hooks"
|
|
138
|
+
: "effective-git-hooks";
|
|
139
|
+
add({ absolute: effectiveHooks, label: effectiveLabel });
|
|
140
|
+
for (const hookRoot of [
|
|
141
|
+
{ absolute: defaultHooks, label: ".git/hooks" },
|
|
142
|
+
{ absolute: managedHooks, label: ".git/knodin-hooks" },
|
|
143
|
+
{ absolute: effectiveHooks, label: effectiveLabel },
|
|
144
|
+
]) {
|
|
145
|
+
for (const entry of boundedDirectoryEntries(hookRoot.absolute))
|
|
146
|
+
add({
|
|
147
|
+
absolute: path.join(hookRoot.absolute, entry.name),
|
|
148
|
+
label: `${hookRoot.label}/${entry.name}`,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
for (const hookName of HOOK_NAMES) {
|
|
152
|
+
add({ absolute: path.join(effectiveHooks, hookName), label: `${effectiveLabel}/${hookName}` });
|
|
153
|
+
add({
|
|
154
|
+
absolute: path.join(effectiveHooks, `${hookName}.knodin-original`),
|
|
155
|
+
label: `${effectiveLabel}/${hookName}.knodin-original`,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
for (const hookRoot of [
|
|
159
|
+
{ absolute: defaultHooks, label: ".git/hooks" },
|
|
160
|
+
{ absolute: managedHooks, label: ".git/knodin-hooks" },
|
|
161
|
+
]) {
|
|
162
|
+
for (const hookName of HOOK_NAMES) {
|
|
163
|
+
add({
|
|
164
|
+
absolute: path.join(hookRoot.absolute, hookName),
|
|
165
|
+
label: `${hookRoot.label}/${hookName}`,
|
|
166
|
+
});
|
|
167
|
+
add({
|
|
168
|
+
absolute: path.join(hookRoot.absolute, `${hookName}.knodin-original`),
|
|
169
|
+
label: `${hookRoot.label}/${hookName}.knodin-original`,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const configured = configuredHooksPath(repo);
|
|
174
|
+
const relativeConfigured = configured ? repoRelativeHooksPath(repo, configured) : null;
|
|
175
|
+
if (relativeConfigured && hasTrackedHooks(repo, relativeConfigured)) {
|
|
176
|
+
const configuredRoot = path.join(repo, relativeConfigured);
|
|
177
|
+
for (const entry of boundedDirectoryEntries(configuredRoot)) {
|
|
178
|
+
if (!entry.isFile() || HOOK_NAMES.includes(entry.name))
|
|
179
|
+
continue;
|
|
180
|
+
const source = path.join(configuredRoot, entry.name);
|
|
181
|
+
if ((fs.statSync(source).mode & 0o111) === 0)
|
|
182
|
+
continue;
|
|
183
|
+
add({
|
|
184
|
+
absolute: path.join(managedHooks, entry.name),
|
|
185
|
+
label: `.git/knodin-hooks/${entry.name}`,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (targets.size > MUTATION_AUDIT_MAX_ENTRIES)
|
|
190
|
+
throw new Error(`configure mutation audit exceeds ${MUTATION_AUDIT_MAX_ENTRIES} managed paths`);
|
|
191
|
+
return [...targets.values()];
|
|
192
|
+
}
|
|
193
|
+
function fileSignature(filePath, allowOversized = false) {
|
|
194
|
+
try {
|
|
195
|
+
const stat = fs.lstatSync(filePath);
|
|
196
|
+
if (stat.isDirectory())
|
|
197
|
+
return `directory:${stat.mode & 0o777}`;
|
|
198
|
+
if (stat.isSymbolicLink()) {
|
|
199
|
+
const link = fs.readlinkSync(filePath);
|
|
200
|
+
const resolved = fs.realpathSync(filePath);
|
|
201
|
+
return `symlink:${stat.mode & 0o777}:${link}:${fileSignature(resolved, allowOversized)}`;
|
|
202
|
+
}
|
|
203
|
+
if (!stat.isFile())
|
|
204
|
+
throw new Error(`${filePath}: unsupported filesystem entry`);
|
|
205
|
+
if (stat.size > MUTATION_AUDIT_MAX_FILE_BYTES && !allowOversized)
|
|
206
|
+
throw new Error(`${filePath}: configure mutation audit refuses files larger than ${MUTATION_AUDIT_MAX_FILE_BYTES} bytes`);
|
|
207
|
+
if (stat.size > MUTATION_AUDIT_MAX_FILE_BYTES)
|
|
208
|
+
return `oversized-file:${stat.mode & 0o777}:${stat.size}`;
|
|
209
|
+
const digest = crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
|
|
210
|
+
return `file:${stat.mode & 0o777}:${stat.size}:${digest}`;
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
if (error.code === "ENOENT")
|
|
214
|
+
return null;
|
|
215
|
+
throw error;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function mutationSnapshot(targets, allowOversized = false) {
|
|
219
|
+
const snapshot = new Map();
|
|
220
|
+
for (const target of targets) {
|
|
221
|
+
const rootSignature = fileSignature(target.absolute, allowOversized);
|
|
222
|
+
if (rootSignature !== null)
|
|
223
|
+
snapshot.set(target.label, rootSignature);
|
|
224
|
+
}
|
|
225
|
+
return snapshot;
|
|
226
|
+
}
|
|
227
|
+
function filesystemChanges(before, after) {
|
|
228
|
+
return [...new Set([...before.keys(), ...after.keys()])].sort().flatMap((changedPath) => {
|
|
229
|
+
const prior = before.get(changedPath);
|
|
230
|
+
const current = after.get(changedPath);
|
|
231
|
+
if (prior === current)
|
|
232
|
+
return [];
|
|
233
|
+
return [
|
|
234
|
+
{
|
|
235
|
+
path: changedPath,
|
|
236
|
+
action: prior === undefined ? "created" : current === undefined ? "removed" : "modified",
|
|
237
|
+
},
|
|
238
|
+
];
|
|
239
|
+
});
|
|
240
|
+
}
|
|
43
241
|
function shellQuote(value) {
|
|
44
242
|
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
45
243
|
}
|
|
@@ -78,14 +276,14 @@ export async function refreshFromGitEvent(repo, event, index) {
|
|
|
78
276
|
changed = diffPaths(resolvedRepo, event.before, event.after);
|
|
79
277
|
}
|
|
80
278
|
else {
|
|
81
|
-
const hooksRoot = path.join(resolvedRepo, ".
|
|
279
|
+
const hooksRoot = path.join(resolvedRepo, ".knodin", "hooks");
|
|
82
280
|
const inputPath = path.resolve(event.inputPath);
|
|
83
281
|
const relativeInput = path.relative(hooksRoot, inputPath);
|
|
84
282
|
if (relativeInput === "" ||
|
|
85
283
|
relativeInput === ".." ||
|
|
86
284
|
relativeInput.startsWith(`..${path.sep}`) ||
|
|
87
285
|
path.isAbsolute(relativeInput)) {
|
|
88
|
-
throw new Error("knodin hook refresh: rewrite input must be inside .
|
|
286
|
+
throw new Error("knodin hook refresh: rewrite input must be inside .knodin/hooks");
|
|
89
287
|
}
|
|
90
288
|
const [realHooksRoot, realInputPath, inputStat] = await Promise.all([
|
|
91
289
|
fs.promises.realpath(hooksRoot),
|
|
@@ -99,7 +297,7 @@ export async function refreshFromGitEvent(repo, event, index) {
|
|
|
99
297
|
realRelativeInput === ".." ||
|
|
100
298
|
realRelativeInput.startsWith(`..${path.sep}`) ||
|
|
101
299
|
path.isAbsolute(realRelativeInput)) {
|
|
102
|
-
throw new Error("knodin hook refresh: rewrite input must be a regular file inside .
|
|
300
|
+
throw new Error("knodin hook refresh: rewrite input must be a regular file inside .knodin/hooks");
|
|
103
301
|
}
|
|
104
302
|
rewriteInputToRemove = inputPath;
|
|
105
303
|
const pairs = (await fs.promises.readFile(inputPath, "utf-8"))
|
|
@@ -130,11 +328,11 @@ set -u
|
|
|
130
328
|
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
|
131
329
|
[ -n "$REPO_ROOT" ] || exit 0
|
|
132
330
|
cd "$REPO_ROOT" || exit 0
|
|
133
|
-
mkdir -p "$REPO_ROOT/.
|
|
134
|
-
LOCK_DIR="$REPO_ROOT/.
|
|
135
|
-
EVENT_DIR="$REPO_ROOT/.
|
|
136
|
-
LAST_EVENT_PATH="$REPO_ROOT/.
|
|
137
|
-
FAILURE_PATH="$REPO_ROOT/.
|
|
331
|
+
mkdir -p "$REPO_ROOT/.knodin"
|
|
332
|
+
LOCK_DIR="$REPO_ROOT/.knodin/hooks/refresh.lock"
|
|
333
|
+
EVENT_DIR="$REPO_ROOT/.knodin/hooks/events"
|
|
334
|
+
LAST_EVENT_PATH="$REPO_ROOT/.knodin/hooks/last-refresh-event"
|
|
335
|
+
FAILURE_PATH="$REPO_ROOT/.knodin/hooks/${HOOK_FAILURE_FILE}"
|
|
138
336
|
FAILURE_TMP="$FAILURE_PATH.$$"
|
|
139
337
|
mkdir -p "$EVENT_DIR"
|
|
140
338
|
if [ "$#" -gt 0 ]; then
|
|
@@ -165,7 +363,7 @@ if ! mkdir "$LOCK_DIR" 2>/dev/null; then
|
|
|
165
363
|
fi
|
|
166
364
|
printf '%s\\n' "$$" > "$LOCK_DIR/pid"
|
|
167
365
|
trap 'rm -rf "$LOCK_DIR"; rm -f "$FAILURE_TMP"' EXIT HUP INT TERM
|
|
168
|
-
LOG_PATH="$REPO_ROOT/.
|
|
366
|
+
LOG_PATH="$REPO_ROOT/.knodin/indexer.log"
|
|
169
367
|
if [ -f "$LOG_PATH" ]; then
|
|
170
368
|
LOG_BYTES="$(wc -c < "$LOG_PATH" 2>/dev/null | tr -d '[:space:]')"
|
|
171
369
|
case "$LOG_BYTES" in ''|*[!0-9]*) LOG_BYTES=0 ;; esac
|
|
@@ -197,7 +395,7 @@ while :; do
|
|
|
197
395
|
{
|
|
198
396
|
printf 'knodin background refresh failed with exit code %s.\\n' "$STATUS"
|
|
199
397
|
printf 'Pending event retained at %s.\\n' "$EVENT"
|
|
200
|
-
printf 'See .
|
|
398
|
+
printf 'See .knodin/indexer.log for details.\\n'
|
|
201
399
|
} > "$FAILURE_TMP"
|
|
202
400
|
mv -f "$FAILURE_TMP" "$FAILURE_PATH"
|
|
203
401
|
exit "$STATUS"
|
|
@@ -206,7 +404,7 @@ done
|
|
|
206
404
|
`;
|
|
207
405
|
}
|
|
208
406
|
function queuedLifecycleEvents(repo) {
|
|
209
|
-
const eventDirectory = path.join(repo, ".
|
|
407
|
+
const eventDirectory = path.join(repo, ".knodin", "hooks", "events");
|
|
210
408
|
try {
|
|
211
409
|
return fs.readdirSync(eventDirectory).filter((entry) => entry.endsWith(".event"));
|
|
212
410
|
}
|
|
@@ -216,7 +414,7 @@ function queuedLifecycleEvents(repo) {
|
|
|
216
414
|
}
|
|
217
415
|
function refreshProcessorIsRunning(repo) {
|
|
218
416
|
try {
|
|
219
|
-
const pid = Number(fs.readFileSync(path.join(repo, ".
|
|
417
|
+
const pid = Number(fs.readFileSync(path.join(repo, ".knodin", "hooks", "refresh.lock", "pid"), "utf-8"));
|
|
220
418
|
if (!Number.isInteger(pid) || pid <= 0)
|
|
221
419
|
return false;
|
|
222
420
|
process.kill(pid, 0);
|
|
@@ -238,7 +436,7 @@ function drainQueuedLifecycleEvents(repo, backgroundPath) {
|
|
|
238
436
|
});
|
|
239
437
|
if (result.status !== 0) {
|
|
240
438
|
const detail = result.error?.message || result.stderr.trim() || `exit ${result.status ?? "unknown"}`;
|
|
241
|
-
throw new Error(`knodin init: queued lifecycle refresh could not be drained (${detail}); run ${backgroundPath} after resolving .
|
|
439
|
+
throw new Error(`knodin init: queued lifecycle refresh could not be drained (${detail}); run ${backgroundPath} after resolving .knodin/hooks/last-refresh-failure`);
|
|
242
440
|
}
|
|
243
441
|
const remaining = queuedLifecycleEvents(repo);
|
|
244
442
|
if (remaining.length === 0)
|
|
@@ -258,7 +456,7 @@ function managedHookScript(hookName, originalPath) {
|
|
|
258
456
|
: shellQuote(originalPath)
|
|
259
457
|
: "";
|
|
260
458
|
const runOriginal = originalPath
|
|
261
|
-
?
|
|
459
|
+
? `KNODIN_WRAPPER_ACTIVE=1 ${original} "$@"${hookName === "post-rewrite" ? ' < "$REWRITE_INPUT"' : ""} || ORIGINAL_STATUS=$?`
|
|
262
460
|
: ":";
|
|
263
461
|
let trigger = "";
|
|
264
462
|
if (hookName === "post-commit")
|
|
@@ -272,14 +470,14 @@ AFTER="$(git rev-parse HEAD 2>/dev/null || true)"
|
|
|
272
470
|
[ -n "$BEFORE" ] && [ -n "$AFTER" ] && trigger_knodin merge "$BEFORE" "$AFTER"`;
|
|
273
471
|
}
|
|
274
472
|
const rewriteWithoutIndexer = originalPath
|
|
275
|
-
?
|
|
473
|
+
? `KNODIN_WRAPPER_ACTIVE=1 ${original} "$@"
|
|
276
474
|
exit $?`
|
|
277
475
|
: "exit 0";
|
|
278
476
|
const rewriteSetup = hookName === "post-rewrite"
|
|
279
477
|
? `if [ ! -x "$BACKGROUND" ]; then
|
|
280
478
|
${rewriteWithoutIndexer}
|
|
281
479
|
fi
|
|
282
|
-
REWRITE_INPUT="$(mktemp "$REPO_ROOT/.
|
|
480
|
+
REWRITE_INPUT="$(mktemp "$REPO_ROOT/.knodin/hooks/rewrite-input.XXXXXX")" || exit 0
|
|
283
481
|
cat > "$REWRITE_INPUT"
|
|
284
482
|
`
|
|
285
483
|
: "";
|
|
@@ -289,7 +487,7 @@ cat > "$REWRITE_INPUT"
|
|
|
289
487
|
# ${MANAGED_MARKER}: ${hookName}
|
|
290
488
|
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
|
291
489
|
[ -n "$REPO_ROOT" ] || exit 0
|
|
292
|
-
BACKGROUND="$REPO_ROOT/.
|
|
490
|
+
BACKGROUND="$REPO_ROOT/.knodin/hooks/background-index.sh"
|
|
293
491
|
ORIGINAL_STATUS=0
|
|
294
492
|
trigger_knodin() {
|
|
295
493
|
[ -x "$BACKGROUND" ] || return 0
|
|
@@ -304,7 +502,7 @@ trigger_knodin() {
|
|
|
304
502
|
EVENT_BEFORE="$(sed -n '1{s/ .*//;p;}' "$2")"
|
|
305
503
|
EVENT_AFTER="$(sed -n '1{s/^[^ ]* //;s/ .*//;p;}' "$2")"
|
|
306
504
|
fi
|
|
307
|
-
EVENT_DIR="$REPO_ROOT/.
|
|
505
|
+
EVENT_DIR="$REPO_ROOT/.knodin/hooks/events"
|
|
308
506
|
mkdir -p "$EVENT_DIR" || return 0
|
|
309
507
|
EVENT_TMP="$EVENT_DIR/$(date +%s).$$.$PPID.tmp"
|
|
310
508
|
EVENT_PATH="$EVENT_TMP.event"
|
|
@@ -319,7 +517,7 @@ trigger_knodin() {
|
|
|
319
517
|
"$EVENT_SEQUENCE" "$EVENT_WORKTREE" "$EVENT_AT" "$EVENT_BEFORE" "$EVENT_AFTER"
|
|
320
518
|
} > "$EVENT_TMP" || return 0
|
|
321
519
|
mv "$EVENT_TMP" "$EVENT_PATH" || return 0
|
|
322
|
-
if [ "\${
|
|
520
|
+
if [ "\${KNODIN_HOOK_FOREGROUND:-0}" = "1" ]; then
|
|
323
521
|
"$BACKGROUND"
|
|
324
522
|
else
|
|
325
523
|
nohup "$BACKGROUND" >/dev/null 2>&1 &
|
|
@@ -346,13 +544,12 @@ async function installManagedHook(hooksDir, hookName, preservedOriginal = undefi
|
|
|
346
544
|
let originalPath = preservedOriginal ?? null;
|
|
347
545
|
if (preservedOriginal === undefined && fs.existsSync(hookPath)) {
|
|
348
546
|
const content = await fs.promises.readFile(hookPath, "utf-8");
|
|
349
|
-
if (content.startsWith(`#!/bin/sh\n# ${MANAGED_MARKER}: ${hookName}\n`)
|
|
350
|
-
content.startsWith(`#!/bin/sh\n# ${LEGACY_MANAGED_MARKER}: ${hookName}\n`)) {
|
|
547
|
+
if (content.startsWith(`#!/bin/sh\n# ${MANAGED_MARKER}: ${hookName}\n`)) {
|
|
351
548
|
const match = content.match(/^# knodin original: (.+)$/m);
|
|
352
549
|
originalPath = match?.[1] ?? null;
|
|
353
550
|
}
|
|
354
551
|
else {
|
|
355
|
-
const backupPath = `${hookPath}.
|
|
552
|
+
const backupPath = `${hookPath}.knodin-original`;
|
|
356
553
|
const mode = (await fs.promises.stat(hookPath)).mode;
|
|
357
554
|
await fs.promises.copyFile(hookPath, backupPath);
|
|
358
555
|
await fs.promises.rm(hookPath, { force: true });
|
|
@@ -365,9 +562,7 @@ async function installManagedHook(hooksDir, hookName, preservedOriginal = undefi
|
|
|
365
562
|
if (!fs.existsSync(candidate))
|
|
366
563
|
break;
|
|
367
564
|
const candidateContent = await fs.promises.readFile(candidate, "utf-8");
|
|
368
|
-
if (candidateContent === canonical ||
|
|
369
|
-
candidateContent.includes(MANAGED_MARKER) ||
|
|
370
|
-
candidateContent.includes(LEGACY_MANAGED_MARKER))
|
|
565
|
+
if (candidateContent === canonical || candidateContent.includes(MANAGED_MARKER))
|
|
371
566
|
await fs.promises.rm(candidate, { force: true });
|
|
372
567
|
}
|
|
373
568
|
// Match Git's semantics: preserve inactive hook files as backups, but
|
|
@@ -388,7 +583,6 @@ async function installManagedHook(hooksDir, hookName, preservedOriginal = undefi
|
|
|
388
583
|
await fs.promises.chmod(hookPath, 0o755);
|
|
389
584
|
}
|
|
390
585
|
const ORIGINAL_HOOKS_CONFIG = "knodin.originalHooksPath";
|
|
391
|
-
const LEGACY_ORIGINAL_HOOKS_CONFIG = "reckon.originalHooksPath";
|
|
392
586
|
function effectiveHooksDirectory(repo) {
|
|
393
587
|
let raw;
|
|
394
588
|
try {
|
|
@@ -405,15 +599,10 @@ function configuredHooksPath(repo) {
|
|
|
405
599
|
}
|
|
406
600
|
catch {
|
|
407
601
|
try {
|
|
408
|
-
return
|
|
602
|
+
return runGit(repo, ["config", "--local", "--get", "core.hooksPath"]).trim() || null;
|
|
409
603
|
}
|
|
410
604
|
catch {
|
|
411
|
-
|
|
412
|
-
return runGit(repo, ["config", "--local", "--get", "core.hooksPath"]).trim() || null;
|
|
413
|
-
}
|
|
414
|
-
catch {
|
|
415
|
-
return null;
|
|
416
|
-
}
|
|
605
|
+
return null;
|
|
417
606
|
}
|
|
418
607
|
}
|
|
419
608
|
}
|
|
@@ -494,10 +683,10 @@ export const SETTINGS_TEMPLATE = `{
|
|
|
494
683
|
}
|
|
495
684
|
}
|
|
496
685
|
`;
|
|
497
|
-
const
|
|
686
|
+
const KNODIN_AGENT_INSTRUCTIONS = `<!-- knodin:start -->
|
|
498
687
|
## knodin — Local Code Intelligence
|
|
499
688
|
|
|
500
|
-
This repository is indexed locally by knodin. Start with \`knodin status\`; if it reports \`repair-needed\`,
|
|
689
|
+
This repository is indexed locally by knodin. Start with \`knodin status\`; if it reports \`repair-needed\`, follow its repair steps before relying on graph evidence. Use \`knodin repair\` for graph damage and \`knodin init\` for displaced lifecycle hooks.
|
|
501
690
|
|
|
502
691
|
- When the single connected knodin MCP tool is available, use it directly. For cold or unfamiliar work, call its \`context\` operation before broad Read/Grep/Glob traversal. Use shell commands only as a fallback when the gateway is unavailable.
|
|
503
692
|
- Before a meaningful edit, call the gateway's \`query\` operation with the \`impact\` pattern (CLI fallback: \`knodin query impact <symbol> --direction upstream\`) and report material risk to the user.
|
|
@@ -531,38 +720,50 @@ async function installManagedAgentInstructions(repo, fileName = "AGENTS.md") {
|
|
|
531
720
|
await assertSafeRepositoryFile(repo, fileName);
|
|
532
721
|
const instructionsPath = path.join(repo, fileName);
|
|
533
722
|
let existing = "";
|
|
723
|
+
let existed = true;
|
|
534
724
|
try {
|
|
535
725
|
existing = await fs.promises.readFile(instructionsPath, "utf-8");
|
|
536
726
|
}
|
|
537
727
|
catch (error) {
|
|
538
728
|
if (error.code !== "ENOENT")
|
|
539
729
|
throw error;
|
|
730
|
+
existed = false;
|
|
540
731
|
}
|
|
541
732
|
const canonicalStart = "<!-- knodin:start -->";
|
|
542
733
|
const canonicalEnd = "<!-- knodin:end -->";
|
|
543
|
-
const
|
|
544
|
-
const
|
|
545
|
-
const usesCanonical = existing.includes(canonicalStart);
|
|
546
|
-
const startMarker = usesCanonical ? canonicalStart : legacyStart;
|
|
547
|
-
const endMarker = usesCanonical ? canonicalEnd : legacyEnd;
|
|
548
|
-
const start = existing.indexOf(startMarker);
|
|
549
|
-
const end = existing.indexOf(endMarker);
|
|
734
|
+
const start = existing.indexOf(canonicalStart);
|
|
735
|
+
const end = existing.indexOf(canonicalEnd);
|
|
550
736
|
const next = start >= 0 && end >= start
|
|
551
|
-
? `${existing.slice(0, start)}${
|
|
737
|
+
? `${existing.slice(0, start)}${KNODIN_AGENT_INSTRUCTIONS}${existing.slice(end + canonicalEnd.length).replace(/^\r?\n/, "")}`
|
|
552
738
|
: existing.length > 0
|
|
553
|
-
? `${existing.
|
|
554
|
-
:
|
|
555
|
-
if (next
|
|
556
|
-
|
|
739
|
+
? `${existing.trimEnd()}\n\n${KNODIN_AGENT_INSTRUCTIONS}`
|
|
740
|
+
: KNODIN_AGENT_INSTRUCTIONS;
|
|
741
|
+
if (next === existing)
|
|
742
|
+
return null;
|
|
743
|
+
await fs.promises.writeFile(instructionsPath, next, "utf-8");
|
|
744
|
+
return { path: fileName, action: existed ? "modified" : "created" };
|
|
557
745
|
}
|
|
558
746
|
async function hasManagedAgentInstructions(repo, fileName = "AGENTS.md") {
|
|
559
747
|
try {
|
|
560
|
-
return (await fs.promises.readFile(path.join(repo, fileName), "utf-8")).includes(
|
|
748
|
+
return (await fs.promises.readFile(path.join(repo, fileName), "utf-8")).includes(KNODIN_AGENT_INSTRUCTIONS);
|
|
561
749
|
}
|
|
562
750
|
catch {
|
|
563
751
|
return false;
|
|
564
752
|
}
|
|
565
753
|
}
|
|
754
|
+
function removeManagedAgentBlock(content) {
|
|
755
|
+
const start = content.indexOf("<!-- knodin:start -->");
|
|
756
|
+
const end = content.indexOf("<!-- knodin:end -->", start);
|
|
757
|
+
if (start < 0 || end < start)
|
|
758
|
+
return null;
|
|
759
|
+
const prefix = content.slice(0, start);
|
|
760
|
+
const suffix = content.slice(end + "<!-- knodin:end -->".length);
|
|
761
|
+
// The canonical marker-only file owns its final line terminator as part of
|
|
762
|
+
// the generated block. Any other prefix/suffix bytes remain untouched.
|
|
763
|
+
if (prefix.length === 0 && /^\r?\n$/.test(suffix))
|
|
764
|
+
return "";
|
|
765
|
+
return `${prefix}${suffix}`;
|
|
766
|
+
}
|
|
566
767
|
function isTracked(repo, relativePath) {
|
|
567
768
|
try {
|
|
568
769
|
return nulPaths(repo, ["ls-files", "--", relativePath]).length > 0;
|
|
@@ -589,9 +790,7 @@ async function excludeUntrackedPaths(repo, candidates) {
|
|
|
589
790
|
}
|
|
590
791
|
const startMarker = "# knodin:start";
|
|
591
792
|
const endMarker = "# knodin:end";
|
|
592
|
-
const outside = existing
|
|
593
|
-
.replace(/(?:^|\n)# knodin:start\n[\s\S]*?\n# knodin:end\n?/, "\n")
|
|
594
|
-
.replace(/(?:^|\n)# reckon-graph:start\n[\s\S]*?\n# reckon-graph:end\n?/, "\n");
|
|
793
|
+
const outside = existing.replace(/(?:^|\n)# knodin:start\n[\s\S]*?\n# knodin:end\n?/, "\n");
|
|
595
794
|
const outsideLines = new Set(outside.split(/\r?\n/));
|
|
596
795
|
const owned = safe.filter((candidate) => !outsideLines.has(candidate));
|
|
597
796
|
const prefix = outside && !outside.endsWith("\n") ? `${outside}\n` : outside;
|
|
@@ -618,38 +817,35 @@ async function removeManagedExcludes(repo) {
|
|
|
618
817
|
await fs.promises.writeFile(excludePath, next, "utf-8");
|
|
619
818
|
}
|
|
620
819
|
async function removeManagedPersonalArtifacts(repo, allowTrackedTransition = false) {
|
|
820
|
+
const changes = [];
|
|
621
821
|
for (const fileName of ["AGENTS.md", "GEMINI.md"]) {
|
|
622
822
|
const instructionsPath = path.join(repo, fileName);
|
|
623
823
|
const tracked = isTracked(repo, fileName);
|
|
624
824
|
try {
|
|
825
|
+
await assertSafeRepositoryFile(repo, fileName);
|
|
625
826
|
const current = await fs.promises.readFile(instructionsPath, "utf-8");
|
|
827
|
+
const start = current.indexOf("<!-- knodin:start -->");
|
|
828
|
+
const end = current.indexOf("<!-- knodin:end -->", start);
|
|
829
|
+
const without = removeManagedAgentBlock(current);
|
|
626
830
|
if (!tracked) {
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
if (without)
|
|
634
|
-
await fs.promises.writeFile(instructionsPath, `${without}\n`, "utf-8");
|
|
635
|
-
else
|
|
831
|
+
if (without !== null) {
|
|
832
|
+
if (without.length > 0) {
|
|
833
|
+
await fs.promises.writeFile(instructionsPath, without, "utf-8");
|
|
834
|
+
changes.push({ path: fileName, action: "modified" });
|
|
835
|
+
}
|
|
836
|
+
else {
|
|
636
837
|
await fs.promises.rm(instructionsPath, { force: true });
|
|
838
|
+
changes.push({ path: fileName, action: "removed" });
|
|
839
|
+
}
|
|
637
840
|
}
|
|
638
841
|
continue;
|
|
639
842
|
}
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
if (allowTrackedTransition && start >= 0 && end >= start) {
|
|
644
|
-
const without = `${current.slice(0, start)}${current.slice(end + "<!-- knodin:end -->".length)}`
|
|
645
|
-
.replace(/\n{3,}/g, "\n\n")
|
|
646
|
-
.trimEnd();
|
|
647
|
-
if (without)
|
|
648
|
-
await fs.promises.writeFile(instructionsPath, `${without}\n`, "utf-8");
|
|
649
|
-
else
|
|
650
|
-
await fs.promises.rm(instructionsPath, { force: true });
|
|
843
|
+
if (allowTrackedTransition && without !== null) {
|
|
844
|
+
await fs.promises.writeFile(instructionsPath, without, "utf-8");
|
|
845
|
+
changes.push({ path: fileName, action: "modified" });
|
|
651
846
|
continue;
|
|
652
847
|
}
|
|
848
|
+
const head = runGit(repo, ["show", `HEAD:${fileName}`]);
|
|
653
849
|
if (start >= 0 && end >= start && !head.includes("<!-- knodin:start -->")) {
|
|
654
850
|
const without = `${current.slice(0, start)}${current.slice(end + "<!-- knodin:end -->".length)}`
|
|
655
851
|
.replace(/\n{3,}/g, "\n\n")
|
|
@@ -666,7 +862,7 @@ async function removeManagedPersonalArtifacts(repo, allowTrackedTransition = fal
|
|
|
666
862
|
try {
|
|
667
863
|
const current = await fs.promises.readFile(gitIgnorePath, "utf-8");
|
|
668
864
|
const next = current
|
|
669
|
-
.replace(/(?:^|\n)# knodin:start\n\.
|
|
865
|
+
.replace(/(?:^|\n)# knodin:start\n\.knodin\/\n# knodin:end\n?/g, "\n")
|
|
670
866
|
.trim();
|
|
671
867
|
if (isTracked(repo, ".gitignore")) {
|
|
672
868
|
const head = runGit(repo, ["show", "HEAD:.gitignore"]);
|
|
@@ -699,10 +895,11 @@ async function removeManagedPersonalArtifacts(repo, allowTrackedTransition = fal
|
|
|
699
895
|
// Missing or user-modified files are intentionally preserved.
|
|
700
896
|
}
|
|
701
897
|
}
|
|
898
|
+
return changes;
|
|
702
899
|
}
|
|
703
900
|
export function readRepositoryIntegrationConfig(repo) {
|
|
704
901
|
try {
|
|
705
|
-
const value = JSON.parse(fs.readFileSync(path.join(repo, ".
|
|
902
|
+
const value = JSON.parse(fs.readFileSync(path.join(repo, ".knodin", "integration.json"), "utf-8"));
|
|
706
903
|
if ((value.scope === "personal" || value.scope === "team" || value.scope === "cli-only") &&
|
|
707
904
|
Array.isArray(value.agents))
|
|
708
905
|
return { scope: value.scope, agents: value.agents };
|
|
@@ -712,6 +909,22 @@ export function readRepositoryIntegrationConfig(repo) {
|
|
|
712
909
|
}
|
|
713
910
|
return null;
|
|
714
911
|
}
|
|
912
|
+
function hasSafeManagedAgentBlock(repo, relativePath) {
|
|
913
|
+
try {
|
|
914
|
+
let current = repo;
|
|
915
|
+
for (const part of relativePath.split(path.sep)) {
|
|
916
|
+
current = path.join(current, part);
|
|
917
|
+
if (fs.lstatSync(current).isSymbolicLink())
|
|
918
|
+
return false;
|
|
919
|
+
}
|
|
920
|
+
const content = fs.readFileSync(path.join(repo, relativePath), "utf-8");
|
|
921
|
+
const start = content.indexOf("<!-- knodin:start -->");
|
|
922
|
+
return start >= 0 && content.indexOf("<!-- knodin:end -->", start) >= start;
|
|
923
|
+
}
|
|
924
|
+
catch {
|
|
925
|
+
return false;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
715
928
|
/** Detect repository-owned client configuration independently of knodin's local receipt. */
|
|
716
929
|
export function inspectRepositoryIntegrationStatus(repo) {
|
|
717
930
|
const receipt = readRepositoryIntegrationConfig(repo);
|
|
@@ -720,13 +933,27 @@ export function inspectRepositoryIntegrationStatus(repo) {
|
|
|
720
933
|
{ agent: "codex", path: ".codex/config.toml", marker: 'mcp_servers."knodin"' },
|
|
721
934
|
{ agent: "gemini", path: ".gemini/settings.json", marker: '"knodin"' },
|
|
722
935
|
{ agent: "antigravity", path: ".agents/mcp_config.json", marker: '"knodin"' },
|
|
936
|
+
{
|
|
937
|
+
agent: "codex",
|
|
938
|
+
path: "AGENTS.md",
|
|
939
|
+
marker: "<!-- knodin:start -->",
|
|
940
|
+
managedGuidance: true,
|
|
941
|
+
},
|
|
942
|
+
{
|
|
943
|
+
agent: "gemini",
|
|
944
|
+
path: "GEMINI.md",
|
|
945
|
+
marker: "<!-- knodin:start -->",
|
|
946
|
+
managedGuidance: true,
|
|
947
|
+
},
|
|
723
948
|
];
|
|
724
|
-
const configurations = candidates.map(({ agent, path: relative, marker }) => {
|
|
949
|
+
const configurations = candidates.map(({ agent, path: relative, marker, managedGuidance }) => {
|
|
725
950
|
try {
|
|
726
951
|
return {
|
|
727
952
|
agent,
|
|
728
953
|
path: relative,
|
|
729
|
-
present:
|
|
954
|
+
present: managedGuidance
|
|
955
|
+
? hasSafeManagedAgentBlock(repo, relative)
|
|
956
|
+
: fs.readFileSync(path.join(repo, relative), "utf-8").includes(marker),
|
|
730
957
|
};
|
|
731
958
|
}
|
|
732
959
|
catch {
|
|
@@ -745,10 +972,10 @@ export function inspectRepositoryIntegrationStatus(repo) {
|
|
|
745
972
|
};
|
|
746
973
|
}
|
|
747
974
|
async function writeRepositoryIntegrationConfig(repo, config) {
|
|
748
|
-
await fs.promises.writeFile(path.join(repo, ".
|
|
975
|
+
await fs.promises.writeFile(path.join(repo, ".knodin", "integration.json"), `${JSON.stringify(config, null, 2)}\n`, "utf-8");
|
|
749
976
|
}
|
|
750
977
|
async function ensureTeamGitIgnore(repo) {
|
|
751
|
-
if (isTracked(repo, ".
|
|
978
|
+
if (isTracked(repo, ".knodin"))
|
|
752
979
|
return;
|
|
753
980
|
await assertSafeRepositoryFile(repo, ".gitignore");
|
|
754
981
|
const ignorePath = path.join(repo, ".gitignore");
|
|
@@ -760,10 +987,10 @@ async function ensureTeamGitIgnore(repo) {
|
|
|
760
987
|
if (error.code !== "ENOENT")
|
|
761
988
|
throw error;
|
|
762
989
|
}
|
|
763
|
-
if (existing.split(/\r?\n/).includes(".
|
|
990
|
+
if (existing.split(/\r?\n/).includes(".knodin/"))
|
|
764
991
|
return;
|
|
765
992
|
const prefix = existing && !existing.endsWith("\n") ? `${existing}\n` : existing;
|
|
766
|
-
await fs.promises.writeFile(ignorePath, `${prefix}# knodin:start\n.
|
|
993
|
+
await fs.promises.writeFile(ignorePath, `${prefix}# knodin:start\n.knodin/\n# knodin:end\n`, "utf-8");
|
|
767
994
|
}
|
|
768
995
|
export function detectTrackedTeamIntegration(repo) {
|
|
769
996
|
const checks = [
|
|
@@ -797,12 +1024,16 @@ export async function initializeRepository(repo, options) {
|
|
|
797
1024
|
if (!path.isAbsolute(options.command[0])) {
|
|
798
1025
|
throw new Error("knodin init: packaged runtime command must be an absolute path");
|
|
799
1026
|
}
|
|
1027
|
+
const mutationTargets = options.auditConfigurationChanges
|
|
1028
|
+
? configureMutationAuditTargets(resolvedRepo)
|
|
1029
|
+
: [];
|
|
1030
|
+
const beforeMutation = mutationSnapshot(mutationTargets);
|
|
800
1031
|
if (scope !== "team" &&
|
|
801
1032
|
!options.allowTrackedTransition &&
|
|
802
1033
|
detectTrackedTeamIntegration(resolvedRepo)) {
|
|
803
1034
|
throw new Error("knodin init: tracked team integration is active; use `knodin configure --scope personal` or `--scope cli-only` for an explicit commit-ready transition");
|
|
804
1035
|
}
|
|
805
|
-
const knodinHooksDir = path.join(resolvedRepo, ".
|
|
1036
|
+
const knodinHooksDir = path.join(resolvedRepo, ".knodin", "hooks");
|
|
806
1037
|
const hookInstallation = await prepareHookInstallation(resolvedRepo);
|
|
807
1038
|
const hooksDir = hookInstallation.hooksDir;
|
|
808
1039
|
await Promise.all([
|
|
@@ -815,6 +1046,7 @@ export async function initializeRepository(repo, options) {
|
|
|
815
1046
|
mode: 0o755,
|
|
816
1047
|
});
|
|
817
1048
|
await fs.promises.chmod(backgroundPath, 0o755);
|
|
1049
|
+
const hookManagerIntegration = installHookManagerIntegration(resolvedRepo);
|
|
818
1050
|
for (const hookName of HOOK_NAMES) {
|
|
819
1051
|
let preservedOriginal;
|
|
820
1052
|
if (hookInstallation.trackedHooksDir) {
|
|
@@ -845,12 +1077,16 @@ export async function initializeRepository(repo, options) {
|
|
|
845
1077
|
}
|
|
846
1078
|
const lifecycleRefresh = drainQueuedLifecycleEvents(resolvedRepo, backgroundPath);
|
|
847
1079
|
await fs.promises.rm(path.join(knodinHooksDir, HOOK_FAILURE_FILE), { force: true });
|
|
1080
|
+
let agentInstructionChanges = [];
|
|
848
1081
|
if (scope === "team") {
|
|
849
|
-
await
|
|
850
|
-
|
|
1082
|
+
const installed = await Promise.all([
|
|
1083
|
+
installManagedAgentInstructions(resolvedRepo, "AGENTS.md"),
|
|
1084
|
+
installManagedAgentInstructions(resolvedRepo, "GEMINI.md"),
|
|
1085
|
+
]);
|
|
1086
|
+
agentInstructionChanges = installed.filter((change) => change !== null);
|
|
851
1087
|
}
|
|
852
1088
|
else {
|
|
853
|
-
await removeManagedPersonalArtifacts(resolvedRepo, options.allowTrackedTransition);
|
|
1089
|
+
agentInstructionChanges = await removeManagedPersonalArtifacts(resolvedRepo, options.allowTrackedTransition);
|
|
854
1090
|
}
|
|
855
1091
|
const requestedAgents = options.agents ?? [];
|
|
856
1092
|
const projectPathByAgent = {
|
|
@@ -859,6 +1095,7 @@ export async function initializeRepository(repo, options) {
|
|
|
859
1095
|
antigravity: ".agents/mcp_config.json",
|
|
860
1096
|
};
|
|
861
1097
|
const blockedAgents = [];
|
|
1098
|
+
const externalConfigurationOutcomes = [];
|
|
862
1099
|
if (scope === "personal" &&
|
|
863
1100
|
options.allowTrackedTransition &&
|
|
864
1101
|
priorIntegration?.scope === "team") {
|
|
@@ -870,6 +1107,11 @@ export async function initializeRepository(repo, options) {
|
|
|
870
1107
|
});
|
|
871
1108
|
if (cleanup.failed.length > 0)
|
|
872
1109
|
throw new Error(`knodin configure: could not remove tracked team integration: ${cleanup.failed[0]?.message}`);
|
|
1110
|
+
if (cleanup.removed.includes("claude"))
|
|
1111
|
+
externalConfigurationOutcomes.push({
|
|
1112
|
+
system: "claude-local-mcp",
|
|
1113
|
+
state: "removal-attempted",
|
|
1114
|
+
});
|
|
873
1115
|
}
|
|
874
1116
|
const eligibleAgents = scope === "team"
|
|
875
1117
|
? requestedAgents
|
|
@@ -893,6 +1135,18 @@ export async function initializeRepository(repo, options) {
|
|
|
893
1135
|
agents: eligibleAgents,
|
|
894
1136
|
run: options.runAgentCommand,
|
|
895
1137
|
});
|
|
1138
|
+
if (scope === "personal" && agentIntegration.configured.includes("claude"))
|
|
1139
|
+
externalConfigurationOutcomes.push({
|
|
1140
|
+
system: "claude-local-mcp",
|
|
1141
|
+
state: "configured-or-present",
|
|
1142
|
+
});
|
|
1143
|
+
if (scope === "cli-only" &&
|
|
1144
|
+
agentIntegration.removed.includes("claude") &&
|
|
1145
|
+
!externalConfigurationOutcomes.some(({ system, state }) => system === "claude-local-mcp" && state === "removal-attempted"))
|
|
1146
|
+
externalConfigurationOutcomes.push({
|
|
1147
|
+
system: "claude-local-mcp",
|
|
1148
|
+
state: "removal-attempted",
|
|
1149
|
+
});
|
|
896
1150
|
agentIntegration.failed.push(...blockedAgents);
|
|
897
1151
|
await writeRepositoryIntegrationConfig(resolvedRepo, {
|
|
898
1152
|
scope,
|
|
@@ -905,25 +1159,36 @@ export async function initializeRepository(repo, options) {
|
|
|
905
1159
|
else if (scope === "cli-only") {
|
|
906
1160
|
await removeManagedExcludes(resolvedRepo);
|
|
907
1161
|
}
|
|
908
|
-
|
|
1162
|
+
let excluded = scope === "team"
|
|
909
1163
|
? []
|
|
910
1164
|
: await excludeUntrackedPaths(resolvedRepo, [
|
|
911
|
-
...(isTracked(resolvedRepo, ".
|
|
1165
|
+
...(isTracked(resolvedRepo, ".knodin") ? [] : [".knodin/"]),
|
|
912
1166
|
...agentIntegration.projectFiles,
|
|
913
1167
|
]);
|
|
1168
|
+
if (hookManagerIntegration) {
|
|
1169
|
+
const managerExcludes = await excludeUntrackedPaths(resolvedRepo, [
|
|
1170
|
+
hookManagerIntegration.localConfig,
|
|
1171
|
+
]);
|
|
1172
|
+
excluded = [...new Set([...excluded, ...managerExcludes])].sort();
|
|
1173
|
+
}
|
|
914
1174
|
registerInitializedWorktree(resolvedRepo);
|
|
1175
|
+
const detectedFilesystemChanges = filesystemChanges(beforeMutation, mutationSnapshot(mutationTargets, true));
|
|
915
1176
|
return {
|
|
916
1177
|
scope,
|
|
917
|
-
knodin: ".
|
|
918
|
-
database: ".
|
|
919
|
-
backgroundIndexer: ".
|
|
1178
|
+
knodin: ".knodin",
|
|
1179
|
+
database: ".knodin/db.sqlite",
|
|
1180
|
+
backgroundIndexer: ".knodin/hooks/background-index.sh",
|
|
920
1181
|
hooksDirectory: path.relative(resolvedRepo, hooksDir) || ".",
|
|
921
1182
|
gitHooks: [...HOOK_NAMES],
|
|
922
1183
|
claudeSettings: null,
|
|
923
1184
|
agentInstructions: scope === "team" ? "AGENTS.md" : null,
|
|
924
1185
|
agentIntegration,
|
|
1186
|
+
agentInstructionChanges,
|
|
1187
|
+
filesystemChanges: detectedFilesystemChanges,
|
|
1188
|
+
externalConfigurationOutcomes,
|
|
925
1189
|
excluded,
|
|
926
1190
|
lifecycleRefresh,
|
|
1191
|
+
hookManagerIntegration,
|
|
927
1192
|
};
|
|
928
1193
|
}
|
|
929
1194
|
async function matchesManagedFile(filePath, expected, executable) {
|
|
@@ -956,6 +1221,7 @@ export async function isRepositoryInitializationCurrent(repo, command, scope = "
|
|
|
956
1221
|
catch {
|
|
957
1222
|
return false;
|
|
958
1223
|
}
|
|
1224
|
+
const lefthook = inspectLefthookIntegration(resolvedRepo);
|
|
959
1225
|
const managedHooks = await Promise.all(HOOK_NAMES.map(async (hookName) => {
|
|
960
1226
|
const hookPath = path.join(hooksDir, hookName);
|
|
961
1227
|
try {
|
|
@@ -967,7 +1233,9 @@ export async function isRepositoryInitializationCurrent(repo, command, scope = "
|
|
|
967
1233
|
let expected = managedHookScript(hookName, originalPath);
|
|
968
1234
|
if (originalPath)
|
|
969
1235
|
expected = expected.replace("\nREPO_ROOT=", `\n# knodin original: ${originalPath}\nREPO_ROOT=`);
|
|
970
|
-
return stat.isFile() &&
|
|
1236
|
+
return (stat.isFile() &&
|
|
1237
|
+
(stat.mode & 0o111) !== 0 &&
|
|
1238
|
+
(content === expected || (lefthook.configured && isActiveLefthookHook(content))));
|
|
971
1239
|
}
|
|
972
1240
|
catch {
|
|
973
1241
|
return false;
|
|
@@ -979,7 +1247,8 @@ export async function isRepositoryInitializationCurrent(repo, command, scope = "
|
|
|
979
1247
|
((await hasManagedAgentInstructions(resolvedRepo, "AGENTS.md")) &&
|
|
980
1248
|
(await hasManagedAgentInstructions(resolvedRepo, "GEMINI.md")));
|
|
981
1249
|
const integrationCurrent = readRepositoryIntegrationConfig(resolvedRepo)?.scope === scope;
|
|
982
|
-
return ((await matchesManagedFile(path.join(resolvedRepo, ".
|
|
1250
|
+
return ((await matchesManagedFile(path.join(resolvedRepo, ".knodin", "hooks", "background-index.sh"), backgroundScript(command), true)) &&
|
|
983
1251
|
sharedFilesCurrent &&
|
|
984
|
-
integrationCurrent
|
|
1252
|
+
integrationCurrent &&
|
|
1253
|
+
(lefthook.localConfig === null || lefthook.configured));
|
|
985
1254
|
}
|