killeros 2.1.25 → 2.1.27
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/CHANGELOG.md +28 -0
- package/Killeros.ts +5 -12
- package/README.md +6 -8
- package/killeros/auto-compaction.ts +0 -2
- package/killeros/change-receipt.ts +111 -63
- package/killeros/codex-fast.ts +8 -3
- package/killeros/footer.ts +76 -27
- package/killeros/goal-interface.ts +8 -15
- package/killeros/goal-runtime.ts +3 -6
- package/killeros/goal-settlement.ts +17 -29
- package/killeros/goal-state.ts +55 -3
- package/killeros/handoff.ts +36 -4
- package/killeros/passive-git-status.ts +206 -0
- package/killeros/personal-instructions.ts +2 -3
- package/killeros/runtime.ts +0 -38
- package/killeros/shell-ui.ts +13 -4
- package/killeros/worked-for.ts +3 -2
- package/package.json +1 -1
- package/killeros/init-evidence.ts +0 -291
- package/killeros/init-target.ts +0 -298
- package/killeros/init.ts +0 -281
|
@@ -3,7 +3,6 @@ import os from "node:os";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { StringDecoder } from "node:string_decoder";
|
|
5
5
|
import { getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
6
|
-
import type { InitRuntime } from "./runtime.ts";
|
|
7
6
|
|
|
8
7
|
const PERSONAL_INSTRUCTIONS_FILE = "AGENTS.local.md";
|
|
9
8
|
const PERSONAL_INSTRUCTIONS_LIMIT = 32 * 1024;
|
|
@@ -107,9 +106,9 @@ export function resolvePersonalInstructions(cwd: string): string | undefined {
|
|
|
107
106
|
return `<personal_instructions>\n${content}\n</personal_instructions>`;
|
|
108
107
|
}
|
|
109
108
|
|
|
110
|
-
export function registerPersonalInstructions(pi: ExtensionAPI
|
|
109
|
+
export function registerPersonalInstructions(pi: ExtensionAPI): void {
|
|
111
110
|
pi.on("before_agent_start", (event, ctx) => {
|
|
112
|
-
if (
|
|
111
|
+
if (!ctx.isProjectTrusted()) return;
|
|
113
112
|
const personal = resolvePersonalInstructions(ctx.cwd);
|
|
114
113
|
if (!personal) return;
|
|
115
114
|
return {
|
package/killeros/runtime.ts
CHANGED
|
@@ -1,25 +1,3 @@
|
|
|
1
|
-
import type { InitEvidenceIndex } from "./init-evidence.ts";
|
|
2
|
-
import type { InitTargetBaseline } from "./init-target.ts";
|
|
3
|
-
|
|
4
|
-
export type InitOutcome =
|
|
5
|
-
| { kind: "pending" }
|
|
6
|
-
| { kind: "written" }
|
|
7
|
-
| { kind: "policy-conflict"; reason: string }
|
|
8
|
-
| { kind: "cancelled" }
|
|
9
|
-
| { kind: "no-outcome" };
|
|
10
|
-
|
|
11
|
-
export interface InitRuntime {
|
|
12
|
-
active: boolean;
|
|
13
|
-
starting?: symbol;
|
|
14
|
-
targetPath?: string;
|
|
15
|
-
projectRoot?: string;
|
|
16
|
-
activeTools?: string[];
|
|
17
|
-
evidence?: InitEvidenceIndex;
|
|
18
|
-
baseline?: InitTargetBaseline;
|
|
19
|
-
outcome: InitOutcome;
|
|
20
|
-
settle?: (outcome: InitOutcome) => void;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
1
|
export type GoalStatus = "active" | "paused" | "blocked" | "complete";
|
|
24
2
|
|
|
25
3
|
export interface GoalBlockerAudit {
|
|
@@ -106,10 +84,6 @@ export interface GoalRuntime {
|
|
|
106
84
|
requestRender?: () => void;
|
|
107
85
|
}
|
|
108
86
|
|
|
109
|
-
export function createInitRuntime(): InitRuntime {
|
|
110
|
-
return { active: false, outcome: { kind: "pending" } };
|
|
111
|
-
}
|
|
112
|
-
|
|
113
87
|
export function createGoalRuntime(): GoalRuntime {
|
|
114
88
|
return {
|
|
115
89
|
continuationScheduled: false,
|
|
@@ -120,15 +94,3 @@ export function createGoalRuntime(): GoalRuntime {
|
|
|
120
94
|
persistenceRetryNeeded: false,
|
|
121
95
|
};
|
|
122
96
|
}
|
|
123
|
-
|
|
124
|
-
export function resetInitRuntime(state: InitRuntime): void {
|
|
125
|
-
state.active = false;
|
|
126
|
-
state.starting = undefined;
|
|
127
|
-
state.targetPath = undefined;
|
|
128
|
-
state.projectRoot = undefined;
|
|
129
|
-
state.activeTools = undefined;
|
|
130
|
-
state.evidence = undefined;
|
|
131
|
-
state.baseline = undefined;
|
|
132
|
-
state.outcome = { kind: "pending" };
|
|
133
|
-
state.settle = undefined;
|
|
134
|
-
}
|
package/killeros/shell-ui.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
type SlashCommandResolver,
|
|
25
25
|
} from "./commands.ts";
|
|
26
26
|
import { reportError } from "./errors.ts";
|
|
27
|
+
import { passiveGitCommand, passiveGitEnv } from "./passive-git-status.ts";
|
|
27
28
|
|
|
28
29
|
function readPackageVersion(path: string | URL): string | undefined {
|
|
29
30
|
try {
|
|
@@ -47,7 +48,6 @@ const STARTUP_TIPS = [
|
|
|
47
48
|
"Type / to browse every command available in this session.",
|
|
48
49
|
"Run /notification to enable a terminal bell when work settles.",
|
|
49
50
|
"Run /goal <objective> to keep long-running work moving across turns.",
|
|
50
|
-
"Run /init to generate root AGENTS.md from bounded repository evidence.",
|
|
51
51
|
"Run /handoff [focus] to continue work in a fresh linked session.",
|
|
52
52
|
"Run /codex-fast to toggle priority requests for Codex models.",
|
|
53
53
|
"Run /clear to start a fresh session after confirmation.",
|
|
@@ -67,13 +67,22 @@ const EDITOR_SUGGESTIONS = [
|
|
|
67
67
|
'Try "draft an implementation plan for <feature>"',
|
|
68
68
|
] as const;
|
|
69
69
|
|
|
70
|
-
export function resolveGitBranch(cwd: string): Promise<string | undefined> {
|
|
70
|
+
export function resolveGitBranch(cwd: string, trusted = true): Promise<string | undefined> {
|
|
71
|
+
if (!trusted) return Promise.resolve(undefined);
|
|
72
|
+
let gitCommand: string | undefined;
|
|
73
|
+
try {
|
|
74
|
+
gitCommand = passiveGitCommand(cwd);
|
|
75
|
+
} catch {
|
|
76
|
+
return Promise.resolve(undefined);
|
|
77
|
+
}
|
|
78
|
+
if (!gitCommand) return Promise.resolve(undefined);
|
|
71
79
|
return new Promise((resolve) => {
|
|
72
80
|
execFile(
|
|
73
|
-
|
|
81
|
+
gitCommand,
|
|
74
82
|
["-C", cwd, "rev-parse", "--abbrev-ref", "HEAD"],
|
|
75
83
|
{
|
|
76
84
|
encoding: "utf8",
|
|
85
|
+
env: passiveGitEnv(),
|
|
77
86
|
maxBuffer: 64 * 1024,
|
|
78
87
|
timeout: 500,
|
|
79
88
|
windowsHide: true,
|
|
@@ -132,7 +141,7 @@ class PiStartupHeader {
|
|
|
132
141
|
this.ctx = ctx;
|
|
133
142
|
this.tip = tip;
|
|
134
143
|
this.tui = tui;
|
|
135
|
-
void resolveGitBranch(ctx.cwd).then((branch) => {
|
|
144
|
+
void resolveGitBranch(ctx.cwd, ctx.isProjectTrusted()).then((branch) => {
|
|
136
145
|
if (this.disposed) return;
|
|
137
146
|
this.branch = branch;
|
|
138
147
|
this.tui.requestRender();
|
package/killeros/worked-for.ts
CHANGED
|
@@ -142,7 +142,8 @@ function parseV4(data: Record<string, unknown>): WorkedForEntryDataV4 | undefine
|
|
|
142
142
|
const checks: CheckAttempt[] = [];
|
|
143
143
|
for (const check of data.checks) {
|
|
144
144
|
if (!record(check) || check.outcome !== "passed" && check.outcome !== "failed") return undefined;
|
|
145
|
-
const label = CHECK_LABELS.find((candidate) => candidate === check.label)
|
|
145
|
+
const label = CHECK_LABELS.find((candidate) => candidate === check.label)
|
|
146
|
+
?? (check.label === "node --test (focused)" ? "node --test (focused)" : undefined);
|
|
146
147
|
if (!label) return undefined;
|
|
147
148
|
checks.push({ label, outcome: check.outcome });
|
|
148
149
|
}
|
|
@@ -337,7 +338,7 @@ export function registerWorkedFor(
|
|
|
337
338
|
});
|
|
338
339
|
|
|
339
340
|
pi.on("agent_start", async (_event, ctx) => {
|
|
340
|
-
if (ctx.mode !== "tui" || active) return;
|
|
341
|
+
if (ctx.mode !== "tui" || active || !ctx.isProjectTrusted()) return;
|
|
341
342
|
const state: ActiveReceipt = {
|
|
342
343
|
startedAt: now(),
|
|
343
344
|
startedTokens: sessionTokenTotal(ctx),
|
package/package.json
CHANGED
|
@@ -1,291 +0,0 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import { promises as fs } from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { StringDecoder } from "node:string_decoder";
|
|
5
|
-
import { containsLikelySecret } from "./secret-detector.ts";
|
|
6
|
-
|
|
7
|
-
export const INIT_READ_TOOL = "killeros_init_read";
|
|
8
|
-
export const INIT_LIST_TOOL = "killeros_init_list";
|
|
9
|
-
|
|
10
|
-
const SNAPSHOT_LIMIT = 40 * 1024;
|
|
11
|
-
const AUTOMATIC_FILE_LIMIT = 8 * 1024;
|
|
12
|
-
const READ_LIMIT = 32 * 1024;
|
|
13
|
-
const PATH_LIMIT = 400;
|
|
14
|
-
const DIRECTORY_LIMIT = 120;
|
|
15
|
-
const DEPTH_LIMIT = 4;
|
|
16
|
-
const EXCLUDED_DIRS = new Set([
|
|
17
|
-
".agents", ".claude", ".git", ".next", ".pi", ".pytest_cache", ".turbo", ".venv", "__pycache__", "archive", "build", "coverage", "data", "dist", "logs", "node_modules", "target", "test-results", "vendor",
|
|
18
|
-
]);
|
|
19
|
-
const EXCLUDED_GUIDANCE = new Set([
|
|
20
|
-
".cursorrules", "agents.md", "agents.local.md", "claude.md", "claude.local.md", "copilot-instructions.md", "gemini.md", "memory.md", "skill.md",
|
|
21
|
-
]);
|
|
22
|
-
const ROOT_EVIDENCE = [
|
|
23
|
-
"README.md", "README.rst", "README.txt", "CONTRIBUTING.md", "package.json", "pyproject.toml", "requirements.txt", "Cargo.toml", "go.mod", "Makefile", "Dockerfile", "compose.yaml", "compose.yml", "tsconfig.json", "vite.config.ts", "vite.config.js", "eslint.config.js", "eslint.config.mjs",
|
|
24
|
-
] as const;
|
|
25
|
-
const NESTED_EVIDENCE = new Set(["package.json", "pyproject.toml", "requirements.txt", "Cargo.toml", "go.mod"]);
|
|
26
|
-
|
|
27
|
-
export interface InitEvidenceIndex {
|
|
28
|
-
projectRoot: string;
|
|
29
|
-
canonicalPaths: ReadonlyMap<string, string>;
|
|
30
|
-
snapshot: string;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface InitEvidenceBuildResult {
|
|
34
|
-
index: InitEvidenceIndex;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function evidenceKey(relativePath: string): string {
|
|
38
|
-
const normalized = relativePath.replaceAll("\\", "/");
|
|
39
|
-
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function sensitiveEvidencePath(relativePath: string): boolean {
|
|
43
|
-
const normalized = relativePath.replaceAll("\\", "/").toLowerCase();
|
|
44
|
-
const name = path.posix.basename(normalized);
|
|
45
|
-
return /^\.env(?:\.|$)/u.test(name)
|
|
46
|
-
|| [".npmrc", ".pypirc", ".netrc", "id_rsa", "id_ed25519", "credentials.json"].includes(name)
|
|
47
|
-
|| /(?:^|\/)\.aws\/credentials$/u.test(normalized)
|
|
48
|
-
|| /(?:^|\/)\.docker\/config\.json$/u.test(normalized)
|
|
49
|
-
|| /(?:^|\/)\.kube\/config$/u.test(normalized)
|
|
50
|
-
|| /(?:credential|secret|token)/u.test(name)
|
|
51
|
-
|| /^service-account.*\.json$/u.test(name)
|
|
52
|
-
|| /\.(?:pem|key|p12|pfx|jks|keystore)$/u.test(name);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function excludedPath(relativePath: string): boolean {
|
|
56
|
-
const segments = relativePath.replaceAll("\\", "/").split("/");
|
|
57
|
-
return segments.some((segment, index) =>
|
|
58
|
-
(index < segments.length - 1 && EXCLUDED_DIRS.has(segment.toLowerCase()))
|
|
59
|
-
|| EXCLUDED_GUIDANCE.has(segment.toLowerCase()))
|
|
60
|
-
|| sensitiveEvidencePath(relativePath);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async function collectCandidates(projectRoot: string): Promise<string[]> {
|
|
64
|
-
const files: string[] = [];
|
|
65
|
-
const queue: Array<{ relativePath: string; depth: number }> = [{ relativePath: "", depth: 0 }];
|
|
66
|
-
let directoriesRead = 0;
|
|
67
|
-
while (queue.length && files.length < PATH_LIMIT && directoriesRead < DIRECTORY_LIMIT) {
|
|
68
|
-
const current = queue.shift();
|
|
69
|
-
if (!current) break;
|
|
70
|
-
directoriesRead += 1;
|
|
71
|
-
let entries;
|
|
72
|
-
try {
|
|
73
|
-
entries = await fs.readdir(path.join(projectRoot, current.relativePath), { withFileTypes: true });
|
|
74
|
-
} catch (error) {
|
|
75
|
-
if (!current.relativePath) throw error;
|
|
76
|
-
continue;
|
|
77
|
-
}
|
|
78
|
-
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
79
|
-
for (const entry of entries) {
|
|
80
|
-
if (files.length >= PATH_LIMIT) break;
|
|
81
|
-
const relativePath = path.posix.join(current.relativePath.replaceAll("\\", "/"), entry.name);
|
|
82
|
-
if (entry.isDirectory()) {
|
|
83
|
-
if (current.depth < DEPTH_LIMIT && !EXCLUDED_DIRS.has(entry.name.toLowerCase())) {
|
|
84
|
-
queue.push({ relativePath, depth: current.depth + 1 });
|
|
85
|
-
}
|
|
86
|
-
} else if (entry.isFile() && !excludedPath(relativePath)) {
|
|
87
|
-
try {
|
|
88
|
-
const stat = await fs.lstat(path.join(projectRoot, relativePath));
|
|
89
|
-
if (stat.isFile() && !stat.isSymbolicLink() && stat.nlink === 1) files.push(relativePath);
|
|
90
|
-
} catch {
|
|
91
|
-
// Files may disappear while the bounded map is collected.
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
return files;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
async function gitIgnoredPaths(projectRoot: string, candidates: readonly string[]): Promise<ReadonlySet<string>> {
|
|
100
|
-
if (!candidates.length) return new Set();
|
|
101
|
-
return new Promise((resolve, reject) => {
|
|
102
|
-
let settled = false;
|
|
103
|
-
let stdout = Buffer.alloc(0);
|
|
104
|
-
const child = spawn("git", ["-C", projectRoot, "check-ignore", "--stdin", "-z"], {
|
|
105
|
-
shell: false,
|
|
106
|
-
stdio: ["pipe", "pipe", "ignore"],
|
|
107
|
-
windowsHide: true,
|
|
108
|
-
});
|
|
109
|
-
const fail = (): void => {
|
|
110
|
-
if (settled) return;
|
|
111
|
-
settled = true;
|
|
112
|
-
clearTimeout(timer);
|
|
113
|
-
reject(new Error("Git ignore inspection failed; /init did not build repository evidence"));
|
|
114
|
-
};
|
|
115
|
-
const succeed = (value: ReadonlySet<string>): void => {
|
|
116
|
-
if (settled) return;
|
|
117
|
-
settled = true;
|
|
118
|
-
clearTimeout(timer);
|
|
119
|
-
resolve(value);
|
|
120
|
-
};
|
|
121
|
-
const timer = setTimeout(() => {
|
|
122
|
-
child.kill("SIGKILL");
|
|
123
|
-
fail();
|
|
124
|
-
}, 2_000);
|
|
125
|
-
child.stdout.on("data", (chunk: Buffer) => {
|
|
126
|
-
if (stdout.length + chunk.length > 256 * 1024) {
|
|
127
|
-
child.kill("SIGKILL");
|
|
128
|
-
fail();
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
stdout = Buffer.concat([stdout, chunk]);
|
|
132
|
-
});
|
|
133
|
-
child.once("error", fail);
|
|
134
|
-
child.stdin.once("error", () => {
|
|
135
|
-
child.kill("SIGKILL");
|
|
136
|
-
fail();
|
|
137
|
-
});
|
|
138
|
-
child.once("close", (code) => {
|
|
139
|
-
if (code === 1) {
|
|
140
|
-
if (stdout.length) fail();
|
|
141
|
-
else succeed(new Set());
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
if (code !== 0 || !stdout.length || stdout.at(-1) !== 0) {
|
|
145
|
-
fail();
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
let values: string[];
|
|
149
|
-
try {
|
|
150
|
-
values = new TextDecoder("utf-8", { fatal: true }).decode(stdout.subarray(0, -1)).split("\0");
|
|
151
|
-
} catch {
|
|
152
|
-
fail();
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
const candidateSet = new Set(candidates.map(evidenceKey));
|
|
156
|
-
const ignored = new Set(values.map(evidenceKey));
|
|
157
|
-
if (values.some((value) => !value || !candidateSet.has(evidenceKey(value))) || ignored.size !== values.length) {
|
|
158
|
-
fail();
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
succeed(ignored);
|
|
162
|
-
});
|
|
163
|
-
child.stdin.end(`${candidates.join("\0")}\0`);
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function decodeCompleteUtf8(bytes: Buffer): string {
|
|
168
|
-
return new StringDecoder("utf8").write(bytes);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function appendWithinLimit(current: string, section: string, limit: number): string {
|
|
172
|
-
const remaining = limit - Buffer.byteLength(current, "utf8");
|
|
173
|
-
if (remaining <= 0) return current;
|
|
174
|
-
const bytes = Buffer.from(section, "utf8");
|
|
175
|
-
return current + decodeCompleteUtf8(bytes.subarray(0, remaining));
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
async function validateAndRead(projectRoot: string, absolutePath: string, limit: number): Promise<{ content: string; truncated: boolean }> {
|
|
179
|
-
const relative = path.relative(projectRoot, absolutePath);
|
|
180
|
-
if (!relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
181
|
-
throw new Error("path is not available to /init");
|
|
182
|
-
}
|
|
183
|
-
let current = projectRoot;
|
|
184
|
-
for (const segment of relative.split(path.sep)) {
|
|
185
|
-
current = path.join(current, segment);
|
|
186
|
-
const stat = await fs.lstat(current);
|
|
187
|
-
if (stat.isSymbolicLink()) throw new Error("/init rejects symbolic-link and junction paths");
|
|
188
|
-
}
|
|
189
|
-
const pathStat = await fs.lstat(absolutePath);
|
|
190
|
-
if (!pathStat.isFile() || pathStat.nlink !== 1) throw new Error("/init rejects linked and non-regular files");
|
|
191
|
-
const handle = await fs.open(absolutePath, "r");
|
|
192
|
-
try {
|
|
193
|
-
const openedStat = await handle.stat();
|
|
194
|
-
if (!openedStat.isFile() || openedStat.nlink !== 1
|
|
195
|
-
|| openedStat.dev !== pathStat.dev || openedStat.ino !== pathStat.ino) {
|
|
196
|
-
throw new Error("/init file changed while it was being opened");
|
|
197
|
-
}
|
|
198
|
-
const buffer = Buffer.alloc(limit + 1);
|
|
199
|
-
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
|
|
200
|
-
const data = buffer.subarray(0, Math.min(bytesRead, limit));
|
|
201
|
-
if (data.includes(0)) throw new Error("/init rejects binary files");
|
|
202
|
-
return { content: decodeCompleteUtf8(data), truncated: bytesRead > limit };
|
|
203
|
-
} finally {
|
|
204
|
-
await handle.close();
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/** Reads evidence only when its bounded content does not resemble a credential. */
|
|
209
|
-
async function readEvidenceFile(projectRoot: string, absolutePath: string, limit: number): Promise<{ content: string; truncated: boolean }> {
|
|
210
|
-
const result = await validateAndRead(projectRoot, absolutePath, limit);
|
|
211
|
-
if (containsLikelySecret(result.content)) throw new Error("file is not available to /init");
|
|
212
|
-
return result;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function normalizeRequestedPath(requestedPath: string): string {
|
|
216
|
-
if (!requestedPath || requestedPath.trim() !== requestedPath || requestedPath.startsWith("~")
|
|
217
|
-
|| /^file:/iu.test(requestedPath) || path.isAbsolute(requestedPath)) {
|
|
218
|
-
throw new Error("path is not available to /init");
|
|
219
|
-
}
|
|
220
|
-
const normalized = requestedPath.replaceAll("\\", "/");
|
|
221
|
-
if (!normalized || normalized.split("/").some((segment) => segment === ".." || segment === "")) {
|
|
222
|
-
throw new Error("path is not available to /init");
|
|
223
|
-
}
|
|
224
|
-
return normalized.replace(/^\.\//u, "");
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
export async function buildInitEvidence(projectRoot: string): Promise<InitEvidenceBuildResult> {
|
|
228
|
-
const candidates = await collectCandidates(projectRoot);
|
|
229
|
-
const ignored = await gitIgnoredPaths(projectRoot, candidates);
|
|
230
|
-
const canonicalPaths = new Map<string, string>();
|
|
231
|
-
for (const relativePath of candidates) {
|
|
232
|
-
if (!ignored.has(evidenceKey(relativePath))) canonicalPaths.set(evidenceKey(relativePath), relativePath);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
let snapshot = [
|
|
236
|
-
"# KillerOS repository snapshot",
|
|
237
|
-
"Root AGENTS.md is protected policy and is intentionally not part of this untrusted evidence.",
|
|
238
|
-
"",
|
|
239
|
-
"## Project files",
|
|
240
|
-
[...canonicalPaths.values()].join("\n"),
|
|
241
|
-
].join("\n");
|
|
242
|
-
snapshot = appendWithinLimit("", snapshot, SNAPSHOT_LIMIT);
|
|
243
|
-
const automatic = new Set<string>(ROOT_EVIDENCE);
|
|
244
|
-
for (const relativePath of canonicalPaths.values()) {
|
|
245
|
-
const name = path.posix.basename(relativePath);
|
|
246
|
-
if (NESTED_EVIDENCE.has(name) || /^\.github\/workflows\/[^/]+\.ya?ml$/iu.test(relativePath)) automatic.add(relativePath);
|
|
247
|
-
}
|
|
248
|
-
for (const requested of automatic) {
|
|
249
|
-
const relativePath = canonicalPaths.get(evidenceKey(requested));
|
|
250
|
-
if (!relativePath || Buffer.byteLength(snapshot, "utf8") >= SNAPSHOT_LIMIT) continue;
|
|
251
|
-
try {
|
|
252
|
-
const result = await readEvidenceFile(projectRoot, path.join(projectRoot, relativePath), AUTOMATIC_FILE_LIMIT);
|
|
253
|
-
const suffix = result.truncated ? "\n[truncated by /init]" : "";
|
|
254
|
-
snapshot = appendWithinLimit(snapshot, `\n\n## ${relativePath}\n${result.content}${suffix}`, SNAPSHOT_LIMIT);
|
|
255
|
-
} catch {
|
|
256
|
-
// A mapped file may become unsafe or disappear before snapshot creation.
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
return { index: { projectRoot, canonicalPaths, snapshot } };
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
export async function readInitEvidence(index: InitEvidenceIndex, requestedPath: string): Promise<string> {
|
|
263
|
-
const normalized = normalizeRequestedPath(requestedPath);
|
|
264
|
-
const relativePath = index.canonicalPaths.get(evidenceKey(normalized));
|
|
265
|
-
if (!relativePath) throw new Error(`${requestedPath} is not available to /init`);
|
|
266
|
-
const result = await readEvidenceFile(index.projectRoot, path.join(index.projectRoot, relativePath), READ_LIMIT);
|
|
267
|
-
return result.truncated ? `${result.content}\n[truncated by /init at ${READ_LIMIT} bytes]` : result.content;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
export async function readGeneratedInitTarget(projectRoot: string, targetPath: string): Promise<string> {
|
|
271
|
-
const result = await validateAndRead(projectRoot, targetPath, READ_LIMIT);
|
|
272
|
-
return result.truncated ? `${result.content}\n[truncated by /init at ${READ_LIMIT} bytes]` : result.content;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
export function listInitEvidence(index: InitEvidenceIndex, requestedPath = "."): string[] {
|
|
276
|
-
const prefix = requestedPath === "." ? "" : normalizeRequestedPath(requestedPath).replace(/\/$/u, "");
|
|
277
|
-
const prefixWithSlash = prefix ? `${prefix}/` : "";
|
|
278
|
-
const evidencePrefix = evidenceKey(prefixWithSlash);
|
|
279
|
-
const children = new Set<string>();
|
|
280
|
-
let found = !prefix;
|
|
281
|
-
for (const relativePath of index.canonicalPaths.values()) {
|
|
282
|
-
if (!evidenceKey(relativePath).startsWith(evidencePrefix)) continue;
|
|
283
|
-
const remainder = relativePath.slice(prefixWithSlash.length);
|
|
284
|
-
if (!remainder) continue;
|
|
285
|
-
found = true;
|
|
286
|
-
const child = remainder.split("/", 1)[0];
|
|
287
|
-
if (child) children.add(child);
|
|
288
|
-
}
|
|
289
|
-
if (!found) throw new Error(`${requestedPath} is not available to /init`);
|
|
290
|
-
return [...children].sort((left, right) => left.localeCompare(right)).slice(0, PATH_LIMIT);
|
|
291
|
-
}
|