@heretek-ai/epistemic-swarm 1.1.4 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/tui.js +493 -247
- package/package.json +7 -4
- package/src/agents.ts +77 -6
- package/src/approve-tui.ts +124 -0
- package/src/hooks.ts +15 -3
- package/src/masked-input.tsx +123 -0
- package/src/panels.tsx +7 -2
- package/src/policy.ts +28 -4
- package/src/pr.ts +17 -13
- package/src/research.ts +9 -2
- package/src/rpc-def.ts +30 -9
- package/src/rpc.ts +40 -12
- package/src/runtime.ts +22 -5
- package/src/seats.ts +28 -1
- package/src/server.ts +384 -245
- package/src/tools.ts +5 -1
- package/src/tui.tsx +61 -8
package/src/server.ts
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
// Epistemic Swarm for OpenCode v2 (server plugin). Additive: registers our
|
|
2
2
|
// agents, tools, skills, commands, hooks and RPC through runtime transforms;
|
|
3
|
-
// writes no
|
|
3
|
+
// setup writes no project-tree files and no config files (the engine key goes
|
|
4
|
+
// to the masked state dir; spend and seat tracking later write
|
|
5
|
+
// .factory/runtime/* while sessions run). It never touches built-in agents or
|
|
6
|
+
// defaults.
|
|
4
7
|
import { readdir, readFile } from "node:fs/promises"
|
|
5
8
|
import path from "node:path"
|
|
6
9
|
import {
|
|
7
10
|
describeTarget,
|
|
8
11
|
ensureEngineKey,
|
|
9
12
|
factoryLayout,
|
|
13
|
+
flag,
|
|
10
14
|
formatReport,
|
|
11
15
|
git,
|
|
12
16
|
livenessLines,
|
|
13
17
|
loadSkills,
|
|
18
|
+
parseArgs,
|
|
14
19
|
parseAuditTarget,
|
|
15
20
|
readJson,
|
|
16
21
|
readLiveness,
|
|
@@ -18,20 +23,373 @@ import {
|
|
|
18
23
|
runGates,
|
|
19
24
|
} from "@heretek-ai/es-core"
|
|
20
25
|
import { Plugin } from "@opencode/plugin"
|
|
21
|
-
import { compileAgents } from "./agents.ts"
|
|
26
|
+
import { compileAgents, modelProblems } from "./agents.ts"
|
|
22
27
|
import { createFactoryContinuation } from "./continue.ts"
|
|
23
28
|
import { createHookBridge } from "./hooks.ts"
|
|
24
29
|
import { createPolicyHooks } from "./policy.ts"
|
|
25
30
|
import { createWebCache, PendingSearches, registerWebsearch } from "./research.ts"
|
|
26
31
|
import { createRpcHandlers } from "./rpc.ts"
|
|
27
32
|
import { EsRpc } from "./rpc-def.ts"
|
|
28
|
-
import { createRuntime, parseOptions } from "./runtime.ts"
|
|
33
|
+
import { createRuntime, parseOptions, type Runtime } from "./runtime.ts"
|
|
29
34
|
import { createSeatTracker } from "./seats.ts"
|
|
30
35
|
import { createSessionHooks, createSpendTracker } from "./session.ts"
|
|
31
36
|
import { registerTools } from "./tools.ts"
|
|
32
37
|
|
|
33
38
|
export const PLUGIN_ID = "epistemic-swarm"
|
|
34
39
|
|
|
40
|
+
/** `/research deep <question> --max-usd N`, split with the core CLI grammar (no command-local regex). */
|
|
41
|
+
export type DeepResearchArgs = { query: string; ceiling: string } | { error: "usage" | "ceiling" }
|
|
42
|
+
|
|
43
|
+
export function parseDeepResearchArgs(text: string): DeepResearchArgs {
|
|
44
|
+
const argv = parseArgs(text.split(/\s+/).filter(Boolean))
|
|
45
|
+
const [head, ...rest] = argv.positionals
|
|
46
|
+
const query = rest.join(" ").trim()
|
|
47
|
+
if (head?.toLowerCase() !== "deep" || query === "") return { error: "usage" }
|
|
48
|
+
const ceiling = flag(argv, "max-usd")
|
|
49
|
+
if (ceiling === undefined || !(Number(ceiling) > 0)) return { error: "ceiling" }
|
|
50
|
+
return { query, ceiling }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The session surface slash commands use (the host's `ctx.session`; stubbed in tests). */
|
|
54
|
+
export interface CommandSession {
|
|
55
|
+
switchAgent(sessionID: string, agent: string): Promise<unknown>
|
|
56
|
+
sendPrompt(sessionID: string, prompt: Record<string, unknown>, text: string, delivery: unknown): Promise<unknown>
|
|
57
|
+
synthetic(sessionID: string, text: string): Promise<unknown>
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** One slash command: its name, description and host-independent body. */
|
|
61
|
+
export interface CommandSpec {
|
|
62
|
+
readonly name: string
|
|
63
|
+
readonly description: string
|
|
64
|
+
readonly execute: (args: {
|
|
65
|
+
readonly sessionID: string
|
|
66
|
+
readonly prompt: { readonly text?: string } & Record<string, unknown>
|
|
67
|
+
readonly delivery?: unknown
|
|
68
|
+
}) => Promise<void>
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The hook-bridge surface slash commands read (status for `/hooks`). */
|
|
72
|
+
export interface CommandBridge {
|
|
73
|
+
readonly engine: {
|
|
74
|
+
refresh(): Promise<void>
|
|
75
|
+
status(): {
|
|
76
|
+
readonly handlers: number
|
|
77
|
+
readonly projectHandlers: number
|
|
78
|
+
readonly trusted: boolean
|
|
79
|
+
readonly projectLines: readonly string[]
|
|
80
|
+
readonly diagnostics: readonly string[]
|
|
81
|
+
}
|
|
82
|
+
readonly recent: readonly string[]
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Every slash command the plugin registers, as data: the host loop below
|
|
88
|
+
* adds each one, and tests invoke each body against a stub session to prove
|
|
89
|
+
* routing (seat/verb per command) and name uniqueness (one `research`).
|
|
90
|
+
*/
|
|
91
|
+
export function esCommandSpecs(
|
|
92
|
+
io: CommandSession,
|
|
93
|
+
runtime: Runtime,
|
|
94
|
+
bridge: CommandBridge,
|
|
95
|
+
runners: { readonly runGates?: typeof runGates; readonly git?: typeof git } = {},
|
|
96
|
+
): CommandSpec[] {
|
|
97
|
+
const specs: CommandSpec[] = []
|
|
98
|
+
const gates = runners.runGates ?? runGates
|
|
99
|
+
const gitStatus = runners.git ?? git
|
|
100
|
+
specs.push({
|
|
101
|
+
name: "grill",
|
|
102
|
+
description: "Grill an idea until the design and spend ceiling are settled (Epistemic Swarm)",
|
|
103
|
+
execute: async ({ sessionID, prompt, delivery }) => {
|
|
104
|
+
await io.switchAgent(sessionID, "grill")
|
|
105
|
+
await io.sendPrompt(
|
|
106
|
+
sessionID,
|
|
107
|
+
prompt,
|
|
108
|
+
prompt.text?.trim() ? `Grill me on: ${prompt.text}` : "Start grilling me on my idea.",
|
|
109
|
+
delivery,
|
|
110
|
+
)
|
|
111
|
+
},
|
|
112
|
+
})
|
|
113
|
+
specs.push({
|
|
114
|
+
name: "brainstorm",
|
|
115
|
+
description: "Fan out divergent lenses on an idea and return a diversified shortlist (Epistemic Swarm)",
|
|
116
|
+
execute: async ({ sessionID, prompt, delivery }) => {
|
|
117
|
+
await io.switchAgent(sessionID, "brainstormer")
|
|
118
|
+
await io.sendPrompt(
|
|
119
|
+
sessionID,
|
|
120
|
+
prompt,
|
|
121
|
+
prompt.text?.trim()
|
|
122
|
+
? `Brainstorm: ${prompt.text}`
|
|
123
|
+
: "Ask me for the idea to brainstorm (one sentence), then run the lens fan-out.",
|
|
124
|
+
delivery,
|
|
125
|
+
)
|
|
126
|
+
},
|
|
127
|
+
})
|
|
128
|
+
specs.push({
|
|
129
|
+
name: "harvest",
|
|
130
|
+
description: "Tear down competitor projects: licences, feature matrix, clean-room specs (Epistemic Swarm)",
|
|
131
|
+
execute: async ({ sessionID, prompt, delivery }) => {
|
|
132
|
+
await io.switchAgent(sessionID, "harvester")
|
|
133
|
+
await io.sendPrompt(
|
|
134
|
+
sessionID,
|
|
135
|
+
prompt,
|
|
136
|
+
prompt.text?.trim()
|
|
137
|
+
? `Darkharvest: ${prompt.text}`
|
|
138
|
+
: "Ask me for the teardown objective and the candidate projects, then run the harvest.",
|
|
139
|
+
delivery,
|
|
140
|
+
)
|
|
141
|
+
},
|
|
142
|
+
})
|
|
143
|
+
specs.push({
|
|
144
|
+
name: "design",
|
|
145
|
+
description: "Interview me into a design system: tokens, contrast gates, style guide (Epistemic Swarm)",
|
|
146
|
+
execute: async ({ sessionID, prompt, delivery }) => {
|
|
147
|
+
await io.switchAgent(sessionID, "designer")
|
|
148
|
+
await io.sendPrompt(
|
|
149
|
+
sessionID,
|
|
150
|
+
prompt,
|
|
151
|
+
prompt.text?.trim()
|
|
152
|
+
? `Run the design interview. Product: ${prompt.text}`
|
|
153
|
+
: "Ask me for the product name and start the design interview, one question at a time.",
|
|
154
|
+
delivery,
|
|
155
|
+
)
|
|
156
|
+
},
|
|
157
|
+
})
|
|
158
|
+
specs.push({
|
|
159
|
+
name: "scout",
|
|
160
|
+
description: "Find and vet open-source candidates for a feature: licenses, maintenance, CVEs (Epistemic Swarm)",
|
|
161
|
+
execute: async ({ sessionID, prompt, delivery }) => {
|
|
162
|
+
await io.switchAgent(sessionID, "scout")
|
|
163
|
+
await io.sendPrompt(
|
|
164
|
+
sessionID,
|
|
165
|
+
prompt,
|
|
166
|
+
prompt.text?.trim()
|
|
167
|
+
? `Scout: ${prompt.text}`
|
|
168
|
+
: "Ask me for the feature to scout and our license posture, then plan the candidates.",
|
|
169
|
+
delivery,
|
|
170
|
+
)
|
|
171
|
+
},
|
|
172
|
+
})
|
|
173
|
+
specs.push({
|
|
174
|
+
name: "research",
|
|
175
|
+
// One name, one body (#274): a leading `deep` runs the adversarial deep
|
|
176
|
+
// flow (switching to the deep-researcher); anything else reads the
|
|
177
|
+
// research stage (report, audit coverage, cached sources).
|
|
178
|
+
description:
|
|
179
|
+
"Deep research to a grounded report, or the research stage: report, audit coverage and cached sources (Epistemic Swarm)",
|
|
180
|
+
execute: async ({ sessionID, prompt, delivery }) => {
|
|
181
|
+
const say = async (text: string) => {
|
|
182
|
+
await io.synthetic(sessionID, text)
|
|
183
|
+
}
|
|
184
|
+
const text = prompt.text?.trim() ?? ""
|
|
185
|
+
if (text.split(/\s+/)[0]?.toLowerCase() !== "deep") {
|
|
186
|
+
const layout = factoryLayout(runtime.root)
|
|
187
|
+
const report = await readFile(layout.researchReport, "utf8").catch(() => undefined)
|
|
188
|
+
const coverage = await readJson<{
|
|
189
|
+
claims?: number
|
|
190
|
+
grounded?: number
|
|
191
|
+
untagged?: number
|
|
192
|
+
ungrounded?: number
|
|
193
|
+
malformed?: number
|
|
194
|
+
sources?: number
|
|
195
|
+
passed?: boolean
|
|
196
|
+
}>(path.join(layout.research, "coverage.json")).catch(() => undefined)
|
|
197
|
+
const sources = (await readdir(researchSourcesDir(runtime.root)).catch(() => [])).filter((file) =>
|
|
198
|
+
file.endsWith(".md"),
|
|
199
|
+
)
|
|
200
|
+
const lines = [
|
|
201
|
+
report === undefined
|
|
202
|
+
? "No research report yet (.factory/research/REPORT.md)."
|
|
203
|
+
: `Report: ${report.length} characters in .factory/research/REPORT.md.`,
|
|
204
|
+
coverage
|
|
205
|
+
? `Audit: ${coverage.grounded ?? 0}/${coverage.claims ?? 0} claims grounded (untagged ${coverage.untagged ?? 0}, ungrounded ${coverage.ungrounded ?? 0}, malformed ${coverage.malformed ?? 0}); cited sources ${coverage.sources ?? 0}; passed ${coverage.passed ? "yes" : "no"}.`
|
|
206
|
+
: "No coverage.json yet (es_research_complete writes it).",
|
|
207
|
+
`Cached sources: ${sources.length}.`,
|
|
208
|
+
]
|
|
209
|
+
return say(lines.join("\n"))
|
|
210
|
+
}
|
|
211
|
+
const parsed = parseDeepResearchArgs(text)
|
|
212
|
+
if ("error" in parsed && parsed.error === "usage") {
|
|
213
|
+
return say("Usage: /research deep <question> --max-usd N")
|
|
214
|
+
}
|
|
215
|
+
if ("error" in parsed) {
|
|
216
|
+
return say(
|
|
217
|
+
"Deep research needs a spend ceiling you set: /research deep <question> --max-usd N. Seats halt when the spend reaches it.",
|
|
218
|
+
)
|
|
219
|
+
}
|
|
220
|
+
const { query, ceiling } = parsed
|
|
221
|
+
const existing = await runtime.factory.read().catch(() => undefined)
|
|
222
|
+
if (existing && existing.mode !== "research") {
|
|
223
|
+
return say(
|
|
224
|
+
"This project holds a build run. Start deep research in a directory without one (`es research deep <question> --output <dir> --max-usd N` at a terminal).",
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
if (existing?.stage === "HALTED") {
|
|
228
|
+
return say(
|
|
229
|
+
"This research run is halted; resume it first (`es factory resume` at a terminal, or /es-resume to preview).",
|
|
230
|
+
)
|
|
231
|
+
}
|
|
232
|
+
if (existing?.stage === "DONE") {
|
|
233
|
+
return say(
|
|
234
|
+
"This research run is done. For a new question, start one in a fresh directory (`es research deep <question> --output <dir> --max-usd N` at a terminal).",
|
|
235
|
+
)
|
|
236
|
+
}
|
|
237
|
+
if (!existing) {
|
|
238
|
+
try {
|
|
239
|
+
// The human typed /research deep: beginning the run is their action.
|
|
240
|
+
await runtime.factory.beginResearchRun({ objective: query, ceilingUSD: Number(ceiling) }, "human:tui")
|
|
241
|
+
} catch (error) {
|
|
242
|
+
return say(`Could not start the research run: ${error instanceof Error ? error.message : String(error)}`)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
await io.switchAgent(sessionID, "deep-researcher")
|
|
246
|
+
await io.sendPrompt(
|
|
247
|
+
sessionID,
|
|
248
|
+
prompt,
|
|
249
|
+
`${await runtime.factory.summary()}\nDeep research runs adversarially, with no human answering questions mid-flow; follow your Flow through es_research_complete.`,
|
|
250
|
+
delivery,
|
|
251
|
+
)
|
|
252
|
+
},
|
|
253
|
+
})
|
|
254
|
+
specs.push({
|
|
255
|
+
name: "audit",
|
|
256
|
+
description: "Open a code audit of the active phase or a path and run the auditor pair (Epistemic Swarm)",
|
|
257
|
+
execute: async ({ sessionID, prompt, delivery }) => {
|
|
258
|
+
const target = prompt.text?.trim()
|
|
259
|
+
const say = async (text: string) => {
|
|
260
|
+
await io.synthetic(sessionID, text)
|
|
261
|
+
}
|
|
262
|
+
if (!target) return say("Usage: /audit <active phase id | path in the project>")
|
|
263
|
+
const state = await runtime.factory.read().catch(() => undefined)
|
|
264
|
+
if (state?.spendCeilingUSD === undefined)
|
|
265
|
+
return say(
|
|
266
|
+
"Audits run inside a factory run with a spend ceiling. Start one at a terminal with `es audit <target> --max-usd N`, or run /grill first.",
|
|
267
|
+
)
|
|
268
|
+
let opened: Awaited<ReturnType<typeof runtime.factory.openAudit>>
|
|
269
|
+
try {
|
|
270
|
+
// The human typed /audit: opening it is their action.
|
|
271
|
+
opened = await runtime.factory.openAudit(
|
|
272
|
+
"human:tui",
|
|
273
|
+
parseAuditTarget(
|
|
274
|
+
target,
|
|
275
|
+
state.phases.map((phase) => phase.id),
|
|
276
|
+
),
|
|
277
|
+
)
|
|
278
|
+
} catch (error) {
|
|
279
|
+
return say(`Could not open the audit: ${error instanceof Error ? error.message : String(error)}`)
|
|
280
|
+
}
|
|
281
|
+
const { audit } = opened
|
|
282
|
+
await io.switchAgent(sessionID, "factory")
|
|
283
|
+
await io.sendPrompt(
|
|
284
|
+
sessionID,
|
|
285
|
+
prompt,
|
|
286
|
+
`${await runtime.factory.summary()}\nThe human opened code audit ${audit.id} (round ${audit.round}) on ${describeTarget(audit.target)}. Launch es-auditor-thesis and es-auditor-antithesis in parallel with the audit id and target; if they split, have es-manager break the tie (es_tiebreak with audit). Report the verdicts and the report path when the audit settles.`,
|
|
287
|
+
delivery,
|
|
288
|
+
)
|
|
289
|
+
},
|
|
290
|
+
})
|
|
291
|
+
specs.push({
|
|
292
|
+
name: "factory",
|
|
293
|
+
description: "Drive the build factory from its current stage (Epistemic Swarm)",
|
|
294
|
+
execute: async ({ sessionID, prompt, delivery }) => {
|
|
295
|
+
const state = await runtime.factory.read()
|
|
296
|
+
if (!state) {
|
|
297
|
+
await io.synthetic(sessionID, "No factory run here yet — run /grill first.")
|
|
298
|
+
return
|
|
299
|
+
}
|
|
300
|
+
await io.switchAgent(sessionID, "factory")
|
|
301
|
+
await io.sendPrompt(
|
|
302
|
+
sessionID,
|
|
303
|
+
prompt,
|
|
304
|
+
`${await runtime.factory.summary(state)}\nContinue the factory run from its current stage.${prompt.text?.trim() ? `\nHuman note: ${prompt.text}` : ""}`,
|
|
305
|
+
delivery,
|
|
306
|
+
)
|
|
307
|
+
},
|
|
308
|
+
})
|
|
309
|
+
specs.push({
|
|
310
|
+
name: "gates",
|
|
311
|
+
description: "Run the mechanical gates on changed files (Epistemic Swarm)",
|
|
312
|
+
execute: async ({ sessionID }) => {
|
|
313
|
+
const changed = (await gitStatus(runtime.root, ["status", "--porcelain=v1", "-uall"], { allowFail: true })).stdout
|
|
314
|
+
.split("\n")
|
|
315
|
+
.filter(Boolean)
|
|
316
|
+
.map((line) => line.slice(3).split(" -> ").at(-1)!)
|
|
317
|
+
const report = await gates({
|
|
318
|
+
root: runtime.root,
|
|
319
|
+
scope: changed.length ? "touched" : "full",
|
|
320
|
+
touched: changed,
|
|
321
|
+
stateDir: runtime.stateDir,
|
|
322
|
+
})
|
|
323
|
+
await io.synthetic(sessionID, formatReport(report))
|
|
324
|
+
},
|
|
325
|
+
})
|
|
326
|
+
specs.push({
|
|
327
|
+
name: "lsp",
|
|
328
|
+
description: "Language servers: configured, available and running (Epistemic Swarm)",
|
|
329
|
+
execute: async ({ sessionID }) => {
|
|
330
|
+
const status = await runtime.lsp.status()
|
|
331
|
+
const settings = await runtime.lsp.settings()
|
|
332
|
+
const lines = [status.enabled ? "LSP is on." : "LSP is disabled (lsp: false)."]
|
|
333
|
+
for (const server of settings.servers) {
|
|
334
|
+
const command = await runtime.lsp.command(server, runtime.root)
|
|
335
|
+
const running = status.running.filter((item) => item.id === server.id)
|
|
336
|
+
lines.push(
|
|
337
|
+
`${server.id} [${server.extensions.join(" ")}]: ${Array.isArray(command) ? `available (${command.join(" ")})` : command.unavailable}${running.length ? ` · running for ${running.length} root(s)` : ""}`,
|
|
338
|
+
)
|
|
339
|
+
}
|
|
340
|
+
lines.push(...status.diagnostics.map((item) => `! ${item}`))
|
|
341
|
+
await io.synthetic(sessionID, lines.join("\n"))
|
|
342
|
+
},
|
|
343
|
+
})
|
|
344
|
+
specs.push({
|
|
345
|
+
name: "hooks",
|
|
346
|
+
description: "Show the hook bridge: sources, handlers, trust and diagnostics (Epistemic Swarm)",
|
|
347
|
+
execute: async ({ sessionID }) => {
|
|
348
|
+
await bridge.engine.refresh()
|
|
349
|
+
const status = bridge.engine.status()
|
|
350
|
+
const trust =
|
|
351
|
+
status.projectHandlers === 0
|
|
352
|
+
? "nothing to trust"
|
|
353
|
+
: status.trusted
|
|
354
|
+
? "trusted"
|
|
355
|
+
: "NOT trusted: run /es-trust or `es trust`"
|
|
356
|
+
const lines = [
|
|
357
|
+
`Hook bridge: ${status.handlers} handler(s), ${status.projectHandlers} from the project (${trust}).`,
|
|
358
|
+
...status.projectLines,
|
|
359
|
+
...status.diagnostics.map((item) => `! ${item}`),
|
|
360
|
+
...bridge.engine.recent.slice(-10).map((item) => `recent error: ${item}`),
|
|
361
|
+
"Stop hooks are emulated after the turn ends (advisory for plain chat).",
|
|
362
|
+
]
|
|
363
|
+
await io.synthetic(sessionID, lines.join("\n"))
|
|
364
|
+
},
|
|
365
|
+
})
|
|
366
|
+
specs.push({
|
|
367
|
+
name: "status",
|
|
368
|
+
description: "Show the factory state (Epistemic Swarm)",
|
|
369
|
+
execute: async ({ sessionID }) => {
|
|
370
|
+
const state = await runtime.factory.read()
|
|
371
|
+
const liveness = await readLiveness(runtime.root, state)
|
|
372
|
+
const text = [await runtime.factory.summary(state), ...livenessLines(state, liveness)].join("\n")
|
|
373
|
+
await io.synthetic(sessionID, text)
|
|
374
|
+
},
|
|
375
|
+
})
|
|
376
|
+
specs.push({
|
|
377
|
+
name: "config",
|
|
378
|
+
description: "Show the effective Epistemic Swarm config and where it came from (Epistemic Swarm)",
|
|
379
|
+
execute: async ({ sessionID }) => {
|
|
380
|
+
const lines = [
|
|
381
|
+
runtime.configSources.length
|
|
382
|
+
? `Layers: ${runtime.configSources.join(" → ")} → plugin options`
|
|
383
|
+
: "Layers: defaults only (no config files)",
|
|
384
|
+
...runtime.warnings.map((warning) => `Warning: ${warning}`),
|
|
385
|
+
JSON.stringify(runtime.config, null, 2),
|
|
386
|
+
]
|
|
387
|
+
await io.synthetic(sessionID, lines.join("\n"))
|
|
388
|
+
},
|
|
389
|
+
})
|
|
390
|
+
return specs
|
|
391
|
+
}
|
|
392
|
+
|
|
35
393
|
export default Plugin.define({
|
|
36
394
|
id: PLUGIN_ID,
|
|
37
395
|
async setup(ctx) {
|
|
@@ -47,6 +405,18 @@ export default Plugin.define({
|
|
|
47
405
|
() => [] as string[],
|
|
48
406
|
)
|
|
49
407
|
const agents = await compileAgents(runtime.options, servers)
|
|
408
|
+
// Fail-closed models (#96): every configured ref is checked against the
|
|
409
|
+
// host model list here, once. A missing model refuses every tool call by
|
|
410
|
+
// that seat (the policy before-hook) and shows in es_status; healthy
|
|
411
|
+
// seats are unaffected.
|
|
412
|
+
await ctx.model.transform((editor) => {
|
|
413
|
+
for (const problem of modelProblems(
|
|
414
|
+
runtime.options,
|
|
415
|
+
agents.map((agent) => agent.spec),
|
|
416
|
+
(providerID, modelID) => editor.get(providerID, modelID) !== undefined,
|
|
417
|
+
))
|
|
418
|
+
runtime.modelProblems.set(problem.agent, problem)
|
|
419
|
+
})
|
|
50
420
|
await ctx.agent.transform((editor) => {
|
|
51
421
|
for (const compiled of agents)
|
|
52
422
|
editor.update(compiled.spec.id, (agent) => {
|
|
@@ -156,248 +526,13 @@ export default Plugin.define({
|
|
|
156
526
|
}) as any)
|
|
157
527
|
|
|
158
528
|
await ctx.command.transform((editor) => {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
sessionID,
|
|
167
|
-
text: prompt.text?.trim() ? `Grill me on: ${prompt.text}` : "Start grilling me on my idea.",
|
|
168
|
-
delivery,
|
|
169
|
-
} as any)
|
|
170
|
-
},
|
|
171
|
-
})
|
|
172
|
-
editor.add({
|
|
173
|
-
name: "brainstorm",
|
|
174
|
-
description: "Fan out divergent lenses on an idea and return a diversified shortlist (Epistemic Swarm)",
|
|
175
|
-
execute: async ({ sessionID, prompt, delivery }) => {
|
|
176
|
-
await ctx.session.switchAgent({ sessionID, agent: "brainstormer" } as any)
|
|
177
|
-
await ctx.session.prompt({
|
|
178
|
-
...prompt,
|
|
179
|
-
sessionID,
|
|
180
|
-
text: prompt.text?.trim()
|
|
181
|
-
? `Brainstorm: ${prompt.text}`
|
|
182
|
-
: "Ask me for the idea to brainstorm (one sentence), then run the lens fan-out.",
|
|
183
|
-
delivery,
|
|
184
|
-
} as any)
|
|
185
|
-
},
|
|
186
|
-
})
|
|
187
|
-
editor.add({
|
|
188
|
-
name: "harvest",
|
|
189
|
-
description: "Tear down competitor projects: licences, feature matrix, clean-room specs (Epistemic Swarm)",
|
|
190
|
-
execute: async ({ sessionID, prompt, delivery }) => {
|
|
191
|
-
await ctx.session.switchAgent({ sessionID, agent: "harvester" } as any)
|
|
192
|
-
await ctx.session.prompt({
|
|
193
|
-
...prompt,
|
|
194
|
-
sessionID,
|
|
195
|
-
text: prompt.text?.trim()
|
|
196
|
-
? `Darkharvest: ${prompt.text}`
|
|
197
|
-
: "Ask me for the teardown objective and the candidate projects, then run the harvest.",
|
|
198
|
-
delivery,
|
|
199
|
-
} as any)
|
|
200
|
-
},
|
|
201
|
-
})
|
|
202
|
-
editor.add({
|
|
203
|
-
name: "design",
|
|
204
|
-
description: "Interview me into a design system: tokens, contrast gates, style guide (Epistemic Swarm)",
|
|
205
|
-
execute: async ({ sessionID, prompt, delivery }) => {
|
|
206
|
-
await ctx.session.switchAgent({ sessionID, agent: "designer" } as any)
|
|
207
|
-
await ctx.session.prompt({
|
|
208
|
-
...prompt,
|
|
209
|
-
sessionID,
|
|
210
|
-
text: prompt.text?.trim()
|
|
211
|
-
? `Run the design interview. Product: ${prompt.text}`
|
|
212
|
-
: "Ask me for the product name and start the design interview, one question at a time.",
|
|
213
|
-
delivery,
|
|
214
|
-
} as any)
|
|
215
|
-
},
|
|
216
|
-
})
|
|
217
|
-
editor.add({
|
|
218
|
-
name: "scout",
|
|
219
|
-
description: "Find and vet open-source candidates for a feature: licenses, maintenance, CVEs (Epistemic Swarm)",
|
|
220
|
-
execute: async ({ sessionID, prompt, delivery }) => {
|
|
221
|
-
await ctx.session.switchAgent({ sessionID, agent: "scout" } as any)
|
|
222
|
-
await ctx.session.prompt({
|
|
223
|
-
...prompt,
|
|
224
|
-
sessionID,
|
|
225
|
-
text: prompt.text?.trim()
|
|
226
|
-
? `Scout: ${prompt.text}`
|
|
227
|
-
: "Ask me for the feature to scout and our license posture, then plan the candidates.",
|
|
228
|
-
delivery,
|
|
229
|
-
} as any)
|
|
230
|
-
},
|
|
231
|
-
})
|
|
232
|
-
editor.add({
|
|
233
|
-
name: "audit",
|
|
234
|
-
description: "Open a code audit of the active phase or a path and run the auditor pair (Epistemic Swarm)",
|
|
235
|
-
execute: async ({ sessionID, prompt, delivery }) => {
|
|
236
|
-
const target = prompt.text?.trim()
|
|
237
|
-
const say = async (text: string) => {
|
|
238
|
-
await ctx.session.synthetic({ sessionID, text } as any)
|
|
239
|
-
}
|
|
240
|
-
if (!target) return say("Usage: /audit <active phase id | path in the project>")
|
|
241
|
-
const state = await runtime.factory.read().catch(() => undefined)
|
|
242
|
-
if (state?.spendCeilingUSD === undefined)
|
|
243
|
-
return say(
|
|
244
|
-
"Audits run inside a factory run with a spend ceiling. Start one at a terminal with `es audit <target> --max-usd N`, or run /grill first.",
|
|
245
|
-
)
|
|
246
|
-
let opened: Awaited<ReturnType<typeof runtime.factory.openAudit>>
|
|
247
|
-
try {
|
|
248
|
-
// The human typed /audit: opening it is their action.
|
|
249
|
-
opened = await runtime.factory.openAudit(
|
|
250
|
-
"human:tui",
|
|
251
|
-
parseAuditTarget(
|
|
252
|
-
target,
|
|
253
|
-
state.phases.map((phase) => phase.id),
|
|
254
|
-
),
|
|
255
|
-
)
|
|
256
|
-
} catch (error) {
|
|
257
|
-
return say(`Could not open the audit: ${error instanceof Error ? error.message : String(error)}`)
|
|
258
|
-
}
|
|
259
|
-
const { audit } = opened
|
|
260
|
-
await ctx.session.switchAgent({ sessionID, agent: "factory" } as any)
|
|
261
|
-
await ctx.session.prompt({
|
|
262
|
-
...prompt,
|
|
263
|
-
sessionID,
|
|
264
|
-
text: `${await runtime.factory.summary()}\nThe human opened code audit ${audit.id} (round ${audit.round}) on ${describeTarget(audit.target)}. Launch es-auditor-thesis and es-auditor-antithesis in parallel with the audit id and target; if they split, have es-manager break the tie (es_tiebreak with audit). Report the verdicts and the report path when the audit settles.`,
|
|
265
|
-
delivery,
|
|
266
|
-
} as any)
|
|
267
|
-
},
|
|
268
|
-
})
|
|
269
|
-
editor.add({
|
|
270
|
-
name: "factory",
|
|
271
|
-
description: "Drive the build factory from its current stage (Epistemic Swarm)",
|
|
272
|
-
execute: async ({ sessionID, prompt, delivery }) => {
|
|
273
|
-
const state = await runtime.factory.read()
|
|
274
|
-
if (!state) {
|
|
275
|
-
await ctx.session.synthetic({
|
|
276
|
-
sessionID,
|
|
277
|
-
text: "No factory run here yet — run /grill first.",
|
|
278
|
-
} as any)
|
|
279
|
-
return
|
|
280
|
-
}
|
|
281
|
-
await ctx.session.switchAgent({ sessionID, agent: "factory" } as any)
|
|
282
|
-
await ctx.session.prompt({
|
|
283
|
-
...prompt,
|
|
284
|
-
sessionID,
|
|
285
|
-
text: `${await runtime.factory.summary(state)}\nContinue the factory run from its current stage.${prompt.text?.trim() ? `\nHuman note: ${prompt.text}` : ""}`,
|
|
286
|
-
delivery,
|
|
287
|
-
} as any)
|
|
288
|
-
},
|
|
289
|
-
})
|
|
290
|
-
editor.add({
|
|
291
|
-
name: "gates",
|
|
292
|
-
description: "Run the mechanical gates on changed files (Epistemic Swarm)",
|
|
293
|
-
execute: async ({ sessionID }) => {
|
|
294
|
-
const changed = (await git(runtime.root, ["status", "--porcelain=v1", "-uall"], { allowFail: true })).stdout
|
|
295
|
-
.split("\n")
|
|
296
|
-
.filter(Boolean)
|
|
297
|
-
.map((line) => line.slice(3).split(" -> ").at(-1)!)
|
|
298
|
-
const report = await runGates({
|
|
299
|
-
root: runtime.root,
|
|
300
|
-
scope: changed.length ? "touched" : "full",
|
|
301
|
-
touched: changed,
|
|
302
|
-
stateDir: runtime.stateDir,
|
|
303
|
-
})
|
|
304
|
-
await ctx.session.synthetic({ sessionID, text: formatReport(report) } as any)
|
|
305
|
-
},
|
|
306
|
-
})
|
|
307
|
-
editor.add({
|
|
308
|
-
name: "lsp",
|
|
309
|
-
description: "Language servers: configured, available and running (Epistemic Swarm)",
|
|
310
|
-
execute: async ({ sessionID }) => {
|
|
311
|
-
const status = await runtime.lsp.status()
|
|
312
|
-
const settings = await runtime.lsp.settings()
|
|
313
|
-
const lines = [status.enabled ? "LSP is on." : "LSP is disabled (lsp: false)."]
|
|
314
|
-
for (const server of settings.servers) {
|
|
315
|
-
const command = await runtime.lsp.command(server, runtime.root)
|
|
316
|
-
const running = status.running.filter((item) => item.id === server.id)
|
|
317
|
-
lines.push(
|
|
318
|
-
`${server.id} [${server.extensions.join(" ")}]: ${Array.isArray(command) ? `available (${command.join(" ")})` : command.unavailable}${running.length ? ` · running for ${running.length} root(s)` : ""}`,
|
|
319
|
-
)
|
|
320
|
-
}
|
|
321
|
-
lines.push(...status.diagnostics.map((item) => `! ${item}`))
|
|
322
|
-
await ctx.session.synthetic({ sessionID, text: lines.join("\n") } as any)
|
|
323
|
-
},
|
|
324
|
-
})
|
|
325
|
-
editor.add({
|
|
326
|
-
name: "hooks",
|
|
327
|
-
description: "Show the hook bridge: sources, handlers, trust and diagnostics (Epistemic Swarm)",
|
|
328
|
-
execute: async ({ sessionID }) => {
|
|
329
|
-
await bridge.engine.refresh()
|
|
330
|
-
const status = bridge.engine.status()
|
|
331
|
-
const trust =
|
|
332
|
-
status.projectHandlers === 0
|
|
333
|
-
? "nothing to trust"
|
|
334
|
-
: status.trusted
|
|
335
|
-
? "trusted"
|
|
336
|
-
: "NOT trusted: run /es-trust or `es trust`"
|
|
337
|
-
const lines = [
|
|
338
|
-
`Hook bridge: ${status.handlers} handler(s), ${status.projectHandlers} from the project (${trust}).`,
|
|
339
|
-
...status.projectLines,
|
|
340
|
-
...status.diagnostics.map((item) => `! ${item}`),
|
|
341
|
-
...bridge.engine.recent.slice(-10).map((item) => `recent error: ${item}`),
|
|
342
|
-
"Stop hooks are emulated after the turn ends (advisory for plain chat).",
|
|
343
|
-
]
|
|
344
|
-
await ctx.session.synthetic({ sessionID, text: lines.join("\n") } as any)
|
|
345
|
-
},
|
|
346
|
-
})
|
|
347
|
-
editor.add({
|
|
348
|
-
name: "status",
|
|
349
|
-
description: "Show the factory state (Epistemic Swarm)",
|
|
350
|
-
execute: async ({ sessionID }) => {
|
|
351
|
-
const state = await runtime.factory.read()
|
|
352
|
-
const liveness = await readLiveness(runtime.root, state)
|
|
353
|
-
const text = [await runtime.factory.summary(state), ...livenessLines(state, liveness)].join("\n")
|
|
354
|
-
await ctx.session.synthetic({ sessionID, text } as any)
|
|
355
|
-
},
|
|
356
|
-
})
|
|
357
|
-
editor.add({
|
|
358
|
-
name: "config",
|
|
359
|
-
description: "Show the effective Epistemic Swarm config and where it came from (Epistemic Swarm)",
|
|
360
|
-
execute: async ({ sessionID }) => {
|
|
361
|
-
const lines = [
|
|
362
|
-
runtime.configSources.length
|
|
363
|
-
? `Layers: ${runtime.configSources.join(" → ")} → plugin options`
|
|
364
|
-
: "Layers: defaults only (no config files)",
|
|
365
|
-
...runtime.warnings.map((warning) => `Warning: ${warning}`),
|
|
366
|
-
JSON.stringify(runtime.config, null, 2),
|
|
367
|
-
]
|
|
368
|
-
await ctx.session.synthetic({ sessionID, text: lines.join("\n") } as any)
|
|
369
|
-
},
|
|
370
|
-
})
|
|
371
|
-
editor.add({
|
|
372
|
-
name: "research",
|
|
373
|
-
description: "Research stage: report, audit coverage and cached sources (Epistemic Swarm)",
|
|
374
|
-
execute: async ({ sessionID }) => {
|
|
375
|
-
const layout = factoryLayout(runtime.root)
|
|
376
|
-
const report = await readFile(layout.researchReport, "utf8").catch(() => undefined)
|
|
377
|
-
const coverage = await readJson<{
|
|
378
|
-
claims?: number
|
|
379
|
-
grounded?: number
|
|
380
|
-
untagged?: number
|
|
381
|
-
ungrounded?: number
|
|
382
|
-
malformed?: number
|
|
383
|
-
sources?: number
|
|
384
|
-
passed?: boolean
|
|
385
|
-
}>(path.join(layout.research, "coverage.json")).catch(() => undefined)
|
|
386
|
-
const sources = (await readdir(researchSourcesDir(runtime.root)).catch(() => [])).filter((file) =>
|
|
387
|
-
file.endsWith(".md"),
|
|
388
|
-
)
|
|
389
|
-
const lines = [
|
|
390
|
-
report === undefined
|
|
391
|
-
? "No research report yet (.factory/research/REPORT.md)."
|
|
392
|
-
: `Report: ${report.length} characters in .factory/research/REPORT.md.`,
|
|
393
|
-
coverage
|
|
394
|
-
? `Audit: ${coverage.grounded ?? 0}/${coverage.claims ?? 0} claims grounded (untagged ${coverage.untagged ?? 0}, ungrounded ${coverage.ungrounded ?? 0}, malformed ${coverage.malformed ?? 0}); cited sources ${coverage.sources ?? 0}; passed ${coverage.passed ? "yes" : "no"}.`
|
|
395
|
-
: "No coverage.json yet (es_research_complete writes it).",
|
|
396
|
-
`Cached sources: ${sources.length}.`,
|
|
397
|
-
]
|
|
398
|
-
await ctx.session.synthetic({ sessionID, text: lines.join("\n") } as any)
|
|
399
|
-
},
|
|
400
|
-
})
|
|
529
|
+
const io: CommandSession = {
|
|
530
|
+
switchAgent: (sessionID, agent) => ctx.session.switchAgent({ sessionID, agent } as any),
|
|
531
|
+
sendPrompt: (sessionID, prompt, text, delivery) =>
|
|
532
|
+
ctx.session.prompt({ ...prompt, sessionID, text, delivery } as any),
|
|
533
|
+
synthetic: (sessionID, text) => ctx.session.synthetic({ sessionID, text } as any),
|
|
534
|
+
}
|
|
535
|
+
for (const spec of esCommandSpecs(io, runtime, bridge as unknown as CommandBridge)) editor.add(spec as any)
|
|
401
536
|
})
|
|
402
537
|
|
|
403
538
|
// Spend: per-step host cost for factory-agent sessions (estimated from tokens when cost is 0).
|
|
@@ -415,6 +550,10 @@ export default Plugin.define({
|
|
|
415
550
|
// stream closed on unload
|
|
416
551
|
}
|
|
417
552
|
})()
|
|
553
|
+
// Language-server lifecycle (#275.1): servers stop here on plugin unload;
|
|
554
|
+
// between sessions only the manager's idle sweeper (600 s default) reaps
|
|
555
|
+
// them, so a long-lived server outlives its session by design. The running
|
|
556
|
+
// set stays visible through `/lsp` and the LSP panel.
|
|
418
557
|
return async () => {
|
|
419
558
|
abort.abort()
|
|
420
559
|
if (queued) clearTimeout(queued)
|