@sorenllm/opencode-forge 0.4.0 → 0.6.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 +103 -38
- package/dist/index.js +177 -43
- package/package.json +64 -64
package/README.md
CHANGED
|
@@ -33,8 +33,10 @@ OpenSpec spec workflows remain a third, separate lane.
|
|
|
33
33
|
- Plan files: `.opencode/plan/<date>-<slug>.md` in your project, frontmatter
|
|
34
34
|
state machine `draft → approved → done` (exit: `abandoned`).
|
|
35
35
|
- While a plan is in draft, `write` / `edit` / `bash` / `task` are **denied
|
|
36
|
-
at the permission layer** — including your own `allow` config
|
|
37
|
-
|
|
36
|
+
at the permission layer** — including your own `allow` config, and for
|
|
37
|
+
every agent in that session (the draft protects shared session state; see
|
|
38
|
+
Agent partition below). The only exits are approval and discard. This is
|
|
39
|
+
deliberate; see Design stance.
|
|
38
40
|
- `plan_approve` / `plan_close` are pinned to a confirmation dialog: the
|
|
39
41
|
model can never flip the state itself.
|
|
40
42
|
- Goal files: `.opencode/goal/<date>-<slug>.md`, state machine
|
|
@@ -42,9 +44,40 @@ OpenSpec spec workflows remain a third, separate lane.
|
|
|
42
44
|
session plus a workspace queue. Completion is **verified by the plugin**,
|
|
43
45
|
not attested by the model: `goal_complete` re-executes every check itself
|
|
44
46
|
and refuses (fail-closed) on any failure.
|
|
45
|
-
- The native `build` / `plan` agents
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
- The native `build` / `plan` agents **stay installed and independent**:
|
|
48
|
+
forge coexists with them in the Tab cycle. The plugin hard-partitions the
|
|
49
|
+
two worlds (see Agent partition below) instead of hiding anything; plan
|
|
50
|
+
and goal files are never deleted on uninstall.
|
|
51
|
+
|
|
52
|
+
## Agent partition (who sees what)
|
|
53
|
+
|
|
54
|
+
The plugin draws a hard line between two agent families, enforced at the
|
|
55
|
+
tool layer (verified against the host's `tools` filter):
|
|
56
|
+
|
|
57
|
+
| | **forge family** — `forge` + your forge.json `forge-*` workers | **everyone else** — native `build` / `plan` / `general` / `explore` and your own agents |
|
|
58
|
+
| --- | --- | --- |
|
|
59
|
+
| exec surface | `forge_shell` / `forge_jobs` only; builtin `shell`/`bash` hidden (unconditional, probe-independent; covers user-defined `agent.forge` entries too) | builtin `shell`/`bash`, fully native |
|
|
60
|
+
| forge tools (`plan_*`, `goal_*`, `crew_*`, `forge_shell`, `forge_jobs`) | `forge` sees all; `forge-*` workers only `forge_shell` + `forge_jobs` | none visible |
|
|
61
|
+
| dispatching `forge-*` via `task` | allowed | refused at the tool layer |
|
|
62
|
+
| writing `.opencode/plan/` / `.opencode/goal/` | allowed (state machine governs) | refused at the tool layer (reads stay open) |
|
|
63
|
+
| forge system prompts (job guidance, plan/goal notices) | injected | never injected — zero forge text |
|
|
64
|
+
| watchdog (stuck builtin shell) | governed | untouched — no markers, no kills |
|
|
65
|
+
|
|
66
|
+
Escape hatches: `jobs.keepBuiltinShell: true` gives the forge family the
|
|
67
|
+
builtin shell back; `jobs.mode: "native"` retires `forge_shell`/`forge_jobs`
|
|
68
|
+
entirely (the hide is withdrawn in the same stroke). Mixed sessions (Tab
|
|
69
|
+
between agents in one session): harness state (plans/goals) stays
|
|
70
|
+
session-bound, tool surfaces follow the current speaker, and the goal loop
|
|
71
|
+
parks instead of driving a non-forge turn. One deliberate exception: a
|
|
72
|
+
draft-phase write ban binds the **session** (protecting shared state), so a
|
|
73
|
+
non-forge agent is write-blocked until the draft is approved or discarded in
|
|
74
|
+
forge. Running `/plan` `/goal` `/crew` under a non-forge agent redirects to
|
|
75
|
+
forge instead of executing.
|
|
76
|
+
|
|
77
|
+
**Default subject.** The host otherwise defaults new sessions to `build`
|
|
78
|
+
once a build agent exists — the plugin pins `default_agent: "forge"`
|
|
79
|
+
automatically (a `default_agent` you set yourself always wins), so bare
|
|
80
|
+
runs and fresh sessions still land on forge under coexistence.
|
|
48
81
|
|
|
49
82
|
## Install
|
|
50
83
|
|
|
@@ -78,18 +111,19 @@ the plugin never writes them):
|
|
|
78
111
|
"forge": {
|
|
79
112
|
"model": "provider/model", // pick any model for forge
|
|
80
113
|
"disable": true // one-knob return to native: no forge,
|
|
81
|
-
//
|
|
114
|
+
// no injections at all, no tools/commands
|
|
82
115
|
}
|
|
83
116
|
},
|
|
84
117
|
// per-plugin options ride the plugin entry as a [spec, options] tuple
|
|
85
118
|
"plugin": [
|
|
86
119
|
["@sorenllm/opencode-forge", {
|
|
87
120
|
"jobs": {
|
|
88
|
-
"mode": "auto", // "auto" (default) | "forge"
|
|
89
|
-
|
|
121
|
+
"mode": "auto", // "auto" (default) | "forge" — equivalent, both keep the partition
|
|
122
|
+
// | "native" — retire the supervisor, builtin shell returns
|
|
123
|
+
"keepBuiltinShell": false // true: give the forge family the builtin shell back (the one exec escape hatch)
|
|
90
124
|
},
|
|
91
125
|
"watchdog": {
|
|
92
|
-
"mode": "kill", // "kill" (default) | "dry-run" | "off"
|
|
126
|
+
"mode": "kill", // "kill" (default) | "dry-run" | "off" — forge-family sessions only
|
|
93
127
|
"stallMs": 600000 // stall threshold, min 60000
|
|
94
128
|
}
|
|
95
129
|
}]
|
|
@@ -110,7 +144,8 @@ and the plugin's command of that name is not registered.
|
|
|
110
144
|
`~/.cache/opencode/packages/github_ChengZiiii/opencode-forge/`).
|
|
111
145
|
3. Delete the `agent["forge"]` block from your config if you added one
|
|
112
146
|
(otherwise the name lingers in the agent list).
|
|
113
|
-
4. Done —
|
|
147
|
+
4. Done — all runtime injections (agents, tool hiding, commands) are gone
|
|
148
|
+
with the plugin; nothing was ever written to your config
|
|
114
149
|
(the hide was runtime-only). Your `.opencode/plan/` and `.opencode/goal/`
|
|
115
150
|
files are yours; delete them yourself if you want.
|
|
116
151
|
5. Optional runtime debris: delete `<tmp>/opencode-forge/` (job logs, the
|
|
@@ -126,7 +161,7 @@ What this plugin touches, exhaustively:
|
|
|
126
161
|
| --- | --- | --- |
|
|
127
162
|
| `<project>/.opencode/plan/*.md` | plan files | user data — kept forever, uninstall never deletes |
|
|
128
163
|
| `<project>/.opencode/goal/*.md` | goal files (contract, Check Log, Turn Ledger) | user data — kept forever, uninstall never deletes |
|
|
129
|
-
| merged config object (RAM only) | forge agent, static `forge-<id>` subagents, native build/plan
|
|
164
|
+
| merged config object (RAM only) | forge agent, static `forge-<id>` subagents, tool-partition injections (forge family shell hide, non-forge forge-tool hide, native build/plan/general/explore minimal entries), `command.plan`, `command.goal`, `command.crew`, goal permission keys, `permission.forge_shell`, `permission.crew_close` | vanishes when the plugin is removed; nothing is written to disk |
|
|
130
165
|
| `<tmp>/opencode-forge/jobs/<jobId>.log` | job output tee (full output; oldest rotated out above 50 files) | runtime debris — delete freely, also after uninstall |
|
|
131
166
|
| `<tmp>/opencode-forge/jobs/ledger.jsonl` | job registry ledger (bounded: 1 MB reset, 200 entries) | runtime debris — delete freely, also after uninstall |
|
|
132
167
|
| `<tmp>/opencode-forge/jobs/registry.json` | persistent survivor registry (bounded: 100 entries) | runtime debris — after uninstall, kill any still-running `survive` jobs yourself first |
|
|
@@ -293,19 +328,20 @@ as usual. Config `jobs.survive: "always"` flips the per-call default; config
|
|
|
293
328
|
whose pid died are detected at next start, ledgered as orphans, and dropped
|
|
294
329
|
from the registry. Stop survivors explicitly — nothing else will.
|
|
295
330
|
|
|
296
|
-
###
|
|
331
|
+
### Mode matrix (supervisor lifecycle)
|
|
297
332
|
|
|
298
|
-
|
|
299
|
-
|
|
333
|
+
The old three-stage capability probe is retired: the forge-side partition
|
|
334
|
+
holds regardless of what the host's builtin shell can do, so `auto` and
|
|
335
|
+
`forge` are equivalent. Only the manual `native` mode retires the
|
|
336
|
+
supervisor:
|
|
300
337
|
|
|
301
|
-
|
|
|
338
|
+
| mode | how you get there | builtin shell (forge family) | forge_shell / forge_jobs |
|
|
302
339
|
| --- | --- | --- | --- |
|
|
303
|
-
|
|
|
304
|
-
|
|
|
305
|
-
| 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 |
|
|
340
|
+
| auto (default) / forge | nothing to do | hidden — the exec surface is forge_shell, always | registered |
|
|
341
|
+
| native | manual only: `jobs.mode: "native"` | restored | retired; calls throw with a pointer to the native parameter |
|
|
306
342
|
|
|
307
|
-
|
|
308
|
-
|
|
343
|
+
`jobs.keepBuiltinShell: true` gives the forge family the builtin shell back
|
|
344
|
+
in any mode (the watchdog keeps governing those calls).
|
|
309
345
|
|
|
310
346
|
### Artifacts and uninstall additions
|
|
311
347
|
|
|
@@ -323,8 +359,10 @@ complete.
|
|
|
323
359
|
## Forge subagents (static worker agents in forge.json)
|
|
324
360
|
|
|
325
361
|
Forge subagents are **static, user-authored worker agents**: each forge.json
|
|
326
|
-
entry
|
|
327
|
-
|
|
362
|
+
entry is either **pinned** (a model + a reasoning depth, bound at
|
|
363
|
+
materialization) or an **Auto worker** (no model — it inherits the parent
|
|
364
|
+
session's model at dispatch), materializes as a native `forge-<id>`
|
|
365
|
+
subagent, and is dispatched through the host's **native `task`
|
|
328
366
|
tool** — which means every run is visible in the TUI, expandable and
|
|
329
367
|
monitorable, exactly like any native subagent. There is no dispatch tool, no
|
|
330
368
|
child-session engine, and no configuration invitation anywhere: the session
|
|
@@ -345,17 +383,33 @@ cascade with a single winning source (never merged):
|
|
|
345
383
|
{
|
|
346
384
|
// forge subagents — definitions; see the restart table below for apply timing
|
|
347
385
|
"agents": {
|
|
386
|
+
// PINNED worker: model + thoughtLevel form an ATOMIC PAIR — set BOTH
|
|
348
387
|
"research": {
|
|
349
|
-
"model": "zai-coding-plan/glm-5.3", // exact "provider/model" string
|
|
350
|
-
"thoughtLevel": "low", //
|
|
388
|
+
"model": "zai-coding-plan/glm-5.3", // exact "provider/model" string
|
|
389
|
+
"thoughtLevel": "low", // none/low/medium/high/max or a native level name
|
|
351
390
|
// "prompt": "optional role prompt; built-in research/review defaults exist",
|
|
352
391
|
// "shape": "write", // default readonly denies write/edit/bash
|
|
353
392
|
// "permission": { "bash": "deny" } // optional override; "task" is ALWAYS denied
|
|
354
|
-
}
|
|
393
|
+
},
|
|
394
|
+
// AUTO worker: set NEITHER — inherits the parent session's model at
|
|
395
|
+
// dispatch (snapshot semantics: later primary model switches affect only
|
|
396
|
+
// future dispatches), runs at the provider's default depth
|
|
397
|
+
"scout": {}
|
|
355
398
|
}
|
|
356
399
|
}
|
|
357
400
|
```
|
|
358
401
|
|
|
402
|
+
**The atomic pair — the one rule that matters:**
|
|
403
|
+
|
|
404
|
+
| `model` | `thoughtLevel` | result |
|
|
405
|
+
| --- | --- | --- |
|
|
406
|
+
| set | set | pinned worker (brain + depth bound at materialization) |
|
|
407
|
+
| — | — | **Auto worker** (inherits the parent session's model; provider-default depth) |
|
|
408
|
+
| set | — | ✗ rejected: entry skipped, error finding names the missing `thoughtLevel` |
|
|
409
|
+
| — | set | ✗ rejected: entry skipped, error finding names the missing `model` |
|
|
410
|
+
|
|
411
|
+
A rejected entry costs only itself — its siblings materialize normally.
|
|
412
|
+
|
|
359
413
|
- **The plugin only ever reads this file.** It never creates, writes, or
|
|
360
414
|
migrates it. The old AI-authored onboarding path (seed placeholder +
|
|
361
415
|
error recipe) is gone: an unconfigured host is inert and silent, and the
|
|
@@ -364,13 +418,17 @@ cascade with a single winning source (never merged):
|
|
|
364
418
|
offered to YOU and happens only on your explicit go-ahead.
|
|
365
419
|
- **Parsing is field-level fail-soft** (ZCode-style): a semantically bad
|
|
366
420
|
agent entry is skipped with an error finding while its siblings apply; a
|
|
367
|
-
mistyped optional field is ignored with a warning
|
|
368
|
-
|
|
369
|
-
|
|
421
|
+
mistyped optional field is ignored with a warning (and counts as absent
|
|
422
|
+
for the atomic pair — so a pinned `model` with a typo'd `thoughtLevel`
|
|
423
|
+
skips the whole entry); a syntactically broken file empties the agent set
|
|
424
|
+
with one parse-location error. There is no seed in any failure path.
|
|
425
|
+
Findings surface in the plugin diagnostics log.
|
|
370
426
|
- **`depths` is deprecated**: the array is ignored with a warning finding.
|
|
371
427
|
Pin ONE `thoughtLevel` per agent instead.
|
|
372
428
|
- Each agent materializes as `forge-<id>` — a subagent-mode agent visible in
|
|
373
|
-
the task tool's vocabulary (never hidden), carrying the pinned `model
|
|
429
|
+
the task tool's vocabulary (never hidden), carrying the pinned `model`
|
|
430
|
+
(pinned workers) or NO model key at all (Auto workers — the description
|
|
431
|
+
says "inherits the parent session's model" instead of naming a brain),
|
|
374
432
|
the worker discipline (workspace-relative paths only, verbatim reporting
|
|
375
433
|
of tool refusals, evidence-bearing conclusions) around its role prompt,
|
|
376
434
|
and a deny-style permission: `shape: "readonly"` (the default) denies
|
|
@@ -378,6 +436,11 @@ cascade with a single winning source (never merged):
|
|
|
378
436
|
`permission` map overrides the shape default — and `task` is ALWAYS
|
|
379
437
|
denied (recursive spawning stays physically impossible).
|
|
380
438
|
|
|
439
|
+
> **Upgrading from ≤ 0.5.0 (breaking):** agent entries that pinned `model`
|
|
440
|
+
> without `thoughtLevel` used to be accepted; they are now SKIPPED with an
|
|
441
|
+
> error finding naming the missing half. Fix per entry: add the missing
|
|
442
|
+
> `thoughtLevel`, or delete `model` to make it an Auto worker.
|
|
443
|
+
|
|
381
444
|
**What applies when** — edits take hold per field:
|
|
382
445
|
|
|
383
446
|
| change | needs a host restart? |
|
|
@@ -453,10 +516,11 @@ delete the directory freely, also after uninstalling.
|
|
|
453
516
|
## Hang watchdog (a stuck builtin shell unblocks itself)
|
|
454
517
|
|
|
455
518
|
`forge_shell` is structurally immune to the stdio-EOF hang, but the builtin
|
|
456
|
-
`shell`/`bash` tool can still hang outside it —
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
519
|
+
`shell`/`bash` tool can still hang outside it — under `jobs.mode: "native"`
|
|
520
|
+
or `jobs.keepBuiltinShell: true`, forge-family sessions carry it again. The
|
|
521
|
+
watchdog is the independent backstop for those paths, in **forge-family
|
|
522
|
+
sessions only** (primary and delegated forge-* workers alike) — non-forge
|
|
523
|
+
agents' builtin shells are entirely native: no markers, no timing, no kills:
|
|
460
524
|
|
|
461
525
|
1. The host's shell environment hook stamps each builtin shell call's
|
|
462
526
|
process with a plugin-namespaced marker (`FORGE_WATCHDOG_MARK`), and the
|
|
@@ -519,11 +583,12 @@ read/grep/glob; if you genuinely need a shell command to decide the plan,
|
|
|
519
583
|
approve the plan first (revising after approval is allowed via a new `/plan`).
|
|
520
584
|
The escape hatches are `plan_approve` and `/plan discard`, by design.
|
|
521
585
|
|
|
522
|
-
Separately,
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
`
|
|
526
|
-
|
|
586
|
+
Separately, the builtin shell is hidden on the forge agent on purpose —
|
|
587
|
+
`forge_shell` is the exec surface there (see Agent partition and the job
|
|
588
|
+
supervisor chapter above). The hide is unconditional: it covers a
|
|
589
|
+
**user-defined** `agent.forge` entry too. The escape hatches are
|
|
590
|
+
`jobs.keepBuiltinShell: true` (builtin shell back on the forge family) and
|
|
591
|
+
`jobs.mode: "native"` (supervisor retired entirely).
|
|
527
592
|
|
|
528
593
|
A process restart forgets the session binding: the write-ban soft-disables
|
|
529
594
|
(safety over strictness) and the next session's system notice + `/plan
|
package/dist/index.js
CHANGED
|
@@ -14867,6 +14867,7 @@ function validateCrewReport(report, declared) {
|
|
|
14867
14867
|
var WORKER_MANDATES = [
|
|
14868
14868
|
"Mandates (non-negotiable):",
|
|
14869
14869
|
"- Use workspace-relative paths ONLY. Never write to or read from absolute paths outside this workspace; the workspace root is where you were started, not the filesystem root.",
|
|
14870
|
+
"- Run every shell command through the forge_shell tool (the builtin shell is not part of your toolset by design). Long-running or non-exiting commands: forge_shell with run_in_background, then collect results via forge_jobs poll before yielding.",
|
|
14870
14871
|
"- If any tool call is refused or denied, report the refusal VERBATIM in your final answer and stop that line of work. Never improvise a workaround around a permission denial.",
|
|
14871
14872
|
"- End with conclusions backed by evidence references (file:line, command output, or the exact file/content you produced)."
|
|
14872
14873
|
].join(`
|
|
@@ -14892,7 +14893,8 @@ function rolePromptFor(agentId, def) {
|
|
|
14892
14893
|
}
|
|
14893
14894
|
function agentDescriptionFor(agentId, def) {
|
|
14894
14895
|
const shape = def.shape === "write" ? "write" : "readonly";
|
|
14895
|
-
|
|
14896
|
+
const brain = def.model !== undefined ? `pinned ${def.model}` : "auto — inherits the parent session's model at dispatch";
|
|
14897
|
+
return `forge subagent "${agentId}" (${shape}, ${brain}) — dispatch via the task tool with subagent_type "forge-${agentId}".`;
|
|
14896
14898
|
}
|
|
14897
14899
|
function forgeAgentDef(agentId, def) {
|
|
14898
14900
|
const permission = { task: "deny" };
|
|
@@ -14903,17 +14905,20 @@ function forgeAgentDef(agentId, def) {
|
|
|
14903
14905
|
}
|
|
14904
14906
|
Object.assign(permission, def.permission ?? {});
|
|
14905
14907
|
permission.task = "deny";
|
|
14906
|
-
|
|
14908
|
+
const entry = {
|
|
14907
14909
|
description: agentDescriptionFor(agentId, def),
|
|
14908
14910
|
mode: "subagent",
|
|
14909
|
-
model: def.model,
|
|
14910
14911
|
prompt: materializeAgentPrompt({
|
|
14911
14912
|
agent: agentId,
|
|
14912
14913
|
shape,
|
|
14913
14914
|
role: rolePromptFor(agentId, def)
|
|
14914
14915
|
}),
|
|
14915
|
-
permission
|
|
14916
|
+
permission,
|
|
14917
|
+
tools: { shell: false, bash: false }
|
|
14916
14918
|
};
|
|
14919
|
+
if (def.model !== undefined)
|
|
14920
|
+
entry.model = def.model;
|
|
14921
|
+
return entry;
|
|
14917
14922
|
}
|
|
14918
14923
|
|
|
14919
14924
|
// src/dispatch-depth.ts
|
|
@@ -15248,21 +15253,36 @@ function validateAgentSet(raw) {
|
|
|
15248
15253
|
findings.push({ level: "error", code: "agent-not-object", message: `${label}: definition must be an object — entry skipped` });
|
|
15249
15254
|
continue;
|
|
15250
15255
|
}
|
|
15251
|
-
if (!nonEmptyString(entry.model)) {
|
|
15252
|
-
findings.push({ level: "error", code: "agent-model-missing", message: `${label}.model: a non-empty "provider/model" string is required — entry skipped, siblings still apply` });
|
|
15253
|
-
continue;
|
|
15254
|
-
}
|
|
15255
|
-
const def = { model: entry.model.trim() };
|
|
15256
15256
|
if (entry.depths !== undefined) {
|
|
15257
15257
|
findings.push({ level: "warn", code: "depths-deprecated", message: `${label}.depths is no longer used (static agents pin one thoughtLevel) — ignored; set "thoughtLevel" instead` });
|
|
15258
15258
|
}
|
|
15259
|
+
let model;
|
|
15260
|
+
if (entry.model !== undefined) {
|
|
15261
|
+
if (nonEmptyString(entry.model)) {
|
|
15262
|
+
model = entry.model.trim();
|
|
15263
|
+
} else {
|
|
15264
|
+
findings.push({ level: "warn", code: "model-invalid", message: `${label}.model must be a non-empty string — field ignored (counts as absent for the model/thoughtLevel pair)` });
|
|
15265
|
+
}
|
|
15266
|
+
}
|
|
15267
|
+
let thoughtLevel;
|
|
15259
15268
|
if (entry.thoughtLevel !== undefined) {
|
|
15260
15269
|
if (nonEmptyString(entry.thoughtLevel)) {
|
|
15261
|
-
|
|
15270
|
+
thoughtLevel = entry.thoughtLevel.trim();
|
|
15262
15271
|
} else {
|
|
15263
|
-
findings.push({ level: "warn", code: "thoughtlevel-invalid", message: `${label}.thoughtLevel must be a non-empty string — field ignored
|
|
15272
|
+
findings.push({ level: "warn", code: "thoughtlevel-invalid", message: `${label}.thoughtLevel must be a non-empty string — field ignored (counts as absent for the model/thoughtLevel pair)` });
|
|
15264
15273
|
}
|
|
15265
15274
|
}
|
|
15275
|
+
if (model === undefined !== (thoughtLevel === undefined)) {
|
|
15276
|
+
const presentHalf = model !== undefined ? "model" : "thoughtLevel";
|
|
15277
|
+
const missingHalf = model !== undefined ? "thoughtLevel" : "model";
|
|
15278
|
+
findings.push({ level: "error", code: "agent-half-configured", message: `${label}: model and thoughtLevel form an atomic pair — ${presentHalf} is set but ${missingHalf} is missing; set BOTH to pin the brain and depth, or NEITHER for an Auto worker — entry skipped, siblings still apply` });
|
|
15279
|
+
continue;
|
|
15280
|
+
}
|
|
15281
|
+
const def = {};
|
|
15282
|
+
if (model !== undefined) {
|
|
15283
|
+
def.model = model;
|
|
15284
|
+
def.thoughtLevel = thoughtLevel;
|
|
15285
|
+
}
|
|
15266
15286
|
if (entry.prompt !== undefined) {
|
|
15267
15287
|
if (nonEmptyString(entry.prompt)) {
|
|
15268
15288
|
def.prompt = entry.prompt;
|
|
@@ -15363,6 +15383,25 @@ function createForgeConfigLoader(deps) {
|
|
|
15363
15383
|
|
|
15364
15384
|
// plugin.ts
|
|
15365
15385
|
var FORGE_AGENT = "forge";
|
|
15386
|
+
var FORGE_STATE_TOOLS = [
|
|
15387
|
+
"plan_write",
|
|
15388
|
+
"plan_tick",
|
|
15389
|
+
"plan_approve",
|
|
15390
|
+
"plan_close",
|
|
15391
|
+
"plan_discard",
|
|
15392
|
+
"goal_write",
|
|
15393
|
+
"goal_check",
|
|
15394
|
+
"goal_complete",
|
|
15395
|
+
"goal_pause",
|
|
15396
|
+
"goal_resume",
|
|
15397
|
+
"goal_discard",
|
|
15398
|
+
"crew_begin",
|
|
15399
|
+
"crew_close"
|
|
15400
|
+
];
|
|
15401
|
+
var FORGE_EXEC_TOOLS = ["forge_shell", "forge_jobs"];
|
|
15402
|
+
var FORGE_ALL_TOOLS = [...FORGE_STATE_TOOLS, ...FORGE_EXEC_TOOLS];
|
|
15403
|
+
var FORGE_TOOL_SET = new Set(FORGE_ALL_TOOLS);
|
|
15404
|
+
var NATIVE_PARTITION_FALLBACKS = ["build", "plan", "general", "explore"];
|
|
15366
15405
|
var FORGE_PROMPT = `You are forge — the single general-purpose coding agent. You handle every task directly: exploration, planning, implementation, and verification. There is no agent switching.
|
|
15367
15406
|
|
|
15368
15407
|
Workflow modes are strictly user-initiated. Never enter plan or goal mode — and never call a plan_* or goal_* tool — unless the user ran /plan or /goal, unmistakably asked for that mode (e.g. "plan first", "set a goal"), or the session already carries a [forge:plan-notice] / [forge:goal-notice] for a mode they started. Ordinary task requests are normal work. Mode-specific rules arrive with those commands and notices; when a notice is present, follow it.
|
|
@@ -15371,6 +15410,8 @@ Subagent dispatch (task tool): when the work suits a scoped worker, prefer a con
|
|
|
15371
15410
|
var PLAN_COMMAND_TEMPLATE = [
|
|
15372
15411
|
'(forge plan harness routing. Argument: "$ARGUMENTS")',
|
|
15373
15412
|
"",
|
|
15413
|
+
"FAMILY GUARD (step 0, before anything else): if you do not have the plan_write tool in your available tools, you are not in a forge session — STOP now: execute nothing below (including the directory listing), and tell the user that /plan belongs to the forge agent, who should switch to forge (Tab) and rerun it there.",
|
|
15414
|
+
"",
|
|
15374
15415
|
"Current .opencode/plan/ directory:",
|
|
15375
15416
|
"!`ls -1 .opencode/plan 2>/dev/null || echo '(empty)'`",
|
|
15376
15417
|
"",
|
|
@@ -15397,6 +15438,8 @@ var PLAN_COMMAND_TEMPLATE = [
|
|
|
15397
15438
|
var GOAL_COMMAND_TEMPLATE = [
|
|
15398
15439
|
'(forge goal harness routing. Argument: "$ARGUMENTS")',
|
|
15399
15440
|
"",
|
|
15441
|
+
"FAMILY GUARD (step 0, before anything else): if you do not have the goal_write tool in your available tools, you are not in a forge session — STOP now: execute nothing below (including the directory listing), and tell the user that /goal belongs to the forge agent, who should switch to forge (Tab) and rerun it there.",
|
|
15442
|
+
"",
|
|
15400
15443
|
"Current .opencode/goal/ directory:",
|
|
15401
15444
|
"!`ls -1 .opencode/goal 2>/dev/null || echo '(empty)'`",
|
|
15402
15445
|
"",
|
|
@@ -15425,6 +15468,23 @@ function effectiveWorktree(worktree, fallback) {
|
|
|
15425
15468
|
return (fallback ?? "").trim();
|
|
15426
15469
|
}
|
|
15427
15470
|
var sessions = new Map;
|
|
15471
|
+
var sessionAgents = new Map;
|
|
15472
|
+
function isForgeFamilyAgent(name) {
|
|
15473
|
+
if (typeof name !== "string")
|
|
15474
|
+
return false;
|
|
15475
|
+
const n = name.toLowerCase();
|
|
15476
|
+
return n === FORGE_AGENT || n.startsWith("forge-");
|
|
15477
|
+
}
|
|
15478
|
+
function sessionIsForgeFamily(sessionID) {
|
|
15479
|
+
if (!sessionID)
|
|
15480
|
+
return false;
|
|
15481
|
+
return isForgeFamilyAgent(sessionAgents.get(sessionID));
|
|
15482
|
+
}
|
|
15483
|
+
function noteToolAgent(context) {
|
|
15484
|
+
if (context && typeof context.sessionID === "string" && context.sessionID && typeof context.agent === "string" && context.agent) {
|
|
15485
|
+
sessionAgents.set(context.sessionID, context.agent);
|
|
15486
|
+
}
|
|
15487
|
+
}
|
|
15428
15488
|
var forgeDisabled = false;
|
|
15429
15489
|
var forgeLoader = null;
|
|
15430
15490
|
var forgeLoaderDir = null;
|
|
@@ -15489,13 +15549,10 @@ function ensureJobLifecycle() {
|
|
|
15489
15549
|
});
|
|
15490
15550
|
return { registry: registry2, fence: jobFence };
|
|
15491
15551
|
}
|
|
15492
|
-
var nativeBackgroundSeen = false;
|
|
15493
15552
|
function jobStage() {
|
|
15494
15553
|
if (jobsMode === "native")
|
|
15495
15554
|
return 2;
|
|
15496
|
-
|
|
15497
|
-
return 0;
|
|
15498
|
-
return nativeBackgroundSeen ? 1 : 0;
|
|
15555
|
+
return 0;
|
|
15499
15556
|
}
|
|
15500
15557
|
var jobLogDir = jobsLogDir();
|
|
15501
15558
|
var jobLedgerPath = join5(jobLogDir, "ledger.jsonl");
|
|
@@ -15577,6 +15634,7 @@ function ensureSession(sessionID, worktree) {
|
|
|
15577
15634
|
return state;
|
|
15578
15635
|
}
|
|
15579
15636
|
function worktreeFor(context) {
|
|
15637
|
+
noteToolAgent(context);
|
|
15580
15638
|
return effectiveWorktree(context.worktree, hostWorktree) || context.worktree;
|
|
15581
15639
|
}
|
|
15582
15640
|
function resolveActivePlan(state) {
|
|
@@ -16040,7 +16098,6 @@ var FORGE_SHELL_DESCRIPTION = [
|
|
|
16040
16098
|
"Long-running or possibly non-exiting commands (dev servers, watchers, installers, anything spawning detached children) MUST use this tool instead of the builtin shell."
|
|
16041
16099
|
].join(`
|
|
16042
16100
|
`);
|
|
16043
|
-
var STAGE1_NOTE = "Stage note: this host's builtin shell already offers a native run_in_background parameter — prefer that for plain backgrounding; keep using forge_shell when you need idle/success_pattern early return or exit wake messages.";
|
|
16044
16101
|
var forgeShellTool = tool({
|
|
16045
16102
|
description: FORGE_SHELL_DESCRIPTION,
|
|
16046
16103
|
args: {
|
|
@@ -16159,6 +16216,7 @@ var forgeJobsTool = tool({
|
|
|
16159
16216
|
execute: async (args, context) => {
|
|
16160
16217
|
if (jobStage() >= 2)
|
|
16161
16218
|
throw new Error("[forge] The job supervisor is retired on this host.");
|
|
16219
|
+
noteToolAgent(context);
|
|
16162
16220
|
const action = String(args.action ?? "");
|
|
16163
16221
|
if (!FORGE_JOBS_ACTIONS.includes(action)) {
|
|
16164
16222
|
throw new Error(`Unknown action "${action}" — use one of: ${FORGE_JOBS_ACTIONS.join(", ")}.`);
|
|
@@ -16223,6 +16281,8 @@ ${p.newOutput || "(none in this window)"}`,
|
|
|
16223
16281
|
var CREW_INIT_TEMPLATE = [
|
|
16224
16282
|
'(forge crew orchestration. Argument: "$ARGUMENTS")',
|
|
16225
16283
|
"",
|
|
16284
|
+
"FAMILY GUARD (step 0, before anything else): if you do not have the crew_begin tool in your available tools, you are not in a forge session — STOP now and tell the user that /crew belongs to the forge agent, who should switch to forge (Tab) and rerun it there.",
|
|
16285
|
+
"",
|
|
16226
16286
|
"CREW IS NOT INITIALIZED: no forge subagents are configured (no usable forge.json agents). Refuse to enter orchestration and tell the user this feature needs a one-time setup, offering BOTH options:",
|
|
16227
16287
|
"",
|
|
16228
16288
|
"1. The user configures it themselves: create ONE of these files (project-level wins, never merged):",
|
|
@@ -16230,7 +16290,7 @@ var CREW_INIT_TEMPLATE = [
|
|
|
16230
16290
|
"",
|
|
16231
16291
|
"2. Or, ONLY if the user explicitly asks you to configure it in this conversation, you may write the file yourself through the normal write path. NEVER write forge.json without that explicit go-ahead.",
|
|
16232
16292
|
"",
|
|
16233
|
-
"Template (JSONC — comments allowed
|
|
16293
|
+
"Template (JSONC — comments allowed): `model` + `thoughtLevel` are an ATOMIC PAIR — set BOTH to pin the brain and depth (model = exact provider/model identity; thoughtLevel = none/low/medium/high/max or a native level name), or NEITHER for an Auto worker that inherits the parent session's model; exactly one of the two is rejected.",
|
|
16234
16294
|
"{template}",
|
|
16235
16295
|
"",
|
|
16236
16296
|
"After the file is saved: NEW agents require a host restart to appear in the task tool; thoughtLevel edits on existing agents apply without restart. Until then, crew stays unavailable."
|
|
@@ -16240,6 +16300,8 @@ function crewOrchestrationTemplate(agentIds) {
|
|
|
16240
16300
|
return [
|
|
16241
16301
|
'(forge crew orchestration. Argument: "$ARGUMENTS")',
|
|
16242
16302
|
"",
|
|
16303
|
+
"FAMILY GUARD (step 0, before anything else): if you do not have the crew_begin tool in your available tools, you are not in a forge session — STOP now and tell the user that /crew belongs to the forge agent, who should switch to forge (Tab) and rerun it there.",
|
|
16304
|
+
"",
|
|
16243
16305
|
`Configured forge subagents (task-tool vocabulary): ${agentIds.map((id) => `forge-${id}`).join(", ")}`,
|
|
16244
16306
|
"",
|
|
16245
16307
|
"You are entering CREW ORCHESTRATION discipline for this session (single subject: do not switch agents). Follow it exactly:",
|
|
@@ -16263,6 +16325,7 @@ var crewBeginTool = tool({
|
|
|
16263
16325
|
})).describe("The declared subtask plan — decompose the objective into these before any execution; crew_close cross-checks the report against exactly this list")
|
|
16264
16326
|
},
|
|
16265
16327
|
execute: async (args, context) => {
|
|
16328
|
+
noteToolAgent(context);
|
|
16266
16329
|
const state = stateForBan(context.sessionID);
|
|
16267
16330
|
const active = state ? resolveActivePlan(state) : null;
|
|
16268
16331
|
if (active && active.doc.status === "draft") {
|
|
@@ -16325,6 +16388,7 @@ var crewCloseTool = tool({
|
|
|
16325
16388
|
})).describe("The full crew report — one entry per declared subtask")
|
|
16326
16389
|
},
|
|
16327
16390
|
execute: async (args, context) => {
|
|
16391
|
+
noteToolAgent(context);
|
|
16328
16392
|
const crew = crews.get(context.sessionID);
|
|
16329
16393
|
if (!crew)
|
|
16330
16394
|
throw new Error("[forge:crew] No active crew in this session (crew state is in-memory and dies on host restart). Start one with /crew <objective>.");
|
|
@@ -16509,6 +16573,10 @@ async function continueIfEligible(client, sessionID) {
|
|
|
16509
16573
|
goalProbe(`skip: not goal owner session=${sessionID} owner=${goal.doc.session || "(none)"}`);
|
|
16510
16574
|
return;
|
|
16511
16575
|
}
|
|
16576
|
+
if (!sessionIsForgeFamily(sessionID)) {
|
|
16577
|
+
goalProbe(`skip: agent switched to non-forge before send session=${sessionID}`);
|
|
16578
|
+
return;
|
|
16579
|
+
}
|
|
16512
16580
|
if (accountedContinuationTurn) {
|
|
16513
16581
|
if (turnHadActivity) {
|
|
16514
16582
|
noProgressStreak.delete(sessionID);
|
|
@@ -16574,6 +16642,10 @@ async function continueIfEligible(client, sessionID) {
|
|
|
16574
16642
|
function scheduleIdleContinuation(client, sessionID) {
|
|
16575
16643
|
if (forgeDisabled)
|
|
16576
16644
|
return;
|
|
16645
|
+
if (!sessionIsForgeFamily(sessionID)) {
|
|
16646
|
+
goalProbe(`skip: non-forge agent at schedule session=${sessionID}`);
|
|
16647
|
+
return;
|
|
16648
|
+
}
|
|
16577
16649
|
const existing = idleTimers.get(sessionID);
|
|
16578
16650
|
if (existing)
|
|
16579
16651
|
clearTimeout(existing);
|
|
@@ -16666,23 +16738,15 @@ var server = async (input, options) => {
|
|
|
16666
16738
|
forgeDisabled = forgeUserCfg?.disable === true;
|
|
16667
16739
|
if (forgeDisabled)
|
|
16668
16740
|
return;
|
|
16669
|
-
|
|
16670
|
-
agentSection[native] = { ...agentSection[native] ?? {}, disable: true };
|
|
16671
|
-
}
|
|
16741
|
+
cfg.default_agent ??= FORGE_AGENT;
|
|
16672
16742
|
const existing = agentSection[FORGE_AGENT];
|
|
16673
|
-
const userDefinedForge = existing !== undefined;
|
|
16674
16743
|
agentSection[FORGE_AGENT] = {
|
|
16675
16744
|
...existing ?? {},
|
|
16676
16745
|
description: existing?.description ?? "forge — the single general-purpose coding agent: takes implementation tasks directly; planning goes through /plan into the plan harness (plans land in .opencode/plan/, with approve/close confirmation gates and tick discipline enforced by tools and the permission layer).",
|
|
16677
16746
|
mode: existing?.mode ?? "primary",
|
|
16678
16747
|
prompt: existing?.prompt ?? FORGE_PROMPT
|
|
16679
16748
|
};
|
|
16680
|
-
|
|
16681
|
-
const exp = JSON.stringify(cfg.experimental ?? "");
|
|
16682
|
-
if (/background/i.test(exp))
|
|
16683
|
-
nativeBackgroundSeen = true;
|
|
16684
|
-
} catch {}
|
|
16685
|
-
if (!userDefinedForge && !jobsKeepBuiltinShell && jobStage() < 1) {
|
|
16749
|
+
if (!jobsKeepBuiltinShell && jobStage() < 2) {
|
|
16686
16750
|
const entry = agentSection[FORGE_AGENT];
|
|
16687
16751
|
entry.tools = { ...entry.tools ?? {}, shell: false, bash: false };
|
|
16688
16752
|
}
|
|
@@ -16721,6 +16785,34 @@ var server = async (input, options) => {
|
|
|
16721
16785
|
agentSection[id] = entry;
|
|
16722
16786
|
materializedSnapshot[id] = entry;
|
|
16723
16787
|
}
|
|
16788
|
+
for (const id of Object.keys(agentSection)) {
|
|
16789
|
+
if (!id.startsWith("forge-"))
|
|
16790
|
+
continue;
|
|
16791
|
+
const entry = agentSection[id];
|
|
16792
|
+
const tools = { ...entry.tools ?? {} };
|
|
16793
|
+
for (const t of FORGE_STATE_TOOLS)
|
|
16794
|
+
if (tools[t] !== true)
|
|
16795
|
+
tools[t] = false;
|
|
16796
|
+
entry.tools = tools;
|
|
16797
|
+
}
|
|
16798
|
+
for (const [id, entry] of Object.entries(agentSection)) {
|
|
16799
|
+
if (id === FORGE_AGENT || id.startsWith("forge-"))
|
|
16800
|
+
continue;
|
|
16801
|
+
const e = entry;
|
|
16802
|
+
const tools = { ...e.tools ?? {} };
|
|
16803
|
+
for (const t of FORGE_ALL_TOOLS)
|
|
16804
|
+
if (tools[t] !== true)
|
|
16805
|
+
tools[t] = false;
|
|
16806
|
+
e.tools = tools;
|
|
16807
|
+
}
|
|
16808
|
+
for (const native of NATIVE_PARTITION_FALLBACKS) {
|
|
16809
|
+
if (agentSection[native] !== undefined)
|
|
16810
|
+
continue;
|
|
16811
|
+
const tools = {};
|
|
16812
|
+
for (const t of FORGE_ALL_TOOLS)
|
|
16813
|
+
tools[t] = false;
|
|
16814
|
+
agentSection[native] = { tools };
|
|
16815
|
+
}
|
|
16724
16816
|
if (process.env.FORGE_TEST_NO_DISPATCH_FETCH === "1") {
|
|
16725
16817
|
modelsDevCatalog = null;
|
|
16726
16818
|
} else {
|
|
@@ -16754,7 +16846,7 @@ var server = async (input, options) => {
|
|
|
16754
16846
|
`);
|
|
16755
16847
|
} catch {}
|
|
16756
16848
|
}
|
|
16757
|
-
if (watchdog.mode !== "off" && (input2.tool === "shell" || input2.tool === "bash")) {
|
|
16849
|
+
if (watchdog.mode !== "off" && (input2.tool === "shell" || input2.tool === "bash") && sessionIsForgeFamily(input2.sessionID)) {
|
|
16758
16850
|
const a = output?.args ?? {};
|
|
16759
16851
|
const cmdText = typeof a.command === "string" ? a.command : typeof a.cmd === "string" ? a.cmd : undefined;
|
|
16760
16852
|
watchdog.track(input2.callID, input2.sessionID, input2.tool, undefined, cmdText !== undefined ? commandNeedle(cmdText) : undefined);
|
|
@@ -16765,13 +16857,57 @@ var server = async (input, options) => {
|
|
|
16765
16857
|
} catch {}
|
|
16766
16858
|
}
|
|
16767
16859
|
}
|
|
16860
|
+
if (input2.tool === "task") {
|
|
16861
|
+
const speaker = sessionAgents.get(input2.sessionID);
|
|
16862
|
+
if (speaker !== undefined && !isForgeFamilyAgent(speaker)) {
|
|
16863
|
+
const a = output?.args ?? {};
|
|
16864
|
+
const target = typeof a.subagent_type === "string" ? a.subagent_type : typeof a.agent === "string" ? a.agent : undefined;
|
|
16865
|
+
if (target && target.toLowerCase().startsWith("forge-")) {
|
|
16866
|
+
throw new Error(`[forge:partition] Dispatch refused: "${target}" is a forge-family worker and the current agent ("${speaker}") is outside the forge family. Use the native general/explore subagents, or switch to the forge agent to dispatch forge-* workers.`);
|
|
16867
|
+
}
|
|
16868
|
+
}
|
|
16869
|
+
}
|
|
16870
|
+
if ((input2.tool === "write" || input2.tool === "edit") && input2.sessionID) {
|
|
16871
|
+
const speaker = sessionAgents.get(input2.sessionID);
|
|
16872
|
+
if (speaker !== undefined && !isForgeFamilyAgent(speaker)) {
|
|
16873
|
+
const a = output?.args ?? {};
|
|
16874
|
+
const raw = typeof a.filePath === "string" ? a.filePath : typeof a.path === "string" ? a.path : undefined;
|
|
16875
|
+
if (raw) {
|
|
16876
|
+
const state = stateForBan(input2.sessionID);
|
|
16877
|
+
const base = state?.worktree ?? hostWorktree ?? "";
|
|
16878
|
+
const abs = (isAbsolute(raw) ? raw : join5(base, raw)).replaceAll("\\", "/").toLowerCase();
|
|
16879
|
+
if (abs.includes("/.opencode/plan/") || abs.includes("/.opencode/goal/")) {
|
|
16880
|
+
throw new Error(`[forge:partition] Write refused: "${raw}" is inside the forge state directories (.opencode/plan, .opencode/goal), which belong to the forge agent in this workspace.`);
|
|
16881
|
+
}
|
|
16882
|
+
}
|
|
16883
|
+
}
|
|
16884
|
+
}
|
|
16768
16885
|
if (typeof input2.tool === "string" && isWriteTool(input2.tool)) {
|
|
16769
16886
|
const state = stateForBan(input2.sessionID);
|
|
16770
16887
|
const active = state ? resolveActivePlan(state) : null;
|
|
16771
16888
|
if (active && active.doc.status === "draft") {
|
|
16772
|
-
throw new Error(`[forge] A plan is in draft; write operations are denied (${input2.tool}).
|
|
16889
|
+
throw new Error(`[forge] A plan is in draft; write operations are denied (${input2.tool}). Switch back to the forge agent to present it and complete plan approval (plan_approve), or run /plan discard to abandon the plan.`);
|
|
16773
16890
|
}
|
|
16774
16891
|
}
|
|
16892
|
+
if (input2.sessionID && typeof input2.tool === "string") {
|
|
16893
|
+
const speaker = sessionAgents.get(input2.sessionID);
|
|
16894
|
+
if (speaker !== undefined) {
|
|
16895
|
+
if (!isForgeFamilyAgent(speaker) && FORGE_TOOL_SET.has(input2.tool)) {
|
|
16896
|
+
throw new Error(`[forge:partition] Tool refused: "${input2.tool}" belongs to the forge agent family and the current agent ("${speaker}") is outside it. Switch to forge (Tab) to use forge tooling.`);
|
|
16897
|
+
}
|
|
16898
|
+
if (isForgeFamilyAgent(speaker) && (input2.tool === "shell" || input2.tool === "bash") && !jobsKeepBuiltinShell && jobStage() < 2) {
|
|
16899
|
+
throw new Error(`[forge:partition] Builtin shell refused: the forge family executes through forge_shell (jobs.keepBuiltinShell or jobs.mode: "native" restores the builtin shell). Re-issue the command through forge_shell.`);
|
|
16900
|
+
}
|
|
16901
|
+
}
|
|
16902
|
+
}
|
|
16903
|
+
},
|
|
16904
|
+
"chat.message": async (input2) => {
|
|
16905
|
+
if (forgeDisabled)
|
|
16906
|
+
return;
|
|
16907
|
+
const raw = input2;
|
|
16908
|
+
if (typeof raw.sessionID === "string" && raw.sessionID && typeof raw.agent === "string" && raw.agent) {
|
|
16909
|
+
sessionAgents.set(raw.sessionID, raw.agent);
|
|
16910
|
+
}
|
|
16775
16911
|
},
|
|
16776
16912
|
"chat.params": async (input2, output) => {
|
|
16777
16913
|
if (forgeDisabled)
|
|
@@ -16781,6 +16917,8 @@ var server = async (input, options) => {
|
|
|
16781
16917
|
if (!sessionID)
|
|
16782
16918
|
return;
|
|
16783
16919
|
const agentName = typeof raw.agent === "string" ? raw.agent : raw.agent?.name;
|
|
16920
|
+
if (typeof agentName === "string" && agentName)
|
|
16921
|
+
sessionAgents.set(sessionID, agentName);
|
|
16784
16922
|
if (typeof agentName !== "string" || !agentName.startsWith("forge-"))
|
|
16785
16923
|
return;
|
|
16786
16924
|
const frozen = sessionDepths.get(sessionID);
|
|
@@ -16863,6 +17001,7 @@ var server = async (input, options) => {
|
|
|
16863
17001
|
if (typeof info?.id === "string" && info.id) {
|
|
16864
17002
|
jobManager.onSessionEnd(info.id);
|
|
16865
17003
|
sessionDepths.delete(info.id);
|
|
17004
|
+
sessionAgents.delete(info.id);
|
|
16866
17005
|
}
|
|
16867
17006
|
}
|
|
16868
17007
|
},
|
|
@@ -16871,6 +17010,8 @@ var server = async (input, options) => {
|
|
|
16871
17010
|
return;
|
|
16872
17011
|
if (!input2.callID)
|
|
16873
17012
|
return;
|
|
17013
|
+
if (!sessionIsForgeFamily(input2.sessionID))
|
|
17014
|
+
return;
|
|
16874
17015
|
output.env[WATCHDOG_ENV_MARK] = markerValue(input2.callID);
|
|
16875
17016
|
watchdog.markSeen(input2.callID);
|
|
16876
17017
|
if (process.env.FORGE_WATCHDOG_PROBE) {
|
|
@@ -16900,21 +17041,11 @@ var server = async (input, options) => {
|
|
|
16900
17041
|
if (input2.tool === "goal_check")
|
|
16901
17042
|
act.checks++;
|
|
16902
17043
|
},
|
|
16903
|
-
"tool.definition": async (input2, output) => {
|
|
16904
|
-
if (input2.toolID === "shell" || input2.toolID === "bash") {
|
|
16905
|
-
const props = output.parameters?.properties;
|
|
16906
|
-
if (props && "run_in_background" in props)
|
|
16907
|
-
nativeBackgroundSeen = true;
|
|
16908
|
-
} else if (input2.toolID === "forge_shell" && jobStage() === 1) {
|
|
16909
|
-
if (!output.description.startsWith(STAGE1_NOTE)) {
|
|
16910
|
-
output.description = `${STAGE1_NOTE}
|
|
16911
|
-
${output.description}`;
|
|
16912
|
-
}
|
|
16913
|
-
}
|
|
16914
|
-
},
|
|
16915
17044
|
"experimental.chat.system.transform": async (input2, output) => {
|
|
16916
17045
|
if (!input2.sessionID)
|
|
16917
17046
|
return;
|
|
17047
|
+
if (!sessionIsForgeFamily(input2.sessionID))
|
|
17048
|
+
return;
|
|
16918
17049
|
const crewActive = crews.get(input2.sessionID);
|
|
16919
17050
|
if (crewActive && !output.system.some((s) => s.startsWith("[forge:crew-active]"))) {
|
|
16920
17051
|
output.system.push(`[forge:crew-active] A crew is ACTIVE in this session: "${crewActive.objective}" (started ${crewActive.startedAt}). A second /crew must be refused; finish with crew_close when every subtask has a verdict.`);
|
|
@@ -16947,6 +17078,8 @@ ${output.description}`;
|
|
|
16947
17078
|
return;
|
|
16948
17079
|
if (!input2.sessionID)
|
|
16949
17080
|
return;
|
|
17081
|
+
if (!sessionIsForgeFamily(input2.sessionID))
|
|
17082
|
+
return;
|
|
16950
17083
|
const state = sessions.get(input2.sessionID);
|
|
16951
17084
|
if (!state)
|
|
16952
17085
|
return;
|
|
@@ -16968,7 +17101,7 @@ Post-compaction turns must keep following this goal and its constraints.`);
|
|
|
16968
17101
|
if (!state)
|
|
16969
17102
|
return;
|
|
16970
17103
|
const goal = resolveLiveGoal(state);
|
|
16971
|
-
if (goal)
|
|
17104
|
+
if (goal && sessionIsForgeFamily(input2.sessionID))
|
|
16972
17105
|
output.enabled = false;
|
|
16973
17106
|
}
|
|
16974
17107
|
};
|
|
@@ -16997,7 +17130,8 @@ async function v2Setup(ctx) {
|
|
|
16997
17130
|
agent.description = v1def.description;
|
|
16998
17131
|
agent.system = v1def.prompt;
|
|
16999
17132
|
agent.mode = v1def.mode;
|
|
17000
|
-
|
|
17133
|
+
if (v1def.model !== undefined)
|
|
17134
|
+
agent.model = v1def.model;
|
|
17001
17135
|
agent.permission = v1def.permission;
|
|
17002
17136
|
});
|
|
17003
17137
|
}
|
package/package.json
CHANGED
|
@@ -1,64 +1,64 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@sorenllm/opencode-forge",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Single general-purpose forge agent for opencode: static worker subagents defined in forge.json (pinned model +
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "./dist/index.js",
|
|
7
|
-
"exports": {
|
|
8
|
-
".": {
|
|
9
|
-
"import": "./dist/index.js"
|
|
10
|
-
},
|
|
11
|
-
"./server": {
|
|
12
|
-
"import": "./dist/index.js"
|
|
13
|
-
}
|
|
14
|
-
},
|
|
15
|
-
"files": [
|
|
16
|
-
"dist",
|
|
17
|
-
"README.md"
|
|
18
|
-
],
|
|
19
|
-
"scripts": {
|
|
20
|
-
"bundle": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && bun build ./plugin.ts --outfile ./dist/index.js --target node --format esm",
|
|
21
|
-
"test": "node --test tests/*.test.mjs",
|
|
22
|
-
"typecheck": "tsc --noEmit"
|
|
23
|
-
},
|
|
24
|
-
"keywords": [
|
|
25
|
-
"opencode",
|
|
26
|
-
"opencode-plugin",
|
|
27
|
-
"subagents",
|
|
28
|
-
"plan",
|
|
29
|
-
"planning",
|
|
30
|
-
"workflow",
|
|
31
|
-
"single-agent",
|
|
32
|
-
"goal",
|
|
33
|
-
"crew",
|
|
34
|
-
"orchestration",
|
|
35
|
-
"background-jobs",
|
|
36
|
-
"shell",
|
|
37
|
-
"watchdog"
|
|
38
|
-
],
|
|
39
|
-
"license": "MIT",
|
|
40
|
-
"author": "chengsongren",
|
|
41
|
-
"homepage": "https://github.com/ChengZiiii/opencode-forge#readme",
|
|
42
|
-
"repository": {
|
|
43
|
-
"type": "git",
|
|
44
|
-
"url": "git+https://github.com/ChengZiiii/opencode-forge.git"
|
|
45
|
-
},
|
|
46
|
-
"bugs": {
|
|
47
|
-
"url": "https://github.com/ChengZiiii/opencode-forge/issues"
|
|
48
|
-
},
|
|
49
|
-
"peerDependencies": {
|
|
50
|
-
"@opencode-ai/plugin": "^1.18.0"
|
|
51
|
-
},
|
|
52
|
-
"devDependencies": {
|
|
53
|
-
"@opencode-ai/plugin": "^1.18.0",
|
|
54
|
-
"@types/node": "^22.13.9",
|
|
55
|
-
"typescript": "^5.8.2"
|
|
56
|
-
},
|
|
57
|
-
"engines": {
|
|
58
|
-
"opencode": "^1.18.0"
|
|
59
|
-
},
|
|
60
|
-
"publishConfig": {
|
|
61
|
-
"access": "public",
|
|
62
|
-
"registry": "https://registry.npmjs.org/"
|
|
63
|
-
}
|
|
64
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@sorenllm/opencode-forge",
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "Single general-purpose forge agent for opencode: static worker subagents defined in forge.json (pinned model + thoughtLevel pair, or an Auto worker inheriting the parent model; dispatched through the native task tool — TUI-visible), a plan harness (file-backed plans with approve/close confirmation gates and a hard draft write-ban), a goal harness (autonomous, host-verified objectives), a /crew orchestration command (declared-plan waves with an evidence-checked close gate), a job supervisor (forge_shell with exit-bound completion, idle/success early return, background jobs with poll/log/kill/handoff), and a builtin-shell hang watchdog.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"./server": {
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"bundle": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && bun build ./plugin.ts --outfile ./dist/index.js --target node --format esm",
|
|
21
|
+
"test": "node --test tests/*.test.mjs",
|
|
22
|
+
"typecheck": "tsc --noEmit"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"opencode",
|
|
26
|
+
"opencode-plugin",
|
|
27
|
+
"subagents",
|
|
28
|
+
"plan",
|
|
29
|
+
"planning",
|
|
30
|
+
"workflow",
|
|
31
|
+
"single-agent",
|
|
32
|
+
"goal",
|
|
33
|
+
"crew",
|
|
34
|
+
"orchestration",
|
|
35
|
+
"background-jobs",
|
|
36
|
+
"shell",
|
|
37
|
+
"watchdog"
|
|
38
|
+
],
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"author": "chengsongren",
|
|
41
|
+
"homepage": "https://github.com/ChengZiiii/opencode-forge#readme",
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "git+https://github.com/ChengZiiii/opencode-forge.git"
|
|
45
|
+
},
|
|
46
|
+
"bugs": {
|
|
47
|
+
"url": "https://github.com/ChengZiiii/opencode-forge/issues"
|
|
48
|
+
},
|
|
49
|
+
"peerDependencies": {
|
|
50
|
+
"@opencode-ai/plugin": "^1.18.0"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@opencode-ai/plugin": "^1.18.0",
|
|
54
|
+
"@types/node": "^22.13.9",
|
|
55
|
+
"typescript": "^5.8.2"
|
|
56
|
+
},
|
|
57
|
+
"engines": {
|
|
58
|
+
"opencode": "^1.18.0"
|
|
59
|
+
},
|
|
60
|
+
"publishConfig": {
|
|
61
|
+
"access": "public",
|
|
62
|
+
"registry": "https://registry.npmjs.org/"
|
|
63
|
+
}
|
|
64
|
+
}
|