@dyyz1993/pi-coding-agent 0.74.23 → 0.74.25
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/dist/extensions/agent-permissions/index.ts +235 -0
- package/dist/extensions/ask-tools/index.ts +115 -0
- package/dist/extensions/auto-memory/contract.d.ts +51 -0
- package/dist/extensions/auto-memory/contract.d.ts.map +1 -0
- package/dist/extensions/auto-memory/contract.js +2 -0
- package/dist/extensions/auto-memory/contract.js.map +1 -0
- package/dist/extensions/auto-memory/contract.ts +56 -0
- package/dist/extensions/auto-memory/index.ts +969 -0
- package/dist/extensions/auto-memory/prompts.ts +202 -0
- package/dist/extensions/auto-memory/skip-rules.ts +297 -0
- package/dist/extensions/auto-memory/utils.ts +208 -0
- package/dist/extensions/auto-session-title/index.ts +83 -0
- package/dist/extensions/bash-ext/contract.d.ts +79 -0
- package/dist/extensions/bash-ext/contract.d.ts.map +1 -0
- package/dist/extensions/bash-ext/contract.js +2 -0
- package/dist/extensions/bash-ext/contract.js.map +1 -0
- package/dist/extensions/bash-ext/contract.ts +69 -0
- package/dist/extensions/bash-ext/index.ts +858 -0
- package/dist/extensions/claude-hooks-compat/config-loader.ts +49 -0
- package/dist/extensions/claude-hooks-compat/handler-runner.ts +377 -0
- package/dist/extensions/claude-hooks-compat/if-parser.ts +53 -0
- package/dist/extensions/claude-hooks-compat/index.ts +178 -0
- package/dist/extensions/claude-hooks-compat/matcher.ts +17 -0
- package/dist/extensions/claude-hooks-compat/stdin-builder.ts +27 -0
- package/dist/extensions/claude-hooks-compat/types.ts +77 -0
- package/dist/extensions/compaction-manager/config.ts +47 -0
- package/dist/extensions/compaction-manager/context-fold.ts +63 -0
- package/dist/extensions/compaction-manager/index.ts +151 -0
- package/dist/extensions/compaction-manager/microcompact.ts +49 -0
- package/dist/extensions/compaction-manager/reactive.ts +9 -0
- package/dist/extensions/compaction-manager/session-memory.ts +48 -0
- package/dist/extensions/coordinator/INTEGRATION.md +376 -0
- package/dist/extensions/coordinator/handler.test.ts +277 -0
- package/dist/extensions/coordinator/handler.ts +189 -0
- package/dist/extensions/coordinator/index.ts +261 -0
- package/dist/extensions/coordinator/types.d.ts +100 -0
- package/dist/extensions/coordinator/types.d.ts.map +1 -0
- package/dist/extensions/coordinator/types.js +2 -0
- package/dist/extensions/coordinator/types.js.map +1 -0
- package/dist/extensions/coordinator/types.ts +72 -0
- package/dist/extensions/file-snapshot/index.ts +131 -0
- package/dist/extensions/file-time-guard/README.md +133 -0
- package/dist/extensions/file-time-guard/config.ts +13 -0
- package/dist/extensions/file-time-guard/index.ts +171 -0
- package/dist/extensions/hooks-engine/index.ts +117 -0
- package/dist/extensions/lsp/lsp/client/file-tracker.ts +70 -0
- package/dist/extensions/lsp/lsp/client/registry.ts +305 -0
- package/dist/extensions/lsp/lsp/client/runtime.ts +832 -0
- package/dist/extensions/lsp/lsp/config/resolver.ts +573 -0
- package/dist/extensions/lsp/lsp/contract.d.ts +101 -0
- package/dist/extensions/lsp/lsp/contract.d.ts.map +1 -0
- package/dist/extensions/lsp/lsp/contract.js +2 -0
- package/dist/extensions/lsp/lsp/contract.js.map +1 -0
- package/dist/extensions/lsp/lsp/contract.ts +103 -0
- package/dist/extensions/lsp/lsp/hooks/agent-end.ts +169 -0
- package/dist/extensions/lsp/lsp/hooks/diagnostics-mode.d.ts +10 -0
- package/dist/extensions/lsp/lsp/hooks/diagnostics-mode.d.ts.map +1 -0
- package/dist/extensions/lsp/lsp/hooks/diagnostics-mode.js +30 -0
- package/dist/extensions/lsp/lsp/hooks/diagnostics-mode.js.map +1 -0
- package/dist/extensions/lsp/lsp/hooks/diagnostics-mode.ts +41 -0
- package/dist/extensions/lsp/lsp/hooks/writethrough.ts +342 -0
- package/dist/extensions/lsp/lsp/index.ts +307 -0
- package/dist/extensions/lsp/lsp/lsp.test.ts +684 -0
- package/dist/extensions/lsp/lsp/monitoring/server-metrics.ts +176 -0
- package/dist/extensions/lsp/lsp/tools/lsp-tool.ts +402 -0
- package/dist/extensions/lsp/lsp/utils/dependency-resolver.ts +147 -0
- package/dist/extensions/lsp/lsp/utils/diagnostics-wait.ts +41 -0
- package/dist/extensions/lsp/lsp/utils/lsp-helpers.d.ts +20 -0
- package/dist/extensions/lsp/lsp/utils/lsp-helpers.d.ts.map +1 -0
- package/dist/extensions/lsp/lsp/utils/lsp-helpers.js +64 -0
- package/dist/extensions/lsp/lsp/utils/lsp-helpers.js.map +1 -0
- package/dist/extensions/lsp/lsp/utils/lsp-helpers.ts +76 -0
- package/dist/extensions/message-bridge/GUIDE.md +210 -0
- package/dist/extensions/message-bridge/index.ts +222 -0
- package/dist/extensions/output-guard/index.ts +384 -0
- package/dist/extensions/preview/index.ts +278 -0
- package/dist/extensions/rules-engine/MATCH_HISTORY_RECONCILIATION.md +111 -0
- package/dist/extensions/rules-engine/RULES-ENGINE-GUIDE.md +470 -0
- package/dist/extensions/rules-engine/cache.js +232 -0
- package/dist/extensions/rules-engine/cache.ts +38 -0
- package/dist/extensions/rules-engine/config.js +63 -0
- package/dist/extensions/rules-engine/config.ts +70 -0
- package/dist/extensions/rules-engine/index.js +1530 -0
- package/dist/extensions/rules-engine/index.ts +552 -0
- package/dist/extensions/rules-engine/injector.js +68 -0
- package/dist/extensions/rules-engine/injector.ts +74 -0
- package/dist/extensions/rules-engine/loader.js +179 -0
- package/dist/extensions/rules-engine/loader.ts +205 -0
- package/dist/extensions/rules-engine/matcher.js +534 -0
- package/dist/extensions/rules-engine/matcher.ts +52 -0
- package/dist/extensions/rules-engine/types.d.ts +156 -0
- package/dist/extensions/rules-engine/types.d.ts.map +1 -0
- package/dist/extensions/rules-engine/types.js +2 -0
- package/dist/extensions/rules-engine/types.js.map +1 -0
- package/dist/extensions/rules-engine/types.ts +169 -0
- package/dist/extensions/session-supervisor/checker.ts +116 -0
- package/dist/extensions/session-supervisor/config.ts +45 -0
- package/dist/extensions/session-supervisor/index.ts +726 -0
- package/dist/extensions/session-supervisor/prompts.ts +132 -0
- package/dist/extensions/session-supervisor/scheduler.ts +69 -0
- package/dist/extensions/session-supervisor/types.ts +215 -0
- package/dist/extensions/subagent/README.md +172 -0
- package/dist/extensions/subagent/agents/explorer.md +25 -0
- package/dist/extensions/subagent/agents/guide.md +27 -0
- package/dist/extensions/subagent/agents/planner.md +37 -0
- package/dist/extensions/subagent/agents/reviewer.md +35 -0
- package/dist/extensions/subagent/agents/scout.md +50 -0
- package/dist/extensions/subagent/agents/verification.md +35 -0
- package/dist/extensions/subagent/agents/worker.md +24 -0
- package/dist/extensions/subagent/agents.ts +25 -0
- package/dist/extensions/subagent/index.ts +987 -0
- package/dist/extensions/subagent/prompts/implement-and-review.md +10 -0
- package/dist/extensions/subagent/prompts/implement.md +10 -0
- package/dist/extensions/subagent/prompts/scout-and-plan.md +9 -0
- package/dist/extensions/subagent-ext/contract.d.ts +2 -0
- package/dist/extensions/subagent-ext/contract.d.ts.map +1 -0
- package/dist/extensions/subagent-ext/contract.js +2 -0
- package/dist/extensions/subagent-ext/contract.js.map +1 -0
- package/dist/extensions/subagent-ext/contract.ts +1 -0
- package/dist/extensions/subagent-ext/index.ts +347 -0
- package/dist/extensions/subagent-shared/contract.d.ts +25 -0
- package/dist/extensions/subagent-shared/contract.d.ts.map +1 -0
- package/dist/extensions/subagent-shared/contract.js +2 -0
- package/dist/extensions/subagent-shared/contract.js.map +1 -0
- package/dist/extensions/subagent-shared/contract.ts +28 -0
- package/dist/extensions/subagent-shared/index.ts +4 -0
- package/dist/extensions/subagent-shared/render.ts +166 -0
- package/dist/extensions/subagent-shared/types.ts +35 -0
- package/dist/extensions/subagent-shared/utils.ts +112 -0
- package/dist/extensions/subagent-v2/contract.d.ts +2 -0
- package/dist/extensions/subagent-v2/contract.d.ts.map +1 -0
- package/dist/extensions/subagent-v2/contract.js +2 -0
- package/dist/extensions/subagent-v2/contract.js.map +1 -0
- package/dist/extensions/subagent-v2/contract.ts +1 -0
- package/dist/extensions/subagent-v2/index.ts +599 -0
- package/dist/extensions/todo-ext/contract.d.ts +27 -0
- package/dist/extensions/todo-ext/contract.d.ts.map +1 -0
- package/dist/extensions/todo-ext/contract.js +2 -0
- package/dist/extensions/todo-ext/contract.js.map +1 -0
- package/dist/extensions/todo-ext/contract.ts +30 -0
- package/dist/extensions/todo-ext/index.ts +419 -0
- package/package.json +3 -2
|
@@ -0,0 +1,726 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
AgentEndEvent,
|
|
4
|
+
} from "@dyyz1993/pi-coding-agent";
|
|
5
|
+
import { createTypedChannel } from "@dyyz1993/pi-coding-agent";
|
|
6
|
+
import type {
|
|
7
|
+
SupervisorChannelContract,
|
|
8
|
+
SupervisorConfig,
|
|
9
|
+
SupervisorStatus,
|
|
10
|
+
CheckResult,
|
|
11
|
+
TaskReport,
|
|
12
|
+
GuardConfig,
|
|
13
|
+
GuardCheckResult,
|
|
14
|
+
} from "./types.js";
|
|
15
|
+
import { loadConfig } from "./config.js";
|
|
16
|
+
import { checkWithSmallModel } from "./checker.js";
|
|
17
|
+
import { Scheduler } from "./scheduler.js";
|
|
18
|
+
import {
|
|
19
|
+
CONTINUE_PROMPT,
|
|
20
|
+
TODO_GUARD_PROMPT,
|
|
21
|
+
SPECS_GUARD_PROMPT,
|
|
22
|
+
SPECS_GUARD_BLOCK_MESSAGE,
|
|
23
|
+
CI_GUARD_PROMPT,
|
|
24
|
+
KEYWORD_GUARD_PROMPT,
|
|
25
|
+
CUSTOM_GUARD_PROMPT,
|
|
26
|
+
TODO_CHECK_PROMPT,
|
|
27
|
+
SPECS_CHECK_PROMPT,
|
|
28
|
+
} from "./prompts.js";
|
|
29
|
+
import { appendFileSync, readFileSync, existsSync } from "node:fs";
|
|
30
|
+
import { join } from "node:path";
|
|
31
|
+
|
|
32
|
+
const LOG_FILE = "/tmp/supervisor-debug.log";
|
|
33
|
+
function log(msg: string) {
|
|
34
|
+
const ts = new Date().toISOString();
|
|
35
|
+
const line = `[${ts}] ${msg}\n`;
|
|
36
|
+
appendFileSync(LOG_FILE, line);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export default function sessionSupervisorExtension(pi: ExtensionAPI) {
|
|
40
|
+
let config: SupervisorConfig;
|
|
41
|
+
let enabled = false;
|
|
42
|
+
let currentState: SupervisorStatus["state"] = "idle";
|
|
43
|
+
let lastCheckResult: CheckResult | undefined;
|
|
44
|
+
let schedulerInstance: Scheduler;
|
|
45
|
+
let lastTaskReports: TaskReport[] = [];
|
|
46
|
+
let specsIterationCount = 0;
|
|
47
|
+
let projectRoot = "";
|
|
48
|
+
|
|
49
|
+
// ── Flags ──
|
|
50
|
+
|
|
51
|
+
pi.registerFlag("disable-supervisor", {
|
|
52
|
+
description: "Disable session supervisor plugin",
|
|
53
|
+
type: "boolean",
|
|
54
|
+
default: false,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
pi.registerFlag("supervisor-max-continues", {
|
|
58
|
+
description: "Max auto-continue count",
|
|
59
|
+
type: "string",
|
|
60
|
+
default: "5",
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
pi.registerFlag("supervisor-model", {
|
|
64
|
+
description: "Small model for supervisor guards (fast/pro/max or model id)",
|
|
65
|
+
type: "string",
|
|
66
|
+
default: "fast",
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// ── Channel ──
|
|
70
|
+
|
|
71
|
+
const rawChannel = pi.registerChannel("supervisor");
|
|
72
|
+
const { server: channel } =
|
|
73
|
+
createTypedChannel<SupervisorChannelContract>(rawChannel);
|
|
74
|
+
|
|
75
|
+
channel.handle("supervisor.getStatus", async () => getStatus());
|
|
76
|
+
channel.handle("supervisor.requestPause", async (params) => {
|
|
77
|
+
const delayMs = params.delayMs ?? config.defaultDelayMs;
|
|
78
|
+
const result = schedulerInstance.scheduleContinue(
|
|
79
|
+
"manual-pause",
|
|
80
|
+
delayMs,
|
|
81
|
+
() => {
|
|
82
|
+
currentState = "continuing";
|
|
83
|
+
emitStatusChanged();
|
|
84
|
+
triggerContinue("Manual pause completed, resuming");
|
|
85
|
+
},
|
|
86
|
+
);
|
|
87
|
+
if (result.scheduled) {
|
|
88
|
+
channel.emit("supervisor.pauseRequested", { delayMs, reason: params.reason });
|
|
89
|
+
}
|
|
90
|
+
return result;
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
channel.handle("supervisor.cancelPause", async () => {
|
|
94
|
+
const cancelled = schedulerInstance.cancelTimer("manual-pause");
|
|
95
|
+
if (cancelled) {
|
|
96
|
+
channel.emit("supervisor.pauseCancelled", { reason: "Cancelled via channel" });
|
|
97
|
+
}
|
|
98
|
+
return { cancelled };
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
channel.handle("supervisor.forceContinue", async (params) => {
|
|
102
|
+
schedulerInstance.cancelAll();
|
|
103
|
+
currentState = "continuing";
|
|
104
|
+
emitStatusChanged();
|
|
105
|
+
triggerContinue(params.reason ?? "Force continue via channel");
|
|
106
|
+
return { triggered: true };
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
channel.handle("supervisor.disable", async () => {
|
|
110
|
+
enabled = false;
|
|
111
|
+
schedulerInstance.cancelAll();
|
|
112
|
+
currentState = "disabled";
|
|
113
|
+
emitStatusChanged();
|
|
114
|
+
return { disabled: true };
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
channel.handle("supervisor.enable", async () => {
|
|
118
|
+
enabled = true;
|
|
119
|
+
currentState = "idle";
|
|
120
|
+
emitStatusChanged();
|
|
121
|
+
return { enabled: true };
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
channel.handle("supervisor.getTaskReport", async () => ({ tasks: lastTaskReports }));
|
|
125
|
+
|
|
126
|
+
channel.handle("supervisor.checkToolStatus", async (params) => {
|
|
127
|
+
const targetChannelName = params.channelName ?? params.toolName;
|
|
128
|
+
try {
|
|
129
|
+
const result = await rawChannel.call(
|
|
130
|
+
`${targetChannelName}.getStatus`,
|
|
131
|
+
{},
|
|
132
|
+
5000,
|
|
133
|
+
);
|
|
134
|
+
return { reachable: true, status: JSON.stringify(result) };
|
|
135
|
+
} catch (err) {
|
|
136
|
+
return {
|
|
137
|
+
reachable: false,
|
|
138
|
+
error: err instanceof Error ? err.message : String(err),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// ── Tool: supervisor_complete ──
|
|
144
|
+
// LLM calls this to declare completion. Guards can reject it.
|
|
145
|
+
|
|
146
|
+
pi.registerTool({
|
|
147
|
+
name: "supervisor_complete",
|
|
148
|
+
label: "Supervisor Complete",
|
|
149
|
+
description: "Declare that the current task is complete. The supervisor will verify with active guards before allowing the session to end.",
|
|
150
|
+
parameters: {
|
|
151
|
+
type: "object",
|
|
152
|
+
properties: {
|
|
153
|
+
summary: {
|
|
154
|
+
type: "string",
|
|
155
|
+
description: "Brief summary of what was accomplished",
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
required: ["summary"],
|
|
159
|
+
},
|
|
160
|
+
execute: async (_toolCallId, params, _signal, _onUpdate, _ctx) => {
|
|
161
|
+
const summary = String(params.summary ?? "");
|
|
162
|
+
|
|
163
|
+
if (!enabled) {
|
|
164
|
+
return {
|
|
165
|
+
content: [{ type: "text" as const, text: "Supervisor complete: approved (supervisor disabled)" }],
|
|
166
|
+
details: { approved: true, reason: "Supervisor is disabled" },
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const activeGuards = getActiveGuards();
|
|
171
|
+
if (activeGuards.length === 0) {
|
|
172
|
+
return {
|
|
173
|
+
content: [{ type: "text" as const, text: "Supervisor complete: approved (no active guards)" }],
|
|
174
|
+
details: { approved: true, reason: "No active guards" },
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
for (const guard of activeGuards) {
|
|
179
|
+
const result = await runGuardCheck(guard, summary);
|
|
180
|
+
|
|
181
|
+
if (!result.completed && result.remainingItems.length > 0) {
|
|
182
|
+
const blockMsg = generateBlockMessage(guard, result);
|
|
183
|
+
log(`supervisor_complete BLOCKED by ${guard.name}: ${result.remainingItems.join(", ")}`);
|
|
184
|
+
return {
|
|
185
|
+
content: [{ type: "text" as const, text: blockMsg }],
|
|
186
|
+
details: {
|
|
187
|
+
approved: false,
|
|
188
|
+
blockedBy: guard.name,
|
|
189
|
+
remainingItems: result.remainingItems,
|
|
190
|
+
},
|
|
191
|
+
terminate: false,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
content: [{ type: "text" as const, text: "Supervisor complete: approved — all guards passed." }],
|
|
198
|
+
details: { approved: true, reason: "All guards passed" },
|
|
199
|
+
};
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// ── Session lifecycle ──
|
|
204
|
+
|
|
205
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
206
|
+
config = loadConfig(ctx.sessionDataDir, ctx.projectDataDir);
|
|
207
|
+
enabled = config.enable;
|
|
208
|
+
specsIterationCount = 0;
|
|
209
|
+
|
|
210
|
+
log(`session_start: enabled=${enabled}, guards=${config.guards.length}, smallModel=${config.smallModel}`);
|
|
211
|
+
|
|
212
|
+
if (pi.getFlag("disable-supervisor") === true) {
|
|
213
|
+
enabled = false;
|
|
214
|
+
}
|
|
215
|
+
const maxContinuesFlag = pi.getFlag("supervisor-max-continues");
|
|
216
|
+
if (typeof maxContinuesFlag === "string") {
|
|
217
|
+
const n = parseInt(maxContinuesFlag, 10);
|
|
218
|
+
if (!isNaN(n)) config.maxContinueCount = n;
|
|
219
|
+
}
|
|
220
|
+
const modelFlag = pi.getFlag("supervisor-model");
|
|
221
|
+
if (typeof modelFlag === "string" && modelFlag) {
|
|
222
|
+
config.smallModel = modelFlag;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Determine project root for specs file resolution
|
|
226
|
+
projectRoot = ctx.projectDataDir ?? process.cwd();
|
|
227
|
+
|
|
228
|
+
schedulerInstance = new Scheduler(
|
|
229
|
+
config.maxContinueCount,
|
|
230
|
+
config.pauseThresholdMs,
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
currentState = enabled ? "idle" : "disabled";
|
|
234
|
+
emitStatusChanged();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// ── agent_end: Guard loop ──
|
|
238
|
+
|
|
239
|
+
pi.on("agent_end", async (event: AgentEndEvent, ctx) => {
|
|
240
|
+
log(`agent_end: enabled=${enabled}, checkOnAgentEnd=${config.checkOnAgentEnd}`);
|
|
241
|
+
if (!enabled || !config.checkOnAgentEnd) return;
|
|
242
|
+
if (pi.getFlag("disable-supervisor") === true) return;
|
|
243
|
+
if (schedulerInstance.isExhausted()) {
|
|
244
|
+
log(`agent_end: scheduler exhausted (${schedulerInstance.getContinueCount()}/${config.maxContinueCount})`);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
currentState = "checking";
|
|
249
|
+
emitStatusChanged();
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
const lastAssistantText = extractLastAssistantText(
|
|
253
|
+
event.messages as Array<{ role: string; content: unknown }>,
|
|
254
|
+
);
|
|
255
|
+
log(`lastAssistantText (200ch): ${lastAssistantText.slice(0, 200)}`);
|
|
256
|
+
|
|
257
|
+
const activeGuards = getActiveGuards();
|
|
258
|
+
log(`activeGuards: [${activeGuards.map((g) => `${g.name}(${g.type})`).join(", ")}]`);
|
|
259
|
+
|
|
260
|
+
// Phase 1: Run all guard checks
|
|
261
|
+
const guardResults: GuardCheckResult[] = [];
|
|
262
|
+
const reports: TaskReport[] = [];
|
|
263
|
+
|
|
264
|
+
for (const guard of activeGuards) {
|
|
265
|
+
const result = await runGuardCheck(guard, lastAssistantText);
|
|
266
|
+
guardResults.push(result);
|
|
267
|
+
|
|
268
|
+
reports.push({
|
|
269
|
+
guardName: guard.name,
|
|
270
|
+
guardType: guard.type,
|
|
271
|
+
status: result.completed ? "completed" : "incomplete",
|
|
272
|
+
details: result.detail,
|
|
273
|
+
remainingItems: result.remainingItems,
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
log(`guard[${guard.name}] completed=${result.completed}, remaining=${result.remainingItems.length}`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Also run the generic model-based check as fallback (if no custom guards or as additional check)
|
|
280
|
+
const modelCheck = await checkWithSmallModel(
|
|
281
|
+
event.messages as Array<{ role: string; content: unknown }>,
|
|
282
|
+
config,
|
|
283
|
+
pi.callLLM.bind(pi),
|
|
284
|
+
ctx.sessionSignal,
|
|
285
|
+
);
|
|
286
|
+
|
|
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
|
+
const hasModelIncomplete = modelCheck.completed === false || modelCheck.incompleteTasks.length > 0;
|
|
293
|
+
|
|
294
|
+
if (!hasIncompleteGuards && !hasModelIncomplete) {
|
|
295
|
+
log(`All guards passed + model check passed → idle`);
|
|
296
|
+
currentState = "idle";
|
|
297
|
+
lastCheckResult = { ...modelCheck, guardResults };
|
|
298
|
+
emitStatusChanged();
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Phase 3: Generate continue message from first incomplete guard
|
|
303
|
+
log(`Incomplete tasks detected, scheduling continue...`);
|
|
304
|
+
specsIterationCount++;
|
|
305
|
+
|
|
306
|
+
const continueMessage = generateContinueMessage(
|
|
307
|
+
activeGuards,
|
|
308
|
+
guardResults,
|
|
309
|
+
modelCheck,
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
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
|
+
});
|
|
348
|
+
} catch (err) {
|
|
349
|
+
log(`agent_end error: ${err instanceof Error ? err.message : String(err)}`);
|
|
350
|
+
currentState = "idle";
|
|
351
|
+
emitStatusChanged();
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
pi.on("session_shutdown", async () => {
|
|
356
|
+
schedulerInstance?.cancelAll();
|
|
357
|
+
currentState = "idle";
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
// ── Guard Check Functions ──
|
|
361
|
+
|
|
362
|
+
function getActiveGuards(): GuardConfig[] {
|
|
363
|
+
return (config.guards ?? []).filter((g) => g.enable !== false);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async function runGuardCheck(
|
|
367
|
+
guard: GuardConfig,
|
|
368
|
+
context: string,
|
|
369
|
+
): Promise<GuardCheckResult> {
|
|
370
|
+
const base: GuardCheckResult = {
|
|
371
|
+
guardName: guard.name,
|
|
372
|
+
completed: true,
|
|
373
|
+
confidence: 1,
|
|
374
|
+
remainingItems: [],
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
try {
|
|
378
|
+
switch (guard.type) {
|
|
379
|
+
case "todo":
|
|
380
|
+
return await checkTodoGuard(guard, context);
|
|
381
|
+
case "specs":
|
|
382
|
+
return await checkSpecsGuard(guard, context);
|
|
383
|
+
case "ci":
|
|
384
|
+
return await checkCiGuard(guard, context);
|
|
385
|
+
case "keyword":
|
|
386
|
+
return checkKeywordGuard(guard, context);
|
|
387
|
+
case "custom":
|
|
388
|
+
return await checkCustomGuard(guard, context);
|
|
389
|
+
default:
|
|
390
|
+
return base;
|
|
391
|
+
}
|
|
392
|
+
} catch (err) {
|
|
393
|
+
return {
|
|
394
|
+
...base,
|
|
395
|
+
completed: false,
|
|
396
|
+
confidence: 0,
|
|
397
|
+
detail: `Guard check error: ${err instanceof Error ? err.message : String(err)}`,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
async function checkTodoGuard(
|
|
403
|
+
_guard: Extract<GuardConfig, { type: "todo" }>,
|
|
404
|
+
_context: string,
|
|
405
|
+
): Promise<GuardCheckResult> {
|
|
406
|
+
try {
|
|
407
|
+
const todoResult = await rawChannel.call("todo.list", {}, 5000);
|
|
408
|
+
const todos = Array.isArray(todoResult) ? todoResult : [];
|
|
409
|
+
|
|
410
|
+
const incomplete = todos.filter(
|
|
411
|
+
(t: Record<string, unknown>) =>
|
|
412
|
+
t.status !== "completed" && t.status !== "done",
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
return {
|
|
416
|
+
guardName: _guard.name,
|
|
417
|
+
completed: incomplete.length === 0,
|
|
418
|
+
confidence: incomplete.length === 0 ? 1 : 0.8,
|
|
419
|
+
remainingItems: incomplete.map(
|
|
420
|
+
(t: Record<string, unknown>) => String(t.content ?? t.text ?? "unknown todo"),
|
|
421
|
+
),
|
|
422
|
+
detail: `${incomplete.length}/${todos.length} todos remaining`,
|
|
423
|
+
};
|
|
424
|
+
} catch (err) {
|
|
425
|
+
log(`todo guard: channel call failed - ${err instanceof Error ? err.message : String(err)}`);
|
|
426
|
+
return {
|
|
427
|
+
guardName: _guard.name,
|
|
428
|
+
completed: true,
|
|
429
|
+
confidence: 0.5,
|
|
430
|
+
remainingItems: [],
|
|
431
|
+
detail: "Todo channel not available, skipping",
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
async function checkSpecsGuard(
|
|
437
|
+
guard: Extract<GuardConfig, { type: "specs" }>,
|
|
438
|
+
context: string,
|
|
439
|
+
): Promise<GuardCheckResult> {
|
|
440
|
+
const specsPath = join(projectRoot, guard.specsFile);
|
|
441
|
+
|
|
442
|
+
if (!existsSync(specsPath)) {
|
|
443
|
+
return {
|
|
444
|
+
guardName: guard.name,
|
|
445
|
+
completed: true,
|
|
446
|
+
confidence: 0.5,
|
|
447
|
+
remainingItems: [],
|
|
448
|
+
detail: `Specs file not found: ${guard.specsFile}`,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const specsContent = readFileSync(specsPath, "utf-8");
|
|
453
|
+
const specItems = parseSpecItems(specsContent);
|
|
454
|
+
|
|
455
|
+
if (specItems.length === 0) {
|
|
456
|
+
return {
|
|
457
|
+
guardName: guard.name,
|
|
458
|
+
completed: true,
|
|
459
|
+
confidence: 1,
|
|
460
|
+
remainingItems: [],
|
|
461
|
+
detail: "No spec items found",
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Use model to check which items are done
|
|
466
|
+
const response = await pi.callLLM({
|
|
467
|
+
systemPrompt: SPECS_CHECK_PROMPT(specsContent, context),
|
|
468
|
+
messages: [{ role: "user", content: "Check completion status" }],
|
|
469
|
+
model: config.smallModel,
|
|
470
|
+
maxTokens: 1024,
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
try {
|
|
474
|
+
const jsonStr = response
|
|
475
|
+
.replace(/^```(?:json)?\s*\n?/m, "")
|
|
476
|
+
.replace(/\n?```\s*$/m, "")
|
|
477
|
+
.trim();
|
|
478
|
+
const parsed = JSON.parse(jsonStr) as {
|
|
479
|
+
completed: boolean;
|
|
480
|
+
remainingItems: string[];
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
// Check iteration limit
|
|
484
|
+
if (guard.maxIterations > 0 && specsIterationCount >= guard.maxIterations) {
|
|
485
|
+
return {
|
|
486
|
+
guardName: guard.name,
|
|
487
|
+
completed: true,
|
|
488
|
+
confidence: 1,
|
|
489
|
+
remainingItems: [],
|
|
490
|
+
detail: `Max iterations (${guard.maxIterations}) reached`,
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return {
|
|
495
|
+
guardName: guard.name,
|
|
496
|
+
completed: parsed.remainingItems.length === 0,
|
|
497
|
+
confidence: parsed.remainingItems.length === 0 ? 1 : 0.9,
|
|
498
|
+
remainingItems: parsed.remainingItems,
|
|
499
|
+
detail: `${specItems.length - parsed.remainingItems.length}/${specItems.length} spec items done`,
|
|
500
|
+
};
|
|
501
|
+
} catch {
|
|
502
|
+
return {
|
|
503
|
+
guardName: guard.name,
|
|
504
|
+
completed: false,
|
|
505
|
+
confidence: 0.3,
|
|
506
|
+
remainingItems: specItems,
|
|
507
|
+
detail: "Failed to parse specs check response",
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
async function checkCiGuard(
|
|
513
|
+
guard: Extract<GuardConfig, { type: "ci" }>,
|
|
514
|
+
_context: string,
|
|
515
|
+
): Promise<GuardCheckResult> {
|
|
516
|
+
if (!guard.checkCommand) {
|
|
517
|
+
return {
|
|
518
|
+
guardName: guard.name,
|
|
519
|
+
completed: true,
|
|
520
|
+
confidence: 1,
|
|
521
|
+
remainingItems: [],
|
|
522
|
+
detail: "No CI check command configured",
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// CI guard is intentionally simple — run command, check exit code
|
|
527
|
+
// For now, just report unknown. Full implementation would use pi.runCommand
|
|
528
|
+
return {
|
|
529
|
+
guardName: guard.name,
|
|
530
|
+
completed: true,
|
|
531
|
+
confidence: 0.5,
|
|
532
|
+
remainingItems: [],
|
|
533
|
+
detail: `CI check: ${guard.checkCommand} (not yet executed)`,
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function checkKeywordGuard(
|
|
538
|
+
guard: Extract<GuardConfig, { type: "keyword" }>,
|
|
539
|
+
context: string,
|
|
540
|
+
): GuardCheckResult {
|
|
541
|
+
const found = guard.keywords.filter((kw) =>
|
|
542
|
+
context.toLowerCase().includes(kw.toLowerCase()),
|
|
543
|
+
);
|
|
544
|
+
|
|
545
|
+
return {
|
|
546
|
+
guardName: guard.name,
|
|
547
|
+
completed: found.length === 0,
|
|
548
|
+
confidence: found.length === 0 ? 1 : 0.7,
|
|
549
|
+
remainingItems: found.length > 0
|
|
550
|
+
? [`Keywords found indicating incomplete work: ${found.join(", ")}`]
|
|
551
|
+
: [],
|
|
552
|
+
detail: found.length > 0
|
|
553
|
+
? `Found: ${found.join(", ")}`
|
|
554
|
+
: "No incomplete keywords",
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
async function checkCustomGuard(
|
|
559
|
+
guard: Extract<GuardConfig, { type: "custom" }>,
|
|
560
|
+
context: string,
|
|
561
|
+
): Promise<GuardCheckResult> {
|
|
562
|
+
const response = await pi.callLLM({
|
|
563
|
+
systemPrompt: guard.checkPrompt,
|
|
564
|
+
messages: [{ role: "user", content: context.slice(0, 2000) }],
|
|
565
|
+
model: config.smallModel,
|
|
566
|
+
maxTokens: 512,
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
try {
|
|
570
|
+
const jsonStr = response
|
|
571
|
+
.replace(/^```(?:json)?\s*\n?/m, "")
|
|
572
|
+
.replace(/\n?```\s*$/m, "")
|
|
573
|
+
.trim();
|
|
574
|
+
const parsed = JSON.parse(jsonStr) as {
|
|
575
|
+
completed: boolean;
|
|
576
|
+
remainingItems?: string[];
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
return {
|
|
580
|
+
guardName: guard.name,
|
|
581
|
+
completed: parsed.completed,
|
|
582
|
+
confidence: parsed.completed ? 0.8 : 0.7,
|
|
583
|
+
remainingItems: parsed.remainingItems ?? [],
|
|
584
|
+
detail: response.slice(0, 200),
|
|
585
|
+
};
|
|
586
|
+
} catch {
|
|
587
|
+
return {
|
|
588
|
+
guardName: guard.name,
|
|
589
|
+
completed: true,
|
|
590
|
+
confidence: 0.3,
|
|
591
|
+
remainingItems: [],
|
|
592
|
+
detail: "Failed to parse custom guard response, assuming complete",
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// ── Message Generation ──
|
|
598
|
+
|
|
599
|
+
function generateContinueMessage(
|
|
600
|
+
guards: GuardConfig[],
|
|
601
|
+
results: GuardCheckResult[],
|
|
602
|
+
modelCheck: CheckResult,
|
|
603
|
+
): string {
|
|
604
|
+
// Priority: first incomplete guard generates the message
|
|
605
|
+
for (let i = 0; i < guards.length; i++) {
|
|
606
|
+
const guard = guards[i];
|
|
607
|
+
const result = results[i];
|
|
608
|
+
if (result.completed || result.remainingItems.length === 0) continue;
|
|
609
|
+
|
|
610
|
+
switch (guard.type) {
|
|
611
|
+
case "todo":
|
|
612
|
+
return TODO_GUARD_PROMPT(result.remainingItems);
|
|
613
|
+
case "specs": {
|
|
614
|
+
const specsGuard = guard as Extract<GuardConfig, { type: "specs" }>;
|
|
615
|
+
const completedItems = results[i].detail?.match(/(\d+)\/(\d+)/);
|
|
616
|
+
const done = completedItems ? completedItems[1] : "?";
|
|
617
|
+
const total = completedItems ? completedItems[2] : "?";
|
|
618
|
+
return SPECS_GUARD_PROMPT(
|
|
619
|
+
specsGuard.specsFile,
|
|
620
|
+
[],
|
|
621
|
+
result.remainingItems,
|
|
622
|
+
`${done}/${total}`,
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
case "ci":
|
|
626
|
+
return CI_GUARD_PROMPT(result.detail ?? "unknown", (guard as Extract<GuardConfig, { type: "ci" }>).checkCommand);
|
|
627
|
+
case "keyword":
|
|
628
|
+
return KEYWORD_GUARD_PROMPT(result.remainingItems, result.detail ?? "");
|
|
629
|
+
case "custom": {
|
|
630
|
+
const customGuard = guard as Extract<GuardConfig, { type: "custom" }>;
|
|
631
|
+
if (customGuard.continuePromptTemplate) {
|
|
632
|
+
return CUSTOM_GUARD_PROMPT(customGuard.continuePromptTemplate, {
|
|
633
|
+
remainingItems: result.remainingItems.join("\n"),
|
|
634
|
+
detail: result.detail ?? "",
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
break;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// 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
|
+
);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function generateBlockMessage(
|
|
651
|
+
guard: GuardConfig,
|
|
652
|
+
result: GuardCheckResult,
|
|
653
|
+
): string {
|
|
654
|
+
if (guard.type === "specs") {
|
|
655
|
+
return SPECS_GUARD_BLOCK_MESSAGE(result.remainingItems, specsIterationCount);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
return `[Supervisor/${guard.name}] Completion rejected. Remaining items:\n${result.remainingItems.map((t, i) => `${i + 1}. ${t}`).join("\n")}\n\nPlease continue working on these items.`;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// ── Helpers ──
|
|
662
|
+
|
|
663
|
+
function parseSpecItems(specsContent: string): string[] {
|
|
664
|
+
const items: string[] = [];
|
|
665
|
+
const lines = specsContent.split("\n");
|
|
666
|
+
for (const line of lines) {
|
|
667
|
+
const trimmed = line.trim();
|
|
668
|
+
if (/^[-*]\s/.test(trimmed) || /^\d+\.\s/.test(trimmed)) {
|
|
669
|
+
items.push(trimmed.replace(/^[-*\d.]\s*/, ""));
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
return items;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function getStatus(): SupervisorStatus {
|
|
676
|
+
return {
|
|
677
|
+
enabled,
|
|
678
|
+
state: currentState,
|
|
679
|
+
continueCount: schedulerInstance?.getContinueCount() ?? 0,
|
|
680
|
+
maxContinueCount: config?.maxContinueCount ?? 0,
|
|
681
|
+
activeGuards: getActiveGuards().map((g) => g.name),
|
|
682
|
+
lastCheckResult,
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function emitStatusChanged(): void {
|
|
687
|
+
channel.emit("supervisor.statusChanged", getStatus());
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function triggerContinue(reason: string): void {
|
|
691
|
+
log(`triggerContinue: ${reason}`);
|
|
692
|
+
const tasks =
|
|
693
|
+
lastCheckResult?.incompleteTasks?.map((t) => t.description) ?? [];
|
|
694
|
+
const prompt = CONTINUE_PROMPT(
|
|
695
|
+
reason,
|
|
696
|
+
tasks.length > 0 ? tasks : [reason],
|
|
697
|
+
);
|
|
698
|
+
|
|
699
|
+
pi.sendMessage(
|
|
700
|
+
{
|
|
701
|
+
customType: "supervisor_continue",
|
|
702
|
+
content: prompt,
|
|
703
|
+
display: true,
|
|
704
|
+
},
|
|
705
|
+
{ triggerTurn: true },
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function extractLastAssistantText(
|
|
710
|
+
messages: Array<{ role: string; content: unknown }>,
|
|
711
|
+
): string {
|
|
712
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
713
|
+
const msg = messages[i];
|
|
714
|
+
if (msg.role === "assistant") {
|
|
715
|
+
if (typeof msg.content === "string") return msg.content;
|
|
716
|
+
if (Array.isArray(msg.content)) {
|
|
717
|
+
return (msg.content as Array<{ type: string; text?: string }>)
|
|
718
|
+
.filter((p) => p.type === "text")
|
|
719
|
+
.map((p) => p.text ?? "")
|
|
720
|
+
.join("\n");
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
return "";
|
|
725
|
+
}
|
|
726
|
+
}
|