@sorenllm/opencode-forge 0.3.2 → 0.3.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.
Files changed (3) hide show
  1. package/README.md +154 -2
  2. package/dist/index.js +2234 -202
  3. package/package.json +61 -61
package/README.md CHANGED
@@ -114,8 +114,8 @@ and the plugin's command of that name is not registered.
114
114
  (the hide was runtime-only). Your `.opencode/plan/` and `.opencode/goal/`
115
115
  files are yours; delete them yourself if you want.
116
116
  5. Optional runtime debris: delete `<tmp>/opencode-forge/` (job logs, the
117
- job registry ledger, and the watchdog ledger — the file ledger table
118
- above lists everything).
117
+ job registry ledger, the watchdog ledger, and the dispatch ledger — the
118
+ file ledger table above lists everything).
119
119
 
120
120
  ## File ledger
121
121
 
@@ -130,6 +130,8 @@ What this plugin touches, exhaustively:
130
130
  | `<tmp>/opencode-forge/jobs/ledger.jsonl` | job registry ledger (bounded: 1 MB reset, 200 entries) | runtime debris — delete freely, also after uninstall |
131
131
  | `<tmp>/opencode-forge/jobs/registry.json` | persistent survivor registry (bounded: 100 entries) | runtime debris — after uninstall, kill any still-running `survive` jobs yourself first |
132
132
  | `<tmp>/opencode-forge/watchdog/log.jsonl` | watchdog interventions ledger (bounded: 200 entries, oldest rotated) | runtime debris — delete freely, also after uninstall |
133
+ | `<tmp>/opencode-forge/dispatch/ledger.jsonl` | dispatch ledger: resolved identity/depth, outcomes, tokens/cost, timeouts, transport interruptions, lost-on-exit, crew summaries (bounded, capped rotation) | runtime debris — delete freely, also after uninstall |
134
+ | `<project>/.opencode/forge.json`, `~/.config/opencode/forge.json` | your dispatch agent definitions (JSONC). **User data — the plugin only reads it, never writes or migrates it** | yours — version the project one, keep the global one out of sync tools if it holds machine-specific models |
133
135
  | `~/.cache/opencode/packages/...` | installed package copy | written by the `opencode plugin` installer, not the plugin |
134
136
  | `~/.config/opencode/opencode.json` | `plugin` array entry | written by the installer |
135
137
 
