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.
Files changed (89) hide show
  1. package/ATTRIBUTION.md +38 -0
  2. package/LICENSE +21 -0
  3. package/README.md +357 -0
  4. package/dist/agents/councillor.d.ts +1 -0
  5. package/dist/agents/councillor.js +14 -0
  6. package/dist/agents/designer.d.ts +1 -0
  7. package/dist/agents/designer.js +31 -0
  8. package/dist/agents/explorer.d.ts +1 -0
  9. package/dist/agents/explorer.js +15 -0
  10. package/dist/agents/fixer.d.ts +1 -0
  11. package/dist/agents/fixer.js +23 -0
  12. package/dist/agents/index.d.ts +2 -0
  13. package/dist/agents/index.js +55 -0
  14. package/dist/agents/lazy.d.ts +1 -0
  15. package/dist/agents/lazy.js +3 -0
  16. package/dist/agents/librarian.d.ts +1 -0
  17. package/dist/agents/librarian.js +26 -0
  18. package/dist/agents/observer.d.ts +1 -0
  19. package/dist/agents/observer.js +20 -0
  20. package/dist/agents/oracle.d.ts +1 -0
  21. package/dist/agents/oracle.js +30 -0
  22. package/dist/council/council-manager.d.ts +42 -0
  23. package/dist/council/council-manager.js +223 -0
  24. package/dist/council/index.d.ts +2 -0
  25. package/dist/council/index.js +1 -0
  26. package/dist/hooks/apply-patch-rescue.d.ts +7 -0
  27. package/dist/hooks/apply-patch-rescue.js +150 -0
  28. package/dist/hooks/background-job-board.d.ts +92 -0
  29. package/dist/hooks/background-job-board.js +452 -0
  30. package/dist/hooks/chat-params.d.ts +16 -0
  31. package/dist/hooks/chat-params.js +30 -0
  32. package/dist/hooks/deepwork.d.ts +9 -0
  33. package/dist/hooks/deepwork.js +55 -0
  34. package/dist/hooks/error-recovery.d.ts +21 -0
  35. package/dist/hooks/error-recovery.js +216 -0
  36. package/dist/hooks/index.d.ts +3 -0
  37. package/dist/hooks/index.js +61 -0
  38. package/dist/hooks/lazy-command.d.ts +16 -0
  39. package/dist/hooks/lazy-command.js +178 -0
  40. package/dist/hooks/messages-transform.d.ts +40 -0
  41. package/dist/hooks/messages-transform.js +358 -0
  42. package/dist/hooks/permission-guard.d.ts +5 -0
  43. package/dist/hooks/permission-guard.js +38 -0
  44. package/dist/hooks/runtime.d.ts +169 -0
  45. package/dist/hooks/runtime.js +653 -0
  46. package/dist/hooks/session-events.d.ts +16 -0
  47. package/dist/hooks/session-events.js +65 -0
  48. package/dist/hooks/system-transform.d.ts +8 -0
  49. package/dist/hooks/system-transform.js +113 -0
  50. package/dist/hooks/task-session.d.ts +32 -0
  51. package/dist/hooks/task-session.js +177 -0
  52. package/dist/hooks/workflow-classifier.d.ts +17 -0
  53. package/dist/hooks/workflow-classifier.js +170 -0
  54. package/dist/index.d.ts +13 -0
  55. package/dist/index.js +85 -0
  56. package/dist/opencode-control-plane.d.ts +20 -0
  57. package/dist/opencode-control-plane.js +95 -0
  58. package/dist/ponytail.d.ts +1 -0
  59. package/dist/ponytail.js +33 -0
  60. package/dist/skills/index.d.ts +5 -0
  61. package/dist/skills/index.js +10 -0
  62. package/dist/skills/lazy/build/SKILL.md +62 -0
  63. package/dist/skills/lazy/debug/SKILL.md +17 -0
  64. package/dist/skills/lazy/grill/SKILL.md +54 -0
  65. package/dist/skills/lazy/plan/SKILL.md +52 -0
  66. package/dist/skills/lazy/review/SKILL.md +29 -0
  67. package/dist/skills/lazy/security/SKILL.md +29 -0
  68. package/dist/skills/lazy/simplify/SKILL.md +52 -0
  69. package/dist/skills/lazy/specify/SKILL.md +62 -0
  70. package/dist/skills/lazy/worktree/SKILL.md +66 -0
  71. package/dist/tools/cancel-task.d.ts +3 -0
  72. package/dist/tools/cancel-task.js +37 -0
  73. package/dist/tools/council.d.ts +6 -0
  74. package/dist/tools/council.js +41 -0
  75. package/dist/tools/index.d.ts +2 -0
  76. package/dist/tools/index.js +2 -0
  77. package/dist/v2.d.ts +1 -0
  78. package/dist/v2.js +42 -0
  79. package/docs/architecture.md +47 -0
  80. package/docs/council.md +200 -0
  81. package/docs/desktop-distribution.md +36 -0
  82. package/docs/opencode-integration.md +54 -0
  83. package/docs/positioning.md +44 -0
  84. package/docs/product-audit.md +187 -0
  85. package/docs/product-plan.md +56 -0
  86. package/docs/state-machine.md +35 -0
  87. package/docs/user-manual.md +439 -0
  88. package/docs/work-plan.md +190 -0
  89. package/package.json +44 -0
