@sorenllm/opencode-forge 0.3.4 → 0.4.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.
Files changed (3) hide show
  1. package/README.md +119 -138
  2. package/dist/index.js +482 -1608
  3. package/package.json +5 -2
package/README.md CHANGED
@@ -113,9 +113,10 @@ and the plugin's command of that name is not registered.
113
113
  4. Done — the hidden native `build`/`plan` agents come back automatically
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
- 5. Optional runtime debris: delete `<tmp>/opencode-forge/` (job logs, the
117
- job registry ledger, the watchdog ledger, and the dispatch ledger — the
118
- file ledger table above lists everything).
116
+ 5. Optional runtime debris: delete `<tmp>/opencode-forge/` (job logs, the
117
+ job registry ledger, the watchdog ledger, and any leftover dispatch
118
+ ledger from older versions — the file ledger table above lists
119
+ everything).
119
120
 
120
121
  ## File ledger
121
122
 
@@ -125,13 +126,13 @@ What this plugin touches, exhaustively:
125
126
  | --- | --- | --- |
126
127
  | `<project>/.opencode/plan/*.md` | plan files | user data — kept forever, uninstall never deletes |
127
128
  | `<project>/.opencode/goal/*.md` | goal files (contract, Check Log, Turn Ledger) | user data — kept forever, uninstall never deletes |
128
- | merged config object (RAM only) | forge agent, native build/plan `disable`, `command.plan`, `command.goal`, goal permission keys, `permission.forge_shell`, stage-0 builtin shell hide | vanishes when the plugin is removed; nothing is written to disk |
129
+ | merged config object (RAM only) | forge agent, static `forge-<id>` subagents, native build/plan `disable`, `command.plan`, `command.goal`, `command.crew`, goal permission keys, `permission.forge_shell`, `permission.crew_close`, stage-0 builtin shell hide | vanishes when the plugin is removed; nothing is written to disk |
129
130
  | `<tmp>/opencode-forge/jobs/<jobId>.log` | job output tee (full output; oldest rotated out above 50 files) | runtime debris — delete freely, also after uninstall |
130
131
  | `<tmp>/opencode-forge/jobs/ledger.jsonl` | job registry ledger (bounded: 1 MB reset, 200 entries) | runtime debris — delete freely, also after uninstall |
131
132
  | `<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
133
  | `<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 |
134
+ | `<tmp>/opencode-forge/dispatch/ledger.jsonl` | dispatch ledger — **no longer written** (the dispatch engine was removed); only present as inert debris from older versions | runtime debris — delete freely |
135
+ | `<project>/.opencode/forge.json`, `~/.config/opencode/forge.json` | your static subagent 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 |
135
136
  | `~/.cache/opencode/packages/...` | installed package copy | written by the `opencode plugin` installer, not the plugin |
136
137
  | `~/.config/opencode/opencode.json` | `plugin` array entry | written by the installer |
137
138
 
@@ -319,155 +320,135 @@ These are runtime debris, not data — with ONE caveat: **if you uninstall with
319
320
  /F /T`, or just reboot); deleting the directory afterwards is safe and
320
321
  complete.
321
322
 
322
- ## Worker dispatch (scoped subagents on a chosen model + depth)
323
+ ## Forge subagents (static worker agents in forge.json)
323
324
 
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.
325
+ Forge subagents are **static, user-authored worker agents**: each forge.json
326
+ entry pins a model and an optional reasoning depth, materializes as a native
327
+ `forge-<id>` subagent, and is dispatched through the host's **native `task`
328
+ tool** — which means every run is visible in the TUI, expandable and
329
+ monitorable, exactly like any native subagent. There is no dispatch tool, no
330
+ child-session engine, and no configuration invitation anywhere: the session
331
+ AI never writes your config.
330
332
 
331
- ### forge.json — the dispatch configuration
333
+ No forge.json? Nothing happens — the plugin registers nothing and the native
334
+ subagent behavior is untouched. Onboarding is this README, for you the human.
332
335
 
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):
336
+ ### forge.json — the configuration
337
+
338
+ A dedicated `forge.json` (JSONC — comments allowed), resolved by a two-level
339
+ cascade with a single winning source (never merged):
335
340
 
336
341
  1. `<project>/.opencode/forge.json` — project-level, versionable, team-shared
337
342
  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
 
343
344
  ```jsonc
344
345
  {
345
- // forge dispatch agents — save the file; changes apply on the next dispatch, no restart
346
+ // forge subagents — definitions; see the restart table below for apply timing
346
347
  "agents": {
347
348
  "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
349
+ "model": "zai-coding-plan/glm-5.3", // exact "provider/model" string — REQUIRED
350
+ "thoughtLevel": "low", // optional: none/low/medium/high/max or a native level name
351
+ // "prompt": "optional role prompt; built-in research/review defaults exist",
352
+ // "shape": "write", // default readonly denies write/edit/bash
353
+ // "permission": { "bash": "deny" } // optional override; "task" is ALWAYS denied
353
354
  }
354
355
  }
355
356
  }