@@ -317,6 +319,156 @@ These are runtime debris, not data — with ONE caveat: **if you uninstall with
317
319
  /F /T`, or just reboot); deleting the directory afterwards is safe and
318
320
  complete.
319
321
 
322
+ ## Worker dispatch (scoped subagents on a chosen model + depth)
323
+
324
+ `forge_dispatch {prompt, agent, depth}` spawns a child session that runs one
325
+ scoped task on the agent's pinned model at the requested reasoning depth. The
326
+ result is an honest report: the actual model/depth used, real token counts, a
327
+ self-computed cost (`null` when the model is unpriced), the
328
+ `depthTranslation` disclosure (`canonical high → native XHigh`, or
329
+ `verbatim`), and the worker's concluding report.
330
+
331
+ ### forge.json — the dispatch configuration
332
+
333
+ Agents live in a dedicated `forge.json` (JSONC — comments allowed), resolved
334
+ by a three-level cascade with a single winning source (never merged):
335
+
336
+ 1. `<project>/.opencode/forge.json` — project-level, versionable, team-shared
337
+ 2. `~/.config/opencode/forge.json` — global
338
+ 3. the built-in seed — `research {depths: [low, medium]}` and
339
+ `review {depths: [medium, high, max]}`, both pinned to the placeholder
340
+ `Local/GPT Luna` (an identity that never resolves: an unconfigured install
341
+ fails every dispatch with the configuration recipe below)
342
+
343
+ ```jsonc
344
+ {
345
+ // forge dispatch agents — save the file; changes apply on the next dispatch, no restart
346
+ "agents": {
347
+ "research": {
348
+ "model": "zai-coding-plan/glm-5.3", // exact "provider/model" string
349
+ "depths": ["low", "medium"], // first entry is the default
350
+ // "prompt": "optional role prompt riding inside the discipline wrapper",
351
+ // "shape": "write", // default readonly denies mutating tools
352
+ // "permission": { "bash": "deny" } // optional override; "task" is always denied
353
+ }
354
+ }
355
+ }
356
+ ```
357
+
358
+ - **The plugin only ever reads this file.** It never creates, writes, or
359
+ migrates it. Unconfigured? The first `forge_dispatch` error carries a
360
+ machine-actionable recipe (detected identities, both file paths, the
361
+ template, a verify dispatch) — or just ask your session AI to configure
362
+ dispatch; `forge_dispatch_config` shows the exact live state.
363
+ - **Hot-apply**: the file is re-read (mtime-cached) before every dispatch and
364
+ on every `forge_dispatch_config` call — edits take effect on the next
365
+ dispatch, no host restart. One boundary: newly added agents materialize
366
+ their hidden `forge-<agent>` entry only on the next config reload; the
367
+ dispatch itself still works (the body names the agent), but a brand-new
368
+ agent's first dispatch may need a host reload if the host validates agent
369
+ names on session create.
370
+ - Each agent materializes as a hidden `mode: subagent` agent `forge-<agent>`
371
+ carrying the role prompt (explicit `prompt` > built-in research/review
372
+ defaults > generic worker) and a deny-style permission: `shape: "readonly"`
373
+ (the default) denies `write`/`edit`/`bash`, `shape: "write"` allows them,
374
+ an explicit `permission` map overrides the shape default — and `task` is
375
+ ALWAYS denied (recursive dispatch stays physically impossible). Agents
376
+ never carry a `model` field; the brain is bound per dispatch.
377
+
378
+ ### The depth metalanguage
379
+
380
+ `depth` comes from the canonical five-word set `none | low | medium | high |
381
+ max`, or the model's **native level names verbatim** (escape hatch — e.g.
382
+ Qwen's `XHigh`). Translation to the provider's native parameter follows three
383
+ hard rules:
384
+
385
+ - **verbatim-first**: a meta word the model natively offers passes as-is
386
+ - **no interpolation**: a meta word with no counterpart errors listing both
387
+ vocabularies — never a nearest guess
388
+ - **full disclosure**: the report always shows `canonical → native`
389
+
390
+ Per family: effort-style providers (OpenAI-compatible) receive
391
+ `reasoningEffort = <word>`; budget-style (Anthropic-style) receive a
392
+ published thinking tier — `low → 8192`, `medium → 16384`, `high → 24576`,
393
+ `max → 32768` budget tokens, `none → thinking off`; toggle-style (zai/GLM)
394
+ receive thinking on/off (that shape has no level slot — disclosed honestly).
395
+ A provider whose option shape the plugin does not know gets nothing injected,
396
+ disclosed as `not injected (unknown provider shape)`. The provider is the
397
+ final judge of every depth word: its raw errors flow back unedited.
398
+
399
+ ### Legacy inline roster (advanced)
400
+
401
+ The pre-forge.json inline form still works when configured (the plugin
402
+ options `dispatch.roster` / `dispatch.tiers`): tier ids it defines keep their
403
+ exact-match resolution, one-retry-excluding-a-failed-identity semantics, and
404
+ startup dead-key warnings. Legacy tier ids win over forge.json agents of the
405
+ same name while inline options exist. Zero-config auto-generation of roster
406
+ entries was removed (configured ≠ usable) — dispatch without configuration is
407
+ the seed + recipe path above.
408
+
409
+ ### Background dispatch and wake briefs
410
+
411
+ `forge_dispatch` takes `background: true`: it returns
412
+ `{dispatchId, agent, requested, resolved, depth, queuedAt}` immediately and
413
+ the full result arrives later as a coalesced `[forge:dispatch-complete]`
414
+ brief — debounced, never interrupting an active turn (delivery waits for an
415
+ idle), each terminal delivered exactly once, and merged into a single
416
+ combined re-prompt when a goal continuation fires on the same idle.
417
+ `forge_dispatch_list` recovers in-flight and recent terminal state (with
418
+ dispatchIds) after context compaction; `forge_dispatch_kill {dispatchId}`
419
+ stops polling, suppresses the brief, and ledgers `killed`. On host exit,
420
+ in-flight dispatches are ledgered `lost-on-exit` — sessions are host memory
421
+ objects, there are no orphan processes and no survive semantics.
422
+
423
+ **Transport-interruption recovery**: the child's prompt is delivered by a
424
+ turn-synchronous HTTP POST that stays open for the whole first turn. If that
425
+ POST dies at the transport layer (fetch abort / network failure — anything
426
+ that is not an HTTP error response), the dispatch does **not** fail: the
427
+ engine ledgers `transport-interrupted` (with the child sessionID) and falls
428
+ back to completion polling under the same deadline — the child session is a
429
+ host-side object and usually keeps running, so its result is still collected
430
+ and delivered normally. The message is never re-POSTed (the prompt may
431
+ already be delivering); a message that never arrived polls as waiting and
432
+ ends in the honest deadline `timeout`. Terminal reports and terminal ledger
433
+ rows carry the child sessionID, dispatchId, parentSessionID, and durationMs.
434
+
435
+ ### Crew workflow (`/crew`)
436
+
437
+ `/crew <objective>` registers a crew: decompose the objective into
438
+ evidence-checked subtasks, dispatch them as background waves paced on
439
+ completion briefs (**never past the concurrency cap — launch the next batch
440
+ only as briefs free capacity**), at most ONE retry per failed subtask, then
441
+ `crew_close` with the full report. The close is a hard gate: every subtask
442
+ needs a PASS/FAIL verdict with evidence, a FAIL needs both failure reports,
443
+ and the report is cross-checked against the dispatch ledger — a dispatched
444
+ subtask missing from the report refuses the close (refusals never bother you
445
+ with a dialog). A valid close asks once, appends a crew summary to the
446
+ ledger, and ends the crew. Crew state is in-memory by design: a host restart
447
+ ends it honestly and the ledger keeps the history.
448
+
449
+ ### Known pitfalls
450
+
451
+ - **Keyless endpoints × readonly agents**: free/keyless providers can return
452
+ a deterministic EMPTY response for any toolset-reduced (readonly) agent —
453
+ 0 tokens, no parts. The plugin detects this and fails the dispatch honestly
454
+ (`empty-response`, never counted as success); use a `shape: "write"` agent
455
+ or a keyed provider for readonly work on such endpoints.
456
+ - **`opencode run` and background**: a finished run-mode session cannot be
457
+ re-prompted, so completion briefs have nowhere to land — completions are
458
+ ledger-only (read them via `forge_dispatch_list`). Background mode targets
459
+ live TUI sessions; prefer sync under `opencode run`.
460
+ - **Host tool ceiling (~262s observed on 1.18.32)**: the host may kill a
461
+ sync tool call that exceeds roughly 262 seconds even though the plugin's
462
+ own deadline is higher. A same-family transport abort (~281s observed,
463
+ 2026-09-27 incident) can also kill the background dispatch's turn POST.
464
+ Transport-interruption recovery (above) absorbs both: the dispatch falls
465
+ back to polling and still delivers the child's result; keep `timeoutMs`
466
+ modest anyway for sync-heavy workflows.
467
+ - **Child permission asks**: a worker that hits a permission ask surfaces the
468
+ ask on the CHILD's session in the TUI. An unanswered ask stalls the child
469
+ until the dispatch deadline — scope worker prompts to avoid permission
470
+ boundaries (readonly tiers and workspace-relative paths help).
471
+
320
472
  ## Hang watchdog (a stuck builtin shell unblocks itself)
321
473
 
322
474
  `forge_shell` is structurally immune to the stdio-EOF hang, but the builtin