@sorenllm/opencode-forge 0.14.1 → 0.15.1
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 +70 -20
- package/dist/index.js +152 -50
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -43,11 +43,13 @@ OpenSpec spec workflows remain a third, separate lane.
|
|
|
43
43
|
counts read the Task List only — and a superseded plan's Task List
|
|
44
44
|
stays the frozen decision snapshot: closure IS the supersession, and
|
|
45
45
|
crew/goal results never tick it.
|
|
46
|
-
- While a plan is in draft, `write` / `edit` / `bash`
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
46
|
+
- While a plan is in draft, `write` / `edit` / `bash` are **denied at the
|
|
47
|
+
permission layer** — including your own `allow` config, and for every agent
|
|
48
|
+
in that session (the draft protects shared session state; see Agent
|
|
49
|
+
partition below) — and `task` is **shape-gated**: dispatch to an
|
|
50
|
+
effective-readonly forge scout passes, every other target is denied (see
|
|
51
|
+
Design stance). The only exits are approval and discard. This is deliberate;
|
|
52
|
+
see Design stance.
|
|
51
53
|
- `plan_approve` / `plan_close` are pinned to a confirmation dialog: the
|
|
52
54
|
model can never flip the state itself.
|
|
53
55
|
- Goal files: `.opencode/goal/<date>-<slug>.md`, state machine
|
|
@@ -498,7 +500,7 @@ out of a materialized id.
|
|
|
498
500
|
// PINNED worker: model + thoughtLevel form an ATOMIC PAIR — set BOTH
|
|
499
501
|
"research": {
|
|
500
502
|
"model": "zai-coding-plan/glm-5.3", // exact "provider/model" string
|
|
501
|
-
"thoughtLevel": "low", // none/low/medium/high/max or
|
|
503
|
+
"thoughtLevel": "low", // none/minimal/low/medium/high/xhigh/max, or any word — write lowercase
|
|
502
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)",
|
|
503
505
|
// "shape": "write", // default readonly denies write/edit/bash
|
|
504
506
|
// "permission": { "bash": "deny" } // optional override; "task" is ALWAYS denied
|
|
@@ -572,8 +574,10 @@ Say "dispatch a subagent..." in a forge session and the model picks from the
|
|
|
572
574
|
task tool vocabulary: a configured `forge-*` agent whose description matches
|
|
573
575
|
runs on its pinned brain; if none matches, forge is instructed to say so
|
|
574
576
|
plainly and fall back to the native task channel. During a plan draft the
|
|
575
|
-
|
|
576
|
-
|
|
577
|
+
`task` tool is gated by the dispatch target's effective readonly shape:
|
|
578
|
+
readonly scout dispatch passes as reconnaissance (breadth evidence without
|
|
579
|
+
burning the planning session's context), write-shaped workers and native
|
|
580
|
+
agents are denied with the dispatchable roster and the two exits.
|
|
577
581
|
|
|
578
582
|
**Known pitfall — keyless endpoints × readonly agents**: free/keyless
|
|
579
583
|
providers can return a deterministic EMPTY response for any toolset-reduced
|
|
@@ -583,18 +587,42 @@ attributable task failure; the mitigation is unchanged — use
|
|
|
583
587
|
|
|
584
588
|
### The thoughtLevel word
|
|
585
589
|
|
|
586
|
-
One word per agent: the canonical
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
590
|
+
One word per agent: the canonical seven-word dictionary
|
|
591
|
+
`none | minimal | low | medium | high | xhigh | max`, or any native level
|
|
592
|
+
name. **Write lowercase**: the value is normalized at the config boundary
|
|
593
|
+
(trimmed and lowercased), so `High` and `XHigh` behave exactly as `high` and
|
|
594
|
+
`xhigh`; normalization that changes what you wrote records one warn finding.
|
|
595
|
+
Native casing on the wire is the catalog's business, never yours. Injection
|
|
596
|
+
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
|
|
598
|
+
`reasoning_options` is translated per that catalog shape (effort word list /
|
|
599
|
+
toggle / token budget); a catalog-silent model falls back to the
|
|
600
|
+
provider-prefix family; a provider absent from both is treated as an effort
|
|
601
|
+
**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
|
|
607
|
+
dictionary, never a whitelist**: a word matching neither the dictionary nor
|
|
608
|
+
the model's ladder passes through VERBATIM on word-shaped channels
|
|
609
|
+
(`reasoningEffort = <word>`, or `effort = <word>` on the adaptive channel),
|
|
610
|
+
disclosed as unverified — the provider is the
|
|
611
|
+
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
|
|
613
|
+
casing (`xhigh` in, `XHigh` out when the catalog says so). **The only dead
|
|
614
|
+
ends are the two shapes with no word channel**: budget-style (legacy
|
|
615
|
+
Anthropic-style) models have a published thinking tier instead
|
|
616
|
+
(`minimal → 1024` — the Anthropic API hard floor, the only cross-project
|
|
617
|
+
consensus number; `low → 8192`, `medium → 16384`, `high → 24576`,
|
|
618
|
+
`xhigh → 28672` — a half step between high and max; `max → 32768`;
|
|
619
|
+
`none → off`. **This tier table is this plugin's selection — there is no
|
|
620
|
+
community-standard tier table**), and pure toggle endpoints (zai/GLM: a level
|
|
621
|
+
word has no toggle counterpart) — there the plugin injects nothing and
|
|
622
|
+
records one bounded finding naming both vocabularies. No interpolation
|
|
623
|
+
anywhere; the session is never broken by a config word. A session's depth is
|
|
624
|
+
frozen at its first request (editing forge.json mid-session never rewrites a
|
|
625
|
+
running session).
|
|
598
626
|
|
|
599
627
|
## Crew workflow (`/crew`)
|
|
600
628
|
|
|
@@ -762,6 +790,28 @@ read/grep/glob; if you genuinely need a shell command to decide the plan,
|
|
|
762
790
|
approve the plan first (revising after approval is allowed via a new `/plan`).
|
|
763
791
|
The escape hatches are `plan_approve` and `/plan discard`, by design.
|
|
764
792
|
|
|
793
|
+
One carve-out, keyed on the target rather than the tool: during a draft the
|
|
794
|
+
`task` tool is gated by the dispatch target's **effective readonly shape**. A
|
|
795
|
+
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
|
|
797
|
+
lifted — fail-closed) passes as a reconnaissance dispatch: readonly scouts
|
|
798
|
+
may be spawned both before and during the draft, so breadth
|
|
799
|
+
evidence-gathering does not burn the planning session's context. Every other
|
|
800
|
+
target — write-shaped workers, native agents, absent or unresolvable targets
|
|
801
|
+
(including resume calls that carry no target) — is denied like any write-class
|
|
802
|
+
tool. The refusal names the currently dispatchable effective-readonly roster,
|
|
803
|
+
points to defining a readonly worker in forge.json when the roster is empty
|
|
804
|
+
(an unconfigured host stays fully denied — same behavior as before, just more
|
|
805
|
+
actionable wording), and names the two exits (`plan_approve`, `/plan
|
|
806
|
+
discard`). Invariant strength is unchanged: the scout's materialized
|
|
807
|
+
permission hard-denies write/edit/bash (and `task`, always — no chained
|
|
808
|
+
spawning), the scout's own session is ambient-bound to the same draft write
|
|
809
|
+
ban, and its `forge_shell` calls ride the same per-call ask gate the primary
|
|
810
|
+
agent faces. The `permission.ask` belt does not follow the exemption: on a
|
|
811
|
+
host configured with `task: "ask"` a draft-phase dispatch is still denied
|
|
812
|
+
(fail-closed degradation by design — the side belt does not resolve target
|
|
813
|
+
shapes).
|
|
814
|
+
|
|
765
815
|
Separately, the builtin shell is hidden on the forge agent on purpose —
|
|
766
816
|
`forge_shell` is the exec surface there (see Agent partition and the job
|
|
767
817
|
supervisor chapter above). The hide is unconditional: it covers a
|
package/dist/index.js
CHANGED
|
@@ -14877,7 +14877,7 @@ function parseIdentity(identity) {
|
|
|
14877
14877
|
return null;
|
|
14878
14878
|
return { provider: identity.slice(0, slash), model: identity.slice(slash + 1) };
|
|
14879
14879
|
}
|
|
14880
|
-
function
|
|
14880
|
+
function modelDepthShape(catalog, identity) {
|
|
14881
14881
|
if (!catalog)
|
|
14882
14882
|
return null;
|
|
14883
14883
|
const parsed = parseIdentity(identity);
|
|
@@ -14886,8 +14886,13 @@ function nativeLadder(catalog, identity) {
|
|
|
14886
14886
|
const entry = catalog.providers?.[parsed.provider]?.models?.[parsed.model];
|
|
14887
14887
|
if (!entry)
|
|
14888
14888
|
return null;
|
|
14889
|
-
|
|
14890
|
-
|
|
14889
|
+
if (entry.reasoningShape === "toggle")
|
|
14890
|
+
return { shape: "toggle" };
|
|
14891
|
+
if (entry.reasoningShape === "budget_tokens")
|
|
14892
|
+
return { shape: "budget_tokens" };
|
|
14893
|
+
if (entry.reasoningShape === "effort")
|
|
14894
|
+
return { shape: "effort", ladder: entry.reasoningOptions ? [...entry.reasoningOptions] : [] };
|
|
14895
|
+
return null;
|
|
14891
14896
|
}
|
|
14892
14897
|
function modelsDevCacheDir(base) {
|
|
14893
14898
|
return join5(base ?? join5(tmpdir2(), "opencode-forge"), "models-dev");
|
|
@@ -14906,6 +14911,16 @@ function namedLevels(rows) {
|
|
|
14906
14911
|
}
|
|
14907
14912
|
return out;
|
|
14908
14913
|
}
|
|
14914
|
+
function shapeOf(rows) {
|
|
14915
|
+
for (const r of rows) {
|
|
14916
|
+
if (r && typeof r === "object") {
|
|
14917
|
+
const t = r.type;
|
|
14918
|
+
if (t === "effort" || t === "toggle" || t === "budget_tokens")
|
|
14919
|
+
return t;
|
|
14920
|
+
}
|
|
14921
|
+
}
|
|
14922
|
+
return;
|
|
14923
|
+
}
|
|
14909
14924
|
function reduceModelsDev(raw) {
|
|
14910
14925
|
const catalog = { providers: {} };
|
|
14911
14926
|
const providers = raw?.constructor === Object ? raw : null;
|
|
@@ -14918,10 +14933,13 @@ function reduceModelsDev(raw) {
|
|
|
14918
14933
|
const out = { models: {} };
|
|
14919
14934
|
for (const [modelID, model] of Object.entries(models)) {
|
|
14920
14935
|
const m = model;
|
|
14921
|
-
const
|
|
14936
|
+
const rows = Array.isArray(m.reasoning_options) ? m.reasoning_options : undefined;
|
|
14937
|
+
const shape = rows !== undefined ? shapeOf(rows) : undefined;
|
|
14938
|
+
const options = rows !== undefined && shape === "effort" ? namedLevels(rows) : undefined;
|
|
14922
14939
|
const cost = m.cost && typeof m.cost === "object" ? m.cost : undefined;
|
|
14923
14940
|
out.models[modelID] = {
|
|
14924
|
-
...
|
|
14941
|
+
...shape !== undefined ? { reasoningShape: shape } : {},
|
|
14942
|
+
...options && options.length > 0 ? { reasoningOptions: options } : {},
|
|
14925
14943
|
...cost ? { cost } : {}
|
|
14926
14944
|
};
|
|
14927
14945
|
}
|
|
@@ -15063,71 +15081,97 @@ function forgeAgentDef(agentId, def) {
|
|
|
15063
15081
|
entry.model = def.model;
|
|
15064
15082
|
return entry;
|
|
15065
15083
|
}
|
|
15084
|
+
function effectiveReadonlyShape(def) {
|
|
15085
|
+
if (def.shape === "write")
|
|
15086
|
+
return false;
|
|
15087
|
+
const perm = def.permission ?? {};
|
|
15088
|
+
for (const t of MUTATING_TOOLS) {
|
|
15089
|
+
const v = perm[t];
|
|
15090
|
+
if (v !== undefined && v !== "deny")
|
|
15091
|
+
return false;
|
|
15092
|
+
}
|
|
15093
|
+
return true;
|
|
15094
|
+
}
|
|
15066
15095
|
|
|
15067
15096
|
// src/dispatch-depth.ts
|
|
15068
|
-
var META_DEPTHS = ["none", "low", "medium", "high", "max"];
|
|
15097
|
+
var META_DEPTHS = ["none", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
15069
15098
|
var BUDGET_TIERS = {
|
|
15099
|
+
minimal: 1024,
|
|
15070
15100
|
low: 8192,
|
|
15071
15101
|
medium: 16384,
|
|
15072
15102
|
high: 24576,
|
|
15103
|
+
xhigh: 28672,
|
|
15073
15104
|
max: 32768
|
|
15074
15105
|
};
|
|
15075
|
-
function noMapping(depth,
|
|
15106
|
+
function noMapping(depth, channel, shapeVocabulary) {
|
|
15076
15107
|
return {
|
|
15077
15108
|
kind: "no-mapping",
|
|
15078
|
-
|
|
15079
|
-
message: `depth "${depth}" has no
|
|
15080
|
-
|
|
15081
|
-
|
|
15109
|
+
channel,
|
|
15110
|
+
message: `depth "${depth}" has no channel on this shape (${channel} — the endpoint cannot express it). Shape vocabulary: ${shapeVocabulary.join(", ") || "(none)"}. Dictionary words: ${META_DEPTHS.join(", ")}. The plugin never interpolates; fix the pinned thoughtLevel or the model's wire shape.`,
|
|
15111
|
+
shapeVocabulary,
|
|
15112
|
+
dictionary: [...META_DEPTHS]
|
|
15082
15113
|
};
|
|
15083
15114
|
}
|
|
15084
|
-
function
|
|
15085
|
-
if (
|
|
15086
|
-
return
|
|
15087
|
-
|
|
15088
|
-
|
|
15089
|
-
|
|
15090
|
-
|
|
15091
|
-
|
|
15092
|
-
|
|
15093
|
-
|
|
15094
|
-
|
|
15095
|
-
|
|
15096
|
-
|
|
15097
|
-
|
|
15098
|
-
|
|
15099
|
-
|
|
15100
|
-
|
|
15101
|
-
|
|
15115
|
+
function ladderWord(ladder, depth) {
|
|
15116
|
+
if (ladder === null)
|
|
15117
|
+
return null;
|
|
15118
|
+
for (const w of ladder)
|
|
15119
|
+
if (w.toLowerCase() === depth)
|
|
15120
|
+
return w;
|
|
15121
|
+
return null;
|
|
15122
|
+
}
|
|
15123
|
+
function translateDepth(depth, channel) {
|
|
15124
|
+
const d = depth.trim().toLowerCase();
|
|
15125
|
+
if (channel.kind === "effort" || channel.kind === "adaptive-effort") {
|
|
15126
|
+
const native = channel.kind === "effort" ? ladderWord(channel.ladder, d) : null;
|
|
15127
|
+
if (native !== null) {
|
|
15128
|
+
return {
|
|
15129
|
+
kind: "verbatim",
|
|
15130
|
+
channel: channel.kind,
|
|
15131
|
+
word: native,
|
|
15132
|
+
confirmed: true,
|
|
15133
|
+
disclose: `canonical ${d} → native "${native}" (catalog-confirmed)`
|
|
15134
|
+
};
|
|
15102
15135
|
}
|
|
15103
|
-
|
|
15136
|
+
const why = channel.kind === "effort" && channel.ladder !== null ? `catalog ladder does not list "${d}"` : "no catalog ladder for this endpoint";
|
|
15137
|
+
return {
|
|
15138
|
+
kind: "verbatim",
|
|
15139
|
+
channel: channel.kind,
|
|
15140
|
+
word: d,
|
|
15141
|
+
confirmed: false,
|
|
15142
|
+
disclose: `verbatim (unverified — ${why}; the provider judges)`
|
|
15143
|
+
};
|
|
15104
15144
|
}
|
|
15105
|
-
|
|
15106
|
-
|
|
15107
|
-
|
|
15145
|
+
if (channel.kind === "toggle") {
|
|
15146
|
+
if (d === "none")
|
|
15147
|
+
return { kind: "off", channel: "toggle", disclose: "canonical none → native thinking:off" };
|
|
15148
|
+
return noMapping(d, "toggle", ["none (thinking on/off only)"]);
|
|
15108
15149
|
}
|
|
15109
|
-
if (
|
|
15110
|
-
return
|
|
15150
|
+
if (d === "none")
|
|
15151
|
+
return { kind: "off", channel: "budget", disclose: "canonical none → native thinking:off" };
|
|
15152
|
+
const budget = BUDGET_TIERS[d];
|
|
15153
|
+
if (budget !== undefined) {
|
|
15154
|
+
return { kind: "tiered", channel: "budget", budgetTokens: budget, disclose: `canonical ${d} → native thinking budget:${budget}` };
|
|
15111
15155
|
}
|
|
15112
|
-
return
|
|
15156
|
+
return noMapping(d, "budget", ["none", ...Object.keys(BUDGET_TIERS)]);
|
|
15113
15157
|
}
|
|
15114
15158
|
function applyDepthTranslation(options, t) {
|
|
15115
15159
|
if (t.kind === "verbatim") {
|
|
15116
|
-
|
|
15160
|
+
if (t.channel === "adaptive-effort") {
|
|
15161
|
+
options.effort = t.word;
|
|
15162
|
+
} else {
|
|
15163
|
+
options.reasoningEffort = t.word;
|
|
15164
|
+
}
|
|
15117
15165
|
} else if (t.kind === "tiered") {
|
|
15118
15166
|
options.thinking = { type: "enabled", budget_tokens: t.budgetTokens };
|
|
15119
15167
|
} else if (t.kind === "off") {
|
|
15120
15168
|
options.thinking = { type: "disabled" };
|
|
15121
|
-
} else if (t.kind === "toggle-on") {
|
|
15122
|
-
options.thinking = { type: "enabled" };
|
|
15123
15169
|
}
|
|
15124
15170
|
}
|
|
15125
|
-
function resolvePinnedDepth(thoughtLevel,
|
|
15126
|
-
const t = translateDepth(thoughtLevel,
|
|
15171
|
+
function resolvePinnedDepth(thoughtLevel, channel) {
|
|
15172
|
+
const t = translateDepth(thoughtLevel, channel);
|
|
15127
15173
|
if (t.kind === "no-mapping")
|
|
15128
15174
|
return { translation: null, findingMessage: t.message };
|
|
15129
|
-
if (t.kind === "not-injected")
|
|
15130
|
-
return { translation: null };
|
|
15131
15175
|
return { translation: t };
|
|
15132
15176
|
}
|
|
15133
15177
|
|
|
@@ -15420,6 +15464,11 @@ function validateAgentSet(raw) {
|
|
|
15420
15464
|
if (entry.thoughtLevel !== undefined) {
|
|
15421
15465
|
if (nonEmptyString(entry.thoughtLevel)) {
|
|
15422
15466
|
thoughtLevel = entry.thoughtLevel.trim();
|
|
15467
|
+
const normalized = thoughtLevel.toLowerCase();
|
|
15468
|
+
if (normalized !== thoughtLevel) {
|
|
15469
|
+
findings.push({ level: "warn", code: "thoughtlevel-normalized", message: `${label}.thoughtLevel "${thoughtLevel}" normalized to "${normalized}" (config is lowercase; the catalog decides wire casing)` });
|
|
15470
|
+
}
|
|
15471
|
+
thoughtLevel = normalized;
|
|
15423
15472
|
} else {
|
|
15424
15473
|
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)` });
|
|
15425
15474
|
}
|
|
@@ -15855,7 +15904,7 @@ var PLAN_COMMAND_TEMPLATE = [
|
|
|
15855
15904
|
"",
|
|
15856
15905
|
"Plans are short-horizon, single-task-goal documents (.opencode/plan/<date>-<slug>.md). The harness enforces the hard parts (draft write-ban, approval/close dialogs); you supply the engineering judgment. Never edit or create plan files by hand — every change goes through the plan_* tools.",
|
|
15857
15906
|
"",
|
|
15858
|
-
"1. Reconnaissance and alignment (read-only): explore with read/grep/glob
|
|
15907
|
+
"1. Reconnaissance and alignment (read-only): explore with read/grep/glob, and dispatch readonly-shape forge scouts (task tool) for breadth evidence — reconnaissance dispatch to effective-readonly workers keeps this session's context lean and is allowed both BEFORE and DURING the draft. All write tools and bash are DENIED while a draft exists, and so is dispatching write-shaped workers or native agents; do not attempt them, do not ask the user to bypass. Gather concrete evidence with file:line references (verified findings, not guesses). If the goal is ambiguous or leaves meaningful choices open (scope, approach, acceptance), settle them with the user FIRST — 1-3 focused questions — and only write the draft once the shape is agreed; never plan against assumptions the user could settle in one line.",
|
|
15859
15908
|
"2. Draft (plan_write): call plan_write with structured fields; the tool renders and validates the fixed sections, so a malformed plan cannot exist. Quality bar: goal = one line, the outcome not the activity; context = verified findings with file:line evidence, including what you ruled out and why; approach = the chosen approach AND at least one rejected alternative with the reason (a plan with no considered alternative is a guess); tasks = 3-8 concrete, independently verifiable steps, each doable in one sitting ('improve the code' is invalid; 'extract the timeout constant into config.ts and default it to 3000' is valid); risks = what could break, blast radius, rollback path; acceptance = criteria verifiable by a command, a file, or an observable behavior (vague criteria will fail the close); nonGoals = explicit out-of-scope items. To revise after feedback, call plan_write again — while in draft it overwrites the same file.",
|
|
15860
15909
|
`3. User review, then approval (plan_approve): after plan_write, present the plan in chat — goal, chosen approach with one line why, the numbered task list, the acceptance criteria — then STOP: end your turn and wait. The user owns the review, at their own pace: feedback → revise with plan_write, re-present, and wait again; rejection → /plan discard (or re-plan together); explicit go-ahead (e.g. "execute", "批准", "looks good") → call plan_approve — its confirmation dialog is the final hard gate, and the user's Allow starts execution. Never call plan_approve in the same turn that presents a draft, and never implement before approval succeeds.`,
|
|
15861
15910
|
"4. Execution (tick as you go): work tasks in order; call plan_tick with the task number immediately after EACH task's work is actually done — never batch ticks, never tick ahead of reality (tick timestamps are an audit trail). If the plan turns out wrong mid-execution, do not silently improvise: tell the user what changed and either finish the affected task or ask about revising (/plan with the same goal re-enters planning).",
|
|
@@ -16032,6 +16081,34 @@ function dispatchProviderFamily(providerID) {
|
|
|
16032
16081
|
return "zai";
|
|
16033
16082
|
return null;
|
|
16034
16083
|
}
|
|
16084
|
+
function isAdaptiveAnthropic(modelID) {
|
|
16085
|
+
const id = modelID.toLowerCase();
|
|
16086
|
+
const m = /^claude-(?:(?:opus|sonnet|haiku)[-.])?(\d+)(?:[.-](\d+))?/.exec(id);
|
|
16087
|
+
if (!m)
|
|
16088
|
+
return false;
|
|
16089
|
+
const major = Number(m[1]);
|
|
16090
|
+
const minor = m[2] !== undefined ? Number(m[2]) : 0;
|
|
16091
|
+
return major > 4 || major === 4 && minor >= 7;
|
|
16092
|
+
}
|
|
16093
|
+
function resolveDepthChannel(providerID, modelID) {
|
|
16094
|
+
const shape = modelDepthShape(modelsDevCatalog, `${providerID}/${modelID}`);
|
|
16095
|
+
if (shape) {
|
|
16096
|
+
if (shape.shape === "effort")
|
|
16097
|
+
return { kind: "effort", ladder: shape.ladder };
|
|
16098
|
+
if (shape.shape === "toggle")
|
|
16099
|
+
return { kind: "toggle" };
|
|
16100
|
+
return { kind: "budget" };
|
|
16101
|
+
}
|
|
16102
|
+
const family = dispatchProviderFamily(providerID);
|
|
16103
|
+
if (family === "openai")
|
|
16104
|
+
return { kind: "effort", ladder: null };
|
|
16105
|
+
if (family === "zai")
|
|
16106
|
+
return { kind: "toggle" };
|
|
16107
|
+
if (family === "anthropic") {
|
|
16108
|
+
return isAdaptiveAnthropic(modelID) ? { kind: "adaptive-effort" } : { kind: "budget" };
|
|
16109
|
+
}
|
|
16110
|
+
return { kind: "effort", ladder: null };
|
|
16111
|
+
}
|
|
16035
16112
|
var jobsMode = "auto";
|
|
16036
16113
|
var jobsKeepBuiltinShell = false;
|
|
16037
16114
|
var jobsSurviveMode = "never";
|
|
@@ -17587,7 +17664,25 @@ var server = async (input, options) => {
|
|
|
17587
17664
|
const state = stateForBan(input2.sessionID);
|
|
17588
17665
|
const active = state ? resolveActivePlan(state) : null;
|
|
17589
17666
|
if (active && active.doc.status === "draft") {
|
|
17590
|
-
|
|
17667
|
+
if (input2.tool === "task") {
|
|
17668
|
+
const a = output?.args ?? {};
|
|
17669
|
+
const target = typeof a.subagent_type === "string" ? a.subagent_type : typeof a.agent === "string" ? a.agent : undefined;
|
|
17670
|
+
const agentId = target !== undefined && target.toLowerCase().startsWith("forge-") ? target.slice("forge-".length) : undefined;
|
|
17671
|
+
const def = agentId !== undefined ? resolvePools()?.agents[agentId]?.def : undefined;
|
|
17672
|
+
if (def !== undefined && effectiveReadonlyShape(def)) {} else {
|
|
17673
|
+
const roster = [];
|
|
17674
|
+
const pools = resolvePools();
|
|
17675
|
+
if (pools) {
|
|
17676
|
+
for (const [id, entry] of Object.entries(pools.agents))
|
|
17677
|
+
if (effectiveReadonlyShape(entry.def))
|
|
17678
|
+
roster.push(`forge-${id}`);
|
|
17679
|
+
}
|
|
17680
|
+
roster.sort();
|
|
17681
|
+
throw new Error(`[forge] A plan is in draft; dispatch is denied for this target. ` + (roster.length > 0 ? `Readonly recon scouts ARE dispatchable during draft: ${roster.join(", ")}. ` : `No readonly worker is configured — define one in forge.json (shape defaults to readonly) to enable draft-phase reconnaissance. `) + `Write-shaped workers and native agents wait for approval (plan_approve); /plan discard to abandon the plan.`);
|
|
17682
|
+
}
|
|
17683
|
+
} else {
|
|
17684
|
+
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.`);
|
|
17685
|
+
}
|
|
17591
17686
|
}
|
|
17592
17687
|
}
|
|
17593
17688
|
if (input2.sessionID && typeof input2.tool === "string") {
|
|
@@ -17637,12 +17732,19 @@ var server = async (input, options) => {
|
|
|
17637
17732
|
const def = resolvePools()?.agents[agentId]?.def;
|
|
17638
17733
|
if (!def?.thoughtLevel)
|
|
17639
17734
|
return;
|
|
17735
|
+
const modelObj = raw.model;
|
|
17640
17736
|
const providerID = typeof raw.model?.providerID === "string" ? raw.model.providerID : "";
|
|
17641
|
-
const modelID = typeof
|
|
17642
|
-
const
|
|
17643
|
-
|
|
17644
|
-
|
|
17645
|
-
|
|
17737
|
+
const modelID = typeof modelObj?.modelID === "string" ? modelObj.modelID : typeof modelObj?.id === "string" ? modelObj.id : "";
|
|
17738
|
+
const channel = resolveDepthChannel(providerID, modelID);
|
|
17739
|
+
const { translation, findingMessage } = resolvePinnedDepth(def.thoughtLevel, channel);
|
|
17740
|
+
if (process.env.FORGE_DEPTH_PROBE) {
|
|
17741
|
+
try {
|
|
17742
|
+
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 ?? "-"}
|
|
17743
|
+
` + ` input=${JSON.stringify(raw).slice(0, 600)}
|
|
17744
|
+
` + ` optionsAfter=${JSON.stringify(output.options ?? {})}
|
|
17745
|
+
`);
|
|
17746
|
+
} catch {}
|
|
17747
|
+
}
|
|
17646
17748
|
if (!translation) {
|
|
17647
17749
|
if (findingMessage && !depthFindingNotes.has(agentId)) {
|
|
17648
17750
|
depthFindingNotes.set(agentId, findingMessage);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sorenllm/opencode-forge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.1",
|
|
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",
|