@sorenllm/opencode-forge 0.2.2 → 0.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/README.md +153 -3
- package/dist/index.js +1236 -68
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -80,7 +80,20 @@ the plugin never writes them):
|
|
|
80
80
|
"disable": true // one-knob return to native: no forge,
|
|
81
81
|
// build/plan restored, no tools/commands
|
|
82
82
|
}
|
|
83
|
-
}
|
|
83
|
+
},
|
|
84
|
+
// per-plugin options ride the plugin entry as a [spec, options] tuple
|
|
85
|
+
"plugin": [
|
|
86
|
+
["@sorenllm/opencode-forge", {
|
|
87
|
+
"jobs": {
|
|
88
|
+
"mode": "auto", // "auto" (default) | "forge" | "native" — see the stage matrix below
|
|
89
|
+
"keepBuiltinShell": false // stage 0: keep the builtin shell visible alongside forge_shell
|
|
90
|
+
},
|
|
91
|
+
"watchdog": {
|
|
92
|
+
"mode": "kill", // "kill" (default) | "dry-run" | "off"
|
|
93
|
+
"stallMs": 600000 // stall threshold, min 60000
|
|
94
|
+
}
|
|
95
|
+
}]
|
|
96
|
+
]
|
|
84
97
|
}
|
|
85
98
|
```
|
|
86
99
|
|
|
@@ -100,6 +113,9 @@ and the plugin's command of that name is not registered.
|
|
|
100
113
|
4. Done — the hidden native `build`/`plan` agents come back automatically
|
|
101
114
|
(the hide was runtime-only). Your `.opencode/plan/` and `.opencode/goal/`
|
|
102
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, and the watchdog ledger — the file ledger table
|
|
118
|
+
above lists everything).
|
|
103
119
|
|
|
104
120
|
## File ledger
|
|
105
121
|
|
|
@@ -109,7 +125,10 @@ What this plugin touches, exhaustively:
|
|
|
109
125
|
| --- | --- | --- |
|
|
110
126
|
| `<project>/.opencode/plan/*.md` | plan files | user data — kept forever, uninstall never deletes |
|
|
111
127
|
| `<project>/.opencode/goal/*.md` | goal files (contract, Check Log, Turn Ledger) | user data — kept forever, uninstall never deletes |
|
|
112
|
-
| merged config object (RAM only) | forge agent, native build/plan `disable`, `command.plan`, `command.goal`, goal permission keys | vanishes when the plugin is removed; nothing is written to disk |
|
|
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
|
+
| `<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
|
+
| `<tmp>/opencode-forge/jobs/ledger.jsonl` | job registry ledger (bounded: 1 MB reset, 200 entries) | runtime debris — delete freely, also after uninstall |
|
|
131
|
+
| `<tmp>/opencode-forge/watchdog/log.jsonl` | watchdog interventions ledger (bounded: 200 entries, oldest rotated) | runtime debris — delete freely, also after uninstall |
|
|
113
132
|
| `~/.cache/opencode/packages/...` | installed package copy | written by the `opencode plugin` installer, not the plugin |
|
|
114
133
|
| `~/.config/opencode/opencode.json` | `plugin` array entry | written by the installer |
|
|
115
134
|
|
|
@@ -187,6 +206,129 @@ They are drafted by the model from your objective. Read them in the dialog
|
|
|
187
206
|
before allowing the arm; `--contains` contracts are strictly
|
|
188
207
|
workspace-relative (path escape is refused).
|
|
189
208
|
|
|
209
|
+
## Job supervisor (a shell that can never hang the session)
|
|
210
|
+
|
|
211
|
+
The builtin `shell` tool treats closed stdio pipes as completion — a
|
|
212
|
+
detached grandchild holding them open suspends the call indefinitely (the
|
|
213
|
+
multi-hour agent hangs behind upstream issues #47350 / #50316 / #49169).
|
|
214
|
+
Subagents have it worse: the native task tool has no timeout at all.
|
|
215
|
+
`forge_shell` is the replacement exec surface for the forge agent.
|
|
216
|
+
|
|
217
|
+
**`forge_shell`** completes on the FIRST of four conditions, and only the
|
|
218
|
+
exit condition is bound to the actual exit event (structural immunity to
|
|
219
|
+
the stdio-EOF bug):
|
|
220
|
+
|
|
221
|
+
| condition | knob | on trigger |
|
|
222
|
+
| --- | --- | --- |
|
|
223
|
+
| process exit | — | final; exit code + output tail returned |
|
|
224
|
+
| `success_pattern` regex matches new output | opt-in | completes as success; process kept alive by default (server semantics), `keep_alive: false` kills its tree |
|
|
225
|
+
| `idle_ms` with no new output | default 60000 | early return `still-running` + `jobId`; process stays alive |
|
|
226
|
+
| `max_wait_ms` hard cap | default 120000, max 600000 | early return `still-running`; never kills |
|
|
227
|
+
|
|
228
|
+
`run_in_background: true` skips all waiting and returns
|
|
229
|
+
`{jobId, logPath}` immediately. Every run pops one permission dialog
|
|
230
|
+
(`permission.forge_shell = "ask"`; an explicit `deny` in your config wins).
|
|
231
|
+
|
|
232
|
+
**`forge_jobs`** manages the registry: `list` / `poll {jobId, waitMs≤30s}`
|
|
233
|
+
(bounded wait for new output or exit, drains it) / `log {jobId, offset?,
|
|
234
|
+
limit?}` (line paging over the on-disk log) / `kill` (whole process tree) /
|
|
235
|
+
`clear` (drop a finished entry) / `handoff` (rebind ownership to the root
|
|
236
|
+
session so a subagent's job survives the subagent). Delegated agents are
|
|
237
|
+
instructed to poll their jobs before yielding a conclusion.
|
|
238
|
+
|
|
239
|
+
**Exit wakes.** A job that exits after its `forge_shell` call already
|
|
240
|
+
returned queues a single `[forge:job-complete]` message, delivered into the
|
|
241
|
+
owning session via `promptAsync` the next time it goes idle (exactly once;
|
|
242
|
+
`notify: false` opts out per job).
|
|
243
|
+
|
|
244
|
+
**Ownership.** Jobs belong to the session that created them. Session
|
|
245
|
+
deleted → its live session-scoped jobs are killed and the event is recorded
|
|
246
|
+
in a bounded ledger (`handoff` beforehand survives). Plugin unload disposes
|
|
247
|
+
everything it still owns.
|
|
248
|
+
|
|
249
|
+
### Stage matrix (future compatibility, by design)
|
|
250
|
+
|
|
251
|
+
OpenCode upstream is converging on native backgrounding (PRs #47231 /
|
|
252
|
+
#50276, umbrella #34366). The supervisor degrades ahead of it:
|
|
253
|
+
|
|
254
|
+
| stage | how you get there | builtin shell | forge_shell / forge_jobs |
|
|
255
|
+
| --- | --- | --- | --- |
|
|
256
|
+
| 0 — full forge path | default (`jobs.mode: "auto"`, no native support detected) | hidden on the plugin-created forge agent (runtime injection only) | registered; the exec surface |
|
|
257
|
+
| 1 — native backgrounding detected | automatic: `config.experimental` background flag, or the builtin shell's schema grows `run_in_background` | visible again | still registered as the additive layer (idle/success/wake supervision); its description now points plain backgrounding at the native parameter |
|
|
258
|
+
| 2 — native confirmed complete | manual only: `jobs.mode: "native"` (never auto-detected — completeness is a semantic judgement) | visible | retired; calls throw with a pointer to the native parameter |
|
|
259
|
+
|
|
260
|
+
Pin `jobs.mode: "forge"` to stay on stage 0 forever; `jobs.keepBuiltinShell:
|
|
261
|
+
true` keeps the builtin shell visible at any stage.
|
|
262
|
+
|
|
263
|
+
### Artifacts and uninstall additions
|
|
264
|
+
|
|
265
|
+
Job output tees to `<tmp>/opencode-forge/jobs/<jobId>.log` (oldest rotated
|
|
266
|
+
out above 50 files) and registry events append to
|
|
267
|
+
`<tmp>/opencode-forge/jobs/ledger.jsonl` (bounded, 1 MB reset, 200 entries).
|
|
268
|
+
These are runtime debris, not data: deleting the directory after uninstall
|
|
269
|
+
is safe and complete.
|
|
270
|
+
|
|
271
|
+
## Hang watchdog (a stuck builtin shell unblocks itself)
|
|
272
|
+
|
|
273
|
+
`forge_shell` is structurally immune to the stdio-EOF hang, but the builtin
|
|
274
|
+
`shell`/`bash` tool can still hang outside it — user-defined agents keep
|
|
275
|
+
the builtin shell, and the stage matrix above restores it at stage 1/2.
|
|
276
|
+
The watchdog is the independent backstop for those paths, in **every**
|
|
277
|
+
session (primary and delegated subagents alike):
|
|
278
|
+
|
|
279
|
+
1. The host's shell environment hook stamps each builtin shell call's
|
|
280
|
+
process with a plugin-namespaced marker (`FORGE_WATCHDOG_MARK`), and the
|
|
281
|
+
call is timed from start to end.
|
|
282
|
+
2. At 80% of the stall budget: a diagnostic entry (the session is busy —
|
|
283
|
+
injecting a message would just queue, so nothing is sent to it).
|
|
284
|
+
3. At `watchdog.stallMs` (default 600000): the call's process tree is
|
|
285
|
+
located and **killed** — the pipes hit EOF and the pending tool call
|
|
286
|
+
resolves immediately with whatever output was captured.
|
|
287
|
+
4. If the call still hasn't returned after the kill, or no matching
|
|
288
|
+
process exists (a hang with nothing to kill): an honest
|
|
289
|
+
diagnostics-only `unresolved` report.
|
|
290
|
+
|
|
291
|
+
Location is exact where foreign process environments are readable (POSIX
|
|
292
|
+
`/proc/*/environ` marker match). On Windows they are not readable, so the
|
|
293
|
+
watchdog infers in two guarded waves, both restricted to processes created
|
|
294
|
+
during the stalled call: wave 1 matches descendants of the opencode host
|
|
295
|
+
process or processes whose command line carries the stalled call's own
|
|
296
|
+
command text; wave 2 — only when wave 1 found nothing, or killed and the
|
|
297
|
+
call still didn't return within the observation window — additionally
|
|
298
|
+
matches processes whose command lives in the stalled command's own
|
|
299
|
+
directory, which is exactly where the detached stdio holders of the
|
|
300
|
+
exit-with-inherited-stdio hang class sit. The locator never targets its
|
|
301
|
+
own probe processes, console hosts (`conhost.exe`), or anything created
|
|
302
|
+
before the call's time window. If the shell environment hook itself ever
|
|
303
|
+
stops firing (host API drift), the watchdog detects the missing marker and
|
|
304
|
+
degrades itself to dry-run instead of killing by inference alone.
|
|
305
|
+
|
|
306
|
+
**Modes** (`watchdog.mode`, default `kill`): `dry-run` records the exact
|
|
307
|
+
process list it *would* terminate and kills nothing — recommended for a
|
|
308
|
+
first observation round on a new host; `off` disables timing entirely.
|
|
309
|
+
`watchdog.stallMs` is clamped to a 60 s protection floor. Invalid option
|
|
310
|
+
values fall back to the defaults and the fallback is ledgered.
|
|
311
|
+
|
|
312
|
+
**Known trade-off.** The builtin shell exposes no output visibility, so
|
|
313
|
+
the watchdog cannot tell "hung" from "quietly working" — a legitimate
|
|
314
|
+
silent command longer than the threshold will be killed. That is the
|
|
315
|
+
deliberated price of stopping multi-hour hangs; the guidance layer already
|
|
316
|
+
routes legitimate long-running work to `forge_shell`, which has real idle
|
|
317
|
+
detection. On Windows, concurrent builtin shell calls started inside the
|
|
318
|
+
same window are inferred together (the rare case) — `dry-run` makes the
|
|
319
|
+
exact blast radius auditable before you trust `kill`.
|
|
320
|
+
|
|
321
|
+
**Retiring it.** When upstream ships exit-based completion for the builtin
|
|
322
|
+
shell, switch `watchdog.mode` to `dry-run` for an observation round, then
|
|
323
|
+
`off`.
|
|
324
|
+
|
|
325
|
+
### Watchdog ledger
|
|
326
|
+
|
|
327
|
+
Every intervention (warn / kill / dry-run candidate / unresolved /
|
|
328
|
+
config fallback) appends to `<tmp>/opencode-forge/watchdog/log.jsonl`
|
|
329
|
+
(bounded: 200 entries, oldest rotated out). Deleting the directory after
|
|
330
|
+
uninstall is safe and complete.
|
|
331
|
+
|
|
190
332
|
## Design stance (read before filing "bash is blocked" issues)
|
|
191
333
|
|
|
192
334
|
During a plan's draft phase every mutating tool — `bash` included — is
|
|
@@ -195,6 +337,12 @@ read/grep/glob; if you genuinely need a shell command to decide the plan,
|
|
|
195
337
|
approve the plan first (revising after approval is allowed via a new `/plan`).
|
|
196
338
|
The escape hatches are `plan_approve` and `/plan discard`, by design.
|
|
197
339
|
|
|
340
|
+
Separately, at stage 0 the builtin shell is hidden on the plugin-created
|
|
341
|
+
forge agent on purpose — `forge_shell` is the exec surface there (see the
|
|
342
|
+
job supervisor chapter above). A **user-defined** `agent.forge` entry,
|
|
343
|
+
`jobs.keepBuiltinShell: true`, or any stage ≥ 1 keeps the builtin shell
|
|
344
|
+
visible.
|
|
345
|
+
|
|
198
346
|
A process restart forgets the session binding: the write-ban soft-disables
|
|
199
347
|
(safety over strictness) and the next session's system notice + `/plan
|
|
200
348
|
resume` re-bind from the plan file on disk, which is the source of truth.
|
|
@@ -214,7 +362,9 @@ carry their own full discipline, hermes-style: the entry turn is the
|
|
|
214
362
|
rulebook) + `src/plan-file.ts` / `src/goal-file.ts`
|
|
215
363
|
(pure document cores, unit-tested, no opencode imports) + `src/run-check.ts`
|
|
216
364
|
(shell/file-contract runner with tree-kill timeouts and a workspace path
|
|
217
|
-
guard).
|
|
365
|
+
guard) + `src/proc.ts` (shared spawn/tree-kill muscle) + `src/job-manager.ts`
|
|
366
|
+
(job registry, ownership, wake queue — pure logic) + `src/job-runner.ts`
|
|
367
|
+
(four-condition race, pipe capture, log tee). Behavioral changes go through the
|
|
218
368
|
OpenSpec workflow in `openspec/` — see AGENTS.md. Common pitfalls live in
|
|
219
369
|
`../opencode-plugin-dev-pitfalls.md`.
|
|
220
370
|
|