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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-ultracode",
3
- "version": "0.1.0",
3
+ "version": "1.0.0",
4
4
  "description": "Workflow orchestration for opencode: model-authored scripts, parallel sub-agent fleet, live TUI progress view",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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
 
@@ -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
  }