killeros 2.1.26 → 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.
@@ -2,7 +2,7 @@ import { type ExtensionAPI, type ExtensionCommandContext, type ExtensionContext
2
2
  import { reportError } from "./errors.ts";
3
3
  import { beginGoalTurnState, checkpointActiveGoalState, GOAL_VERSION, parseGoalState, pauseGoalState, transitionGoalState, type GoalTransitionOptions } from "./goal-state.ts";
4
4
  import { resolvePersonalInstructions } from "./personal-instructions.ts";
5
- import type { GoalRuntime, GoalState, GoalStatus, InitRuntime } from "./runtime.ts";
5
+ import type { GoalRuntime, GoalState, GoalStatus } from "./runtime.ts";
6
6
  import { safeTerminalText } from "./safe-terminal-text.ts";
7
7
 
8
8
  export const GOAL_ENTRY_TYPE = "killeros-goal";
@@ -204,7 +204,6 @@ function beginGoalTurn(
204
204
  export function scheduleGoalContinuation(
205
205
  pi: ExtensionAPI,
206
206
  runtime: GoalRuntime,
207
- initState: InitRuntime,
208
207
  ctx: ExtensionContext,
209
208
  ): boolean {
210
209
  if (isGoalModeSupported(ctx) && isSavedSession(ctx) && pauseGoalAtTurnLimit(pi, runtime, ctx)) return false;
@@ -214,7 +213,6 @@ export function scheduleGoalContinuation(
214
213
  || runtime.continuationScheduled
215
214
  || runtime.continuationHeld
216
215
  || runtime.goalTurnInFlight
217
- || initState.active
218
216
  || !ctx.isIdle()
219
217
  || ctx.hasPendingMessages()) return false;
220
218
  runtime.continuationScheduled = true;
@@ -282,7 +280,6 @@ export function isSavedSession(ctx: ExtensionContext): boolean {
282
280
  export function registerGoalRuntime(
283
281
  pi: ExtensionAPI,
284
282
  runtime: GoalRuntime,
285
- initState: InitRuntime,
286
283
  ): void {
287
284
  const restoreGoal = (ctx: ExtensionContext): void => {
288
285
  const restored = restoreGoalState(ctx);
@@ -298,7 +295,7 @@ export function registerGoalRuntime(
298
295
  runtime.lastError = undefined;
299
296
  runtime.requestRender?.();
300
297
  if (runtime.state?.status === "active") {
301
- setImmediate(() => scheduleGoalContinuation(pi, runtime, initState, ctx));
298
+ setImmediate(() => scheduleGoalContinuation(pi, runtime, ctx));
302
299
  }
303
300
  };
304
301
 
@@ -330,7 +327,7 @@ export function registerGoalRuntime(
330
327
  runtime.continuationScheduled = false;
331
328
  if (!runtime.goalTurnInFlight && isGoalModeSupported(ctx) && isSavedSession(ctx) && pauseGoalAtTurnLimit(pi, runtime, ctx)) return;
332
329
  const current = runtime.state;
333
- if (!isGoalModeSupported(ctx) || !isSavedSession(ctx) || !current || current.status !== "active" || initState.active) return;
330
+ if (!isGoalModeSupported(ctx) || !isSavedSession(ctx) || !current || current.status !== "active") return;
334
331
  if (runtime.goalTurnInFlight) return { systemPrompt: `${event.systemPrompt}\n\n${goalSystemPrompt(current)}` };
335
332
  const next = beginGoalTurn(pi, runtime, ctx, current);
336
333
  if (!next) return;
@@ -3,7 +3,7 @@ import type { AutoCompactionGoalHandlers } from "./auto-compaction.ts";
3
3
  import { reportError } from "./errors.ts";
4
4
  import { isGoalModeSupported, isSavedSession, pauseGoalAfterFailure, pauseGoalAtTurnLimit, scheduleGoalContinuation, syncGoalUpdateTool, transitionGoal } from "./goal-runtime.ts";
5
5
  import { pauseGoalState } from "./goal-state.ts";
6
- import type { GoalRuntime, InitRuntime } from "./runtime.ts";
6
+ import type { GoalRuntime } from "./runtime.ts";
7
7
  import { safeTerminalText } from "./safe-terminal-text.ts";
8
8
 
9
9
  function pauseGoalForPossibleManualCompaction(
@@ -36,12 +36,10 @@ function pauseGoalForPossibleManualCompaction(
36
36
  function recoverGoalAfterManualCompaction(
37
37
  pi: ExtensionAPI,
38
38
  runtime: GoalRuntime,
39
- initState: InitRuntime,
40
39
  ctx: ExtensionContext,
41
40
  ): boolean {
42
41
  if (runtime.state?.status !== "paused"
43
- || runtime.state.resumeAfterManualCompaction !== true
44
- || initState.active) return false;
42
+ || runtime.state.resumeAfterManualCompaction !== true) return false;
45
43
  try {
46
44
  transitionGoal(pi, runtime, "resume", "active", undefined, { resetBlockedAudit: true });
47
45
  } catch (error) {
@@ -52,7 +50,7 @@ function recoverGoalAfterManualCompaction(
52
50
  runtime.continuationScheduled = false;
53
51
  runtime.automaticCompaction = undefined;
54
52
  ctx.ui.notify("Manual compaction complete. Goal resumed.", "info");
55
- setImmediate(() => scheduleGoalContinuation(pi, runtime, initState, ctx));
53
+ setImmediate(() => scheduleGoalContinuation(pi, runtime, ctx));
56
54
  return true;
57
55
  }
58
56
 
@@ -60,7 +58,6 @@ function recoverGoalAfterManualCompaction(
60
58
  function finalizeAutomaticCompaction(
61
59
  pi: ExtensionAPI,
62
60
  runtime: GoalRuntime,
63
- initState: InitRuntime,
64
61
  ctx: ExtensionContext,
65
62
  ): void {
66
63
  const recovery = runtime.automaticCompaction;
@@ -68,8 +65,7 @@ function finalizeAutomaticCompaction(
68
65
  const skipped = recovery.outcome === "skipped";
69
66
  runtime.automaticCompaction = undefined;
70
67
  if (runtime.state?.status !== "paused"
71
- || runtime.state.revision !== recovery.pausedRevision
72
- || initState.active) return;
68
+ || runtime.state.revision !== recovery.pausedRevision) return;
73
69
  try {
74
70
  transitionGoal(pi, runtime, "resume", "active", undefined, { resetBlockedAudit: true });
75
71
  } catch (error) {
@@ -80,31 +76,29 @@ function finalizeAutomaticCompaction(
80
76
  return;
81
77
  }
82
78
  runtime.continuationScheduled = false;
83
- setImmediate(() => scheduleGoalContinuation(pi, runtime, initState, ctx));
79
+ setImmediate(() => scheduleGoalContinuation(pi, runtime, ctx));
84
80
  }
85
81
 
86
82
  /** Records Pi's successful compaction callback and attempts guarded recovery. */
87
83
  function completeAutomaticCompaction(
88
84
  pi: ExtensionAPI,
89
85
  runtime: GoalRuntime,
90
- initState: InitRuntime,
91
86
  ctx: ExtensionContext,
92
87
  ): void {
93
88
  if (!runtime.automaticCompaction) return;
94
89
  runtime.automaticCompaction.outcome = "completed";
95
- finalizeAutomaticCompaction(pi, runtime, initState, ctx);
90
+ finalizeAutomaticCompaction(pi, runtime, ctx);
96
91
  }
97
92
 
98
93
  /** Records Pi's expected session-too-small rejection and resumes without claiming compaction succeeded. */
99
94
  function skipAutomaticCompaction(
100
95
  pi: ExtensionAPI,
101
96
  runtime: GoalRuntime,
102
- initState: InitRuntime,
103
97
  ctx: ExtensionContext,
104
98
  ): void {
105
99
  if (!runtime.automaticCompaction) return;
106
100
  runtime.automaticCompaction.outcome = "skipped";
107
- finalizeAutomaticCompaction(pi, runtime, initState, ctx);
101
+ finalizeAutomaticCompaction(pi, runtime, ctx);
108
102
  }
109
103
 
110
104
  /** Consumes automatic recovery and records its failure on the eligible paused goal. */
@@ -151,7 +145,6 @@ function failAutomaticCompaction(
151
145
  export function registerGoalSettlement(
152
146
  pi: ExtensionAPI,
153
147
  runtime: GoalRuntime,
154
- initState: InitRuntime,
155
148
  ): AutoCompactionGoalHandlers {
156
149
  pi.on("agent_settled", (_event, ctx) => {
157
150
  const wasGoalTurn = runtime.goalTurnInFlight;
@@ -182,15 +175,15 @@ export function registerGoalSettlement(
182
175
  return;
183
176
  }
184
177
  runtime.automaticCompaction.turnSettled = true;
185
- finalizeAutomaticCompaction(pi, runtime, initState, ctx);
178
+ finalizeAutomaticCompaction(pi, runtime, ctx);
186
179
  return;
187
180
  }
188
181
 
189
- if (!wasGoalTurn || runtime.state?.status !== "active" || initState.active) {
190
- if (continuationWasScheduled && runtime.state?.status === "active" && !initState.active) {
182
+ if (!wasGoalTurn || runtime.state?.status !== "active") {
183
+ if (continuationWasScheduled && runtime.state?.status === "active") {
191
184
  pauseGoalAfterFailure(pi, runtime, ctx, "the goal continuation ended before an agent turn started");
192
- } else if (runtime.state?.status === "active" && !initState.active) {
193
- scheduleGoalContinuation(pi, runtime, initState, ctx);
185
+ } else if (runtime.state?.status === "active") {
186
+ scheduleGoalContinuation(pi, runtime, ctx);
194
187
  }
195
188
  return;
196
189
  }
@@ -215,20 +208,19 @@ export function registerGoalSettlement(
215
208
  runtime.lastStopReason = undefined;
216
209
  runtime.lastError = undefined;
217
210
  if (pauseGoalAtTurnLimit(pi, runtime, ctx)) return;
218
- scheduleGoalContinuation(pi, runtime, initState, ctx);
211
+ scheduleGoalContinuation(pi, runtime, ctx);
219
212
  });
220
213
 
221
214
  pi.on("session_compact", (event, ctx) => {
222
215
  if (runtime.automaticCompaction !== undefined) return;
223
216
  if (event.reason !== "manual") return;
224
- recoverGoalAfterManualCompaction(pi, runtime, initState, ctx);
217
+ recoverGoalAfterManualCompaction(pi, runtime, ctx);
225
218
  });
226
219
 
227
220
  return {
228
221
  isActive: (ctx: ExtensionContext): boolean => isGoalModeSupported(ctx)
229
222
  && isSavedSession(ctx)
230
- && runtime.state?.status === "active"
231
- && !initState.active,
223
+ && runtime.state?.status === "active",
232
224
  onRequested: (): void => {
233
225
  if (runtime.state?.status !== "active") return;
234
226
  try {
@@ -250,8 +242,8 @@ export function registerGoalSettlement(
250
242
  throw error;
251
243
  }
252
244
  },
253
- onCompleted: (ctx: ExtensionContext): void => completeAutomaticCompaction(pi, runtime, initState, ctx),
245
+ onCompleted: (ctx: ExtensionContext): void => completeAutomaticCompaction(pi, runtime, ctx),
254
246
  onFailed: (ctx: ExtensionContext, error: unknown): void => failAutomaticCompaction(pi, runtime, ctx, error),
255
- onSkipped: (ctx: ExtensionContext): void => skipAutomaticCompaction(pi, runtime, initState, ctx),
247
+ onSkipped: (ctx: ExtensionContext): void => skipAutomaticCompaction(pi, runtime, ctx),
256
248
  };
257
249
  }
@@ -242,6 +242,45 @@ export async function captureGoalFileBaseline(
242
242
  }
243
243
  }
244
244
 
245
+ const GOAL_URL_PATTERN = /(?:https?|file):\/\/[^\s"'`]+/giu;
246
+ const GOAL_QUOTED_PATTERN = /`([^`\r\n]+)`|"([^"\r\n]+)"|'([^'\r\n]+)'/gu;
247
+ const GOAL_ABSOLUTE_PATTERN = /(?:^|[\s"'`(\[{,;])([A-Za-z]:[\\/][^\s,;'"`]+|\/[^\s,;'"`]+)/gu;
248
+ const GOAL_LIST_FILE_PATTERN = /(?:\band\b|\bor\b|[,;&+])\s*(?:`([^`\r\n]+)`|"([^"\r\n]+)"|'([^'\r\n]+)'|([A-Za-z0-9_][A-Za-z0-9_.-]*(?:[\\/][A-Za-z0-9_.-]+)*\.[A-Za-z0-9]{1,12})\b)/giu;
249
+
250
+ function isQuotedGoalFileMention(raw: string): boolean {
251
+ const value = raw.trim();
252
+ if (!value || /[\s\p{Cc}]/u.test(value) || /^(?:https?|file):\/\//iu.test(value) || /[\\/]$/u.test(value)) return false;
253
+ return /[\/\\.:]/u.test(value);
254
+ }
255
+
256
+ function normalizeGoalFileMention(raw: string, cwd: string): string | undefined {
257
+ const value = raw.trim().replace(/[.,;:!?)\]}]+$/u, "");
258
+ if (!value || /[\s\p{Cc}]/u.test(value)) return undefined;
259
+ const absolute = path.isAbsolute(value) || path.win32.isAbsolute(value) ? value : path.resolve(cwd, value);
260
+ if (!isAbsoluteFilePath(absolute)) return undefined;
261
+ return process.platform === "win32" ? absolute.toLowerCase() : absolute;
262
+ }
263
+
264
+ /** Counts distinct path-shaped mentions so several files cannot bind proof to one. */
265
+ function countGoalTargetFiles(objective: string, cwd: string, target: string): number {
266
+ const prose = objective.replace(GOAL_URL_PATTERN, " ");
267
+ const mentions = new Set<string>();
268
+ const add = (raw: string): void => {
269
+ const normalized = normalizeGoalFileMention(raw, cwd);
270
+ if (normalized) mentions.add(normalized);
271
+ };
272
+ add(target);
273
+ for (const match of prose.matchAll(GOAL_QUOTED_PATTERN)) {
274
+ const raw = match[1] ?? match[2] ?? match[3] ?? "";
275
+ if (isQuotedGoalFileMention(raw)) add(raw);
276
+ }
277
+ for (const match of prose.matchAll(GOAL_ABSOLUTE_PATTERN)) {
278
+ if (match[1]) add(stripUnquotedPathPunctuation(match[1].trim()));
279
+ }
280
+ for (const match of prose.matchAll(GOAL_LIST_FILE_PATTERN)) add(match[1] ?? match[2] ?? match[3] ?? match[4] ?? "");
281
+ return mentions.size;
282
+ }
283
+
245
284
  /** Captures one explicit output path so goal completion can verify its creation or modification. */
246
285
  export async function inferGoalVerification(objective: string, cwd: string): Promise<GoalFileVerification | undefined> {
247
286
  const candidates: string[] = [];
@@ -262,8 +301,12 @@ export async function inferGoalVerification(objective: string, cwd: string): Pro
262
301
  if (isAbsoluteFilePath(absolute)) resolved.push(absolute);
263
302
  }
264
303
  const unique = [...new Set(resolved)];
265
- const filePath = unique.length === 1 ? unique[0] : undefined;
266
- return filePath ? { kind: "file", path: filePath, baseline: await captureGoalFileBaseline(filePath) } : undefined;
304
+ if (unique.length !== 1) return undefined;
305
+ // A goal that names several files must not verify from one file alone.
306
+ // Repeated references to the same file count as one target.
307
+ const filePath = unique[0];
308
+ if (countGoalTargetFiles(objective, cwd, filePath) > 1) return undefined;
309
+ return { kind: "file", path: filePath, baseline: await captureGoalFileBaseline(filePath) };
267
310
  }
268
311
 
269
312
  export async function verifyGoalDeliverable(verification: GoalFileVerification): Promise<void> {
@@ -104,6 +104,33 @@ function hasRequiredHandoffContent(document: string, focus: string): boolean {
104
104
  });
105
105
  }
106
106
 
107
+ function handoffAvailable(ctx: ExtensionCommandContext, goalRuntime: GoalRuntime): boolean {
108
+ return ctx.isIdle() && !ctx.hasPendingMessages() && goalRuntime.state?.status !== "active";
109
+ }
110
+
111
+ function sourceHandoffAvailable(
112
+ ctx: ExtensionCommandContext,
113
+ goalRuntime: GoalRuntime,
114
+ sourceSession: string,
115
+ sourceLeaf: string | undefined,
116
+ ): boolean {
117
+ try {
118
+ return ctx.sessionManager.getSessionFile() === sourceSession
119
+ && handoffAvailable(ctx, goalRuntime)
120
+ && ctx.sessionManager.buildContextEntries().at(-1)?.id === sourceLeaf;
121
+ } catch {
122
+ return false;
123
+ }
124
+ }
125
+
126
+ function notifyHandoffUnavailable(ctx: ExtensionCommandContext): void {
127
+ try {
128
+ ctx.ui.notify(HANDOFF_UNAVAILABLE, "error");
129
+ } catch {
130
+ // A session replaced during generation has no valid source UI to notify.
131
+ }
132
+ }
133
+
107
134
  function assertHandoffContextReserve(ctx: ExtensionCommandContext, maxTokens: number): void {
108
135
  let usage: ReturnType<ExtensionCommandContext["getContextUsage"]>;
109
136
  try {
@@ -160,8 +187,8 @@ export function registerHandoff(pi: ExtensionAPI, goalRuntime: GoalRuntime, hand
160
187
  pi.registerCommand("handoff", {
161
188
  description: "Create a fresh session with a continuation handoff",
162
189
  handler: async (args, ctx) => {
163
- if (!ctx.isIdle() || ctx.hasPendingMessages() || goalRuntime.state?.status === "active") {
164
- ctx.ui.notify(HANDOFF_UNAVAILABLE, "error");
190
+ if (!handoffAvailable(ctx, goalRuntime)) {
191
+ notifyHandoffUnavailable(ctx);
165
192
  return;
166
193
  }
167
194
 
@@ -175,8 +202,9 @@ export function registerHandoff(pi: ExtensionAPI, goalRuntime: GoalRuntime, hand
175
202
  let document: string;
176
203
  let focus: string;
177
204
  try {
178
- const messages = ctx.sessionManager.buildContextEntries().flatMap(sessionEntryToContextMessages);
179
- const conversation = serializeConversation(convertToLlm(messages));
205
+ const entries = ctx.sessionManager.buildContextEntries();
206
+ const sourceLeaf = entries.at(-1)?.id;
207
+ const conversation = serializeConversation(convertToLlm(entries.flatMap(sessionEntryToContextMessages)));
180
208
  if (!conversation.trim()) throw new Error("No usable session context is available");
181
209
  focus = safeTerminalText(args).trim();
182
210
  let maxTokens = handoffMaxTokens;
@@ -215,6 +243,10 @@ export function registerHandoff(pi: ExtensionAPI, goalRuntime: GoalRuntime, hand
215
243
  if (!hasRequiredHandoffContent(document, focus)) {
216
244
  throw new Error("The handoff summary did not contain every required section");
217
245
  }
246
+ if (!sourceHandoffAvailable(ctx, goalRuntime, sourceSession, sourceLeaf)) {
247
+ notifyHandoffUnavailable(ctx);
248
+ return;
249
+ }
218
250
  } catch (error) {
219
251
  reportError(ctx, "Handoff failed", error);
220
252
  return;
@@ -0,0 +1,206 @@
1
+ import { accessSync, constants, existsSync, realpathSync, statSync } from "node:fs";
2
+ import path from "node:path";
3
+
4
+ // Passive Git inspection disables fsmonitor, known clean/process filters,
5
+ // promisor fetches, and repository-supplied Git executables. Config discovery
6
+ // and status run in separate Git processes, so a filter configured between
7
+ // them would be absent from the safety overrides. Automatic Git children run
8
+ // without PATH resolution, and each scan rejects results when the effective
9
+ // filter set changed. Absolute filter commands remain possible, so product
10
+ // callers run these scans only after project trust is granted.
11
+ //
12
+ // Executable discovery itself is passive: it never starts a command shell,
13
+ // locator process, or other helper executable, and never executes a bare
14
+ // program name. It scans absolute search-path entries for an absolute Git
15
+ // path outside the inspected repository and fails closed when none exists.
16
+ export const PASSIVE_GIT_CONFIG_ARGS = ["config", "--includes", "--null", "--name-only", "--list"] as const;
17
+
18
+ const SAFE_FILTER_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/u;
19
+
20
+ function searchPathEntries(env: NodeJS.ProcessEnv): string[] {
21
+ const values: string[] = [];
22
+ for (const [key, value] of Object.entries(env)) {
23
+ if (key.toLowerCase() === "path" && typeof value === "string") values.push(value);
24
+ }
25
+ const entries: string[] = [];
26
+ for (const value of values) entries.push(...value.split(path.delimiter));
27
+ return entries;
28
+ }
29
+
30
+ function pathExtensionEntries(env: NodeJS.ProcessEnv): string[] {
31
+ let raw: string | undefined;
32
+ for (const [key, value] of Object.entries(env)) {
33
+ if (key.toLowerCase() === "pathext" && typeof value === "string") {
34
+ raw = value;
35
+ break;
36
+ }
37
+ }
38
+ raw ??= ".COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC;.CPL";
39
+ return raw.split(";").map((entry) => entry.trim()).filter(Boolean).map((entry) => entry.startsWith(".") ? entry : `.${entry}`);
40
+ }
41
+
42
+ function unquoted(entry: string): string {
43
+ if (entry.length >= 2) {
44
+ const first = entry[0];
45
+ const last = entry[entry.length - 1];
46
+ if ((first === '"' && last === '"') || (first === "'" && last === "'")) return entry.slice(1, -1);
47
+ }
48
+ return entry;
49
+ }
50
+
51
+ function inspectedRoot(cwd: string): string | undefined {
52
+ if (!cwd || typeof cwd !== "string") return undefined;
53
+ let start: string;
54
+ try {
55
+ start = path.resolve(cwd);
56
+ } catch {
57
+ return undefined;
58
+ }
59
+ let base: string;
60
+ try {
61
+ base = realpathSync(start);
62
+ } catch {
63
+ base = start;
64
+ }
65
+ let current = base;
66
+ for (;;) {
67
+ try {
68
+ if (existsSync(path.join(current, ".git"))) {
69
+ try {
70
+ return realpathSync(current);
71
+ } catch {
72
+ return current;
73
+ }
74
+ }
75
+ } catch {
76
+ // Unreadable directory: keep walking toward the filesystem root.
77
+ }
78
+ const parent = path.dirname(current);
79
+ if (parent === current) break;
80
+ current = parent;
81
+ }
82
+ return base;
83
+ }
84
+
85
+ function insideInspected(candidate: string, root: string): boolean {
86
+ if (process.platform === "win32") {
87
+ const normalizedCandidate = path.win32.normalize(candidate).toLowerCase();
88
+ const normalizedRoot = path.win32.normalize(root).toLowerCase();
89
+ const trimmed = normalizedRoot.length > 3 && normalizedRoot.endsWith(path.win32.sep)
90
+ ? normalizedRoot.slice(0, -1)
91
+ : normalizedRoot;
92
+ if (normalizedCandidate === trimmed) return true;
93
+ return normalizedCandidate.startsWith(`${trimmed}${path.win32.sep}`);
94
+ }
95
+ const normalizedCandidate = path.normalize(candidate);
96
+ const normalizedRoot = path.normalize(root);
97
+ const trimmed = normalizedRoot.length > 1 && normalizedRoot.endsWith(path.sep)
98
+ ? normalizedRoot.slice(0, -1)
99
+ : normalizedRoot;
100
+ if (normalizedCandidate === trimmed) return true;
101
+ return normalizedCandidate.startsWith(`${trimmed}${path.sep}`);
102
+ }
103
+
104
+ // Absolute Git binary outside the inspected repository, or undefined when
105
+ // no safe candidate exists. Never starts a helper process and never
106
+ // returns a bare command name, so opening a repository cannot execute a
107
+ // repository-local locator or Git executable. Empty and relative
108
+ // search-path entries are ignored because they can resolve against the
109
+ // current directory. A candidate that resolves through a link returns its
110
+ // final path only when that path is also outside the repository.
111
+ export function passiveGitCommand(cwd: string, env: NodeJS.ProcessEnv = process.env): string | undefined {
112
+ const root = inspectedRoot(cwd);
113
+ if (!root) return undefined;
114
+ const entries = searchPathEntries(env);
115
+ if (entries.length === 0) return undefined;
116
+ const windows = process.platform === "win32";
117
+ const baseNames = windows ? ["git", ...pathExtensionEntries(env).map((extension) => `git${extension}`)] : ["git"];
118
+ for (const raw of entries) {
119
+ if (raw === "" || raw.trim() === "") continue;
120
+ const directory = unquoted(raw);
121
+ if (directory === "" || directory.trim() === "") continue;
122
+ if (!path.isAbsolute(directory)) continue;
123
+ for (const base of baseNames) {
124
+ const candidate = path.join(directory, base);
125
+ try {
126
+ if (!statSync(candidate).isFile()) continue;
127
+ } catch {
128
+ continue;
129
+ }
130
+ if (!windows) {
131
+ try {
132
+ accessSync(candidate, constants.X_OK);
133
+ } catch {
134
+ continue;
135
+ }
136
+ }
137
+ let resolved: string;
138
+ try {
139
+ resolved = realpathSync(candidate);
140
+ } catch {
141
+ continue;
142
+ }
143
+ if (!path.isAbsolute(resolved)) continue;
144
+ if (insideInspected(resolved, root)) continue;
145
+ return resolved;
146
+ }
147
+ }
148
+ return undefined;
149
+ }
150
+
151
+ // Lists effective clean/process filter drivers in discovery order, or
152
+ // undefined when discovery output is incomplete or names a driver the
153
+ // safety overrides cannot represent.
154
+ export function passiveFilterNames(config: string): string[] | undefined {
155
+ const records = config.split("\0");
156
+ if (records.at(-1) !== "") return undefined;
157
+ const names = new Set<string>();
158
+ for (const key of records) {
159
+ if (!key) continue;
160
+ const name = /^filter\.(.*)\.(?:clean|process)$/us.exec(key)?.[1];
161
+ if (name === undefined) continue;
162
+ if (!SAFE_FILTER_NAME.test(name)) return undefined;
163
+ names.add(name);
164
+ }
165
+ return [...names];
166
+ }
167
+
168
+ // Builds safe overrides from null-delimited `git config --name-only --list`
169
+ // output. Returns undefined when output is incomplete or names an unsafe
170
+ // filter, so the caller skips the status call.
171
+ export function passiveStatusSafetyArgs(config: string): string[] | undefined {
172
+ const names = passiveFilterNames(config);
173
+ if (!names) return undefined;
174
+ return ["-c", "core.fsmonitor=false", ...names.flatMap((name) => ["-c", `filter.${name}.clean=`, "-c", `filter.${name}.process=`, "-c", `filter.${name}.required=false`])];
175
+ }
176
+
177
+ export function samePassiveFilters(before: string, after: string): boolean {
178
+ const earlier = passiveFilterNames(before);
179
+ const later = passiveFilterNames(after);
180
+ if (!earlier || !later) return false;
181
+ if (earlier.length !== later.length) return false;
182
+ const ordered = [...later].sort();
183
+ return [...earlier].sort().every((name, index) => name === ordered[index]);
184
+ }
185
+
186
+ // Environment for automatic Git children: no optional locks, no lazy fetch
187
+ // from a promisor remote, and no PATH so a filter command that becomes
188
+ // effective after discovery cannot resolve a bare command name. Absolute
189
+ // filter paths are still possible; trusted-project callers detect mid-scan
190
+ // config changes with samePassiveFilters and skip those results.
191
+ export function passiveGitEnv(base: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
192
+ const env: NodeJS.ProcessEnv = {
193
+ ...base,
194
+ GIT_OPTIONAL_LOCKS: "0",
195
+ GIT_NO_LAZY_FETCH: "1",
196
+ };
197
+ let hasPath = false;
198
+ for (const key of Object.keys(env)) {
199
+ if (key.toLowerCase() === "path") {
200
+ env[key] = "";
201
+ hasPath = true;
202
+ }
203
+ }
204
+ if (!hasPath) env.PATH = "";
205
+ return env;
206
+ }
@@ -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, initState: InitRuntime): void {
109
+ export function registerPersonalInstructions(pi: ExtensionAPI): void {
111
110
  pi.on("before_agent_start", (event, ctx) => {
112
- if (initState.active || !ctx.isProjectTrusted()) return;
111
+ if (!ctx.isProjectTrusted()) return;
113
112
  const personal = resolvePersonalInstructions(ctx.cwd);
114
113
  if (!personal) return;
115
114
  return {
@@ -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"; recoveryPath?: string }
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
- }
@@ -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
- "git",
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();
@@ -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),