@sorenllm/opencode-forge 0.5.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.
Files changed (3) hide show
  1. package/README.md +36 -9
  2. package/dist/index.js +31 -12
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -359,8 +359,10 @@ complete.
359
359
  ## Forge subagents (static worker agents in forge.json)
360
360
 
361
361
  Forge subagents are **static, user-authored worker agents**: each forge.json
362
- entry pins a model and an optional reasoning depth, materializes as a native
363
- `forge-<id>` subagent, and is dispatched through the host's **native `task`
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`
364
366
  tool** — which means every run is visible in the TUI, expandable and
365
367
  monitorable, exactly like any native subagent. There is no dispatch tool, no
366
368
  child-session engine, and no configuration invitation anywhere: the session
@@ -381,17 +383,33 @@ cascade with a single winning source (never merged):
381
383
  {
382
384
  // forge subagents — definitions; see the restart table below for apply timing
383
385
  "agents": {
386
+ // PINNED worker: model + thoughtLevel form an ATOMIC PAIR — set BOTH
384
387
  "research": {
385
- "model": "zai-coding-plan/glm-5.3", // exact "provider/model" string — REQUIRED
386
- "thoughtLevel": "low", // optional: none/low/medium/high/max or a native level name
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
387
390
  // "prompt": "optional role prompt; built-in research/review defaults exist",
388
391
  // "shape": "write", // default readonly denies write/edit/bash
389
392
  // "permission": { "bash": "deny" } // optional override; "task" is ALWAYS denied
390
- }
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": {}
391
398
  }
392
399
  }
393
400
  ```
394
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
+
395
413
  - **The plugin only ever reads this file.** It never creates, writes, or
396
414
  migrates it. The old AI-authored onboarding path (seed placeholder +
397
415
  error recipe) is gone: an unconfigured host is inert and silent, and the
@@ -400,13 +418,17 @@ cascade with a single winning source (never merged):
400
418
  offered to YOU and happens only on your explicit go-ahead.
401
419
  - **Parsing is field-level fail-soft** (ZCode-style): a semantically bad
402
420
  agent entry is skipped with an error finding while its siblings apply; a
403
- mistyped optional field is ignored with a warning; a syntactically broken
404
- file empties the agent set with one parse-location error. There is no seed
405
- in any failure path. Findings surface in the plugin diagnostics log.
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.
406
426
  - **`depths` is deprecated**: the array is ignored with a warning finding.
407
427
  Pin ONE `thoughtLevel` per agent instead.
408
428
  - Each agent materializes as `forge-<id>` — a subagent-mode agent visible in
409
- 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),
410
432
  the worker discipline (workspace-relative paths only, verbatim reporting
411
433
  of tool refusals, evidence-bearing conclusions) around its role prompt,
412
434
  and a deny-style permission: `shape: "readonly"` (the default) denies
@@ -414,6 +436,11 @@ cascade with a single winning source (never merged):
414
436
  `permission` map overrides the shape default — and `task` is ALWAYS
415
437
  denied (recursive spawning stays physically impossible).
416
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
+
417
444
  **What applies when** — edits take hold per field:
418
445
 
419
446
  | change | needs a host restart? |
package/dist/index.js CHANGED
@@ -14893,7 +14893,8 @@ function rolePromptFor(agentId, def) {
14893
14893
  }
14894
14894
  function agentDescriptionFor(agentId, def) {
14895
14895
  const shape = def.shape === "write" ? "write" : "readonly";
14896
- return `forge subagent "${agentId}" (${shape}, pinned ${def.model}) — dispatch via the task tool with subagent_type "forge-${agentId}".`;
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}".`;
14897
14898
  }
14898
14899
  function forgeAgentDef(agentId, def) {
14899
14900
  const permission = { task: "deny" };
@@ -14904,10 +14905,9 @@ function forgeAgentDef(agentId, def) {
14904
14905
  }
14905
14906
  Object.assign(permission, def.permission ?? {});
14906
14907
  permission.task = "deny";
14907
- return {
14908
+ const entry = {
14908
14909
  description: agentDescriptionFor(agentId, def),
14909
14910
  mode: "subagent",
14910
- model: def.model,
14911
14911
  prompt: materializeAgentPrompt({
14912
14912
  agent: agentId,
14913
14913
  shape,
@@ -14916,6 +14916,9 @@ function forgeAgentDef(agentId, def) {
14916
14916
  permission,
14917
14917
  tools: { shell: false, bash: false }
14918
14918
  };
14919
+ if (def.model !== undefined)
14920
+ entry.model = def.model;
14921
+ return entry;
14919
14922
  }
14920
14923
 
14921
14924
  // src/dispatch-depth.ts
@@ -15250,21 +15253,36 @@ function validateAgentSet(raw) {
15250
15253
  findings.push({ level: "error", code: "agent-not-object", message: `${label}: definition must be an object — entry skipped` });
15251
15254
  continue;
15252
15255
  }
15253
- if (!nonEmptyString(entry.model)) {
15254
- findings.push({ level: "error", code: "agent-model-missing", message: `${label}.model: a non-empty "provider/model" string is required — entry skipped, siblings still apply` });
15255
- continue;
15256
- }
15257
- const def = { model: entry.model.trim() };
15258
15256
  if (entry.depths !== undefined) {
15259
15257
  findings.push({ level: "warn", code: "depths-deprecated", message: `${label}.depths is no longer used (static agents pin one thoughtLevel) — ignored; set "thoughtLevel" instead` });
15260
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;
15261
15268
  if (entry.thoughtLevel !== undefined) {
15262
15269
  if (nonEmptyString(entry.thoughtLevel)) {
15263
- def.thoughtLevel = entry.thoughtLevel.trim();
15270
+ thoughtLevel = entry.thoughtLevel.trim();
15264
15271
  } else {
15265
- findings.push({ level: "warn", code: "thoughtlevel-invalid", message: `${label}.thoughtLevel must be a non-empty string — field ignored, the agent still materializes` });
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)` });
15266
15273
  }
15267
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
+ }
15268
15286
  if (entry.prompt !== undefined) {
15269
15287
  if (nonEmptyString(entry.prompt)) {
15270
15288
  def.prompt = entry.prompt;
@@ -16272,7 +16290,7 @@ var CREW_INIT_TEMPLATE = [
16272
16290
  "",
16273
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.",
16274
16292
  "",
16275
- "Template (JSONC — comments allowed; `model` is the exact provider/model identity, `thoughtLevel` is optional: none/low/medium/high/max or a native level name):",
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.",
16276
16294
  "{template}",
16277
16295
  "",
16278
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."
@@ -17112,7 +17130,8 @@ async function v2Setup(ctx) {
17112
17130
  agent.description = v1def.description;
17113
17131
  agent.system = v1def.prompt;
17114
17132
  agent.mode = v1def.mode;
17115
- agent.model = v1def.model;
17133
+ if (v1def.model !== undefined)
17134
+ agent.model = v1def.model;
17116
17135
  agent.permission = v1def.permission;
17117
17136
  });
17118
17137
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sorenllm/opencode-forge",
3
- "version": "0.5.0",
4
- "description": "Single general-purpose forge agent for opencode: static worker subagents defined in forge.json (pinned model + optional reasoning depth, 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.",
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
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "exports": {