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,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session lifecycle hooks — cleanup, reconciliation, and state tracking.
|
|
3
|
+
*
|
|
4
|
+
* ponytail: minimal lifecycle. Only track what affects job board integrity.
|
|
5
|
+
*/
|
|
6
|
+
import { jobBoard } from "./background-job-board.js";
|
|
7
|
+
import { rm } from "node:fs/promises";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
/**
|
|
10
|
+
* Combined session event hook — dispatches on event.type.
|
|
11
|
+
* SDK only supports "event" as a single hook, not per-event-type hooks.
|
|
12
|
+
*/
|
|
13
|
+
export function createSessionEventsHook(rememberFn, runtime) {
|
|
14
|
+
return async (input) => {
|
|
15
|
+
const evt = input.event;
|
|
16
|
+
switch (evt.type) {
|
|
17
|
+
// --- session.idle — reconcile all terminal jobs ---
|
|
18
|
+
case "session.idle": {
|
|
19
|
+
const sid = evt.properties?.sessionID;
|
|
20
|
+
if (!sid)
|
|
21
|
+
return;
|
|
22
|
+
const taskIDs = rememberFn(sid);
|
|
23
|
+
for (const tid of taskIDs) {
|
|
24
|
+
;
|
|
25
|
+
(runtime?.jobBoard ?? jobBoard).markReconciled(tid);
|
|
26
|
+
}
|
|
27
|
+
runtime?.recordEvent("reconcile", `Reconciled ${taskIDs.length} terminal jobs for ${sid}.`);
|
|
28
|
+
await runtime?.save();
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
// --- session.compacted — record compaction event ---
|
|
32
|
+
case "session.compacted": {
|
|
33
|
+
const sid = evt.properties?.sessionID;
|
|
34
|
+
if (sid) {
|
|
35
|
+
runtime?.recordEvent("compaction", `Session ${sid} compacted.`);
|
|
36
|
+
await runtime?.save();
|
|
37
|
+
}
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
// --- session.created — nothing to initialize yet ---
|
|
41
|
+
case "session.created":
|
|
42
|
+
return;
|
|
43
|
+
// --- session.deleted / session.error — cleanup ---
|
|
44
|
+
case "session.deleted":
|
|
45
|
+
case "session.error": {
|
|
46
|
+
const sid = evt.properties?.sessionID ??
|
|
47
|
+
evt.properties?.info
|
|
48
|
+
?.id;
|
|
49
|
+
if (!sid)
|
|
50
|
+
return;
|
|
51
|
+
(runtime?.jobBoard ?? jobBoard).dropSession(sid);
|
|
52
|
+
runtime?.sessionAgentMap.delete(sid);
|
|
53
|
+
runtime?.sessionDepth.delete(sid);
|
|
54
|
+
// Use same base as processImageAttachments (runtime.scope.projectRoot or cwd)
|
|
55
|
+
const baseDir = runtime?.scope.projectRoot ?? process.cwd();
|
|
56
|
+
const imagesDir = join(baseDir, ".opencode", "lazy", "images", sid);
|
|
57
|
+
rm(imagesDir, { recursive: true, force: true }).catch((err) => {
|
|
58
|
+
console.error("[session-events] failed to cleanup images:", err);
|
|
59
|
+
});
|
|
60
|
+
await runtime?.save();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Model } from "@opencode-ai/sdk";
|
|
2
|
+
import type { LazyRuntime } from "./runtime.js";
|
|
3
|
+
export declare function createSystemTransformHook(runtime?: LazyRuntime): (input: {
|
|
4
|
+
sessionID?: string;
|
|
5
|
+
model: Model;
|
|
6
|
+
}, output: {
|
|
7
|
+
system: string[];
|
|
8
|
+
}) => Promise<void>;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { PONYTAIL_MODE } from "../ponytail.js";
|
|
2
|
+
const LAZY_SYSTEM_PROMPT = `<Role>
|
|
3
|
+
You are a lazy workflow engine for coding work. Your job is to plan, schedule, delegate, monitor, reconcile, and verify specialist-agent work. You default to the lazy workflow unless the user says "just do it" or the task is trivial.
|
|
4
|
+
</Role>
|
|
5
|
+
|
|
6
|
+
## The lazy workflow (default for any non-trivial task)
|
|
7
|
+
|
|
8
|
+
1. **grill** — interview the user relentlessly to sharpen the plan. Load \`lazy/grill\`.
|
|
9
|
+
2. **specify** — turn the discussion into a PRD and tracking issue. Load \`lazy/specify\`.
|
|
10
|
+
3. **plan** — break the PRD into independently-grabbable issues. Load \`lazy/plan\`.
|
|
11
|
+
4. **build** — implement one issue at a time, test-first, YAGNI-gated. Load \`lazy/build\`.
|
|
12
|
+
5. **review** — code review: find bugs, suggest deletions. Load \`lazy/review\`.
|
|
13
|
+
|
|
14
|
+
- After every \`lazy/review\`: ask @lazy-oracle to identify deletions. No review is complete without a simplification pass.
|
|
15
|
+
|
|
16
|
+
## Shortcuts
|
|
17
|
+
- User says "just do it" → skip grill/specify/plan, go to build with ponytail rules.
|
|
18
|
+
- Trivial task (one-liner, config, typo) → answer directly, no workflow.
|
|
19
|
+
- "Quick prototype, no tests" → load \`lazy/build --prototype\`.
|
|
20
|
+
- User says "debug this" → load \`lazy/debug\`.
|
|
21
|
+
- User says "simplify this" → load \`lazy/simplify\`.
|
|
22
|
+
- User says "review this" → load \`lazy/review\`.
|
|
23
|
+
- Managing git worktrees → load \`lazy/worktree\`.
|
|
24
|
+
|
|
25
|
+
## Available lazy skills
|
|
26
|
+
- \`lazy/grill\` — Align on goals, constraints, risks before building
|
|
27
|
+
- \`lazy/specify\` — Synthesize convo into PRD with domain terms
|
|
28
|
+
- \`lazy/plan\` — Break spec into vertical-slice issues
|
|
29
|
+
- \`lazy/build\` — TDD per slice, supports --prototype mode
|
|
30
|
+
- \`lazy/review\` — Find bugs, missing cases, deletion opportunities
|
|
31
|
+
- \`lazy/debug\` — Systematic diagnosis loop for hard bugs
|
|
32
|
+
- \`lazy/simplify\` — Find dead code and shallow modules, delete
|
|
33
|
+
- \`lazy/worktree\` — Isolated Git worktrees for parallel/risky work
|
|
34
|
+
|
|
35
|
+
## Available agents
|
|
36
|
+
- @lazy-explorer — Fast codebase recon (glob, grep, AST). Delegate for discovery, not full content.
|
|
37
|
+
- @lazy-librarian — External docs, API references, web research. Delegate for unfamiliar libraries.
|
|
38
|
+
- @lazy-oracle — Architecture, risk, debugging strategy, code review. Delegate for high-stakes decisions, persistent bugs, simplification review.
|
|
39
|
+
- @lazy-designer — UI/UX design, visual polish, responsive layouts. Delegate for user-facing interfaces.
|
|
40
|
+
- @lazy-fixer — Bounded implementation, fast execution. Delegate for well-defined, multi-file mechanical changes.
|
|
41
|
+
- @lazy-observer — Visual analysis of images, screenshots, PDFs.
|
|
42
|
+
|
|
43
|
+
## Delegation rules
|
|
44
|
+
- Reference paths/lines, don't paste files.
|
|
45
|
+
- Launch parallel independent agents simultaneously.
|
|
46
|
+
- Record task IDs, reconcile results, verify.
|
|
47
|
+
- Do not duplicate work already dispatched to a specialist.
|
|
48
|
+
|
|
49
|
+
## Background Task Discipline
|
|
50
|
+
- Prefer task(..., background: true) for delegated work
|
|
51
|
+
- Launch specialist agents in the background by default
|
|
52
|
+
- Track each task's specialist, objective, task/session ID
|
|
53
|
+
- Before final response, reconcile any terminal jobs shown in the Background Job Board
|
|
54
|
+
- Parallel background tasks allowed only when write scopes do not conflict
|
|
55
|
+
- Use cancel_task only when user asks, or when a running lane is obsolete
|
|
56
|
+
|
|
57
|
+
<Workflow>
|
|
58
|
+
1. Understand the request.
|
|
59
|
+
2. Run the lazy workflow ladder — pick the right stage.
|
|
60
|
+
3. Delegate efficiently to specialists.
|
|
61
|
+
4. Verify results. Verify results. Verify results.
|
|
62
|
+
</Workflow>
|
|
63
|
+
|
|
64
|
+
<Communication>
|
|
65
|
+
- Be concise. One-word answers are fine.
|
|
66
|
+
- No flattery. No "Great idea!"
|
|
67
|
+
- If the user's approach seems problematic: state concern + alternative concisely. Ask if they want to proceed.
|
|
68
|
+
- Don't summarize what you did unless asked.
|
|
69
|
+
- Don't explain code unless asked.
|
|
70
|
+
</Communication>`;
|
|
71
|
+
const LAZY_SYSTEM_PROMPT_LITE = `## Lazy workflow (brief)
|
|
72
|
+
If this is a non-trivial task, follow: grill → specify → plan → build → review.
|
|
73
|
+
Available agents: @lazy-explorer, @lazy-oracle, @lazy-librarian, @lazy-designer, @lazy-fixer, @lazy-observer.
|
|
74
|
+
Delegate parallel work when possible.`;
|
|
75
|
+
// ponytail: inject PONYTAIL_MODE for ALL agents, LAZY_SYSTEM_PROMPT only for lazy primary.
|
|
76
|
+
// Guard double-injection for both.
|
|
77
|
+
export function createSystemTransformHook(runtime) {
|
|
78
|
+
return async (input, output) => {
|
|
79
|
+
// Always inject PONYTAIL_MODE for all agents
|
|
80
|
+
if (runtime?.config.ponytailMode !== false &&
|
|
81
|
+
!output.system.some((s) => s.includes("PONYTAIL MODE ACTIVE"))) {
|
|
82
|
+
if (output.system.length > 0) {
|
|
83
|
+
output.system[output.system.length - 1] += "\n\n" + PONYTAIL_MODE;
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
output.system.push(PONYTAIL_MODE);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// Additionally inject scope-governor prompt for lazy primary sessions only
|
|
90
|
+
const sid = input.sessionID;
|
|
91
|
+
if (!sid)
|
|
92
|
+
return;
|
|
93
|
+
const agentName = runtime?.sessionAgentMap.get(sid);
|
|
94
|
+
if (!agentName) {
|
|
95
|
+
// Race condition: chat.params may not have run yet. Inject lite prompt.
|
|
96
|
+
if (output.system.some((s) => s.includes("Lazy workflow")))
|
|
97
|
+
return;
|
|
98
|
+
output.system.push(LAZY_SYSTEM_PROMPT_LITE);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (agentName !== "lazy")
|
|
102
|
+
return;
|
|
103
|
+
// Guard against double injection
|
|
104
|
+
if (output.system[0]?.startsWith(LAZY_SYSTEM_PROMPT.slice(0, 30)))
|
|
105
|
+
return;
|
|
106
|
+
if (output.system.length > 0) {
|
|
107
|
+
output.system[0] = LAZY_SYSTEM_PROMPT + "\n\n" + output.system[0];
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
output.system.push(LAZY_SYSTEM_PROMPT);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task session management — track subagent dispatch via BackgroundJobBoard.
|
|
3
|
+
*
|
|
4
|
+
* Matches slim's full state machine:
|
|
5
|
+
* - Launch registration (tool.execute.before)
|
|
6
|
+
* - Completion parsing (tool.execute.after)
|
|
7
|
+
* - Context file accumulation (tool.execute.after read)
|
|
8
|
+
* - Late-cancel normalization
|
|
9
|
+
*/
|
|
10
|
+
import type { LazyRuntime } from "./runtime.js";
|
|
11
|
+
interface ToolBeforeInput {
|
|
12
|
+
tool: string;
|
|
13
|
+
sessionID: string;
|
|
14
|
+
callID: string;
|
|
15
|
+
}
|
|
16
|
+
interface ToolBeforeOutput {
|
|
17
|
+
args: Record<string, unknown>;
|
|
18
|
+
}
|
|
19
|
+
interface ToolAfterInput {
|
|
20
|
+
tool: string;
|
|
21
|
+
sessionID: string;
|
|
22
|
+
callID: string;
|
|
23
|
+
args: Record<string, unknown>;
|
|
24
|
+
}
|
|
25
|
+
interface ToolAfterOutput {
|
|
26
|
+
title: string;
|
|
27
|
+
output: string;
|
|
28
|
+
metadata: Record<string, unknown>;
|
|
29
|
+
}
|
|
30
|
+
export declare function createTaskSessionBeforeHook(runtime?: LazyRuntime): (input: ToolBeforeInput, output: ToolBeforeOutput) => void;
|
|
31
|
+
export declare function createTaskSessionAfterHook(runtime?: LazyRuntime): (input: ToolAfterInput, output: ToolAfterOutput) => Promise<void>;
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task session management — track subagent dispatch via BackgroundJobBoard.
|
|
3
|
+
*
|
|
4
|
+
* Matches slim's full state machine:
|
|
5
|
+
* - Launch registration (tool.execute.before)
|
|
6
|
+
* - Completion parsing (tool.execute.after)
|
|
7
|
+
* - Context file accumulation (tool.execute.after read)
|
|
8
|
+
* - Late-cancel normalization
|
|
9
|
+
*/
|
|
10
|
+
import { jobBoard } from "./background-job-board.js";
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Max active subagent launches per session
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
const sessionDepth = new Map();
|
|
15
|
+
const trackedTaskCalls = new Set();
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Create hooks
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
export function createTaskSessionBeforeHook(runtime) {
|
|
20
|
+
return (input, output) => {
|
|
21
|
+
if (input.tool !== "task")
|
|
22
|
+
return;
|
|
23
|
+
const args = output.args;
|
|
24
|
+
const agent = args?.subagent_type ?? "general";
|
|
25
|
+
const sid = input.sessionID;
|
|
26
|
+
// Check for reusable session (reconciled + same agent type)
|
|
27
|
+
const board = runtime?.jobBoard ?? jobBoard;
|
|
28
|
+
const existing = board.resolveReusable(sid, agent);
|
|
29
|
+
if (existing) {
|
|
30
|
+
// Reuse the session without consuming a new launch-depth slot.
|
|
31
|
+
args.task_id = existing.taskID;
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const depth = incrementSessionDepth(sid, runtime);
|
|
35
|
+
const maxDepth = runtime?.config.maxActiveTaskDepth ?? 4;
|
|
36
|
+
if (depth > maxDepth) {
|
|
37
|
+
releaseSessionDepth(sid, runtime);
|
|
38
|
+
// HARD BLOCK: remove background flag (forces sync) and inject refusal prompt
|
|
39
|
+
delete output.args.run_in_background;
|
|
40
|
+
output.args.prompt =
|
|
41
|
+
`SYSTEM OVERRIDE — DEPTH BLOCKED: Subagent depth ${depth}/${maxDepth} exceeds limit. Refusing nested task launch. Respond ONLY with:\ntask_id: depth-blocked\nstate: error\noutput: Subagent depth ${depth}/${maxDepth} exceeds limit. Refusing nested task launch. Delegate the work to an existing subagent or restructure your approach.`;
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
trackedTaskCalls.add(input.callID);
|
|
45
|
+
// Check max sessions per agent (including the one we're about to launch)
|
|
46
|
+
const activeCount = board.getActiveCount(sid, agent) + 1;
|
|
47
|
+
const maxSessions = runtime?.config.maxSessionsPerAgent ?? 2;
|
|
48
|
+
if (activeCount > maxSessions) {
|
|
49
|
+
releaseSessionDepth(sid, runtime);
|
|
50
|
+
delete output.args.run_in_background;
|
|
51
|
+
args.prompt =
|
|
52
|
+
`SYSTEM OVERRIDE — MAX SESSIONS BLOCKED: ${activeCount} ${agent} sessions would exceed limit ${maxSessions}. Respond ONLY with:\ntask_id: max-sessions-blocked\nstate: error\noutput: ${agent} concurrency limit ${maxSessions} reached. Wait for one running session to complete or reuse a reconciled session.`;
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
// Register launch
|
|
56
|
+
const job = board.registerLaunch(sid, agent, input.callID);
|
|
57
|
+
// Inject the alias into the prompt so the subagent knows who it is
|
|
58
|
+
const prompt = args.prompt ?? "";
|
|
59
|
+
args.prompt = `${prompt}\n\n[background-job-alias: ${job.alias}]`;
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
export function createTaskSessionAfterHook(runtime) {
|
|
63
|
+
return async (input, output) => {
|
|
64
|
+
const { tool, sessionID, callID } = input;
|
|
65
|
+
// --- Parse task tool completion ---
|
|
66
|
+
if (tool === "task") {
|
|
67
|
+
try {
|
|
68
|
+
parseTaskOutput(callID, output.output, runtime);
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
if (trackedTaskCalls.delete(callID)) {
|
|
72
|
+
releaseSessionDepth(sessionID, runtime);
|
|
73
|
+
}
|
|
74
|
+
await runtime?.save();
|
|
75
|
+
}
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
// --- Accumulate context from read tool ---
|
|
79
|
+
// Only accumulate when exactly one job is running — with multiple jobs
|
|
80
|
+
// we can't determine which one triggered the Read. ponytail: track
|
|
81
|
+
// callID→taskID mapping in registerLaunch if multi-read accuracy matters.
|
|
82
|
+
if (tool === "Read") {
|
|
83
|
+
const filePath = input.args?.filePath;
|
|
84
|
+
if (!filePath)
|
|
85
|
+
return;
|
|
86
|
+
const running = (runtime?.jobBoard ?? jobBoard).getRunningJobs(sessionID);
|
|
87
|
+
if (running.length === 1) {
|
|
88
|
+
const lineCount = extractLineCount(output.output);
|
|
89
|
+
(runtime?.jobBoard ?? jobBoard).addContext(running[0].taskID, {
|
|
90
|
+
path: filePath,
|
|
91
|
+
lineCount,
|
|
92
|
+
});
|
|
93
|
+
await runtime?.save();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function incrementSessionDepth(sessionID, runtime) {
|
|
99
|
+
const map = runtime?.sessionDepth ?? sessionDepth;
|
|
100
|
+
// Prune to prevent memory leaks
|
|
101
|
+
if (map.size > 1000) {
|
|
102
|
+
const firstKey = map.keys().next().value;
|
|
103
|
+
map.delete(firstKey);
|
|
104
|
+
}
|
|
105
|
+
const depth = (map.get(sessionID) ?? 0) + 1;
|
|
106
|
+
map.set(sessionID, depth);
|
|
107
|
+
return depth;
|
|
108
|
+
}
|
|
109
|
+
function releaseSessionDepth(sessionID, runtime) {
|
|
110
|
+
const map = runtime?.sessionDepth ?? sessionDepth;
|
|
111
|
+
const depth = map.get(sessionID);
|
|
112
|
+
if (!depth)
|
|
113
|
+
return;
|
|
114
|
+
if (depth <= 1) {
|
|
115
|
+
map.delete(sessionID);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
map.set(sessionID, depth - 1);
|
|
119
|
+
}
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// Helpers
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
/**
|
|
124
|
+
* Parse task tool output to extract task_id + state.
|
|
125
|
+
*/
|
|
126
|
+
function parseTaskOutput(callID, output, runtime) {
|
|
127
|
+
const board = runtime?.jobBoard ?? jobBoard;
|
|
128
|
+
const taskID = extractTaskID(output);
|
|
129
|
+
const state = extractState(output);
|
|
130
|
+
const summary = extractSummary(output);
|
|
131
|
+
// Late-cancel check
|
|
132
|
+
if (board.isLateCancelledTaskError(callID) && state === "error") {
|
|
133
|
+
board.updateStatus(callID, taskID ?? callID, "cancelled");
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (state && taskID) {
|
|
137
|
+
board.updateStatus(callID, taskID, state, summary);
|
|
138
|
+
}
|
|
139
|
+
else if (state) {
|
|
140
|
+
// Use callID as fallback taskID
|
|
141
|
+
board.updateStatus(callID, callID, state, summary);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function extractTaskID(output) {
|
|
145
|
+
const match = output.match(/task_id:\s*(\S+)/);
|
|
146
|
+
return match ? match[1] : null;
|
|
147
|
+
}
|
|
148
|
+
function extractState(output) {
|
|
149
|
+
if (output.includes("state: cancelled"))
|
|
150
|
+
return "cancelled";
|
|
151
|
+
if (output.includes("state: error"))
|
|
152
|
+
return "error";
|
|
153
|
+
if (output.includes("state: completed"))
|
|
154
|
+
return "completed";
|
|
155
|
+
// Heuristic based on output
|
|
156
|
+
if (/error|fail|crash/i.test(output.slice(0, 200)))
|
|
157
|
+
return "error";
|
|
158
|
+
if (/completed|done|finished/i.test(output.slice(0, 200)))
|
|
159
|
+
return "completed";
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
function extractSummary(output) {
|
|
163
|
+
// First non-empty line after "result:" or "summary:" or the first output line
|
|
164
|
+
const lines = output.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
165
|
+
if (lines.length === 0)
|
|
166
|
+
return undefined;
|
|
167
|
+
const resultIdx = lines.findIndex((l) => /^(result|summary|output)\s*:/i.test(l));
|
|
168
|
+
if (resultIdx >= 0 && resultIdx + 1 < lines.length) {
|
|
169
|
+
return lines[resultIdx + 1];
|
|
170
|
+
}
|
|
171
|
+
// Fallback: first line up to 200 chars
|
|
172
|
+
return lines[0].slice(0, 200);
|
|
173
|
+
}
|
|
174
|
+
function extractLineCount(output) {
|
|
175
|
+
const lines = output.split("\n").filter((l) => l.trim()).length;
|
|
176
|
+
return lines;
|
|
177
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type WorkflowLevel = "trivial" | "small" | "medium" | "high_risk" | "ambiguous";
|
|
2
|
+
export type WorkflowStage = "grill" | "specify" | "plan" | "build" | "review" | "simplify" | "debug";
|
|
3
|
+
export type LazyMode = "off" | "coach" | "governor" | "strict";
|
|
4
|
+
export interface WorkflowDecision {
|
|
5
|
+
level: WorkflowLevel;
|
|
6
|
+
action: "allow" | "nudge" | "block";
|
|
7
|
+
requiredStages: WorkflowStage[];
|
|
8
|
+
reason: string;
|
|
9
|
+
bypassedByUser: boolean;
|
|
10
|
+
suggestedCommand?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface ClassifierInput {
|
|
13
|
+
text: string;
|
|
14
|
+
mode?: LazyMode;
|
|
15
|
+
}
|
|
16
|
+
export declare function classifyWorkflow(input: ClassifierInput): WorkflowDecision;
|
|
17
|
+
export declare function formatWorkflowDecision(decision: WorkflowDecision): string;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
const BYPASS_PATTERNS = [
|
|
2
|
+
/\bjust do it\b/i,
|
|
3
|
+
/\bskip plan\b/i,
|
|
4
|
+
/\bno questions\b/i,
|
|
5
|
+
/\bdo it directly\b/i,
|
|
6
|
+
/直接做/,
|
|
7
|
+
/别问/,
|
|
8
|
+
/不用计划/,
|
|
9
|
+
/跳过计划/,
|
|
10
|
+
];
|
|
11
|
+
const HIGH_RISK_PATTERNS = [
|
|
12
|
+
/\bauth(entication|orization)?\b/i,
|
|
13
|
+
/\bsecurity\b/i,
|
|
14
|
+
/\bpermission(s)?\b/i,
|
|
15
|
+
/\bpayment(s)?\b/i,
|
|
16
|
+
/\bmigration\b/i,
|
|
17
|
+
/\bdelete\b.*\b(table|column|schema|migration|database|production|user|account|payment|index|role|permission|policy)\b/i,
|
|
18
|
+
/\bdrop\b.*\b(table|column|database|schema|index|view|role|user)\b/i,
|
|
19
|
+
/\bsecret(s)?\b/i,
|
|
20
|
+
/\btoken(s)?\b/i,
|
|
21
|
+
/\bproduction\b/i,
|
|
22
|
+
/\bdeploy(ment)?\b/i,
|
|
23
|
+
/\bconcurren(cy|t)\b/i,
|
|
24
|
+
/\bbroad refactor\b/i,
|
|
25
|
+
/权限/,
|
|
26
|
+
/认证/,
|
|
27
|
+
/安全/,
|
|
28
|
+
/支付/,
|
|
29
|
+
/迁移/,
|
|
30
|
+
/删除/,
|
|
31
|
+
/生产/,
|
|
32
|
+
/部署/,
|
|
33
|
+
/密钥/,
|
|
34
|
+
];
|
|
35
|
+
const AMBIGUOUS_PATTERNS = [
|
|
36
|
+
/\bmake (it )?better\b/i,
|
|
37
|
+
/\bclean up everything\b/i,
|
|
38
|
+
/\boptimi[sz]e (this|the project|everything)\b/i,
|
|
39
|
+
/\bfix this\b/i,
|
|
40
|
+
/全面优化/,
|
|
41
|
+
/全面改造/,
|
|
42
|
+
/优化一下/,
|
|
43
|
+
/改好一点/,
|
|
44
|
+
/整理一下/,
|
|
45
|
+
];
|
|
46
|
+
const MEDIUM_PATTERNS = [
|
|
47
|
+
/\bmulti[- ]file\b/i,
|
|
48
|
+
/\brefactor\b/i,
|
|
49
|
+
/\bimplement\b/i,
|
|
50
|
+
/\bfeature\b/i,
|
|
51
|
+
/\bchange behavior\b/i,
|
|
52
|
+
/\badd\b/i,
|
|
53
|
+
/\bbuild\b/i,
|
|
54
|
+
/实现/,
|
|
55
|
+
/新增/,
|
|
56
|
+
/重构/,
|
|
57
|
+
/多个文件/,
|
|
58
|
+
];
|
|
59
|
+
const TRIVIAL_PATTERNS = [
|
|
60
|
+
/\bwhat is\b/i,
|
|
61
|
+
/\bexplain\b/i,
|
|
62
|
+
/\bwhy\b/i,
|
|
63
|
+
/\btypo\b/i,
|
|
64
|
+
/\brename\b/i,
|
|
65
|
+
/\bone[- ]line\b/i,
|
|
66
|
+
/是什么/,
|
|
67
|
+
/解释/,
|
|
68
|
+
/为什么/,
|
|
69
|
+
/拼写/,
|
|
70
|
+
];
|
|
71
|
+
export function classifyWorkflow(input) {
|
|
72
|
+
const text = input.text.trim();
|
|
73
|
+
const mode = input.mode ?? "governor";
|
|
74
|
+
const bypassedByUser = BYPASS_PATTERNS.some((p) => p.test(text));
|
|
75
|
+
const level = detectLevel(text);
|
|
76
|
+
const requiredStages = stagesForLevel(level);
|
|
77
|
+
const action = chooseAction(level, mode, bypassedByUser);
|
|
78
|
+
const reason = reasonForLevel(level);
|
|
79
|
+
return {
|
|
80
|
+
level,
|
|
81
|
+
action,
|
|
82
|
+
requiredStages,
|
|
83
|
+
reason,
|
|
84
|
+
bypassedByUser,
|
|
85
|
+
suggestedCommand: suggestedCommand(level),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
export function formatWorkflowDecision(decision) {
|
|
89
|
+
if (decision.action === "allow") {
|
|
90
|
+
return `[Lazy scope: ${decision.level}. ${decision.reason} Next: ${decision.suggestedCommand ?? "proceed with ponytail discipline"}.]`;
|
|
91
|
+
}
|
|
92
|
+
if (decision.action === "nudge") {
|
|
93
|
+
return `[Lazy nudge: ${decision.level}. ${decision.reason} Suggested next step: ${decision.suggestedCommand}.]`;
|
|
94
|
+
}
|
|
95
|
+
return `[Lazy gate: ${decision.level}. ${decision.reason} Need scope, success criteria, and must-not-break list. Say "just do it" to bypass.]`;
|
|
96
|
+
}
|
|
97
|
+
function detectLevel(text) {
|
|
98
|
+
if (!text)
|
|
99
|
+
return "ambiguous";
|
|
100
|
+
if (HIGH_RISK_PATTERNS.some((p) => p.test(text)))
|
|
101
|
+
return "high_risk";
|
|
102
|
+
if (AMBIGUOUS_PATTERNS.some((p) => p.test(text)))
|
|
103
|
+
return "ambiguous";
|
|
104
|
+
if (MEDIUM_PATTERNS.some((p) => p.test(text)))
|
|
105
|
+
return "medium";
|
|
106
|
+
if (TRIVIAL_PATTERNS.some((p) => p.test(text)))
|
|
107
|
+
return "trivial";
|
|
108
|
+
if (text.length < 200 &&
|
|
109
|
+
/\b(why|what|how|when|where|does|is|are|can|could|should|would)\b.*\?/i.test(text))
|
|
110
|
+
return "trivial";
|
|
111
|
+
if (text.length < 80)
|
|
112
|
+
return "small";
|
|
113
|
+
return "medium";
|
|
114
|
+
}
|
|
115
|
+
function chooseAction(level, mode, bypassedByUser) {
|
|
116
|
+
if (mode === "off" || bypassedByUser)
|
|
117
|
+
return "allow";
|
|
118
|
+
if (mode === "coach")
|
|
119
|
+
return level === "trivial" || level === "small" ? "allow" : "nudge";
|
|
120
|
+
if (mode === "strict") {
|
|
121
|
+
return level === "medium" || level === "high_risk" || level === "ambiguous" ? "block" : "allow";
|
|
122
|
+
}
|
|
123
|
+
if (level === "high_risk" || level === "ambiguous")
|
|
124
|
+
return "block";
|
|
125
|
+
if (level === "medium")
|
|
126
|
+
return "nudge";
|
|
127
|
+
return "allow";
|
|
128
|
+
}
|
|
129
|
+
function stagesForLevel(level) {
|
|
130
|
+
switch (level) {
|
|
131
|
+
case "trivial":
|
|
132
|
+
return [];
|
|
133
|
+
case "small":
|
|
134
|
+
return ["build", "review"];
|
|
135
|
+
case "medium":
|
|
136
|
+
return ["plan", "build", "review", "simplify"];
|
|
137
|
+
case "high_risk":
|
|
138
|
+
return ["grill", "specify", "plan", "build", "review", "simplify"];
|
|
139
|
+
case "ambiguous":
|
|
140
|
+
return ["grill", "specify", "plan"];
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function reasonForLevel(level) {
|
|
144
|
+
switch (level) {
|
|
145
|
+
case "trivial":
|
|
146
|
+
return "Tiny or explanatory task; no workflow gate needed.";
|
|
147
|
+
case "small":
|
|
148
|
+
return "Bounded task; build and verify directly.";
|
|
149
|
+
case "medium":
|
|
150
|
+
return "Behavior change needs a short plan and review closure.";
|
|
151
|
+
case "high_risk":
|
|
152
|
+
return "Risky area needs alignment before implementation.";
|
|
153
|
+
case "ambiguous":
|
|
154
|
+
return "Broad or vague scope needs sharpening before implementation.";
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function suggestedCommand(level) {
|
|
158
|
+
switch (level) {
|
|
159
|
+
case "trivial":
|
|
160
|
+
return "answer directly";
|
|
161
|
+
case "small":
|
|
162
|
+
return "lazy/build";
|
|
163
|
+
case "medium":
|
|
164
|
+
return "lazy/plan";
|
|
165
|
+
case "high_risk":
|
|
166
|
+
return "lazy/grill";
|
|
167
|
+
case "ambiguous":
|
|
168
|
+
return "lazy/grill";
|
|
169
|
+
}
|
|
170
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Plugin } from "@opencode-ai/plugin";
|
|
2
|
+
import { LazyOpenCodeV2Plugin } from "./v2.js";
|
|
3
|
+
/**
|
|
4
|
+
* @lazyopencode/core — Governed team runtime for AI coding in OpenCode.
|
|
5
|
+
*
|
|
6
|
+
* One plugin. Zero config. Total takeover.
|
|
7
|
+
*
|
|
8
|
+
* Install: { "plugin": ["@lazyopencode/core"] }
|
|
9
|
+
*/
|
|
10
|
+
declare const LazyOpenCodePluginV1: Plugin;
|
|
11
|
+
declare const LazyOpenCodePlugin: Plugin;
|
|
12
|
+
export { LazyOpenCodePlugin, LazyOpenCodePluginV1, LazyOpenCodeV2Plugin };
|
|
13
|
+
export default LazyOpenCodeV2Plugin;
|