opencode-ultracode 0.1.0 → 1.0.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/README.md +14 -0
- package/package.json +1 -1
- package/skills/workflow-authoring/SKILL.md +3 -1
- package/src/server/index.ts +68 -0
package/README.md
CHANGED
|
@@ -108,6 +108,20 @@ Sub-agents run in their own child sessions with the project's tools and
|
|
|
108
108
|
permissions. When one of them is blocked on a permission or a question, the
|
|
109
109
|
workflow views flag it and let you answer without leaving the screen.
|
|
110
110
|
|
|
111
|
+
For unattended runs you can have the plugin approve sub-agent permission
|
|
112
|
+
prompts automatically. This is off by default and controlled by an environment
|
|
113
|
+
variable when you start opencode:
|
|
114
|
+
|
|
115
|
+
```sh
|
|
116
|
+
ULTRACODE_AUTO_ALLOW=1 opencode # approve every permission sub-agents ask for
|
|
117
|
+
ULTRACODE_AUTO_ALLOW=bash,edit opencode # approve only these permission types
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Only workflow sessions are affected: the sub-agent sessions themselves and any
|
|
121
|
+
session they open in turn. Your chat session and the plan
|
|
122
|
+
approval prompt still ask as usual, and questions from sub-agents are still
|
|
123
|
+
routed to you. Every auto-approval is written to the opencode log.
|
|
124
|
+
|
|
111
125
|
If opencode exits while a run is in progress, the run is not lost. Every
|
|
112
126
|
completed agent is journaled, so resuming replays those results and only runs
|
|
113
127
|
the remaining agents again.
|
package/package.json
CHANGED
|
@@ -65,7 +65,9 @@ ALL of stage N-1's results at once (dedup across findings, early-exit on zero, c
|
|
|
65
65
|
Pass timestamps in via `args`; vary prompts/labels by index instead of randomness.
|
|
66
66
|
- Concurrency is capped (~min(16, cpus-2) parallel agents); the total agent cap is 1000.
|
|
67
67
|
- Sub-agents run in their own sessions with the project's tools and permissions — for
|
|
68
|
-
long autonomous runs, pre-allow the tools agents need
|
|
68
|
+
long autonomous runs, pre-allow the tools agents need, or start opencode with
|
|
69
|
+
`ULTRACODE_AUTO_ALLOW=1` (or a type list like `bash,edit`) so sub-agent permission
|
|
70
|
+
prompts are approved automatically. The chat session itself keeps asking.
|
|
69
71
|
|
|
70
72
|
## Quality patterns
|
|
71
73
|
|
package/src/server/index.ts
CHANGED
|
@@ -54,6 +54,50 @@ export default async (input: PluginInput): Promise<Hooks> => {
|
|
|
54
54
|
const childSessionRun = new Map<string, string>()
|
|
55
55
|
const childSessions = new Set<string>()
|
|
56
56
|
|
|
57
|
+
// Opt-in auto-approval of permission prompts raised by workflow sub-agents.
|
|
58
|
+
// ULTRACODE_AUTO_ALLOW=1|true|all approves every permission a child session
|
|
59
|
+
// asks for; a comma list ("bash,edit,webfetch") approves only those types.
|
|
60
|
+
// Off by default: the project's permission config applies unchanged.
|
|
61
|
+
const autoAllow = parseAutoAllow(process.env.ULTRACODE_AUTO_ALLOW)
|
|
62
|
+
|
|
63
|
+
// Sessions opened *by* sub-agents (a task / sub-agent tool used inside a
|
|
64
|
+
// workflow agent) are workflow sessions too. They are picked up from
|
|
65
|
+
// session events; when a permission arrives before the event did, the
|
|
66
|
+
// parentID chain is walked through the API instead. Both paths memoize.
|
|
67
|
+
const descendantSessions = new Set<string>()
|
|
68
|
+
const parentOf = new Map<string, string | undefined>()
|
|
69
|
+
const isTracked = (id: string): boolean => childSessions.has(id) || descendantSessions.has(id)
|
|
70
|
+
const noteSession = (info: { id?: string; parentID?: string } | undefined): void => {
|
|
71
|
+
if (!info?.id) return
|
|
72
|
+
parentOf.set(info.id, info.parentID)
|
|
73
|
+
if (info.parentID && isTracked(info.parentID)) descendantSessions.add(info.id)
|
|
74
|
+
}
|
|
75
|
+
const isWorkflowSession = async (sessionID: string): Promise<boolean> => {
|
|
76
|
+
if (isTracked(sessionID)) return true
|
|
77
|
+
const chain: string[] = []
|
|
78
|
+
let id: string | undefined = sessionID
|
|
79
|
+
while (id && chain.length < 16 && !chain.includes(id)) {
|
|
80
|
+
chain.push(id)
|
|
81
|
+
if (!parentOf.has(id)) {
|
|
82
|
+
try {
|
|
83
|
+
const res: any = await input.client.session.get({ path: { id } })
|
|
84
|
+
const info = res?.data ?? res
|
|
85
|
+
parentOf.set(id, typeof info?.parentID === "string" ? info.parentID : undefined)
|
|
86
|
+
} catch {
|
|
87
|
+
return false
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const parent: string | undefined = parentOf.get(id)
|
|
91
|
+
if (!parent) return false
|
|
92
|
+
if (isTracked(parent)) {
|
|
93
|
+
for (const s of chain) descendantSessions.add(s)
|
|
94
|
+
return true
|
|
95
|
+
}
|
|
96
|
+
id = parent
|
|
97
|
+
}
|
|
98
|
+
return false
|
|
99
|
+
}
|
|
100
|
+
|
|
57
101
|
const log = (level: "debug" | "info" | "warn" | "error", message: string, meta?: Record<string, unknown>) => {
|
|
58
102
|
try {
|
|
59
103
|
input.client.app.log({ body: { level, service: "workflow", message, ...meta } }).catch(() => {})
|
|
@@ -329,6 +373,20 @@ export default async (input: PluginInput): Promise<Hooks> => {
|
|
|
329
373
|
if (label && !childSessions.has(i.sessionID)) sessionModel.set(i.sessionID, label)
|
|
330
374
|
},
|
|
331
375
|
|
|
376
|
+
// With auto-allow on, permission prompts raised inside workflow child
|
|
377
|
+
// sessions are approved here instead of reaching the user. The main
|
|
378
|
+
// session (including the plan approval prompt) is never affected.
|
|
379
|
+
"permission.ask": async (perm, output) => {
|
|
380
|
+
if (!autoAllow) return
|
|
381
|
+
if (!(await isWorkflowSession(perm.sessionID))) return
|
|
382
|
+
if (autoAllow !== "all" && !autoAllow.has(perm.type)) return
|
|
383
|
+
output.status = "allow"
|
|
384
|
+
log("info", `auto-allowed ${perm.type} permission for workflow sub-agent`, {
|
|
385
|
+
sessionID: perm.sessionID,
|
|
386
|
+
pattern: Array.isArray(perm.pattern) ? perm.pattern.join(", ") : perm.pattern,
|
|
387
|
+
})
|
|
388
|
+
},
|
|
389
|
+
|
|
332
390
|
"chat.message": async (i, o) => {
|
|
333
391
|
if (childSessions.has(i.sessionID)) return
|
|
334
392
|
const text = (o.parts ?? []).map((p) => (p as any).type === "text" ? (p as any).text ?? "" : "").join("\n")
|
|
@@ -348,6 +406,7 @@ export default async (input: PluginInput): Promise<Hooks> => {
|
|
|
348
406
|
|
|
349
407
|
event: async ({ event }) => {
|
|
350
408
|
const e = event as any
|
|
409
|
+
if (e?.type === "session.created" || e?.type === "session.updated") noteSession(e.properties?.info)
|
|
351
410
|
if (e?.type === "message.part.updated") {
|
|
352
411
|
const part = e.properties?.part
|
|
353
412
|
const runId = part?.sessionID ? childSessionRun.get(part.sessionID) : undefined
|
|
@@ -379,6 +438,15 @@ export default async (input: PluginInput): Promise<Hooks> => {
|
|
|
379
438
|
|
|
380
439
|
// --- small helpers -----------------------------------------------------------
|
|
381
440
|
|
|
441
|
+
/** ULTRACODE_AUTO_ALLOW → false (off), "all", or the set of permission types to allow. */
|
|
442
|
+
function parseAutoAllow(raw: string | undefined): false | "all" | Set<string> {
|
|
443
|
+
const v = (raw ?? "").trim().toLowerCase()
|
|
444
|
+
if (!v || v === "0" || v === "false" || v === "off" || v === "no") return false
|
|
445
|
+
if (v === "1" || v === "true" || v === "all" || v === "*" || v === "yes") return "all"
|
|
446
|
+
const types = new Set(v.split(",").map((t) => t.trim()).filter(Boolean))
|
|
447
|
+
return types.size ? types : false
|
|
448
|
+
}
|
|
449
|
+
|
|
382
450
|
function safeName(n: string): string {
|
|
383
451
|
return n.replace(/[^a-zA-Z0-9_-]/g, "-")
|
|
384
452
|
}
|