@dyyz1993/pi-coding-agent 0.74.28 → 0.74.29

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.
@@ -73,6 +73,7 @@ type BashToolDetails = _BashToolDetails & {
73
73
  };
74
74
 
75
75
  const DEFAULT_TIMEOUT_SECONDS = 300;
76
+ const DEFAULT_BACKGROUND_AFTER_SECONDS = 120;
76
77
 
77
78
  const bashSchema = Type.Object({
78
79
  command: Type.String({ description: "Bash command to execute" }),
@@ -179,83 +180,105 @@ export default function (pi: ExtensionAPI) {
179
180
  });
180
181
 
181
182
  channel.handle("kill", ({ toolCallId }) => {
182
- if (!toolCallId) return;
183
+ if (!toolCallId) return { ok: false, reason: "not_found" };
183
184
  const m = managed.get(toolCallId);
184
- if (m?.proc.pid) {
185
- killProcessTree(m.proc.pid);
186
- m.proc.status = "terminated";
187
- m.proc.endedAt = Date.now();
188
- m.resolved = true;
189
- m.killedByUser = true;
190
- const durationMs = m.proc.endedAt - m.proc.startedAt;
191
- if (m.logStream) m.logStream.end();
185
+ if (!m) {
186
+ // Process already exited — emit terminated event so frontend can sync state
192
187
  channel?.emit("terminated", {
193
188
  type: "terminated",
194
189
  toolCallId,
195
- pid: m.proc.pid,
190
+ pid: undefined,
196
191
  processes: Array.from(managed.values()).map((x) => x.proc),
197
192
  timestamp: Date.now(),
198
193
  });
199
- m.resolve({
200
- content: [
201
- {
202
- type: "text",
203
- text: `${m.proc.output || "(no output)"}\n\n[User cancelled after ${formatDuration(durationMs)}, PID: ${m.proc.pid}${m.proc.logPath ? `. Log: ${m.proc.logPath}` : ""}]`,
204
- },
205
- ],
206
- details: {
207
- terminated: {
208
- reason: "user_cancel",
209
- pid: m.proc.pid,
210
- command: m.proc.command,
211
- startedAt: m.proc.startedAt,
212
- endedAt: m.proc.endedAt,
213
- durationMs,
214
- logPath: m.proc.logPath,
215
- },
216
- },
217
- });
194
+ return { ok: true, alreadyExited: true };
218
195
  }
196
+ if (m.proc.pid) {
197
+ killProcessTree(m.proc.pid);
198
+ }
199
+ m.proc.status = "terminated";
200
+ m.proc.endedAt = Date.now();
201
+ m.resolved = true;
202
+ m.killedByUser = true;
203
+ const durationMs = m.proc.endedAt - m.proc.startedAt;
204
+ if (m.logStream) m.logStream.end();
205
+ channel?.emit("terminated", {
206
+ type: "terminated",
207
+ toolCallId,
208
+ pid: m.proc.pid,
209
+ processes: Array.from(managed.values()).map((x) => x.proc),
210
+ timestamp: Date.now(),
211
+ });
212
+ m.resolve({
213
+ content: [
214
+ {
215
+ type: "text",
216
+ text: `${m.proc.output || "(no output)"}\n\n[User cancelled after ${formatDuration(durationMs)}, PID: ${m.proc.pid ?? "unknown"}${m.proc.logPath ? `. Log: ${m.proc.logPath}` : ""}]`,
217
+ },
218
+ ],
219
+ details: {
220
+ terminated: {
221
+ reason: "user_cancel",
222
+ pid: m.proc.pid,
223
+ command: m.proc.command,
224
+ startedAt: m.proc.startedAt,
225
+ endedAt: m.proc.endedAt,
226
+ durationMs,
227
+ logPath: m.proc.logPath,
228
+ },
229
+ },
230
+ });
231
+ return { ok: true };
219
232
  });
220
233
 
221
234
  channel.handle("background", ({ toolCallId }) => {
222
- if (!toolCallId) return;
235
+ if (!toolCallId) return { ok: false, reason: "not_found" };
223
236
  const m = managed.get(toolCallId);
224
- if (m) {
225
- m.proc.status = "background";
226
- m.resolved = true;
227
- m.backgrounded = true;
228
- m.outputSubscribed = false;
229
- createLogStream(m);
230
- const durationMs = Date.now() - m.proc.startedAt;
231
- channel?.emit("background", {
232
- type: "background",
237
+ if (!m) {
238
+ // Process already exited — emit terminated event so frontend can sync state
239
+ channel?.emit("terminated", {
240
+ type: "terminated",
233
241
  toolCallId,
234
- pid: m.proc.pid,
235
- data: m.proc.output.slice(-2000),
242
+ pid: undefined,
236
243
  processes: Array.from(managed.values()).map((x) => x.proc),
237
244
  timestamp: Date.now(),
238
245
  });
239
- const outputPreview = m.proc.output ? takeLastLines(m.proc.output, BG_PREVIEW_LINES) : "(no output yet)";
240
- m.resolve({
241
- content: [
242
- {
243
- type: "text",
244
- text: `${outputPreview}\n\n[Moved to background after ${formatDuration(durationMs)}, PID: ${m.proc.pid ?? "unknown"}. <bashId>${m.proc.bashId}</bashId>. Log: ${m.proc.logPath}. Use get_background_process with <bashId>${m.proc.bashId}</bashId> to check progress.]`,
245
- },
246
- ],
247
- details: {
248
- background: {
249
- pid: m.proc.pid,
250
- command: m.proc.command,
251
- startedAt: m.proc.startedAt,
252
- durationMs,
253
- logPath: m.proc.logPath,
254
- detached: false,
255
- },
256
- },
257
- });
246
+ return { ok: true, alreadyExited: true };
258
247
  }
248
+ m.proc.status = "background";
249
+ m.resolved = true;
250
+ m.backgrounded = true;
251
+ m.outputSubscribed = false;
252
+ createLogStream(m);
253
+ const durationMs = Date.now() - m.proc.startedAt;
254
+ channel?.emit("background", {
255
+ type: "background",
256
+ toolCallId,
257
+ pid: m.proc.pid,
258
+ data: m.proc.output.slice(-2000),
259
+ processes: Array.from(managed.values()).map((x) => x.proc),
260
+ timestamp: Date.now(),
261
+ });
262
+ const outputPreview = m.proc.output ? takeLastLines(m.proc.output, BG_PREVIEW_LINES) : "(no output yet)";
263
+ m.resolve({
264
+ content: [
265
+ {
266
+ type: "text",
267
+ text: `${outputPreview}\n\n[Moved to background after ${formatDuration(durationMs)}, PID: ${m.proc.pid ?? "unknown"}. <bashId>${m.proc.bashId}</bashId>. Log: ${m.proc.logPath}. Use get_background_process with <bashId>${m.proc.bashId}</bashId> to check progress.]`,
268
+ },
269
+ ],
270
+ details: {
271
+ background: {
272
+ pid: m.proc.pid,
273
+ command: m.proc.command,
274
+ startedAt: m.proc.startedAt,
275
+ durationMs,
276
+ logPath: m.proc.logPath,
277
+ detached: false,
278
+ },
279
+ },
280
+ });
281
+ return { ok: true };
259
282
  });
260
283
 
261
284
  channel.handle("subscribe_output", ({ toolCallId }) => {
@@ -318,8 +341,9 @@ export default function (pi: ExtensionAPI) {
318
341
  _ctx?: ExtensionContext,
319
342
  ): Promise<AgentToolResult<BashToolDetails>> {
320
343
  return new Promise((resolve, reject) => {
321
- const effectiveTimeout = timeout ?? DEFAULT_TIMEOUT_SECONDS;
322
- const effectiveBackgroundAfter = backgroundAfter !== undefined && backgroundAfter < effectiveTimeout ? backgroundAfter : undefined;
344
+ const effectiveTimeout = timeout ?? DEFAULT_TIMEOUT_SECONDS;
345
+ const rawBackgroundAfter = backgroundAfter ?? DEFAULT_BACKGROUND_AFTER_SECONDS;
346
+ const effectiveBackgroundAfter = rawBackgroundAfter < effectiveTimeout ? rawBackgroundAfter : undefined;
323
347
  const cwd = cwdParam ?? _ctx?.cwd ?? process.cwd();
324
348
  const bashId = generateBashId();
325
349
 
@@ -4,7 +4,10 @@ export default function fileSnapshot(pi: ExtensionAPI) {
4
4
  const channel = pi.registerChannel("file-snapshot");
5
5
 
6
6
  channel.onReceive(async (msg) => {
7
- const ctx = msg.context as ExtensionContext;
7
+ const ctx = msg.context as ExtensionContext | undefined;
8
+ if (!ctx) {
9
+ return { error: "Extension context not available in channel message. This operation is not supported via RPC client channel calls." };
10
+ }
8
11
  const mgr = ctx.fileSnapshotManager;
9
12
  if (!mgr) {
10
13
  return { error: "fileSnapshotManager not available" };
@@ -11,6 +11,7 @@ import { createDependencyResolver } from "./utils/dependency-resolver.js";
11
11
  import { createWriteThroughHooks } from "./hooks/writethrough.js";
12
12
  import { createLspToolRouter } from "./tools/lsp-tool.js";
13
13
  import { createServerMetricsCollector } from "./monitoring/server-metrics.js";
14
+ import { scanProjectFileTypes, filterServersByProject } from "./utils/project-scanner.js";
14
15
 
15
16
  export interface LspChannelEvent {
16
17
  event:
@@ -143,14 +144,31 @@ export default function lspExtension(pi: ExtensionAPI): void {
143
144
 
144
145
  const config = configResolver.resolve();
145
146
 
147
+ // Scan project for file types and filter servers
148
+ const cwd = process.cwd();
149
+ const scanResult = scanProjectFileTypes(cwd);
150
+ const filteredServers = filterServersByProject(config.servers, scanResult);
151
+ const skippedNames = config.servers
152
+ .filter((s) => !filteredServers.some((f) => f.name === s.name))
153
+ .map((s) => s.name);
154
+ const discoveredExts = [...scanResult.discoveredExtensions].sort();
155
+
156
+ if (skippedNames.length > 0) {
157
+ console.log(
158
+ `[lsp] Project scan found [${discoveredExts.join(", ")}], starting ${filteredServers.length}/${config.servers.length} servers (skipped: ${skippedNames.join(", ")})`,
159
+ );
160
+ }
161
+
162
+ const filteredConfig = { ...config, servers: filteredServers };
163
+
146
164
  lspChannel?.emit("startup_begin", {
147
165
  event: "startup_begin",
148
166
  timestamp: Date.now(),
149
- servers: config.servers.map((s) => ({ name: s.name, state: "starting", fileTypes: s.fileTypes })),
150
- totalServers: config.servers.length,
167
+ servers: filteredConfig.servers.map((s) => ({ name: s.name, state: "starting", fileTypes: s.fileTypes })),
168
+ totalServers: filteredConfig.servers.length,
151
169
  });
152
170
 
153
- await runtime.start(config);
171
+ await runtime.start(filteredConfig);
154
172
  const status = runtime.getStatus();
155
173
 
156
174
  for (const srv of status.servers) {
@@ -0,0 +1,102 @@
1
+ import { execSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { extname, join } from "node:path";
4
+ import type { ResolvedLspServerConfig } from "../config/resolver.js";
5
+
6
+ export interface ProjectScanResult {
7
+ discoveredExtensions: Set<string>;
8
+ }
9
+
10
+ /**
11
+ * Scan the project for file types present on disk.
12
+ * Uses `git ls-files` when available (fast, respects .gitignore),
13
+ * falls back to a shallow `find` otherwise.
14
+ */
15
+ export function scanProjectFileTypes(cwd: string): ProjectScanResult {
16
+ const extensions = new Set<string>();
17
+
18
+ // Strategy 1: git ls-files (fast, respects gitignore)
19
+ const gitFiles = tryGitLsFiles(cwd);
20
+ if (gitFiles.length > 0) {
21
+ for (const file of gitFiles) {
22
+ const ext = extname(file).toLowerCase();
23
+ if (ext) {
24
+ extensions.add(ext);
25
+ }
26
+ }
27
+ return { discoveredExtensions: extensions };
28
+ }
29
+
30
+ // Strategy 2: shallow find (maxdepth 3, skip node_modules etc.)
31
+ try {
32
+ const output = execSync(
33
+ 'find . -maxdepth 3 -type f -not -path "*/node_modules/*" -not -path "*/.git/*" -not -path "*/target/*" -not -path "*/dist/*" -not -path "*/.pi/*" 2>/dev/null | head -2000',
34
+ { cwd, timeout: 3000, encoding: "utf8" },
35
+ );
36
+ for (const line of output.split("\n")) {
37
+ const trimmed = line.trim();
38
+ if (!trimmed) continue;
39
+ const ext = extname(trimmed).toLowerCase();
40
+ if (ext) {
41
+ extensions.add(ext);
42
+ }
43
+ }
44
+ } catch {
45
+ // If scan fails, return empty — will fall back to starting all servers
46
+ }
47
+
48
+ return { discoveredExtensions: extensions };
49
+ }
50
+
51
+ function tryGitLsFiles(cwd: string): string[] {
52
+ // Check if we're in a git repo
53
+ if (!existsSync(join(cwd, ".git"))) {
54
+ return [];
55
+ }
56
+
57
+ try {
58
+ const output = execSync("git ls-files --cached --others --exclude-standard 2>/dev/null | head -2000", {
59
+ cwd,
60
+ timeout: 3000,
61
+ encoding: "utf8",
62
+ });
63
+ const files = output.split("\n").filter(Boolean);
64
+ return files.length > 0 ? files : [];
65
+ } catch {
66
+ return [];
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Filter server configs to only those whose fileTypes match files in the project.
72
+ * Servers WITHOUT fileTypes (catch-all) are always included.
73
+ * If the project has no discoverable files, all servers are started (safe fallback).
74
+ */
75
+ export function filterServersByProject(
76
+ servers: ResolvedLspServerConfig[],
77
+ scanResult: ProjectScanResult,
78
+ ): ResolvedLspServerConfig[] {
79
+ const { discoveredExtensions } = scanResult;
80
+
81
+ // Safe fallback: if scan found nothing, start everything
82
+ if (discoveredExtensions.size === 0) {
83
+ return servers;
84
+ }
85
+
86
+ const filtered: ResolvedLspServerConfig[] = [];
87
+ for (const server of servers) {
88
+ // No fileTypes = catch-all server, always include
89
+ if (!server.fileTypes || server.fileTypes.length === 0) {
90
+ filtered.push(server);
91
+ continue;
92
+ }
93
+
94
+ // Include if ANY of the server's fileTypes exist in the project
95
+ const hasMatch = server.fileTypes.some((ft) => discoveredExtensions.has(ft.toLowerCase()));
96
+ if (hasMatch) {
97
+ filtered.push(server);
98
+ }
99
+ }
100
+
101
+ return filtered;
102
+ }
@@ -15,7 +15,9 @@ const DEFAULT_CONFIG: SupervisorConfig = {
15
15
  maxContinueCount: 5,
16
16
  defaultDelayMs: 30_000,
17
17
  pauseThresholdMs: 300_000,
18
- guards: [],
18
+ guards: [
19
+ { name: "incomplete-keywords", type: "keyword", enable: true, keywords: ["TODO", "FIXME", "WIP", "HACK"] },
20
+ ],
19
21
  };
20
22
 
21
23
  export function loadConfig(sessionDataDir: string, projectDataDir: string): SupervisorConfig {
@@ -36,6 +36,10 @@ function log(msg: string) {
36
36
  appendFileSync(LOG_FILE, line);
37
37
  }
38
38
 
39
+ const DEFAULT_GUARDS: GuardConfig[] = [
40
+ { name: "incomplete-keywords", type: "keyword", enable: true, keywords: ["TODO", "FIXME", "WIP", "HACK"] },
41
+ ];
42
+
39
43
  export default function sessionSupervisorExtension(pi: ExtensionAPI) {
40
44
  let config: SupervisorConfig;
41
45
  let enabled = false;
@@ -72,8 +76,8 @@ export default function sessionSupervisorExtension(pi: ExtensionAPI) {
72
76
  const { server: channel } =
73
77
  createTypedChannel<SupervisorChannelContract>(rawChannel);
74
78
 
75
- channel.handle("supervisor.getStatus", async () => getStatus());
76
- channel.handle("supervisor.requestPause", async (params) => {
79
+ channel.handle("getStatus", async () => getStatus());
80
+ channel.handle("requestPause", async (params) => {
77
81
  const delayMs = params.delayMs ?? config.defaultDelayMs;
78
82
  const result = schedulerInstance.scheduleContinue(
79
83
  "manual-pause",
@@ -90,7 +94,7 @@ export default function sessionSupervisorExtension(pi: ExtensionAPI) {
90
94
  return result;
91
95
  });
92
96
 
93
- channel.handle("supervisor.cancelPause", async () => {
97
+ channel.handle("cancelPause", async () => {
94
98
  const cancelled = schedulerInstance.cancelTimer("manual-pause");
95
99
  if (cancelled) {
96
100
  channel.emit("supervisor.pauseCancelled", { reason: "Cancelled via channel" });
@@ -98,7 +102,7 @@ export default function sessionSupervisorExtension(pi: ExtensionAPI) {
98
102
  return { cancelled };
99
103
  });
100
104
 
101
- channel.handle("supervisor.forceContinue", async (params) => {
105
+ channel.handle("forceContinue", async (params) => {
102
106
  schedulerInstance.cancelAll();
103
107
  currentState = "continuing";
104
108
  emitStatusChanged();
@@ -106,7 +110,7 @@ export default function sessionSupervisorExtension(pi: ExtensionAPI) {
106
110
  return { triggered: true };
107
111
  });
108
112
 
109
- channel.handle("supervisor.disable", async () => {
113
+ channel.handle("disable", async () => {
110
114
  enabled = false;
111
115
  schedulerInstance.cancelAll();
112
116
  currentState = "disabled";
@@ -114,16 +118,16 @@ export default function sessionSupervisorExtension(pi: ExtensionAPI) {
114
118
  return { disabled: true };
115
119
  });
116
120
 
117
- channel.handle("supervisor.enable", async () => {
121
+ channel.handle("enable", async () => {
118
122
  enabled = true;
119
123
  currentState = "idle";
120
124
  emitStatusChanged();
121
125
  return { enabled: true };
122
126
  });
123
127
 
124
- channel.handle("supervisor.getTaskReport", async () => ({ tasks: lastTaskReports }));
128
+ channel.handle("getTaskReport", async () => ({ tasks: lastTaskReports }));
125
129
 
126
- channel.handle("supervisor.checkToolStatus", async (params) => {
130
+ channel.handle("checkToolStatus", async (params) => {
127
131
  const targetChannelName = params.channelName ?? params.toolName;
128
132
  try {
129
133
  const result = await rawChannel.call(
@@ -222,8 +226,8 @@ export default function sessionSupervisorExtension(pi: ExtensionAPI) {
222
226
  config.smallModel = modelFlag;
223
227
  }
224
228
 
225
- // Determine project root for specs file resolution
226
- projectRoot = ctx.projectDataDir ?? process.cwd();
229
+ // projectRoot is the git root (worktree-aware), correct for specs file resolution
230
+ projectRoot = ctx.projectRoot ?? ctx.cwd;
227
231
 
228
232
  schedulerInstance = new Scheduler(
229
233
  config.maxContinueCount,
@@ -276,7 +280,29 @@ export default function sessionSupervisorExtension(pi: ExtensionAPI) {
276
280
  log(`guard[${guard.name}] completed=${result.completed}, remaining=${result.remainingItems.length}`);
277
281
  }
278
282
 
279
- // Also run the generic model-based check as fallback (if no custom guards or as additional check)
283
+ lastTaskReports = reports;
284
+ channel.emit("supervisor.taskReport", { tasks: reports });
285
+
286
+ // Phase 2: If any guard says incomplete → continue immediately
287
+ const hasIncompleteGuards = guardResults.some((r) => !r.completed && r.remainingItems.length > 0);
288
+
289
+ if (hasIncompleteGuards) {
290
+ log(`Guards detected incomplete tasks`);
291
+ specsIterationCount++;
292
+
293
+ const continueMessage = generateContinueMessage(
294
+ activeGuards,
295
+ guardResults,
296
+ null,
297
+ );
298
+
299
+ lastCheckResult = { completed: false, confidence: 0.9, incompleteTasks: [], guardResults };
300
+
301
+ scheduleContinue(continueMessage);
302
+ return;
303
+ }
304
+
305
+ // Phase 3: All guards passed → run fallback model check
280
306
  const modelCheck = await checkWithSmallModel(
281
307
  event.messages as Array<{ role: string; content: unknown }>,
282
308
  config,
@@ -284,14 +310,9 @@ export default function sessionSupervisorExtension(pi: ExtensionAPI) {
284
310
  ctx.sessionSignal,
285
311
  );
286
312
 
287
- lastTaskReports = reports;
288
- channel.emit("supervisor.taskReport", { tasks: reports });
289
-
290
- // Phase 2: Determine if we should continue
291
- const hasIncompleteGuards = guardResults.some((r) => !r.completed && r.remainingItems.length > 0);
292
313
  const hasModelIncomplete = modelCheck.completed === false || modelCheck.incompleteTasks.length > 0;
293
314
 
294
- if (!hasIncompleteGuards && !hasModelIncomplete) {
315
+ if (!hasModelIncomplete) {
295
316
  log(`All guards passed + model check passed → idle`);
296
317
  currentState = "idle";
297
318
  lastCheckResult = { ...modelCheck, guardResults };
@@ -299,10 +320,8 @@ export default function sessionSupervisorExtension(pi: ExtensionAPI) {
299
320
  return;
300
321
  }
301
322
 
302
- // Phase 3: Generate continue message from first incomplete guard
303
- log(`Incomplete tasks detected, scheduling continue...`);
304
- specsIterationCount++;
305
-
323
+ // Phase 4: Model detected incompleteness continue with model's assessment
324
+ log(`Model detected incomplete tasks`);
306
325
  const continueMessage = generateContinueMessage(
307
326
  activeGuards,
308
327
  guardResults,
@@ -310,41 +329,7 @@ export default function sessionSupervisorExtension(pi: ExtensionAPI) {
310
329
  );
311
330
 
312
331
  lastCheckResult = { ...modelCheck, guardResults };
313
-
314
- // Phase 4: Schedule continue
315
- const delayMs = config.defaultDelayMs;
316
-
317
- if (schedulerInstance.shouldPause(delayMs)) {
318
- currentState = "paused";
319
- emitStatusChanged();
320
- channel.emit("supervisor.pauseRequested", {
321
- delayMs,
322
- reason: continueMessage.slice(0, 200),
323
- });
324
- }
325
-
326
- pi.background(async (signal) => {
327
- await new Promise<void>((resolve) => {
328
- const timer = setTimeout(resolve, delayMs);
329
- signal.addEventListener("abort", () => {
330
- clearTimeout(timer);
331
- resolve();
332
- });
333
- });
334
-
335
- if (signal.aborted) return;
336
-
337
- currentState = "continuing";
338
- emitStatusChanged();
339
- pi.sendMessage(
340
- {
341
- customType: "supervisor_continue",
342
- content: continueMessage,
343
- display: true,
344
- },
345
- { triggerTurn: true },
346
- );
347
- });
332
+ scheduleContinue(continueMessage);
348
333
  } catch (err) {
349
334
  log(`agent_end error: ${err instanceof Error ? err.message : String(err)}`);
350
335
  currentState = "idle";
@@ -359,8 +344,46 @@ export default function sessionSupervisorExtension(pi: ExtensionAPI) {
359
344
 
360
345
  // ── Guard Check Functions ──
361
346
 
347
+ function scheduleContinue(continueMessage: string): void {
348
+ const delayMs = config.defaultDelayMs;
349
+
350
+ if (schedulerInstance.shouldPause(delayMs)) {
351
+ currentState = "paused";
352
+ emitStatusChanged();
353
+ channel.emit("supervisor.pauseRequested", {
354
+ delayMs,
355
+ reason: continueMessage.slice(0, 200),
356
+ });
357
+ }
358
+
359
+ pi.background(async (signal) => {
360
+ await new Promise<void>((resolve) => {
361
+ const timer = setTimeout(resolve, delayMs);
362
+ signal.addEventListener("abort", () => {
363
+ clearTimeout(timer);
364
+ resolve();
365
+ });
366
+ });
367
+
368
+ if (signal.aborted) return;
369
+
370
+ currentState = "continuing";
371
+ emitStatusChanged();
372
+ pi.sendMessage(
373
+ {
374
+ customType: "supervisor_continue",
375
+ content: continueMessage,
376
+ display: true,
377
+ },
378
+ { triggerTurn: true },
379
+ );
380
+ });
381
+ }
382
+
362
383
  function getActiveGuards(): GuardConfig[] {
363
- return (config.guards ?? []).filter((g) => g.enable !== false);
384
+ const guards = config.guards ?? [];
385
+ const source = guards.length > 0 ? guards : DEFAULT_GUARDS;
386
+ return source.filter((g) => g.enable !== false);
364
387
  }
365
388
 
366
389
  async function runGuardCheck(
@@ -599,7 +622,7 @@ export default function sessionSupervisorExtension(pi: ExtensionAPI) {
599
622
  function generateContinueMessage(
600
623
  guards: GuardConfig[],
601
624
  results: GuardCheckResult[],
602
- modelCheck: CheckResult,
625
+ modelCheck: CheckResult | null,
603
626
  ): string {
604
627
  // Priority: first incomplete guard generates the message
605
628
  for (let i = 0; i < guards.length; i++) {
@@ -640,11 +663,15 @@ export default function sessionSupervisorExtension(pi: ExtensionAPI) {
640
663
  }
641
664
 
642
665
  // Fallback: generic continue from model check
643
- const tasks = modelCheck.incompleteTasks.map((t) => `[${t.severity}] ${t.description}`);
644
- return CONTINUE_PROMPT(
645
- modelCheck.modelResponse ?? "Model detected incomplete tasks",
646
- tasks.length > 0 ? tasks : ["Continue working"],
647
- );
666
+ if (modelCheck) {
667
+ const tasks = modelCheck.incompleteTasks.map((t) => `[${t.severity}] ${t.description}`);
668
+ return CONTINUE_PROMPT(
669
+ modelCheck.modelResponse ?? "Model detected incomplete tasks",
670
+ tasks.length > 0 ? tasks : ["Continue working"],
671
+ );
672
+ }
673
+
674
+ return CONTINUE_PROMPT("Incomplete tasks detected", ["Please continue working on remaining items."]);
648
675
  }
649
676
 
650
677
  function generateBlockMessage(