lazyopencode-core 0.0.3 → 0.0.4
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 +1 -1
- package/README.md +41 -5
- package/dist/agents/index.js +1 -1
- package/dist/agents/librarian.d.ts +1 -1
- package/dist/agents/librarian.js +2 -2
- package/dist/agents/oracle.d.ts +1 -1
- package/dist/agents/oracle.js +7 -0
- package/dist/hooks/lazy-command.js +21 -2
- package/dist/hooks/messages-transform.js +5 -2
- package/dist/hooks/runtime.d.ts +37 -4
- package/dist/hooks/runtime.js +123 -3
- package/dist/hooks/system-transform.js +6 -12
- package/dist/index.d.ts +3 -2
- package/dist/index.js +39 -16
- package/dist/opencode-control-plane.d.ts +16 -2
- package/dist/opencode-control-plane.js +241 -68
- package/dist/skills/lazy/debug/SKILL.md +1 -1
- package/dist/v2.js +1 -1
- package/docs/desktop-distribution.md +2 -2
- package/docs/opencode-integration.md +24 -6
- package/docs/positioning.md +2 -2
- package/docs/product-audit.md +4 -4
- package/docs/user-manual.md +36 -8
- package/docs/work-plan.md +5 -5
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -6,11 +6,11 @@ import { getSkillsDir } from "./skills/index.js";
|
|
|
6
6
|
import { createCancelTaskTool, createCouncilTool } from "./tools/index.js";
|
|
7
7
|
import { createOpenCodeControlPlane } from "./opencode-control-plane.js";
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
9
|
+
* lazyopencode-core — Governed team runtime for AI coding in OpenCode.
|
|
10
10
|
*
|
|
11
11
|
* One plugin. Zero config. Total takeover.
|
|
12
12
|
*
|
|
13
|
-
* Install: { "plugin": ["
|
|
13
|
+
* Install: { "plugin": ["lazyopencode-core"] }
|
|
14
14
|
*/
|
|
15
15
|
const LazyOpenCodePluginV1 = async (ctx) => {
|
|
16
16
|
const agents = createAgents();
|
|
@@ -19,7 +19,7 @@ const LazyOpenCodePluginV1 = async (ctx) => {
|
|
|
19
19
|
directory: ctx.directory,
|
|
20
20
|
worktree: ctx.worktree,
|
|
21
21
|
});
|
|
22
|
-
runtime.setControlPlane(createOpenCodeControlPlane(ctx.client));
|
|
22
|
+
runtime.setControlPlane(createOpenCodeControlPlane(ctx.client, ctx.directory));
|
|
23
23
|
const hooks = createHooks(runtime);
|
|
24
24
|
const councilTool = createCouncilTool(ctx.client, () => runtime.config.council, () => {
|
|
25
25
|
const council = runtime.config.council;
|
|
@@ -46,7 +46,7 @@ const LazyOpenCodePluginV1 = async (ctx) => {
|
|
|
46
46
|
const cfg = config;
|
|
47
47
|
runtime.configure(cfg.lazyopencode);
|
|
48
48
|
await runtime.load();
|
|
49
|
-
cfg.agent = mergeAgents(cfg.agent ?? {}, agents);
|
|
49
|
+
cfg.agent = mergeAgents(cfg.agent ?? {}, agents, runtime.config);
|
|
50
50
|
cfg.skills = cfg.skills || {};
|
|
51
51
|
const paths = cfg.skills.paths || [];
|
|
52
52
|
const skillsDir = getSkillsDir();
|
|
@@ -54,16 +54,7 @@ const LazyOpenCodePluginV1 = async (ctx) => {
|
|
|
54
54
|
paths.push(skillsDir);
|
|
55
55
|
}
|
|
56
56
|
cfg.skills.paths = paths;
|
|
57
|
-
|
|
58
|
-
const mcp = cfg.mcp;
|
|
59
|
-
if (!mcp?.context7) {
|
|
60
|
-
// deno-lint-ignore no-explicit-any
|
|
61
|
-
;
|
|
62
|
-
cfg.mcp = {
|
|
63
|
-
...(mcp || {}),
|
|
64
|
-
context7: { command: ["npx", "-y", "@agentdesk/context7-mcp"] },
|
|
65
|
-
};
|
|
66
|
-
}
|
|
57
|
+
maybeInjectContext7(cfg, runtime.config);
|
|
67
58
|
registerLazyCommands(cfg, runtime);
|
|
68
59
|
},
|
|
69
60
|
dispose: async () => {
|
|
@@ -73,12 +64,44 @@ const LazyOpenCodePluginV1 = async (ctx) => {
|
|
|
73
64
|
};
|
|
74
65
|
};
|
|
75
66
|
const LazyOpenCodePlugin = LazyOpenCodePluginV1;
|
|
67
|
+
export { LazyOpenCodeV2Plugin } from "./v2.js";
|
|
76
68
|
export { LazyOpenCodePlugin, LazyOpenCodePluginV1 };
|
|
77
69
|
export default LazyOpenCodePluginV1;
|
|
78
|
-
function mergeAgents(existing, lazyAgents) {
|
|
70
|
+
function mergeAgents(existing, lazyAgents, config) {
|
|
79
71
|
const merged = { ...existing };
|
|
80
72
|
for (const [name, defaults] of Object.entries(lazyAgents)) {
|
|
81
|
-
|
|
73
|
+
const profileModel = modelForAgent(name, defaults, config);
|
|
74
|
+
const lazyDefaults = profileModel ? { ...defaults, model: profileModel } : defaults;
|
|
75
|
+
merged[name] = { ...lazyDefaults, ...merged[name] };
|
|
82
76
|
}
|
|
83
77
|
return merged;
|
|
84
78
|
}
|
|
79
|
+
function modelForAgent(name, defaults, config) {
|
|
80
|
+
if (config.models.mode !== "profile")
|
|
81
|
+
return undefined;
|
|
82
|
+
if (config.models.byAgent[name])
|
|
83
|
+
return config.models.byAgent[name];
|
|
84
|
+
if (name === "lazy")
|
|
85
|
+
return config.models.primary;
|
|
86
|
+
if (name === "lazy-oracle")
|
|
87
|
+
return config.models.escalation.oracle ?? config.models.primary;
|
|
88
|
+
if (name === "lazy-councillor") {
|
|
89
|
+
return config.models.escalation.council ?? config.models.defaultSubagent;
|
|
90
|
+
}
|
|
91
|
+
if (defaults.mode === "subagent")
|
|
92
|
+
return config.models.defaultSubagent;
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
function maybeInjectContext7(cfg, config) {
|
|
96
|
+
if (config.opencode.context7 !== "inject")
|
|
97
|
+
return;
|
|
98
|
+
// deno-lint-ignore no-explicit-any
|
|
99
|
+
const mcp = cfg.mcp;
|
|
100
|
+
if (mcp?.context7)
|
|
101
|
+
return // deno-lint-ignore no-explicit-any
|
|
102
|
+
;
|
|
103
|
+
cfg.mcp = {
|
|
104
|
+
...(mcp || {}),
|
|
105
|
+
context7: { command: ["npx", "-y", "@agentdesk/context7-mcp"] },
|
|
106
|
+
};
|
|
107
|
+
}
|
|
@@ -1,20 +1,34 @@
|
|
|
1
|
+
export interface ModelProfileValidation {
|
|
2
|
+
currentModel: string;
|
|
3
|
+
availableModels: string[];
|
|
4
|
+
invalidModels: string[];
|
|
5
|
+
warnings: string[];
|
|
6
|
+
}
|
|
1
7
|
export interface OpenCodeControlPlaneSnapshot {
|
|
2
8
|
sessionStatus: string;
|
|
9
|
+
childSessions: number;
|
|
3
10
|
pendingPermissions: number;
|
|
4
11
|
todos: number;
|
|
5
12
|
diffSummary: string;
|
|
13
|
+
changedFiles: number;
|
|
6
14
|
worktree: string;
|
|
15
|
+
currentModel: string;
|
|
16
|
+
availableModels: string[];
|
|
7
17
|
capabilities: string[];
|
|
18
|
+
warnings: string[];
|
|
8
19
|
}
|
|
9
20
|
export interface OpenCodeControlPlane {
|
|
10
21
|
snapshot(sessionID?: string): Promise<OpenCodeControlPlaneSnapshot>;
|
|
22
|
+
validateModels(models: string[]): Promise<ModelProfileValidation>;
|
|
11
23
|
wait(sessionID: string): Promise<{
|
|
12
24
|
ok: boolean;
|
|
13
25
|
reason?: string;
|
|
14
26
|
}>;
|
|
15
|
-
revert(
|
|
27
|
+
revert(sessionID: string, messageID?: string): Promise<{
|
|
16
28
|
ok: boolean;
|
|
17
29
|
reason?: string;
|
|
18
30
|
}>;
|
|
31
|
+
log(level: "debug" | "info" | "warn" | "error", message: string, metadata?: unknown): Promise<void>;
|
|
32
|
+
notify(kind: "info" | "warn" | "error", message: string): Promise<void>;
|
|
19
33
|
}
|
|
20
|
-
export declare function createOpenCodeControlPlane(client: unknown): OpenCodeControlPlane;
|
|
34
|
+
export declare function createOpenCodeControlPlane(client: unknown, directory?: string): OpenCodeControlPlane;
|
|
@@ -1,95 +1,268 @@
|
|
|
1
|
-
export function createOpenCodeControlPlane(client) {
|
|
1
|
+
export function createOpenCodeControlPlane(client, directory) {
|
|
2
2
|
const c = (client ?? {});
|
|
3
3
|
return {
|
|
4
4
|
async snapshot(sessionID) {
|
|
5
|
+
const warnings = [];
|
|
5
6
|
const capabilities = detectCapabilities(c);
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
|
|
7
|
+
const session = asRecord(c.session);
|
|
8
|
+
const v2Session = asRecord(asRecord(c.v2)?.session);
|
|
9
|
+
const v2Permission = asRecord(v2Session?.permission);
|
|
10
|
+
const status = await callSdk(session, "status", [{ directory }], warnings);
|
|
11
|
+
const sessionInfo = sessionID
|
|
12
|
+
? await callSdk(session, "get", [{ sessionID, directory }], warnings)
|
|
13
|
+
: undefined;
|
|
14
|
+
const children = sessionID
|
|
15
|
+
? await callSdk(session, "children", [{ sessionID, directory }], warnings)
|
|
16
|
+
: undefined;
|
|
17
|
+
const todos = sessionID
|
|
18
|
+
? await callSdk(session, "todo", [{ sessionID, directory }], warnings)
|
|
19
|
+
: undefined;
|
|
20
|
+
const diff = sessionID
|
|
21
|
+
? await callSdk(session, "diff", [{ sessionID, directory }], warnings)
|
|
22
|
+
: undefined;
|
|
23
|
+
const permissions = sessionID
|
|
24
|
+
? await callSdk(v2Permission, "list", [{ sessionID }], warnings)
|
|
25
|
+
: undefined;
|
|
26
|
+
const providers = await getProviderModels(c, directory, warnings);
|
|
27
|
+
const fileStatus = await getFileStatus(c, directory, warnings);
|
|
28
|
+
return {
|
|
29
|
+
sessionStatus: extractSessionStatus(status, sessionInfo),
|
|
30
|
+
childSessions: countItems(children),
|
|
31
|
+
pendingPermissions: countItems(permissions),
|
|
32
|
+
todos: countItems(todos),
|
|
33
|
+
diffSummary: summarizeDiff(diff),
|
|
34
|
+
changedFiles: fileStatus.changedFiles,
|
|
35
|
+
worktree: extractWorktree(sessionInfo) ?? directory ?? "unknown",
|
|
36
|
+
currentModel: providers.currentModel,
|
|
37
|
+
availableModels: providers.availableModels,
|
|
38
|
+
capabilities,
|
|
39
|
+
warnings: unique(warnings),
|
|
40
|
+
};
|
|
41
|
+
},
|
|
42
|
+
async validateModels(models) {
|
|
43
|
+
const warnings = [];
|
|
44
|
+
const providers = await getProviderModels(c, directory, warnings);
|
|
45
|
+
const available = new Set(providers.availableModels);
|
|
46
|
+
const invalidModels = models
|
|
47
|
+
.filter(Boolean)
|
|
48
|
+
.filter((model) => !isProviderModel(model) || (available.size > 0 && !available.has(model)));
|
|
49
|
+
return {
|
|
50
|
+
currentModel: providers.currentModel,
|
|
51
|
+
availableModels: providers.availableModels,
|
|
52
|
+
invalidModels,
|
|
53
|
+
warnings: unique(warnings),
|
|
54
|
+
};
|
|
12
55
|
},
|
|
13
56
|
async wait(sessionID) {
|
|
14
|
-
|
|
57
|
+
const warnings = [];
|
|
58
|
+
const session = asRecord(c.session);
|
|
59
|
+
const v2Session = asRecord(asRecord(c.v2)?.session);
|
|
60
|
+
const value = await callSdk(v2Session, "wait", [{ sessionID }], warnings) ??
|
|
61
|
+
await callSdk(session, "wait", [{ sessionID, directory }], warnings);
|
|
62
|
+
if (value !== undefined)
|
|
63
|
+
return { ok: true };
|
|
64
|
+
return { ok: false, reason: warnings.join("; ") || "session.wait unavailable" };
|
|
65
|
+
},
|
|
66
|
+
async revert(sessionID, messageID) {
|
|
67
|
+
const warnings = [];
|
|
68
|
+
const session = asRecord(c.session);
|
|
69
|
+
const value = await callSdk(session, "revert", [{ sessionID, messageID, directory }], warnings);
|
|
70
|
+
if (value !== undefined)
|
|
71
|
+
return { ok: true };
|
|
72
|
+
return { ok: false, reason: warnings.join("; ") || "session.revert unavailable" };
|
|
73
|
+
},
|
|
74
|
+
async log(level, message, metadata) {
|
|
75
|
+
const app = asRecord(c.app);
|
|
76
|
+
const global = asRecord(c.global);
|
|
77
|
+
await callSdk(app, "log", [{ level, message, metadata }], []) ??
|
|
78
|
+
await callSdk(global, "log", [{ level, message, metadata }], []);
|
|
15
79
|
},
|
|
16
|
-
async
|
|
17
|
-
|
|
80
|
+
async notify(kind, message) {
|
|
81
|
+
const tui = asRecord(c.tui);
|
|
82
|
+
await callSdk(tui, "showToast", [{ type: kind, message }], []) ??
|
|
83
|
+
await callSdk(tui, "publish", [{ body: { type: "toast.show", variant: kind, message } }], []);
|
|
18
84
|
},
|
|
19
85
|
};
|
|
20
86
|
}
|
|
21
87
|
function detectCapabilities(client) {
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
["
|
|
29
|
-
["
|
|
30
|
-
["
|
|
31
|
-
["
|
|
32
|
-
["
|
|
88
|
+
const session = asRecord(client.session);
|
|
89
|
+
const v2 = asRecord(client.v2);
|
|
90
|
+
const v2Session = asRecord(v2?.session);
|
|
91
|
+
const v2Permission = asRecord(v2Session?.permission);
|
|
92
|
+
const fs = asRecord(v2?.fs);
|
|
93
|
+
const groups = [
|
|
94
|
+
["session.status", session?.status],
|
|
95
|
+
["session.get", session?.get],
|
|
96
|
+
["session.children", session?.children],
|
|
97
|
+
["session.todo", session?.todo],
|
|
98
|
+
["session.diff", session?.diff],
|
|
99
|
+
["session.messages", session?.messages],
|
|
100
|
+
["session.wait", session?.wait ?? v2Session?.wait],
|
|
101
|
+
["session.revert", session?.revert],
|
|
102
|
+
["v2.session.context", v2Session?.context],
|
|
103
|
+
["v2.session.permission", v2Permission?.list],
|
|
104
|
+
["config.get", asRecord(client.config)?.get],
|
|
105
|
+
["config.providers", asRecord(client.config)?.providers],
|
|
106
|
+
["provider.list", asRecord(client.provider)?.list],
|
|
107
|
+
["file.status", asRecord(client.file)?.status],
|
|
108
|
+
["find.files", asRecord(client.find)?.files ?? fs?.find],
|
|
109
|
+
["app.log", asRecord(client.app)?.log ?? asRecord(client.global)?.log],
|
|
110
|
+
["tui.showToast", asRecord(client.tui)?.showToast],
|
|
33
111
|
];
|
|
34
|
-
return
|
|
35
|
-
.filter((group) => group.some((name) => typeof client[name] === "function"))
|
|
36
|
-
.map((group) => group[0]);
|
|
112
|
+
return groups.filter(([, fn]) => typeof fn === "function").map(([name]) => name);
|
|
37
113
|
}
|
|
38
|
-
async function
|
|
114
|
+
async function getProviderModels(client, directory, warnings) {
|
|
115
|
+
const config = asRecord(client.config);
|
|
116
|
+
const provider = asRecord(client.provider);
|
|
117
|
+
const configData = await callSdk(config, "get", [{ directory }], warnings);
|
|
118
|
+
const providerData = await callSdk(config, "providers", [{ directory }], warnings) ??
|
|
119
|
+
await callSdk(provider, "list", [{ directory }], warnings);
|
|
120
|
+
return {
|
|
121
|
+
currentModel: extractCurrentModel(configData),
|
|
122
|
+
availableModels: extractAvailableModels(providerData),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
async function getFileStatus(client, directory, warnings) {
|
|
126
|
+
const value = await callSdk(asRecord(client.file), "status", [{ directory }], warnings) ??
|
|
127
|
+
await callSdk(asRecord(client.vcs), "status", [{ directory }], warnings);
|
|
128
|
+
return { changedFiles: countItems(value) };
|
|
129
|
+
}
|
|
130
|
+
async function callSdk(target, name, args, warnings) {
|
|
131
|
+
const fn = target?.[name];
|
|
132
|
+
if (typeof fn !== "function")
|
|
133
|
+
return undefined;
|
|
39
134
|
try {
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
return value;
|
|
45
|
-
if (typeof value === "object") {
|
|
46
|
-
const record = value;
|
|
47
|
-
return String(record.summary ?? record.status ?? record.path ?? fallback);
|
|
135
|
+
const result = await fn(...args);
|
|
136
|
+
const unwrapped = unwrapResult(result);
|
|
137
|
+
if (isErrorResult(result)) {
|
|
138
|
+
warnings.push(`${name}: ${stringify(result.error)}`);
|
|
48
139
|
}
|
|
49
|
-
return
|
|
140
|
+
return unwrapped;
|
|
50
141
|
}
|
|
51
|
-
catch {
|
|
52
|
-
|
|
142
|
+
catch (error) {
|
|
143
|
+
warnings.push(`${name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
144
|
+
return undefined;
|
|
53
145
|
}
|
|
54
146
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (Array.isArray(value))
|
|
59
|
-
return value.length;
|
|
60
|
-
if (typeof value === "number")
|
|
61
|
-
return value;
|
|
62
|
-
if (value && typeof value === "object") {
|
|
63
|
-
const record = value;
|
|
64
|
-
if (typeof record.count === "number")
|
|
65
|
-
return record.count;
|
|
66
|
-
if (Array.isArray(record.items))
|
|
67
|
-
return record.items.length;
|
|
68
|
-
}
|
|
147
|
+
function unwrapResult(value) {
|
|
148
|
+
if (value && typeof value === "object" && ("data" in value || "error" in value)) {
|
|
149
|
+
return value.data;
|
|
69
150
|
}
|
|
70
|
-
|
|
151
|
+
return value;
|
|
152
|
+
}
|
|
153
|
+
function isErrorResult(value) {
|
|
154
|
+
return Boolean(value && typeof value === "object" && "error" in value && value.error);
|
|
155
|
+
}
|
|
156
|
+
function asRecord(value) {
|
|
157
|
+
return value && typeof value === "object" ? value : undefined;
|
|
158
|
+
}
|
|
159
|
+
function countItems(value) {
|
|
160
|
+
const data = unwrapResult(value);
|
|
161
|
+
if (Array.isArray(data))
|
|
162
|
+
return data.length;
|
|
163
|
+
if (typeof data === "number")
|
|
164
|
+
return data;
|
|
165
|
+
const record = asRecord(data);
|
|
166
|
+
if (!record)
|
|
71
167
|
return 0;
|
|
168
|
+
for (const key of ["count", "total"]) {
|
|
169
|
+
if (typeof record[key] === "number")
|
|
170
|
+
return record[key];
|
|
171
|
+
}
|
|
172
|
+
for (const key of ["items", "data", "children", "todos", "permissions", "requests", "files"]) {
|
|
173
|
+
if (Array.isArray(record[key]))
|
|
174
|
+
return record[key].length;
|
|
72
175
|
}
|
|
73
|
-
return
|
|
176
|
+
return Object.keys(record).length;
|
|
74
177
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
178
|
+
function extractSessionStatus(status, sessionInfo) {
|
|
179
|
+
const session = asRecord(sessionInfo);
|
|
180
|
+
const statusRecord = asRecord(status);
|
|
181
|
+
return String(session?.status ??
|
|
182
|
+
session?.state ??
|
|
183
|
+
statusRecord?.status ??
|
|
184
|
+
statusRecord?.state ??
|
|
185
|
+
(statusRecord && Object.keys(statusRecord).length > 0 ? "available" : "unknown"));
|
|
186
|
+
}
|
|
187
|
+
function summarizeDiff(value) {
|
|
188
|
+
const data = unwrapResult(value);
|
|
189
|
+
if (!data)
|
|
190
|
+
return "not collected";
|
|
191
|
+
if (typeof data === "string")
|
|
192
|
+
return data || "empty";
|
|
193
|
+
const record = asRecord(data);
|
|
194
|
+
if (!record)
|
|
195
|
+
return String(data);
|
|
196
|
+
const summary = record.summary ?? record.text ?? record.diff;
|
|
197
|
+
if (typeof summary === "string" && summary.trim())
|
|
198
|
+
return summary;
|
|
199
|
+
const changed = countItems(record);
|
|
200
|
+
return changed > 0 ? `${changed} changed file(s)` : "empty";
|
|
201
|
+
}
|
|
202
|
+
function extractWorktree(value) {
|
|
203
|
+
const record = asRecord(value);
|
|
204
|
+
return String(record?.worktree ?? record?.directory ?? record?.path ?? record?.location ?? "") ||
|
|
205
|
+
undefined;
|
|
206
|
+
}
|
|
207
|
+
function extractCurrentModel(value) {
|
|
208
|
+
const data = asRecord(unwrapResult(value));
|
|
209
|
+
const raw = data?.model ?? data?.default_model ?? data?.small_model;
|
|
210
|
+
if (typeof raw === "string")
|
|
211
|
+
return raw;
|
|
212
|
+
const model = asRecord(raw);
|
|
213
|
+
if (model)
|
|
214
|
+
return joinModel(model.providerID, model.modelID ?? model.id);
|
|
215
|
+
return "OpenCode selected model";
|
|
216
|
+
}
|
|
217
|
+
function extractAvailableModels(value) {
|
|
218
|
+
const data = unwrapResult(value);
|
|
219
|
+
const providers = Array.isArray(data)
|
|
220
|
+
? data
|
|
221
|
+
: asRecord(data)?.providers ?? asRecord(data)?.items ?? asRecord(data)?.data;
|
|
222
|
+
if (!Array.isArray(providers))
|
|
223
|
+
return [];
|
|
224
|
+
const models = [];
|
|
225
|
+
for (const provider of providers) {
|
|
226
|
+
const p = asRecord(provider);
|
|
227
|
+
const providerID = String(p?.id ?? p?.providerID ?? p?.name ?? "");
|
|
228
|
+
const modelList = p?.models;
|
|
229
|
+
if (Array.isArray(modelList)) {
|
|
230
|
+
for (const model of modelList) {
|
|
231
|
+
const m = asRecord(model);
|
|
232
|
+
const modelID = String(m?.id ?? m?.modelID ?? m?.name ?? "");
|
|
233
|
+
const joined = joinModel(providerID, modelID);
|
|
234
|
+
if (joined)
|
|
235
|
+
models.push(joined);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
else if (modelList && typeof modelList === "object") {
|
|
239
|
+
for (const modelID of Object.keys(modelList)) {
|
|
240
|
+
const joined = joinModel(providerID, modelID);
|
|
241
|
+
if (joined)
|
|
242
|
+
models.push(joined);
|
|
243
|
+
}
|
|
80
244
|
}
|
|
81
|
-
return { ok: true };
|
|
82
|
-
}
|
|
83
|
-
catch (error) {
|
|
84
|
-
return { ok: false, reason: error instanceof Error ? error.message : String(error) };
|
|
85
245
|
}
|
|
246
|
+
return unique(models);
|
|
86
247
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
248
|
+
function joinModel(providerID, modelID) {
|
|
249
|
+
const provider = String(providerID ?? "");
|
|
250
|
+
const model = String(modelID ?? "");
|
|
251
|
+
return provider && model ? `${provider}/${model}` : "";
|
|
252
|
+
}
|
|
253
|
+
function isProviderModel(value) {
|
|
254
|
+
return /^[^/\s]+\/[^/\s]+$/.test(value);
|
|
255
|
+
}
|
|
256
|
+
function stringify(value) {
|
|
257
|
+
if (typeof value === "string")
|
|
258
|
+
return value;
|
|
259
|
+
try {
|
|
260
|
+
return JSON.stringify(value);
|
|
93
261
|
}
|
|
94
|
-
|
|
262
|
+
catch {
|
|
263
|
+
return String(value);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
function unique(items) {
|
|
267
|
+
return Array.from(new Set(items.filter(Boolean)));
|
|
95
268
|
}
|
|
@@ -6,7 +6,7 @@ description: Systematic diagnosis loop for bugs where the cause is unclear.
|
|
|
6
6
|
## Process
|
|
7
7
|
|
|
8
8
|
1. **Reproduce.** Can you reliably trigger it? Gather exact steps/logs. If unreproducible, document environment/conditions.
|
|
9
|
-
2. **Isolate.** Narrow scope: what changed (git log/bisect)? Smallest triggering input? Boundary? If bug involves a library API, check current docs via
|
|
9
|
+
2. **Isolate.** Narrow scope: what changed (git log/bisect)? Smallest triggering input? Boundary? If bug involves a library API, check current docs via configured documentation tools.
|
|
10
10
|
3. **Hypothesize.** Form exactly one hypothesis. State it clearly.
|
|
11
11
|
4. **Test hypothesis.** Smallest test/log that proves/disproves it. Do NOT fix yet.
|
|
12
12
|
5. **Iterate.** Disproven → new hypothesis. Proven → go to 6. Stuck after 3 cycles → escalate to @lazy-oracle.
|
package/dist/v2.js
CHANGED
|
@@ -2,7 +2,7 @@ import { define } from "@opencode-ai/plugin/v2/promise";
|
|
|
2
2
|
import { createAgents } from "./agents/index.js";
|
|
3
3
|
import { getSkillsDir } from "./skills/index.js";
|
|
4
4
|
export const LazyOpenCodeV2Plugin = define({
|
|
5
|
-
id: "
|
|
5
|
+
id: "lazyopencode-core",
|
|
6
6
|
setup(context) {
|
|
7
7
|
const ctx = context;
|
|
8
8
|
ctx.agent?.transform((draft) => {
|
|
@@ -9,7 +9,7 @@ LazyOpenCode Desktop ships the governed defaults and health surface.
|
|
|
9
9
|
## Strategy
|
|
10
10
|
|
|
11
11
|
The plugin remains the source of truth. Desktop bundles and enables
|
|
12
|
-
|
|
12
|
+
`lazyopencode-core`; it does not duplicate LazyOpenCode governance logic.
|
|
13
13
|
|
|
14
14
|
## First-Run Defaults
|
|
15
15
|
|
|
@@ -17,7 +17,7 @@ Desktop should merge the defaults from
|
|
|
17
17
|
`apps/lazyopencode-desktop/lazyopencode.default.jsonc` into the user's generated
|
|
18
18
|
OpenCode config:
|
|
19
19
|
|
|
20
|
-
- Add
|
|
20
|
+
- Add `lazyopencode-core` to `plugin` if absent.
|
|
21
21
|
- Add `lazyopencode` defaults only where the user has not set values.
|
|
22
22
|
- Preserve provider, auth, model, MCP, session, and project settings.
|
|
23
23
|
|
|
@@ -3,12 +3,11 @@
|
|
|
3
3
|
LazyOpenCode is an OpenCode-native workflow governor. The plugin should work
|
|
4
4
|
with no `lazyopencode` config block; configuration only customizes defaults.
|
|
5
5
|
|
|
6
|
-
OpenCode loads `dist/index.js` from the npm package. The default export
|
|
7
|
-
|
|
8
|
-
The named `
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
and tool hooks.
|
|
6
|
+
OpenCode loads `dist/index.js` from the npm package. The default export remains
|
|
7
|
+
the legacy hook adapter because current governance depends on chat, message,
|
|
8
|
+
command, permission, and tool hooks. The named `LazyOpenCodeV2Plugin` export
|
|
9
|
+
keeps the v2 promise registration surface available for agents, commands,
|
|
10
|
+
skills, and references while v2 hook coverage matures.
|
|
12
11
|
|
|
13
12
|
## Hook Boundaries
|
|
14
13
|
|
|
@@ -22,6 +21,22 @@ and tool hooks.
|
|
|
22
21
|
- chat transforms: inject workflow guidance, job-board status, token-control
|
|
23
22
|
pruning, image redirects, and Ponytail behavior.
|
|
24
23
|
|
|
24
|
+
## SDK Control Plane
|
|
25
|
+
|
|
26
|
+
`0.0.4` uses the OpenCode SDK as a best-effort control plane for status, doctor,
|
|
27
|
+
and close reports. The adapter prefers real SDK groups such as `session`,
|
|
28
|
+
`config`, `file`, `find`, `app`, `tui`, and v2 session permission/fs APIs.
|
|
29
|
+
|
|
30
|
+
Collected evidence includes:
|
|
31
|
+
|
|
32
|
+
- session status, child sessions, todos, messages, diffs, wait, and revert
|
|
33
|
+
- pending session permissions from v2 permission APIs when available
|
|
34
|
+
- configured providers and models for model profile validation
|
|
35
|
+
- file status / changed file counts
|
|
36
|
+
- app logging and TUI notifications when available
|
|
37
|
+
|
|
38
|
+
Missing SDK APIs degrade to warnings. They should never block normal governance.
|
|
39
|
+
|
|
25
40
|
## Zero Config
|
|
26
41
|
|
|
27
42
|
Outside Desktop, users still install/load the plugin through OpenCode's normal
|
|
@@ -36,6 +51,9 @@ plugin mechanism. Once loaded, LazyOpenCode defaults are complete:
|
|
|
36
51
|
- `sdk.legacyHookAdapter: true`
|
|
37
52
|
- `takeover: "governed"`
|
|
38
53
|
- `opencode.worktreeIsolation: "risky-only"`
|
|
54
|
+
- `opencode.sdkControlPlane: true`
|
|
55
|
+
- `opencode.sdkTelemetry: true`
|
|
56
|
+
- `opencode.tuiNotifications: true`
|
|
39
57
|
- `closeReport.autoCollect: true`
|
|
40
58
|
|
|
41
59
|
## Config Merge Contract
|
package/docs/positioning.md
CHANGED
|
@@ -39,6 +39,6 @@ Autonomous coding assistants optimize for longer independent runs, memory, and
|
|
|
39
39
|
self-improvement loops. LazyOpenCode emphasizes governed OpenCode operation:
|
|
40
40
|
small scope, explicit risk gates, budget visibility, and boring closure.
|
|
41
41
|
|
|
42
|
-
Desktop is a later `0.1.0` task. The current `0.0.
|
|
43
|
-
|
|
42
|
+
Desktop is a later `0.1.0` task. The current `0.0.x` target is to make
|
|
43
|
+
`lazyopencode-core` a complete plugin kernel that a Desktop fork can preinstall
|
|
44
44
|
safely.
|
package/docs/product-audit.md
CHANGED
|
@@ -11,7 +11,7 @@ prompt toolbox. The product combines three things:
|
|
|
11
11
|
- A governance layer for scope, risk, budget, permissions, and closure.
|
|
12
12
|
- A future Desktop distribution that makes those defaults visible and easy.
|
|
13
13
|
|
|
14
|
-
The current `0.0.
|
|
14
|
+
The current `0.0.x` core is close to a usable kernel: commands, agents, skills,
|
|
15
15
|
runtime state, job board, council guard, token control, doctor output, and close
|
|
16
16
|
report all exist and pass verification. The next work is mostly deepening,
|
|
17
17
|
simplifying, and making the boundaries sharper.
|
|
@@ -73,7 +73,7 @@ Small scope, visible work, bounded context, reviewed output.
|
|
|
73
73
|
- Worktree isolation is advice/policy, not full automation.
|
|
74
74
|
- Doctor output checks the important basics, but should become a deeper module
|
|
75
75
|
with structured checks.
|
|
76
|
-
- Desktop is intentionally not implemented in `0.0.
|
|
76
|
+
- Desktop is intentionally not implemented in `0.0.x`.
|
|
77
77
|
|
|
78
78
|
## Project Design
|
|
79
79
|
|
|
@@ -171,12 +171,12 @@ but adds governance and closure.
|
|
|
171
171
|
Compared with autonomous coding assistants, LazyOpenCode stays OpenCode-native
|
|
172
172
|
and prioritizes controlled engineering workflow over long independent runs.
|
|
173
173
|
|
|
174
|
-
Compared with future Desktop distributions,
|
|
174
|
+
Compared with future Desktop distributions, `lazyopencode-core` is the source
|
|
175
175
|
of truth. Desktop should package and visualize core behavior, not duplicate it.
|
|
176
176
|
|
|
177
177
|
## Release Readiness
|
|
178
178
|
|
|
179
|
-
`0.0.
|
|
179
|
+
`0.0.4` is releaseable once these stay true:
|
|
180
180
|
|
|
181
181
|
- `npm run verify` passes.
|
|
182
182
|
- README and docs use governed team runtime positioning.
|