lazyopencode-core 0.0.1
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/ATTRIBUTION.md +38 -0
- package/LICENSE +21 -0
- package/README.md +357 -0
- package/dist/agents/councillor.d.ts +1 -0
- package/dist/agents/councillor.js +14 -0
- package/dist/agents/designer.d.ts +1 -0
- package/dist/agents/designer.js +31 -0
- package/dist/agents/explorer.d.ts +1 -0
- package/dist/agents/explorer.js +15 -0
- package/dist/agents/fixer.d.ts +1 -0
- package/dist/agents/fixer.js +23 -0
- package/dist/agents/index.d.ts +2 -0
- package/dist/agents/index.js +55 -0
- package/dist/agents/lazy.d.ts +1 -0
- package/dist/agents/lazy.js +3 -0
- package/dist/agents/librarian.d.ts +1 -0
- package/dist/agents/librarian.js +26 -0
- package/dist/agents/observer.d.ts +1 -0
- package/dist/agents/observer.js +20 -0
- package/dist/agents/oracle.d.ts +1 -0
- package/dist/agents/oracle.js +30 -0
- package/dist/council/council-manager.d.ts +42 -0
- package/dist/council/council-manager.js +223 -0
- package/dist/council/index.d.ts +2 -0
- package/dist/council/index.js +1 -0
- package/dist/hooks/apply-patch-rescue.d.ts +7 -0
- package/dist/hooks/apply-patch-rescue.js +150 -0
- package/dist/hooks/background-job-board.d.ts +92 -0
- package/dist/hooks/background-job-board.js +452 -0
- package/dist/hooks/chat-params.d.ts +16 -0
- package/dist/hooks/chat-params.js +30 -0
- package/dist/hooks/deepwork.d.ts +9 -0
- package/dist/hooks/deepwork.js +55 -0
- package/dist/hooks/error-recovery.d.ts +21 -0
- package/dist/hooks/error-recovery.js +216 -0
- package/dist/hooks/index.d.ts +3 -0
- package/dist/hooks/index.js +61 -0
- package/dist/hooks/lazy-command.d.ts +16 -0
- package/dist/hooks/lazy-command.js +178 -0
- package/dist/hooks/messages-transform.d.ts +40 -0
- package/dist/hooks/messages-transform.js +358 -0
- package/dist/hooks/permission-guard.d.ts +5 -0
- package/dist/hooks/permission-guard.js +38 -0
- package/dist/hooks/runtime.d.ts +169 -0
- package/dist/hooks/runtime.js +653 -0
- package/dist/hooks/session-events.d.ts +16 -0
- package/dist/hooks/session-events.js +65 -0
- package/dist/hooks/system-transform.d.ts +8 -0
- package/dist/hooks/system-transform.js +113 -0
- package/dist/hooks/task-session.d.ts +32 -0
- package/dist/hooks/task-session.js +177 -0
- package/dist/hooks/workflow-classifier.d.ts +17 -0
- package/dist/hooks/workflow-classifier.js +170 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +85 -0
- package/dist/opencode-control-plane.d.ts +20 -0
- package/dist/opencode-control-plane.js +95 -0
- package/dist/ponytail.d.ts +1 -0
- package/dist/ponytail.js +33 -0
- package/dist/skills/index.d.ts +5 -0
- package/dist/skills/index.js +10 -0
- package/dist/skills/lazy/build/SKILL.md +62 -0
- package/dist/skills/lazy/debug/SKILL.md +17 -0
- package/dist/skills/lazy/grill/SKILL.md +54 -0
- package/dist/skills/lazy/plan/SKILL.md +52 -0
- package/dist/skills/lazy/review/SKILL.md +29 -0
- package/dist/skills/lazy/security/SKILL.md +29 -0
- package/dist/skills/lazy/simplify/SKILL.md +52 -0
- package/dist/skills/lazy/specify/SKILL.md +62 -0
- package/dist/skills/lazy/worktree/SKILL.md +66 -0
- package/dist/tools/cancel-task.d.ts +3 -0
- package/dist/tools/cancel-task.js +37 -0
- package/dist/tools/council.d.ts +6 -0
- package/dist/tools/council.js +41 -0
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.js +2 -0
- package/dist/v2.d.ts +1 -0
- package/dist/v2.js +42 -0
- package/docs/architecture.md +47 -0
- package/docs/council.md +200 -0
- package/docs/desktop-distribution.md +36 -0
- package/docs/opencode-integration.md +54 -0
- package/docs/positioning.md +44 -0
- package/docs/product-audit.md +187 -0
- package/docs/product-plan.md +56 -0
- package/docs/state-machine.md +35 -0
- package/docs/user-manual.md +439 -0
- package/docs/work-plan.md +190 -0
- package/package.json +44 -0
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context pruning + image redirect + job board + workflow gate via messages.transform.
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* 1. Enhanced pruning: keep system + last N user turns + everything between
|
|
6
|
+
* 2. Image processing (strip images, inject @lazy-observer redirect)
|
|
7
|
+
* 3. Job board injection for lazy primary (terminal unreconciled jobs only)
|
|
8
|
+
* 4. Workflow gate: detect skipped steps, inject STOP message
|
|
9
|
+
* 5. Skill filtering per-agent
|
|
10
|
+
*
|
|
11
|
+
* ponytail: Don't remind — gate. Only inject when a condition is met.
|
|
12
|
+
*/
|
|
13
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
14
|
+
import { jobBoard } from "./background-job-board.js";
|
|
15
|
+
import { classifyWorkflow, formatWorkflowDecision } from "./workflow-classifier.js";
|
|
16
|
+
const DEFAULT_MAX_MESSAGES = 80;
|
|
17
|
+
// Agent → skills allowed. Empty = all allowed.
|
|
18
|
+
// ponytail: hardcoded. Upgrade: load from config.
|
|
19
|
+
const AGENT_SKILL_ALLOWLIST = {
|
|
20
|
+
lazy: new Set([
|
|
21
|
+
"lazy/grill",
|
|
22
|
+
"lazy/specify",
|
|
23
|
+
"lazy/plan",
|
|
24
|
+
"lazy/build",
|
|
25
|
+
"lazy/review",
|
|
26
|
+
"lazy/debug",
|
|
27
|
+
"lazy/simplify",
|
|
28
|
+
"lazy/worktree",
|
|
29
|
+
]),
|
|
30
|
+
// All others: unrestricted by default
|
|
31
|
+
};
|
|
32
|
+
// ponytail: hardcoded. Upgrade: load from agent config.
|
|
33
|
+
const _AGENT_DESCRIPTIONS = {
|
|
34
|
+
lazy: "Runtime coordinator: classify → gate → delegate → track → close. Ponytail-first.",
|
|
35
|
+
"lazy-explorer": "Fast codebase recon: glob, grep, AST search.",
|
|
36
|
+
"lazy-oracle": "Architecture, risk, debugging, simplification review.",
|
|
37
|
+
"lazy-fixer": "Mechanical code: stdlib first, one line over fifty.",
|
|
38
|
+
"lazy-librarian": "External docs, API references, web research.",
|
|
39
|
+
"lazy-designer": "UI/UX design, visual polish, responsive layouts.",
|
|
40
|
+
"lazy-observer": "Visual analysis of images, screenshots, PDFs.",
|
|
41
|
+
};
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Workflow gate detection (ponytail: hook-detected, not prompt-based)
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
const BUILD_KEYWORDS = [
|
|
46
|
+
"implement",
|
|
47
|
+
"build",
|
|
48
|
+
"code",
|
|
49
|
+
"write",
|
|
50
|
+
"add",
|
|
51
|
+
"fix",
|
|
52
|
+
"change",
|
|
53
|
+
];
|
|
54
|
+
const PRD_INDICATORS = [
|
|
55
|
+
"PRD",
|
|
56
|
+
"spec",
|
|
57
|
+
"requirement",
|
|
58
|
+
"issue #",
|
|
59
|
+
"grill",
|
|
60
|
+
"specify",
|
|
61
|
+
];
|
|
62
|
+
const SPECIFY_KEYWORDS = [
|
|
63
|
+
"specify",
|
|
64
|
+
"write a spec",
|
|
65
|
+
"create a PRD",
|
|
66
|
+
];
|
|
67
|
+
const GRILL_INDICATORS = [
|
|
68
|
+
"grill",
|
|
69
|
+
"what does success look like",
|
|
70
|
+
"what must not break",
|
|
71
|
+
"alignment",
|
|
72
|
+
];
|
|
73
|
+
const REVIEW_KEYWORDS = [
|
|
74
|
+
"review this",
|
|
75
|
+
"code review",
|
|
76
|
+
"review my",
|
|
77
|
+
"review the",
|
|
78
|
+
];
|
|
79
|
+
const DEBUG_KEYWORDS = [
|
|
80
|
+
"debug this",
|
|
81
|
+
"debugging",
|
|
82
|
+
"diagnose",
|
|
83
|
+
];
|
|
84
|
+
function detectWorkflowSkip(msgs) {
|
|
85
|
+
const userTexts = msgs
|
|
86
|
+
.filter((m) => m.info.role === "user")
|
|
87
|
+
.map((m) => m.parts.filter((p) => p.type === "text" && p.text).map((p) => p.text).join(" "))
|
|
88
|
+
.filter(Boolean);
|
|
89
|
+
// Last 5 user messages for trigger, last 10 for context
|
|
90
|
+
const recent5 = userTexts.slice(-5).join(" ");
|
|
91
|
+
const recent10 = userTexts.slice(-10).join(" ");
|
|
92
|
+
// BUILD without PRD → "grill first"
|
|
93
|
+
const hasBuild = BUILD_KEYWORDS.some((kw) => new RegExp(`\\b${kw}\\b`, "i").test(recent5));
|
|
94
|
+
if (hasBuild) {
|
|
95
|
+
const hasPrd = PRD_INDICATORS.some((kw) => new RegExp(kw, "i").test(recent10));
|
|
96
|
+
if (!hasPrd) {
|
|
97
|
+
return "STOP. No alignment (grill) done yet. What does success look like? What must not break?";
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// SPECIFY without GRILL → "align first"
|
|
101
|
+
const hasSpecify = SPECIFY_KEYWORDS.some((kw) => new RegExp(kw, "i").test(recent5));
|
|
102
|
+
if (hasSpecify) {
|
|
103
|
+
const hasGrill = GRILL_INDICATORS.some((kw) => new RegExp(kw, "i").test(recent10));
|
|
104
|
+
if (!hasGrill) {
|
|
105
|
+
return "STOP. No alignment done yet. Run lazy/grill first.";
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// REVIEW without BUILD → "build first"
|
|
109
|
+
const hasReview = REVIEW_KEYWORDS.some((kw) => new RegExp(kw, "i").test(recent5));
|
|
110
|
+
if (hasReview) {
|
|
111
|
+
const hasBuildRecent = BUILD_KEYWORDS.some((kw) => new RegExp(`\\b${kw}\\b`, "i").test(recent10));
|
|
112
|
+
const hasToolResults = msgs.filter((m) => m.info.role === "tool_result").length > 2;
|
|
113
|
+
if (!hasBuildRecent && !hasToolResults) {
|
|
114
|
+
return "STOP. Nothing to review yet. Build something first.";
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// DEBUG without repro info → "gather repro first"
|
|
118
|
+
const hasDebug = DEBUG_KEYWORDS.some((kw) => new RegExp(`\\b${kw}\\b`, "i").test(recent5));
|
|
119
|
+
if (hasDebug) {
|
|
120
|
+
const hasRepro = /repro|steps? to reproduce|logs?|error message|stack trace|expected.*actual|input.*output/i
|
|
121
|
+
.test(recent10);
|
|
122
|
+
if (!hasRepro) {
|
|
123
|
+
return "STOP. Debugging without repro info. Gather: exact steps, logs, error message, or minimal test case.";
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// Image processing (ponytail: strip images, inject @lazy-observer redirect)
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
/**
|
|
132
|
+
* Detect image/file parts that need redirecting to @lazy-observer.
|
|
133
|
+
* ponytail: match slim's isImagePart but simpler — no MIME map needed.
|
|
134
|
+
*/
|
|
135
|
+
function isImagePart(p) {
|
|
136
|
+
if (p.type === "image")
|
|
137
|
+
return true;
|
|
138
|
+
if (p.type === "file") {
|
|
139
|
+
// deno-lint-ignore no-explicit-any
|
|
140
|
+
const mime = p.mime;
|
|
141
|
+
if (mime?.startsWith("image/"))
|
|
142
|
+
return true;
|
|
143
|
+
// deno-lint-ignore no-explicit-any
|
|
144
|
+
const filename = p.filename ?? p.name;
|
|
145
|
+
if (filename && /\.(png|jpg|jpeg|gif|bmp|webp|svg|ico|tiff?|heic)$/i.test(filename))
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Strip image parts from user messages, save to disk, inject @lazy-observer redirect.
|
|
152
|
+
* Models without vision (DeepSeek, open-source) can't process images.
|
|
153
|
+
*/
|
|
154
|
+
async function processImageAttachments(msgs, workdir, sessionID) {
|
|
155
|
+
const saveDir = `${workdir}/.opencode/lazy/images/${sessionID || "unknown"}`;
|
|
156
|
+
for (const msg of msgs) {
|
|
157
|
+
if (msg.info.role !== "user")
|
|
158
|
+
continue;
|
|
159
|
+
const imageParts = msg.parts.filter(isImagePart);
|
|
160
|
+
if (imageParts.length === 0)
|
|
161
|
+
continue;
|
|
162
|
+
const savedPaths = [];
|
|
163
|
+
for (const p of imageParts) {
|
|
164
|
+
// deno-lint-ignore no-explicit-any
|
|
165
|
+
const url = p.url;
|
|
166
|
+
if (!url)
|
|
167
|
+
continue;
|
|
168
|
+
const match = url.match(/^data:([^;]+);base64,(.+)$/);
|
|
169
|
+
if (!match)
|
|
170
|
+
continue;
|
|
171
|
+
const mime = match[1];
|
|
172
|
+
const ext = extFromMime(mime);
|
|
173
|
+
// Web Standard: atob is available in Deno, Bun, and Node.js
|
|
174
|
+
const binary = atob(match[2]);
|
|
175
|
+
const data = new Uint8Array(binary.length);
|
|
176
|
+
for (let i = 0; i < binary.length; i++) {
|
|
177
|
+
data[i] = binary.charCodeAt(i);
|
|
178
|
+
}
|
|
179
|
+
const hash = Date.now().toString(36).slice(-4) + Math.random().toString(36).slice(2, 6);
|
|
180
|
+
const filename = `image-${hash}${ext}`;
|
|
181
|
+
try {
|
|
182
|
+
await mkdir(saveDir, { recursive: true });
|
|
183
|
+
await writeFile(`${saveDir}/${filename}`, data);
|
|
184
|
+
savedPaths.push(`${saveDir}/${filename}`);
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
// silent fail
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// Strip image parts
|
|
191
|
+
msg.parts = msg.parts.filter((p) => !isImagePart(p));
|
|
192
|
+
if (savedPaths.length === 0)
|
|
193
|
+
continue;
|
|
194
|
+
// Inject redirect text
|
|
195
|
+
msg.parts.push({
|
|
196
|
+
type: "text",
|
|
197
|
+
text: `[Image attachment detected. Saved to: ${savedPaths.join(", ")} Your model may not support image input. Delegate to @lazy-observer with the file path(s) above so it can read the file with its read tool.]`,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function extFromMime(mime) {
|
|
202
|
+
const map = {
|
|
203
|
+
"image/png": ".png",
|
|
204
|
+
"image/jpeg": ".jpg",
|
|
205
|
+
"image/gif": ".gif",
|
|
206
|
+
"image/webp": ".webp",
|
|
207
|
+
"image/svg+xml": ".svg",
|
|
208
|
+
"image/bmp": ".bmp",
|
|
209
|
+
};
|
|
210
|
+
return map[mime] ?? ".png";
|
|
211
|
+
}
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
// Hook factory
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
export function createMessagesTransformHook(runtime) {
|
|
216
|
+
return async (input, output) => {
|
|
217
|
+
const msgs = output.messages;
|
|
218
|
+
const agent = input.agent ?? "lazy";
|
|
219
|
+
const sessionID = input.sessionID ?? "";
|
|
220
|
+
// 1. Enhanced context pruning
|
|
221
|
+
// Keep: system + last N user turns + everything between them and end
|
|
222
|
+
const maxMessages = runtime?.config.maxMessages ?? DEFAULT_MAX_MESSAGES;
|
|
223
|
+
if (msgs.length > maxMessages) {
|
|
224
|
+
const beforePrune = msgs.length;
|
|
225
|
+
const system = msgs.find((m) => m.info.role === "system") ?? msgs[0];
|
|
226
|
+
const systemIdx = msgs.indexOf(system);
|
|
227
|
+
const keepTurns = Math.floor(maxMessages / 2);
|
|
228
|
+
const keepIndices = new Set([systemIdx]);
|
|
229
|
+
let turns = keepTurns;
|
|
230
|
+
// Walk from end, collect last N user messages and everything after each
|
|
231
|
+
for (let i = msgs.length - 1; i > systemIdx && turns > 0; i--) {
|
|
232
|
+
keepIndices.add(i);
|
|
233
|
+
if (msgs[i].info.role === "user")
|
|
234
|
+
turns--;
|
|
235
|
+
}
|
|
236
|
+
let kept = Array.from(keepIndices).sort((a, b) => a - b).map((i) => msgs[i]);
|
|
237
|
+
const maxTotal = maxMessages + (systemIdx >= 0 ? 1 : 0);
|
|
238
|
+
if (kept.length > maxTotal) {
|
|
239
|
+
const tail = msgs.filter((_msg, i) => i !== systemIdx).slice(-maxMessages);
|
|
240
|
+
kept = systemIdx >= 0 ? [system, ...tail] : tail;
|
|
241
|
+
}
|
|
242
|
+
msgs.splice(0, msgs.length, ...kept);
|
|
243
|
+
await runtime?.recordPruning(beforePrune, msgs.length);
|
|
244
|
+
}
|
|
245
|
+
// 2. Image processing (save to disk and strip, inject @lazy-observer redirect)
|
|
246
|
+
await processImageAttachments(msgs, runtime?.scope.projectRoot ?? process.cwd(), sessionID);
|
|
247
|
+
// 2.5. Detect injected background completions and mark them
|
|
248
|
+
for (const msg of msgs) {
|
|
249
|
+
// deno-lint-ignore no-explicit-any
|
|
250
|
+
const role = msg.role ?? msg.info?.role;
|
|
251
|
+
if (role !== "tool_result")
|
|
252
|
+
continue;
|
|
253
|
+
// deno-lint-ignore no-explicit-any
|
|
254
|
+
const meta = msg.metadata;
|
|
255
|
+
if (!meta || meta.background_job !== true)
|
|
256
|
+
continue;
|
|
257
|
+
const taskID = meta.task_id;
|
|
258
|
+
if (!taskID)
|
|
259
|
+
continue;
|
|
260
|
+
(runtime?.jobBoard ?? jobBoard).markInjectedCompletionSeen(taskID);
|
|
261
|
+
}
|
|
262
|
+
// 3. Job board injection (lazy primary only)
|
|
263
|
+
if (agent === "lazy" && sessionID) {
|
|
264
|
+
const board = runtime?.jobBoard ?? jobBoard;
|
|
265
|
+
if (board.isDirty()) {
|
|
266
|
+
const full = board.formatForPrompt(sessionID);
|
|
267
|
+
if (full)
|
|
268
|
+
injectIntoLastUserMessage(msgs, `\n\n${full}`);
|
|
269
|
+
board.markClean();
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
const mini = board.formatMini(sessionID);
|
|
273
|
+
if (mini)
|
|
274
|
+
injectIntoLastUserMessage(msgs, `\n\n${mini}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
// 4. Workflow gate (lazy primary only)
|
|
278
|
+
if (agent === "lazy" && runtime?.config.workflowGate !== false) {
|
|
279
|
+
const recentText = getRecentUserText(msgs);
|
|
280
|
+
if (recentText) {
|
|
281
|
+
const decision = classifyWorkflow({
|
|
282
|
+
text: recentText,
|
|
283
|
+
mode: runtime?.config.mode ?? "governor",
|
|
284
|
+
});
|
|
285
|
+
runtime?.recordDecision(decision);
|
|
286
|
+
if (decision.action !== "allow") {
|
|
287
|
+
injectIntoLastUserMessage(msgs, `\n\n${formatWorkflowDecision(decision)}`);
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
// classifyWorkflow allowed — still check for skipped workflow steps
|
|
291
|
+
const gate = detectWorkflowSkip(msgs);
|
|
292
|
+
if (gate)
|
|
293
|
+
injectIntoLastUserMessage(msgs, `\n\n${gate}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
// 5. Skill filtering
|
|
298
|
+
filterSkills(msgs, agent);
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
function getRecentUserText(msgs) {
|
|
302
|
+
return msgs
|
|
303
|
+
.filter((m) => m.info.role === "user")
|
|
304
|
+
.slice(-5)
|
|
305
|
+
.map((m) => m.parts
|
|
306
|
+
.filter((p) => p.type === "text" && p.text)
|
|
307
|
+
.map((p) => p.text)
|
|
308
|
+
.join(" "))
|
|
309
|
+
.filter(Boolean)
|
|
310
|
+
.join(" ");
|
|
311
|
+
}
|
|
312
|
+
// ---------------------------------------------------------------------------
|
|
313
|
+
// Helpers
|
|
314
|
+
// ---------------------------------------------------------------------------
|
|
315
|
+
function injectIntoLastUserMessage(msgs, text) {
|
|
316
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
317
|
+
const msg = msgs[i];
|
|
318
|
+
if (msg.info.role !== "user")
|
|
319
|
+
continue;
|
|
320
|
+
// Append to last text part if one exists, otherwise push new part
|
|
321
|
+
for (let j = msg.parts.length - 1; j >= 0; j--) {
|
|
322
|
+
if (msg.parts[j].type === "text" && msg.parts[j].text !== undefined) {
|
|
323
|
+
msg.parts[j].text += text;
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
msg.parts.push({ type: "text", text });
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Filter `<available_skills>` blocks in system messages per-agent permissions.
|
|
333
|
+
*/
|
|
334
|
+
function filterSkills(msgs, agent) {
|
|
335
|
+
const allowed = AGENT_SKILL_ALLOWLIST[agent];
|
|
336
|
+
if (!allowed)
|
|
337
|
+
return; // unrestricted
|
|
338
|
+
for (const msg of msgs) {
|
|
339
|
+
for (const part of msg.parts) {
|
|
340
|
+
if (part.type !== "text" || !part.text)
|
|
341
|
+
continue;
|
|
342
|
+
part.text = part.text.replace(/<available_skills>([\s\S]*?)<\/available_skills>/g, (_full, inner) => {
|
|
343
|
+
const filtered = filterSkillList(inner, allowed);
|
|
344
|
+
return `<available_skills>\n${filtered}</available_skills>`;
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
function filterSkillList(inner, allowed) {
|
|
350
|
+
// Match <skill> blocks that may span multiple lines
|
|
351
|
+
return inner.replace(/<skill>[\s\S]*?<\/skill>/g, (block) => {
|
|
352
|
+
const nameMatch = block.match(/<name>([^<]+)<\/name>/);
|
|
353
|
+
if (nameMatch && !allowed.has(nameMatch[1])) {
|
|
354
|
+
return ""; // remove entire block
|
|
355
|
+
}
|
|
356
|
+
return block;
|
|
357
|
+
});
|
|
358
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const DESTRUCTIVE_PATTERNS = [
|
|
2
|
+
/\brm\s+-rf\b/i,
|
|
3
|
+
/\bgit\s+reset\s+--hard\b/i,
|
|
4
|
+
/\bgit\s+clean\s+-f[dDxX]*\b/i,
|
|
5
|
+
/\bdrop\s+(database|schema|table|view|index|role|user)\b/i,
|
|
6
|
+
/\bdelete\s+from\b/i,
|
|
7
|
+
/\btruncate\s+table\b/i,
|
|
8
|
+
/\bdeploy\b.*\bproduction\b/i,
|
|
9
|
+
/\bproduction\b.*\bdeploy\b/i,
|
|
10
|
+
/\bsecret(s)?\b.*\b(leak|expose|compromise|steal|hard[- ]?coded)\b/i,
|
|
11
|
+
/\btoken(s)?\b.*\b(revoke|leak|expose|compromise|steal)\b/i,
|
|
12
|
+
/删除/,
|
|
13
|
+
/生产/,
|
|
14
|
+
/部署/,
|
|
15
|
+
/密钥/,
|
|
16
|
+
];
|
|
17
|
+
export function createPermissionGuardHook(runtime) {
|
|
18
|
+
return async (input, output) => {
|
|
19
|
+
if (runtime?.config.permissionGuard === false)
|
|
20
|
+
return;
|
|
21
|
+
if (!looksDestructive(input))
|
|
22
|
+
return;
|
|
23
|
+
output.status = "ask";
|
|
24
|
+
runtime?.recordEvent("gate", `Permission guard asked for ${input.type}: ${input.title}`);
|
|
25
|
+
await runtime?.save();
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function looksDestructive(input) {
|
|
29
|
+
const haystack = [
|
|
30
|
+
input.type,
|
|
31
|
+
input.title,
|
|
32
|
+
Array.isArray(input.pattern) ? input.pattern.join(" ") : input.pattern,
|
|
33
|
+
JSON.stringify(input.metadata ?? {}),
|
|
34
|
+
]
|
|
35
|
+
.filter(Boolean)
|
|
36
|
+
.join("\n");
|
|
37
|
+
return DESTRUCTIVE_PATTERNS.some((pattern) => pattern.test(haystack));
|
|
38
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { BackgroundJobBoard } from "./background-job-board.js";
|
|
2
|
+
import type { LazyMode, WorkflowDecision } from "./workflow-classifier.js";
|
|
3
|
+
import type { OpenCodeControlPlane } from "../opencode-control-plane.js";
|
|
4
|
+
export interface LazyConfig {
|
|
5
|
+
sdk?: {
|
|
6
|
+
mode?: "v2";
|
|
7
|
+
legacyHookAdapter?: boolean;
|
|
8
|
+
};
|
|
9
|
+
takeover?: "governed";
|
|
10
|
+
opencode?: {
|
|
11
|
+
sessionStatus?: boolean;
|
|
12
|
+
vcsDiff?: boolean;
|
|
13
|
+
todos?: boolean;
|
|
14
|
+
permissions?: boolean;
|
|
15
|
+
worktreeIsolation?: "off" | "risky-only" | "always";
|
|
16
|
+
revertCheckpoints?: boolean;
|
|
17
|
+
};
|
|
18
|
+
closeReport?: {
|
|
19
|
+
autoCollect?: boolean;
|
|
20
|
+
maxItems?: number;
|
|
21
|
+
};
|
|
22
|
+
mode?: LazyMode;
|
|
23
|
+
maxSessionsPerAgent?: number;
|
|
24
|
+
maxActiveTaskDepth?: number;
|
|
25
|
+
maxMessages?: number;
|
|
26
|
+
permissionGuard?: boolean;
|
|
27
|
+
persistence?: false | {
|
|
28
|
+
path?: string;
|
|
29
|
+
};
|
|
30
|
+
workflowGate?: boolean;
|
|
31
|
+
ponytailMode?: boolean;
|
|
32
|
+
commands?: {
|
|
33
|
+
lazy?: boolean;
|
|
34
|
+
deepworkAlias?: boolean;
|
|
35
|
+
};
|
|
36
|
+
council?: import("../council/index.js").CouncilConfig;
|
|
37
|
+
}
|
|
38
|
+
export interface RequiredLazyConfig {
|
|
39
|
+
sdk: {
|
|
40
|
+
mode: "v2";
|
|
41
|
+
legacyHookAdapter: boolean;
|
|
42
|
+
};
|
|
43
|
+
takeover: "governed";
|
|
44
|
+
opencode: {
|
|
45
|
+
sessionStatus: boolean;
|
|
46
|
+
vcsDiff: boolean;
|
|
47
|
+
todos: boolean;
|
|
48
|
+
permissions: boolean;
|
|
49
|
+
worktreeIsolation: "off" | "risky-only" | "always";
|
|
50
|
+
revertCheckpoints: boolean;
|
|
51
|
+
};
|
|
52
|
+
closeReport: {
|
|
53
|
+
autoCollect: boolean;
|
|
54
|
+
maxItems: number;
|
|
55
|
+
};
|
|
56
|
+
mode: LazyMode;
|
|
57
|
+
maxSessionsPerAgent: number;
|
|
58
|
+
maxActiveTaskDepth: number;
|
|
59
|
+
maxMessages: number;
|
|
60
|
+
permissionGuard: boolean;
|
|
61
|
+
persistence: false | {
|
|
62
|
+
path: string;
|
|
63
|
+
};
|
|
64
|
+
workflowGate: boolean;
|
|
65
|
+
ponytailMode: boolean;
|
|
66
|
+
commands: {
|
|
67
|
+
lazy: boolean;
|
|
68
|
+
deepworkAlias: boolean;
|
|
69
|
+
};
|
|
70
|
+
council: import("../council/index.js").RequiredCouncilConfig;
|
|
71
|
+
}
|
|
72
|
+
export interface RuntimeScope {
|
|
73
|
+
projectRoot: string;
|
|
74
|
+
worktree: string;
|
|
75
|
+
scopeID: string;
|
|
76
|
+
}
|
|
77
|
+
export type TraceStage = "idle" | "grill" | "specify" | "plan" | "build" | "review" | "simplify" | "debug" | "close";
|
|
78
|
+
export interface WorkflowTrace {
|
|
79
|
+
stage: TraceStage;
|
|
80
|
+
lastDecision?: WorkflowDecision;
|
|
81
|
+
recentEvents: Array<{
|
|
82
|
+
ts: number;
|
|
83
|
+
type: "command" | "gate" | "bypass" | "stage" | "reconcile" | "reset" | "compaction";
|
|
84
|
+
summary: string;
|
|
85
|
+
}>;
|
|
86
|
+
}
|
|
87
|
+
export interface ContextStats {
|
|
88
|
+
maxMessages: number;
|
|
89
|
+
lastBefore?: number;
|
|
90
|
+
lastAfter?: number;
|
|
91
|
+
lastPrunedAt?: number;
|
|
92
|
+
totalPruned: number;
|
|
93
|
+
}
|
|
94
|
+
export type CloseEvidenceKind = "behavior" | "test" | "verification" | "risk" | "deletion";
|
|
95
|
+
export interface CloseReportState {
|
|
96
|
+
behaviorChanges: string[];
|
|
97
|
+
testRuns: Array<{
|
|
98
|
+
command: string;
|
|
99
|
+
result: "pass" | "fail" | "unknown";
|
|
100
|
+
}>;
|
|
101
|
+
verificationResult?: "pass" | "fail" | "pending";
|
|
102
|
+
remainingRisks: string[];
|
|
103
|
+
deletions: string[];
|
|
104
|
+
updatedAt?: number;
|
|
105
|
+
}
|
|
106
|
+
export interface OpenCodeSnapshot {
|
|
107
|
+
pendingPermissions: number;
|
|
108
|
+
todos: number;
|
|
109
|
+
diffSummary: string;
|
|
110
|
+
worktree: string;
|
|
111
|
+
sessionStatus: string;
|
|
112
|
+
capabilities: string[];
|
|
113
|
+
lastUpdatedAt?: number;
|
|
114
|
+
}
|
|
115
|
+
export interface DoctorState {
|
|
116
|
+
v2Registration: boolean;
|
|
117
|
+
legacyHookAdapter: boolean;
|
|
118
|
+
skills: boolean;
|
|
119
|
+
commands: boolean;
|
|
120
|
+
desktopConfig: boolean;
|
|
121
|
+
packageReady: boolean;
|
|
122
|
+
warnings: string[];
|
|
123
|
+
lastCheckedAt?: number;
|
|
124
|
+
}
|
|
125
|
+
export interface LazyRuntime {
|
|
126
|
+
config: RequiredLazyConfig;
|
|
127
|
+
scope: RuntimeScope;
|
|
128
|
+
jobBoard: BackgroundJobBoard;
|
|
129
|
+
sessionAgentMap: Map<string, string>;
|
|
130
|
+
sessionDepth: Map<string, number>;
|
|
131
|
+
workflow: WorkflowTrace;
|
|
132
|
+
contextStats: ContextStats;
|
|
133
|
+
closeReport: CloseReportState;
|
|
134
|
+
openCodeSnapshot: OpenCodeSnapshot;
|
|
135
|
+
doctor: DoctorState;
|
|
136
|
+
recoveryMessage: string | null;
|
|
137
|
+
setControlPlane(controlPlane: OpenCodeControlPlane): void;
|
|
138
|
+
configure(input?: LazyConfig): void;
|
|
139
|
+
load(): Promise<void>;
|
|
140
|
+
save(): Promise<void>;
|
|
141
|
+
reset(): Promise<void>;
|
|
142
|
+
setMode(mode: LazyMode): Promise<void>;
|
|
143
|
+
setStage(stage: TraceStage): void;
|
|
144
|
+
recordDecision(decision: WorkflowDecision): Promise<void>;
|
|
145
|
+
recordPruning(before: number, after: number): Promise<void>;
|
|
146
|
+
refreshOpenCodeSnapshot(sessionID?: string): Promise<void>;
|
|
147
|
+
recordOpenCodeEvent(event: Record<string, unknown>): Promise<void>;
|
|
148
|
+
recordToolEvidence(input: Record<string, unknown>, output: Record<string, unknown>): Promise<void>;
|
|
149
|
+
recordCloseEvidence(kind: CloseEvidenceKind, payload: unknown): Promise<void>;
|
|
150
|
+
recordEvent(type: WorkflowTrace["recentEvents"][number]["type"], summary: string): void;
|
|
151
|
+
formatIsolationAdvice(decision?: WorkflowDecision): string | null;
|
|
152
|
+
formatStatus(sessionID?: string): string;
|
|
153
|
+
formatCloseReport(sessionID?: string): string;
|
|
154
|
+
formatInstallHealth(): string;
|
|
155
|
+
formatDoctorReport(): string;
|
|
156
|
+
getReferenceSnapshot(): Record<string, unknown>;
|
|
157
|
+
}
|
|
158
|
+
interface PluginContext {
|
|
159
|
+
project?: {
|
|
160
|
+
root?: string;
|
|
161
|
+
worktree?: string;
|
|
162
|
+
id?: string;
|
|
163
|
+
};
|
|
164
|
+
directory?: string;
|
|
165
|
+
worktree?: string;
|
|
166
|
+
}
|
|
167
|
+
export declare function createLazyRuntime(ctx?: PluginContext): LazyRuntime;
|
|
168
|
+
export declare function resolveLazyConfig(input: LazyConfig | undefined, scope: RuntimeScope): RequiredLazyConfig;
|
|
169
|
+
export {};
|