356
357
  ```
357
358
 
358
359
  - **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).
360
+ migrates it. The old AI-authored onboarding path (seed placeholder +
361
+ error recipe) is gone: an unconfigured host is inert and silent, and the
362
+ session AI is never invited to touch config. The ONE exception is the
363
+ `/crew` initialization gate (below), where AI-assisted configuration is
364
+ offered to YOU and happens only on your explicit go-ahead.
365
+ - **Parsing is field-level fail-soft** (ZCode-style): a semantically bad
366
+ agent entry is skipped with an error finding while its siblings apply; a
367
+ mistyped optional field is ignored with a warning; a syntactically broken
368
+ file empties the agent set with one parse-location error. There is no seed
369
+ in any failure path. Findings surface in the plugin diagnostics log.
370
+ - **`depths` is deprecated**: the array is ignored with a warning finding.
371
+ Pin ONE `thoughtLevel` per agent instead.
372
+ - Each agent materializes as `forge-<id>` — a subagent-mode agent visible in
373
+ the task tool's vocabulary (never hidden), carrying the pinned `model`,
374
+ the worker discipline (workspace-relative paths only, verbatim reporting
375
+ of tool refusals, evidence-bearing conclusions) around its role prompt,
376
+ and a deny-style permission: `shape: "readonly"` (the default) denies
377
+ `write`/`edit`/`bash`, `shape: "write"` allows them, an explicit
378
+ `permission` map overrides the shape default — and `task` is ALWAYS
379
+ denied (recursive spawning stays physically impossible).
380
+
381
+ **What applies when** — edits take hold per field:
382
+
383
+ | change | needs a host restart? |
384
+ | --- | --- |
385
+ | `thoughtLevel` on an existing agent | no (applies to NEW sessions of that agent; running sessions keep their frozen depth) |
386
+ | agent added / removed, or `model` / `prompt` / `permission` changed | yes (materialization runs on the config hook) |
387
+
388
+ ### Dispatching
389
+
390
+ Say "dispatch a subagent..." in a forge session and the model picks from the
391
+ task tool vocabulary: a configured `forge-*` agent whose description matches
392
+ runs on its pinned brain; if none matches, forge is instructed to say so
393
+ plainly and fall back to the native task channel. During a plan draft the
394
+ native `task` tool is already denied by the plan harness — no plugin-side
395
+ rule needed.
396
+
397
+ **Known pitfall — keyless endpoints × readonly agents**: free/keyless
398
+ providers can return a deterministic EMPTY response for any toolset-reduced
399
+ (readonly) agent. On the native task channel this now surfaces as a visible,
400
+ attributable task failure; the mitigation is unchanged — use
401
+ `shape: "write"` for such endpoints, or a keyed provider.
402
+
403
+ ### The thoughtLevel word
404
+
405
+ One word per agent: the canonical `none | low | medium | high | max`, or the
406
+ model's native level name verbatim (e.g. Qwen's `XHigh`). Injection rides the
407
+ `chat.params` hook keyed by agent name, translated per provider family:
408
+ effort-style (OpenAI-compatible) receives `reasoningEffort = <word>`;
409
+ budget-style (Anthropic-style) receives a published thinking tier
410
+ (`low → 8192`, `medium → 16384`, `high → 24576`, `max → 32768`,
411
+ `none → off`); toggle-style (zai/GLM) receives thinking on/off. Rules:
412
+ verbatim-first, no interpolation, unknown family injects nothing. A word the
413
+ model does not natively offer injects NOTHING and records one finding naming
414
+ both vocabularies — the session is never broken by a config word. A
415
+ session's depth is frozen at its first request (editing forge.json
416
+ mid-session never rewrites a running session).
417
+
418
+ ## Crew workflow (`/crew`)
419
+
420
+ `/crew <objective>` is the third state-activation command next to `/plan` and
421
+ `/goal`: it enters crew orchestration discipline for the session. The flow:
422
+
423
+ 1. **Register** the declared plan: `crew_begin {objective, subtasks}` — one
424
+ titled subtask each, optionally naming the intended `forge-*` agent.
425
+ 2. **Execute in waves** of parallel native `task` calls (results return
426
+ in-turn; the next wave launches only after the previous wave's results).
427
+ 3. **Missing role?** Run the subtask through a native task call anyway and
428
+ tell the user — suggest configuring the missing role.
429
+ 4. **Verify** each subtask against its acceptance-evidence statement;
430
+ retry a failure at most once.
431
+ 5. **Close** with `crew_close` — a hard, ask-gated gate cross-checking the
432
+ report against the DECLARED plan: every declared subtask needs a
433
+ PASS/FAIL verdict with evidence; an undeclared subtask in the report
434
+ refuses the close (fold discoveries into an existing verdict, or discard
435
+ and re-crew); a FAIL needs both failure reports. The close output is the
436
+ crew's record — crew state is in-memory and a host restart ends it
437
+ honestly.
438
+
439
+ **Not initialized?** `/crew` on a host with zero usable forge agents refuses
440
+ with one-time setup guidance: the two file paths, a copy-paste template, and
441
+ the choice of configuring it yourself OR having the session AI do it — the
442
+ latter only on your explicit go-ahead in that conversation, through the
443
+ normal visible write path. This is the plugin's only configuration
444
+ invitation. After saving the file, restart opencode to materialize the
445
+ agents.
446
+
447
+ ### Debris note
448
+
449
+ The old dispatch suite's ledger at `<tmp>/opencode-forge/dispatch/` is no
450
+ longer written. If it exists on your machine it is inert runtime debris —
451
+ delete the directory freely, also after uninstalling.
471
452
 
472
453
  ## Hang watchdog (a stuck builtin shell unblocks itself)
473
454