@@ -0,0 +1,452 @@
1
+ /**
2
+ * BackgroundJobBoard — in-memory state machine for subagent task tracking.
3
+ *
4
+ * ponytail: global singletons, no DI. Upgrade: per-session isolation if starvation occurs.
5
+ */
6
+ // ---------------------------------------------------------------------------
7
+ // Config
8
+ // ---------------------------------------------------------------------------
9
+ const _MAX_SESSIONS_PER_AGENT = 2;
10
+ const MAX_PENDING_CALLS = 100;
11
+ const MAX_INJECTED_COMPLETIONS = 500;
12
+ const CONTEXT_MIN_LINES = 10;
13
+ const CONTEXT_MAX_FILES = 8;
14
+ function findJobByTaskID(jobs, taskID) {
15
+ for (const job of jobs.values()) {
16
+ if (job.taskID === taskID)
17
+ return job;
18
+ }
19
+ return undefined;
20
+ }
21
+ function findJobByCallIDScan(jobs, callID) {
22
+ for (const job of jobs.values()) {
23
+ if (job.callID === callID)
24
+ return job;
25
+ }
26
+ return undefined;
27
+ }
28
+ function isReusableJob(job) {
29
+ return job.state === "reconciled" && !job.terminalUnreconciled;
30
+ }
31
+ // ---------------------------------------------------------------------------
32
+ // Board
33
+ // ---------------------------------------------------------------------------
34
+ export class BackgroundJobBoard {
35
+ jobs = new Map();
36
+ pendingCalls = [];
37
+ agentCounter = new Map();
38
+ processedCompletions = new Set();
39
+ injectedCompletionsSeen = new Set();
40
+ maxReusablePerAgent = 2;
41
+ dirty = false;
42
+ constructor(options) {
43
+ if (options?.maxReusablePerAgent) {
44
+ this.maxReusablePerAgent = options.maxReusablePerAgent;
45
+ }
46
+ }
47
+ configure(options) {
48
+ if (options.maxReusablePerAgent) {
49
+ this.maxReusablePerAgent = options.maxReusablePerAgent;
50
+ }
51
+ }
52
+ // -----------------------------------------------------------------------
53
+ // Launch
54
+ // -----------------------------------------------------------------------
55
+ registerLaunch(parentSessionID, agent, callID) {
56
+ const taskID = Date.now().toString(36).slice(-4) + Math.random().toString(36).slice(2, 6); // placeholder; real taskID set in parseLaunch
57
+ const count = this.agentCounter.get(agent) ?? 0;
58
+ this.agentCounter.set(agent, count + 1);
59
+ const alias = `${agent}-${count + 1}`;
60
+ const now = Date.now();
61
+ const job = {
62
+ taskID,
63
+ parentSessionID,
64
+ agent,
65
+ state: "running",
66
+ terminalUnreconciled: false,
67
+ timedOut: false,
68
+ cancellationRequested: false,
69
+ alias,
70
+ callID,
71
+ contextFiles: [],
72
+ launchedAt: now,
73
+ lastLaunchedAt: now,
74
+ lastUsedAt: now,
75
+ updatedAt: now,
76
+ completedAt: 0,
77
+ };
78
+ // Store by alias (used for lookup before real taskID is known)
79
+ this.jobs.set(alias, job);
80
+ // Track pending call
81
+ this.pendingCalls.push({ callID, sessionID: parentSessionID, alias });
82
+ if (this.pendingCalls.length > MAX_PENDING_CALLS) {
83
+ this.pendingCalls.shift();
84
+ }
85
+ this.dirty = true;
86
+ return job;
87
+ }
88
+ // -----------------------------------------------------------------------
89
+ // Match pending call to job
90
+ // -----------------------------------------------------------------------
91
+ findJobByCallID(callID) {
92
+ const pending = this.pendingCalls.find((p) => p.callID === callID);
93
+ if (!pending?.alias)
94
+ return undefined;
95
+ return this.jobs.get(pending.alias) ?? findJobByCallIDScan(this.jobs, callID);
96
+ }
97
+ findJobByTaskID(taskID) {
98
+ return findJobByTaskID(this.jobs, taskID);
99
+ }
100
+ findJobByAlias(alias) {
101
+ for (const job of this.jobs.values()) {
102
+ if (job.alias === alias)
103
+ return job;
104
+ }
105
+ return undefined;
106
+ }
107
+ // -----------------------------------------------------------------------
108
+ // Update state (called from tool.execute.after)
109
+ // -----------------------------------------------------------------------
110
+ updateStatus(callID, taskID, state, resultSummary) {
111
+ const job = this.findJobByCallID(callID);
112
+ if (!job)
113
+ return;
114
+ // Late-cancel normalization: if cancelled + error → force cancelled
115
+ if (state === "error" && job.cancellationRequested) {
116
+ state = "cancelled";
117
+ }
118
+ const now = Date.now();
119
+ const oldTaskID = job.taskID;
120
+ job.taskID = taskID || job.taskID;
121
+ if (oldTaskID !== job.taskID)
122
+ this.jobs.delete(oldTaskID);
123
+ job.state = state;
124
+ job.updatedAt = now;
125
+ if (resultSummary)
126
+ job.resultSummary = resultSummary;
127
+ if (state === "completed" || state === "error" || state === "cancelled") {
128
+ job.completedAt = now;
129
+ job.terminalUnreconciled = true;
130
+ this.trimReusable(taskID);
131
+ }
132
+ // Re-index by taskID and drop stale alias key
133
+ this.jobs.set(taskID, job);
134
+ const pending = this.pendingCalls.find((p) => p.callID === callID);
135
+ if (pending?.alias)
136
+ this.jobs.delete(pending.alias);
137
+ this.dirty = true;
138
+ }
139
+ // -----------------------------------------------------------------------
140
+ // Context accumulation
141
+ // -----------------------------------------------------------------------
142
+ addContext(taskID, file) {
143
+ if (file.lineCount < CONTEXT_MIN_LINES)
144
+ return;
145
+ const job = findJobByTaskID(this.jobs, taskID);
146
+ if (!job || job.state !== "running")
147
+ return;
148
+ const existing = job.contextFiles.find((f) => f.path === file.path);
149
+ if (existing) {
150
+ existing.lineCount = Math.max(existing.lineCount, file.lineCount);
151
+ }
152
+ else {
153
+ job.contextFiles.push(file);
154
+ }
155
+ job.contextFiles = job.contextFiles
156
+ .sort((a, b) => b.lineCount - a.lineCount)
157
+ .slice(0, CONTEXT_MAX_FILES);
158
+ }
159
+ // -----------------------------------------------------------------------
160
+ // Reconciliation
161
+ // -----------------------------------------------------------------------
162
+ markReconciled(taskID) {
163
+ const job = findJobByTaskID(this.jobs, taskID);
164
+ if (job) {
165
+ job.terminalUnreconciled = false;
166
+ if (job.state === "completed" || job.state === "reconciled") {
167
+ job.state = "reconciled";
168
+ }
169
+ job.updatedAt = Date.now();
170
+ }
171
+ this.injectedCompletionsSeen.delete(taskID);
172
+ this.trimReusable(taskID);
173
+ this.dirty = true;
174
+ }
175
+ trimReusable(taskID) {
176
+ const job = findJobByTaskID(this.jobs, taskID);
177
+ if (!job || !isReusableJob(job))
178
+ return;
179
+ const reusable = [...this.jobs.values()]
180
+ .filter((j) => j.agent === job.agent &&
181
+ j.parentSessionID === job.parentSessionID &&
182
+ isReusableJob(j))
183
+ .sort((a, b) => b.lastUsedAt - a.lastUsedAt);
184
+ for (const stale of reusable.slice(this.maxReusablePerAgent)) {
185
+ this.jobs.delete(stale.taskID);
186
+ }
187
+ }
188
+ getTerminalUnreconciledJobs(parentSessionID) {
189
+ const result = [];
190
+ for (const job of this.jobs.values()) {
191
+ if (job.parentSessionID === parentSessionID &&
192
+ job.terminalUnreconciled) {
193
+ result.push(job);
194
+ }
195
+ }
196
+ return result;
197
+ }
198
+ getRunningJobs(parentSessionID) {
199
+ const result = [];
200
+ for (const job of this.jobs.values()) {
201
+ if (job.parentSessionID === parentSessionID && job.state === "running") {
202
+ result.push(job);
203
+ }
204
+ }
205
+ return result;
206
+ }
207
+ getStaleJobs(parentSessionID) {
208
+ const result = [];
209
+ for (const job of this.jobs.values()) {
210
+ if (job.parentSessionID === parentSessionID && job.state === "stale") {
211
+ result.push(job);
212
+ }
213
+ }
214
+ return result;
215
+ }
216
+ getReusableJobs(parentSessionID) {
217
+ return [...this.jobs.values()].filter((job) => job.parentSessionID === parentSessionID && isReusableJob(job));
218
+ }
219
+ // -----------------------------------------------------------------------
220
+ // Session reuse
221
+ // -----------------------------------------------------------------------
222
+ resolveReusable(parentSessionID, agent) {
223
+ const candidates = [];
224
+ for (const job of this.jobs.values()) {
225
+ if (job.parentSessionID === parentSessionID &&
226
+ job.agent === agent &&
227
+ isReusableJob(job)) {
228
+ candidates.push(job);
229
+ }
230
+ }
231
+ // Return oldest reconciled session
232
+ if (candidates.length > 0) {
233
+ candidates.sort((a, b) => a.completedAt - b.completedAt);
234
+ const job = candidates[0];
235
+ const now = Date.now();
236
+ job.lastLaunchedAt = now;
237
+ job.lastUsedAt = now;
238
+ return job;
239
+ }
240
+ return undefined;
241
+ }
242
+ getReusableJob(taskID) {
243
+ const job = findJobByTaskID(this.jobs, taskID);
244
+ if (job && isReusableJob(job)) {
245
+ return job;
246
+ }
247
+ return undefined;
248
+ }
249
+ getActiveCount(parentSessionID, agent) {
250
+ let count = 0;
251
+ for (const job of this.jobs.values()) {
252
+ if (job.parentSessionID === parentSessionID &&
253
+ job.agent === agent &&
254
+ job.state === "running") {
255
+ count++;
256
+ }
257
+ }
258
+ return count;
259
+ }
260
+ isLateCancelledTaskError(callID) {
261
+ const job = this.findJobByCallID(callID);
262
+ return job?.cancellationRequested === true;
263
+ }
264
+ cancelJob(id) {
265
+ const job = findJobByTaskID(this.jobs, id) ?? this.findJobByAlias(id);
266
+ if (job) {
267
+ job.cancellationRequested = true;
268
+ this.dirty = true;
269
+ }
270
+ }
271
+ // -----------------------------------------------------------------------
272
+ // Prompt injection
273
+ // -----------------------------------------------------------------------
274
+ formatForPrompt(parentSessionID) {
275
+ const running = this.getRunningJobs(parentSessionID);
276
+ const terminal = this.getTerminalUnreconciledJobs(parentSessionID);
277
+ const reusable = this.getReusableJobs(parentSessionID);
278
+ const stale = this.getStaleJobs(parentSessionID);
279
+ if (running.length === 0 && terminal.length === 0 && reusable.length === 0 && stale.length === 0) {
280
+ return null;
281
+ }
282
+ const lines = ["[Background Job Board]"];
283
+ if (running.length > 0) {
284
+ lines.push(" Running:");
285
+ const now = Date.now();
286
+ for (const j of running) {
287
+ const ageMs = now - j.lastLaunchedAt;
288
+ const isResume = j.lastLaunchedAt !== j.launchedAt;
289
+ let ageLabel = "";
290
+ if (j.state === "running" && ageMs < 30000) {
291
+ ageLabel = isResume
292
+ ? ` [resumed, ${Math.floor(ageMs / 1000)}s ago]`
293
+ : ` [just launched, ${Math.floor(ageMs / 1000)}s ago]`;
294
+ }
295
+ lines.push(` - ${j.alias} task_id:${j.taskID} agent:${j.agent}${ageLabel}`);
296
+ }
297
+ }
298
+ if (terminal.length > 0) {
299
+ lines.push(" Terminal (unreconciled):");
300
+ for (const j of terminal) {
301
+ const summary = j.resultSummary ? ` — ${j.resultSummary.slice(0, 120)}` : "";
302
+ lines.push(` - ${j.alias} task_id:${j.taskID} state:${j.state}${summary}`);
303
+ if (j.contextFiles.length > 0) {
304
+ lines.push(" Context files read:");
305
+ const shown = j.contextFiles.slice(0, 5);
306
+ const rest = j.contextFiles.length - shown.length;
307
+ const rendered = shown.map((f) => `${f.path} (${f.lineCount} lines)`);
308
+ lines.push(` ${rendered.join(", ")}${rest > 0 ? ` (+${rest} more)` : ""}`);
309
+ }
310
+ }
311
+ lines.push(" → Call reconcileTerminalJobs() to process these results.");
312
+ }
313
+ const injectedIDs = [...this.injectedCompletionsSeen];
314
+ if (injectedIDs.length > 0) {
315
+ lines.push("");
316
+ lines.push("## Injected Background Completions");
317
+ lines.push("The following subagent results were injected into the chat by opencode (duplicated in the job board above). Use `reconcileTerminalJobs` to acknowledge them and prevent double-response.");
318
+ for (const id of injectedIDs.slice(0, 10)) {
319
+ const job = findJobByTaskID(this.jobs, id);
320
+ if (job) {
321
+ lines.push(`- \`${job.alias}\` — ${job.state} — ${job.resultSummary ?? "(no summary)"}`);
322
+ }
323
+ }
324
+ }
325
+ if (reusable.length > 0) {
326
+ lines.push(" Reusable Sessions:");
327
+ for (const j of reusable) {
328
+ lines.push(` - ${j.alias} task_id:${j.taskID} agent:${j.agent}`);
329
+ if (j.contextFiles.length > 0) {
330
+ lines.push(" Context files:");
331
+ const shown = j.contextFiles.slice(0, 3);
332
+ const rest = j.contextFiles.length - shown.length;
333
+ const rendered = shown.map((f) => `${f.path} (${f.lineCount} lines)`);
334
+ lines.push(` ${rendered.join(", ")}${rest > 0 ? ` (+${rest} more)` : ""}`);
335
+ }
336
+ }
337
+ }
338
+ if (stale.length > 0) {
339
+ lines.push(" Stale Sessions:");
340
+ for (const j of stale) {
341
+ lines.push(` - ${j.alias} task_id:${j.taskID} agent:${j.agent} (restart detected)`);
342
+ }
343
+ }
344
+ if (terminal.length > 0 || running.length > 0) {
345
+ lines.push("", "## Operational Guardrails");
346
+ lines.push("- Do not poll running jobs — wait for hook-driven background completion notifications.");
347
+ lines.push("- Use cancel_task only when the user asks or a running lane becomes obsolete.");
348
+ lines.push("- Reconcile ALL terminal jobs before your final response to the user.");
349
+ lines.push("- Reuse only completed sessions for the same specialist/context — never reuse cancelled or errored ones.");
350
+ }
351
+ lines.push("", "## Summary");
352
+ return lines.join("\n");
353
+ }
354
+ // -----------------------------------------------------------------------
355
+ // Dirty polling (ponytail: 1-byte state-change tracker)
356
+ // -----------------------------------------------------------------------
357
+ isDirty() {
358
+ return this.dirty;
359
+ }
360
+ markClean() {
361
+ this.dirty = false;
362
+ }
363
+ // -----------------------------------------------------------------------
364
+ // Mini status (ponytail: ~20 tokens, injected every round when clean)
365
+ // -----------------------------------------------------------------------
366
+ formatMini(parentSessionID) {
367
+ const running = this.getRunningJobs(parentSessionID);
368
+ const terminal = this.getTerminalUnreconciledJobs(parentSessionID);
369
+ if (running.length === 0 && terminal.length === 0)
370
+ return null;
371
+ let s = `Jobs: ${running.length}r/${terminal.length}u`;
372
+ if (terminal.length > 0)
373
+ s += ` | reconcileTerminalJobs()`;
374
+ return s;
375
+ }
376
+ // -----------------------------------------------------------------------
377
+ // Dedup injected completions
378
+ // -----------------------------------------------------------------------
379
+ isInjectedCompletionProcessed(id) {
380
+ if (this.processedCompletions.has(id))
381
+ return true;
382
+ if (this.processedCompletions.size >= MAX_INJECTED_COMPLETIONS) {
383
+ // Evict oldest half
384
+ const entries = [...this.processedCompletions];
385
+ this.processedCompletions = new Set(entries.slice(entries.length / 2));
386
+ }
387
+ this.processedCompletions.add(id);
388
+ return false;
389
+ }
390
+ markInjectedCompletionSeen(taskID) {
391
+ this.injectedCompletionsSeen.add(taskID);
392
+ }
393
+ // -----------------------------------------------------------------------
394
+ // Cleanup
395
+ // -----------------------------------------------------------------------
396
+ dropSession(sessionID) {
397
+ const toDelete = [];
398
+ for (const [key, job] of this.jobs) {
399
+ if (job.taskID === sessionID || job.parentSessionID === sessionID) {
400
+ toDelete.push(key);
401
+ }
402
+ }
403
+ if (toDelete.length > 0) {
404
+ for (const key of toDelete) {
405
+ this.jobs.delete(key);
406
+ }
407
+ this.dirty = true;
408
+ }
409
+ }
410
+ clear() {
411
+ this.jobs.clear();
412
+ this.pendingCalls = [];
413
+ this.agentCounter.clear();
414
+ this.processedCompletions.clear();
415
+ this.injectedCompletionsSeen.clear();
416
+ this.dirty = true;
417
+ }
418
+ snapshot() {
419
+ return {
420
+ jobs: [...this.jobs.values()],
421
+ pendingCalls: [...this.pendingCalls],
422
+ agentCounter: [...this.agentCounter],
423
+ processedCompletions: [...this.processedCompletions],
424
+ injectedCompletionsSeen: [...this.injectedCompletionsSeen],
425
+ };
426
+ }
427
+ restore(snapshot) {
428
+ this.clear();
429
+ for (const job of snapshot.jobs ?? []) {
430
+ const restored = { ...job };
431
+ if (restored.state === "running") {
432
+ restored.state = "stale";
433
+ restored.terminalUnreconciled = true;
434
+ }
435
+ this.jobs.set(restored.taskID, restored);
436
+ }
437
+ this.pendingCalls = [...(snapshot.pendingCalls ?? [])];
438
+ this.agentCounter = new Map(snapshot.agentCounter ?? []);
439
+ this.processedCompletions = new Set(snapshot.processedCompletions ?? []);
440
+ this.injectedCompletionsSeen = new Set(snapshot.injectedCompletionsSeen ?? []);
441
+ this.dirty = true;
442
+ }
443
+ get size() {
444
+ return this.jobs.size;
445
+ }
446
+ }
447
+ // ---------------------------------------------------------------------------
448
+ // Singleton
449
+ // ---------------------------------------------------------------------------
450
+ // ponytail: single board for entire plugin lifetime.
451
+ // Upgrade: isolate per worktree or project if concurrent sessions collide.
452
+ export const jobBoard = new BackgroundJobBoard();
@@ -0,0 +1,16 @@
1
+ import type { Model, UserMessage } from "@opencode-ai/sdk";
2
+ import type { ProviderContext } from "@opencode-ai/plugin";
3
+ import type { LazyRuntime } from "./runtime.js";
4
+ export declare function createChatParamsHook(runtime?: LazyRuntime): (input: {
5
+ sessionID: string;
6
+ agent: string;
7
+ model: Model;
8
+ provider: ProviderContext;
9
+ message: UserMessage;
10
+ }, output: {
11
+ temperature: number;
12
+ topP: number;
13
+ topK: number;
14
+ maxOutputTokens: number | undefined;
15
+ options: Record<string, unknown>;
16
+ }) => Promise<void>;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Per-agent LLM parameter configuration.
3
+ * Sets temperature and other params based on agent role.
4
+ *
5
+ * ponytail: hardcoded defaults. Designer gets 0.7 for creativity, rest 0.1-0.2.
6
+ */
7
+ const AGENT_TEMPERATURES = {
8
+ lazy: 0.1,
9
+ "lazy-explorer": 0.1,
10
+ "lazy-oracle": 0.1,
11
+ "lazy-librarian": 0.2,
12
+ "lazy-designer": 0.7,
13
+ "lazy-fixer": 0.2,
14
+ "lazy-observer": 0.1,
15
+ };
16
+ export function createChatParamsHook(runtime) {
17
+ return async (input, output) => {
18
+ const temp = AGENT_TEMPERATURES[input.agent] ?? 0.2;
19
+ output.temperature = temp;
20
+ const map = runtime?.sessionAgentMap;
21
+ if (map) {
22
+ map.set(input.sessionID, input.agent);
23
+ // Prune to prevent memory leaks
24
+ if (map.size > 1000) {
25
+ const firstKey = map.keys().next().value;
26
+ map.delete(firstKey);
27
+ }
28
+ }
29
+ };
30
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Deepwork command — heavy multi-phase coding sessions.
3
+ *
4
+ * Activated by: /deepwork <task description>
5
+ *
6
+ * ponytail: command injects deepwork rules directly into the conversation
7
+ * as activation text. No skill file, no two-step activation.
8
+ */
9
+ export declare const DEEPWORK_ACTIVATION: (task: string) => string;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Deepwork command — heavy multi-phase coding sessions.
3
+ *
4
+ * Activated by: /deepwork <task description>
5
+ *
6
+ * ponytail: command injects deepwork rules directly into the conversation
7
+ * as activation text. No skill file, no two-step activation.
8
+ */
9
+ export const DEEPWORK_ACTIVATION = (task) => `DEEPWORK MODE ACTIVE — heavy multi-phase coding session
10
+
11
+ Task: ${task}
12
+
13
+ ## 1. Pre-work Context Gathering
14
+ Before any planning or coding, review existing context thoroughly:
15
+ - Read PRDs, spec issues, GitHub issues — understand the "why" before the "how"
16
+ - Review the existing codebase: current architecture, patterns, conventions
17
+ - Load any related design mockups, Figma links, or visual references
18
+ - Know what's already built and what's net-new
19
+
20
+ ## 2. Plan Loading
21
+ - Multi-file read with Read tool to bring all relevant files into context
22
+ - Summarize findings in the deepwork file before starting implementation
23
+ - Get @lazy-oracle review of the plan before Phase 1 execution begins
24
+
25
+ ## 3. Designer Handoff Discipline
26
+ When @lazy-designer delivers UI/UX components:
27
+ - @lazy-designer must explain every layout, spacing, color, typography decision
28
+ - Use CSS comments to annotate design rationale (not just what, but WHY)
29
+ - NO merge-squashing of designer output — cherry-pick: one component file at a time
30
+ - Preserve design intent across later phases; @lazy-fixer only does mechanical follow-up
31
+ - If a later phase must alter design, flag it to @lazy-designer for re-review
32
+
33
+ ## 4. Multiple Parallel Lanes
34
+ - Launch two @lazy-designer lanes with different aesthetic philosophies when ambiguity exists
35
+ - Each lane gets its own deepwork tracking slug: \`.lazy/deepwork/<task>-<lanename>.md\`
36
+ - Compare lanes independently; @lazy-oracle picks the winning lane before merging
37
+
38
+ ## 5. Progress Tracking
39
+ - Create \`.lazy/deepwork/<slug>.md\` — track goals, plans, oracle reviews, phases, blockers
40
+ - Reference files by path, not content. Keep out of git (\`.lazy/\` is gitignored)
41
+ - Update after every phase: what was done, what was reviewed, what's next
42
+ - Mark each phase as ✓ COMPLETE or ⚠ BLOCKED
43
+
44
+ ## 6. Self-Critique After Each Check-in
45
+ After every check-in (commit, phase completion, designer delivery):
46
+ - @lazy-oracle: found issues → fix actionable ones immediately before continuing
47
+ - @lazy/review: scan the diff for bugs, unnecessary complexity, deviations from plan
48
+ - Add self-critique notes to the deepwork file — what went well, what could be tighter
49
+ - Non-actionable critique becomes ponytail debt (note it, move on)
50
+
51
+ ## Exit Discipline
52
+ - All phases ✓ COMPLETE before declaring the session done
53
+ - Final @lazy-oracle review passes with no blocking issues
54
+ - Deepwork file archived as session record
55
+ - Wait for hook-driven background completion before consuming results`;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Error recovery hook (tool.execute.after).
3
+ * Full slim-equivalent pattern set:
4
+ * 1. JSON parse error recovery (8+ patterns)
5
+ * 2. Apply-patch failure with structured guidance
6
+ * 3. Task delegate retry guidance (8 error patterns)
7
+ * 4. Post-file-tool nudge (phase reminder for lazy primary read/write)
8
+ */
9
+ interface ToolAfterInput {
10
+ tool: string;
11
+ sessionID: string;
12
+ callID: string;
13
+ args: Record<string, unknown>;
14
+ }
15
+ interface ToolAfterOutput {
16
+ title: string;
17
+ output: string;
18
+ metadata: Record<string, unknown>;
19
+ }
20
+ export declare function createErrorRecoveryHook(runtime?: import("./runtime.js").LazyRuntime): (input: ToolAfterInput, output: ToolAfterOutput) => void;
21
+ export {};