@sorenllm/opencode-forge 0.15.0 → 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.
Files changed (3) hide show
  1. package/README.md +37 -15
  2. package/dist/index.js +28 -14
  3. 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
- and a deny-style permission: `shape: "readonly"` (the default) denies
549
- `write`/`edit`/`bash`, `shape: "write"` allows them, an explicit
550
- `permission` map overrides the shape default — and `task` is ALWAYS
551
- denied (recursive spawning stays physically impossible).
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: a model the models.dev catalog knows by
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). Adaptive-era Anthropic models (major > 4 or 4.7+) receive
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. Ladder matching is case-insensitive and emits the catalog's native
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. No interpolation
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` off `deny` (`ask` counts as
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 (and `task`, always — no chained
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,26 +17725,40 @@ var server = async (input, options) => {
17725
17725
  return;
17726
17726
  const frozen = sessionDepths.get(sessionID);
17727
17727
  if (frozen) {
17728
- applyDepthTranslation(output.options ?? (output.options = {}), frozen.translation);
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);
17732
17739
  const def = resolvePools()?.agents[agentId]?.def;
17733
17740
  if (!def?.thoughtLevel)
17734
17741
  return;
17742
+ const modelObj = raw.model;
17735
17743
  const providerID = typeof raw.model?.providerID === "string" ? raw.model.providerID : "";
17736
- const modelID = typeof raw.model?.modelID === "string" ? raw.model.modelID : "";
17744
+ const modelID = typeof modelObj?.modelID === "string" ? modelObj.modelID : typeof modelObj?.id === "string" ? modelObj.id : "";
17737
17745
  const channel = resolveDepthChannel(providerID, modelID);
17738
17746
  const { translation, findingMessage } = resolvePinnedDepth(def.thoughtLevel, channel);
17739
- if (!translation) {
17740
- if (findingMessage && !depthFindingNotes.has(agentId)) {
17741
- depthFindingNotes.set(agentId, findingMessage);
17742
- console.warn(`[forge:depth] agent forge-${agentId}: ${findingMessage}`);
17743
- }
17744
- return;
17745
- }
17746
17747
  sessionDepths.set(sessionID, { translation });
17747
- applyDepthTranslation(output.options ?? (output.options = {}), 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
+ }
17754
+ if (process.env.FORGE_DEPTH_PROBE) {
17755
+ try {
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 ?? "-"}
17757
+ ` + ` input=${JSON.stringify(raw).slice(0, 600)}
17758
+ ` + ` optionsAfter=${JSON.stringify(output.options ?? {})}
17759
+ `);
17760
+ } catch {}
17761
+ }
17748
17762
  },
17749
17763
  __forgeSubagentsTest: {
17750
17764
  crews: () => crews,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sorenllm/opencode-forge",
3
- "version": "0.15.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",