@sorenllm/opencode-forge 0.15.1 → 0.16.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 +37 -15
- package/dist/index.js +19 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -502,7 +502,8 @@ out of a materialized id.
|
|
|
502
502
|
"model": "zai-coding-plan/glm-5.3", // exact "provider/model" string
|
|
503
503
|
"thoughtLevel": "low", // none/minimal/low/medium/high/xhigh/max, or any word — write lowercase
|
|
504
504
|
// "prompt": "short role description — explicit prompts fully override the built-ins (only research/review ids have them; any other id without a prompt gets a generic one-liner)",
|
|
505
|
-
// "shape": "write", // default readonly denies write/edit/bash
|
|
505
|
+
// "shape": "write", // default readonly denies write/edit/bash + the
|
|
506
|
+
// patch family (patch/applypatch/apply/multiedit)
|
|
506
507
|
// "permission": { "bash": "deny" } // optional override; "task" is ALWAYS denied
|
|
507
508
|
},
|
|
508
509
|
// AUTO worker: set NEITHER — inherits the parent session's model at
|
|
@@ -545,10 +546,13 @@ A rejected entry costs only itself — its siblings materialize normally.
|
|
|
545
546
|
says "inherits the parent session's model" instead of naming a brain),
|
|
546
547
|
the worker discipline (workspace-relative paths only, verbatim reporting
|
|
547
548
|
of tool refusals, evidence-bearing conclusions) around its role prompt,
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
549
|
+
and a deny-style permission: `shape: "readonly"` (the default) denies
|
|
550
|
+
the full mutating-tool family — `write`/`edit`/`bash` plus the host's
|
|
551
|
+
patch family (`patch`/`applypatch`/`apply`/`multiedit`) — `shape:
|
|
552
|
+
"write"` allows them, an explicit `permission` map overrides the shape
|
|
553
|
+
default (lifting ANY member of that family off `deny` forfeits the
|
|
554
|
+
draft-phase readonly exemption) — and `task` is ALWAYS
|
|
555
|
+
denied (recursive spawning stays physically impossible).
|
|
552
556
|
|
|
553
557
|
> **Upgrading from ≤ 0.5.0 (breaking):** agent entries that pinned `model`
|
|
554
558
|
> without `thoughtLevel` used to be accepted; they are now SKIPPED with an
|
|
@@ -594,22 +598,32 @@ name. **Write lowercase**: the value is normalized at the config boundary
|
|
|
594
598
|
`xhigh`; normalization that changes what you wrote records one warn finding.
|
|
595
599
|
Native casing on the wire is the catalog's business, never yours. Injection
|
|
596
600
|
rides the `chat.params` hook keyed by agent name. The wire shape comes from
|
|
597
|
-
data, not guessing
|
|
601
|
+
data, not guessing — with ONE family decided ahead of the catalog: for the
|
|
602
|
+
**anthropic family** the channel is read straight off the empirically
|
|
603
|
+
verified transport matrix — adaptive-era models (major > 4 or 4.7+) take
|
|
604
|
+
the adaptive effort key `options.effort` (the host transports it as the
|
|
605
|
+
adaptive-era native `output_config: {effort: <word>}` wire shape), legacy-era
|
|
606
|
+
models take the budget tier table below — and the models.dev catalog is
|
|
607
|
+
**not consulted** for this family: its effort claim cannot sound on the
|
|
608
|
+
current host generation's anthropic transport, which silently drops the
|
|
609
|
+
`reasoningEffort` key a catalog effort shape would select. For every other
|
|
610
|
+
family the catalog remains the shape authority: a model the models.dev
|
|
611
|
+
catalog knows by
|
|
598
612
|
`reasoning_options` is translated per that catalog shape (effort word list /
|
|
599
613
|
toggle / token budget); a catalog-silent model falls back to the
|
|
600
614
|
provider-prefix family; a provider absent from both is treated as an effort
|
|
601
615
|
**word channel** by default (custom relays and unknown endpoints — the most
|
|
602
|
-
universal shape).
|
|
603
|
-
the effort word directly instead of a budget number, on the empirically
|
|
604
|
-
verified `effort` option key (the host transports it as the adaptive-era
|
|
605
|
-
native `output_config: {effort: <word>}` wire shape; a `reasoningEffort`
|
|
606
|
-
key is silently dropped by anthropic transports). **The dictionary is a
|
|
616
|
+
universal shape). **The dictionary is a
|
|
607
617
|
dictionary, never a whitelist**: a word matching neither the dictionary nor
|
|
608
618
|
the model's ladder passes through VERBATIM on word-shaped channels
|
|
609
619
|
(`reasoningEffort = <word>`, or `effort = <word>` on the adaptive channel),
|
|
610
620
|
disclosed as unverified — the provider is the
|
|
611
621
|
final judge, and a provider-side rejection surfaces as the request's own
|
|
612
|
-
error.
|
|
622
|
+
error. Known host-side limitation: the adaptive channel passes `none`
|
|
623
|
+
through verbatim per the dictionary axiom, but the current host generation's
|
|
624
|
+
anthropic transport refuses to send such a request (it silently never
|
|
625
|
+
reaches the wire) — documented here, deliberately not worked around. Ladder
|
|
626
|
+
matching is case-insensitive and emits the catalog's native
|
|
613
627
|
casing (`xhigh` in, `XHigh` out when the catalog says so). **The only dead
|
|
614
628
|
ends are the two shapes with no word channel**: budget-style (legacy
|
|
615
629
|
Anthropic-style) models have a published thinking tier instead
|
|
@@ -619,7 +633,13 @@ consensus number; `low → 8192`, `medium → 16384`, `high → 24576`,
|
|
|
619
633
|
`none → off`. **This tier table is this plugin's selection — there is no
|
|
620
634
|
community-standard tier table**), and pure toggle endpoints (zai/GLM: a level
|
|
621
635
|
word has no toggle counterpart) — there the plugin injects nothing and
|
|
622
|
-
records one bounded finding naming both vocabularies.
|
|
636
|
+
records one bounded finding naming both vocabularies. **Host-transport
|
|
637
|
+
caveat (observed on the opencode 1.18.34 generation):** the anthropic
|
|
638
|
+
transport rewrites the thinking `budget_tokens` value this plugin injects
|
|
639
|
+
to 1024 regardless of the selected tier (characterized by four paired
|
|
640
|
+
probe experiments). The tier table above is the plugin-side truth —
|
|
641
|
+
verifiable with a `FORGE_DEPTH_PROBE=1` trace — while the value that
|
|
642
|
+
actually reaches the wire is the host's to rewrite. No interpolation
|
|
623
643
|
anywhere; the session is never broken by a config word. A session's depth is
|
|
624
644
|
frozen at its first request (editing forge.json mid-session never rewrites a
|
|
625
645
|
running session).
|
|
@@ -793,7 +813,8 @@ The escape hatches are `plan_approve` and `/plan discard`, by design.
|
|
|
793
813
|
One carve-out, keyed on the target rather than the tool: during a draft the
|
|
794
814
|
`task` tool is gated by the dispatch target's **effective readonly shape**. A
|
|
795
815
|
dispatch to a forge worker whose shape is not `write` AND whose permission
|
|
796
|
-
overrides do not lift `write`/`edit`/`bash`
|
|
816
|
+
overrides do not lift `write`/`edit`/`bash` — nor any host patch-family tool
|
|
817
|
+
(`patch`/`applypatch`/`apply`/`multiedit`) — off `deny` (`ask` counts as
|
|
797
818
|
lifted — fail-closed) passes as a reconnaissance dispatch: readonly scouts
|
|
798
819
|
may be spawned both before and during the draft, so breadth
|
|
799
820
|
evidence-gathering does not burn the planning session's context. Every other
|
|
@@ -804,7 +825,8 @@ points to defining a readonly worker in forge.json when the roster is empty
|
|
|
804
825
|
(an unconfigured host stays fully denied — same behavior as before, just more
|
|
805
826
|
actionable wording), and names the two exits (`plan_approve`, `/plan
|
|
806
827
|
discard`). Invariant strength is unchanged: the scout's materialized
|
|
807
|
-
permission hard-denies write/edit/bash
|
|
828
|
+
permission hard-denies the mutating-tool family — write/edit/bash and the
|
|
829
|
+
patch family (and `task`, always — no chained
|
|
808
830
|
spawning), the scout's own session is ambient-bound to the same draft write
|
|
809
831
|
ban, and its `forge_shell` calls ride the same per-call ask gate the primary
|
|
810
832
|
agent faces. The `permission.ask` belt does not follow the exemption: on a
|
package/dist/index.js
CHANGED
|
@@ -15042,7 +15042,7 @@ function materializeAgentPrompt(input) {
|
|
|
15042
15042
|
}
|
|
15043
15043
|
|
|
15044
15044
|
// src/dispatch-tiers.ts
|
|
15045
|
-
var MUTATING_TOOLS = ["write", "edit", "bash"];
|
|
15045
|
+
var MUTATING_TOOLS = ["write", "edit", "bash", "patch", "applypatch", "apply", "multiedit"];
|
|
15046
15046
|
var ROLE_PROMPTS = {
|
|
15047
15047
|
research: "You are a research worker: search, read, and run read-only checks to answer the assigned question. Report findings as a compact brief with sources and file:line evidence; state plainly what could not be verified.",
|
|
15048
15048
|
review: "You are a review worker: read the diff or files under review, run read-only checks/tests, and report findings as a prioritized list, each with file:line evidence and a concrete fix suggestion. End with a single-line verdict."
|
|
@@ -16091,6 +16091,9 @@ function isAdaptiveAnthropic(modelID) {
|
|
|
16091
16091
|
return major > 4 || major === 4 && minor >= 7;
|
|
16092
16092
|
}
|
|
16093
16093
|
function resolveDepthChannel(providerID, modelID) {
|
|
16094
|
+
if (dispatchProviderFamily(providerID) === "anthropic") {
|
|
16095
|
+
return isAdaptiveAnthropic(modelID) ? { kind: "adaptive-effort" } : { kind: "budget" };
|
|
16096
|
+
}
|
|
16094
16097
|
const shape = modelDepthShape(modelsDevCatalog, `${providerID}/${modelID}`);
|
|
16095
16098
|
if (shape) {
|
|
16096
16099
|
if (shape.shape === "effort")
|
|
@@ -16104,9 +16107,6 @@ function resolveDepthChannel(providerID, modelID) {
|
|
|
16104
16107
|
return { kind: "effort", ladder: null };
|
|
16105
16108
|
if (family === "zai")
|
|
16106
16109
|
return { kind: "toggle" };
|
|
16107
|
-
if (family === "anthropic") {
|
|
16108
|
-
return isAdaptiveAnthropic(modelID) ? { kind: "adaptive-effort" } : { kind: "budget" };
|
|
16109
|
-
}
|
|
16110
16110
|
return { kind: "effort", ladder: null };
|
|
16111
16111
|
}
|
|
16112
16112
|
var jobsMode = "auto";
|
|
@@ -17725,7 +17725,14 @@ var server = async (input, options) => {
|
|
|
17725
17725
|
return;
|
|
17726
17726
|
const frozen = sessionDepths.get(sessionID);
|
|
17727
17727
|
if (frozen) {
|
|
17728
|
-
|
|
17728
|
+
if (process.env.FORGE_DEPTH_PROBE) {
|
|
17729
|
+
try {
|
|
17730
|
+
appendFileSync(join6(tmpdir3(), "forge-depth-probe.log"), `${new Date().toISOString()} sid=${sessionID} agent=${agentName} frozen-apply translation=${JSON.stringify(frozen.translation) ?? "null"}
|
|
17731
|
+
`);
|
|
17732
|
+
} catch {}
|
|
17733
|
+
}
|
|
17734
|
+
if (frozen.translation)
|
|
17735
|
+
applyDepthTranslation(output.options ?? (output.options = {}), frozen.translation);
|
|
17729
17736
|
return;
|
|
17730
17737
|
}
|
|
17731
17738
|
const agentId = agentName.slice("forge-".length);
|
|
@@ -17737,6 +17744,13 @@ var server = async (input, options) => {
|
|
|
17737
17744
|
const modelID = typeof modelObj?.modelID === "string" ? modelObj.modelID : typeof modelObj?.id === "string" ? modelObj.id : "";
|
|
17738
17745
|
const channel = resolveDepthChannel(providerID, modelID);
|
|
17739
17746
|
const { translation, findingMessage } = resolvePinnedDepth(def.thoughtLevel, channel);
|
|
17747
|
+
sessionDepths.set(sessionID, { translation });
|
|
17748
|
+
if (translation) {
|
|
17749
|
+
applyDepthTranslation(output.options ?? (output.options = {}), translation);
|
|
17750
|
+
} else if (findingMessage && !depthFindingNotes.has(agentId)) {
|
|
17751
|
+
depthFindingNotes.set(agentId, findingMessage);
|
|
17752
|
+
console.warn(`[forge:depth] agent forge-${agentId}: ${findingMessage}`);
|
|
17753
|
+
}
|
|
17740
17754
|
if (process.env.FORGE_DEPTH_PROBE) {
|
|
17741
17755
|
try {
|
|
17742
17756
|
appendFileSync(join6(tmpdir3(), "forge-depth-probe.log"), `${new Date().toISOString()} sid=${sessionID} agent=${agentName} prov=${providerID} model=${modelID} depth=${def.thoughtLevel} channel=${JSON.stringify(channel)} translation=${JSON.stringify(translation) ?? "null"} finding=${findingMessage ?? "-"}
|
|
@@ -17745,15 +17759,6 @@ var server = async (input, options) => {
|
|
|
17745
17759
|
`);
|
|
17746
17760
|
} catch {}
|
|
17747
17761
|
}
|
|
17748
|
-
if (!translation) {
|
|
17749
|
-
if (findingMessage && !depthFindingNotes.has(agentId)) {
|
|
17750
|
-
depthFindingNotes.set(agentId, findingMessage);
|
|
17751
|
-
console.warn(`[forge:depth] agent forge-${agentId}: ${findingMessage}`);
|
|
17752
|
-
}
|
|
17753
|
-
return;
|
|
17754
|
-
}
|
|
17755
|
-
sessionDepths.set(sessionID, { translation });
|
|
17756
|
-
applyDepthTranslation(output.options ?? (output.options = {}), translation);
|
|
17757
17762
|
},
|
|
17758
17763
|
__forgeSubagentsTest: {
|
|
17759
17764
|
crews: () => crews,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sorenllm/opencode-forge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
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
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|