@sorenllm/opencode-forge 0.17.0 → 0.18.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.
Files changed (3) hide show
  1. package/README.md +72 -21
  2. package/dist/index.js +107 -37
  3. package/package.json +64 -64
package/README.md CHANGED
@@ -189,7 +189,7 @@ What this plugin touches, exhaustively:
189
189
  | `<project>/.opencode/plan/*.md` | plan files | user data — kept forever, uninstall never deletes |
190
190
  | `<project>/.opencode/goal/*.md` | goal files (contract, Check Log, Turn Ledger) | user data — kept forever, uninstall never deletes |
191
191
  | `<project>/.opencode/crew/*.md` | crew records (declared plan + arm/convert/abandon/close history; written at `crew_begin` registration, appended at each transition; record-only — never a resume mechanism) | user data — kept forever, uninstall never deletes |
192
- | 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 |
192
+ | merged config object (RAM only) | forge agent, static `forge-<ns>-<id>` subagents (every pool family namespaced; the global fallback family `forge-global-<id>`), 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 |
193
193
  | `<tmp>/opencode-forge/jobs/<jobId>.log` | job output tee (full output; oldest rotated out above 50 files) | runtime debris — delete freely, also after uninstall |
194
194
  | `<tmp>/opencode-forge/jobs/ledger.jsonl` | job registry ledger (bounded: 1 MB reset, 200 entries) | runtime debris — delete freely, also after uninstall |
195
195
  | `<tmp>/opencode-forge/jobs/registry.json` | persistent survivor registry (bounded: 100 entries) | runtime debris — after uninstall, kill any still-running `survive` jobs yourself first |
@@ -417,8 +417,10 @@ complete.
417
417
  Forge subagents are **static, user-authored worker agents**: each forge.json
418
418
  entry is either **pinned** (a model + a reasoning depth, bound at
419
419
  materialization) or an **Auto worker** (no model — it inherits the parent
420
- session's model at dispatch), materializes as a native `forge-<id>`
421
- subagent, and is dispatched through the host's **native `task`
420
+ session's model at dispatch), materializes as a native `forge-<ns>-<id>`
421
+ subagent — EVERY pool family is namespaced, no exceptions (see
422
+ *Pools on a shared host* below) — and is dispatched through the host's
423
+ **native `task`
422
424
  tool** — which means every run is visible in the TUI, expandable and
423
425
  monitorable, exactly like any native subagent. There is no dispatch tool, no
424
426
  child-session engine, and no configuration invitation anywhere: the session
@@ -461,12 +463,23 @@ directory it was initialized in, and the **host-wide materialization set**
461
463
  is the global layer plus each seen workspace's pool (one pool per
462
464
  directory: its walk-up nearest file). Pool membership is re-derived from
463
465
  disk truth on every resolve — a deleted pool file disappears at the next
464
- config hook. The FIRST seen workspace's pool keeps **plain `forge-<id>`
465
- ids** (a single-project host is unchanged); every other pool materializes
466
- **namespaced** as `forge-<ns>-<id>` (`ns` = the file's optional `pool`
467
- field, else the sanitized pool-directory name; the global fallback family
468
- derives `global`). Namespace and materialized-id collisions resolve
469
- deterministically with an error finding, never by silent shadowing.
466
+ config hook. EVERY pool family materializes **namespaced** as
467
+ `forge-<ns>-<id>` (`ns` = the file's declared top-level `pool` field, else
468
+ the sanitized pool-directory basename); the **global fallback layer
469
+ materializes under the fixed namespace `global`** as `forge-global-<id>`.
470
+ **No family gets plain ids anywhere** — a materialized id is identical on
471
+ every host regardless of the order workspaces were initialized in, so crew
472
+ plans, rosters, and muscle memory transfer between hosts. Pool resolution
473
+ order is deterministic — families ordered lexicographically by absolute
474
+ pool-file path — and that same order governs cross-family materialized-id
475
+ collisions: the earlier pool's agent wins, the colliding later agent is
476
+ skipped with an error finding naming both files. Every materialized agent's
477
+ task-vocabulary **description carries its pool provenance**: its namespace,
478
+ its origin forge.json path, and — for every family except the global
479
+ fallback — that it is dispatchable only from sessions whose workspace pool
480
+ is that file; the global family names the global fallback layer as its
481
+ origin. The same static text ships to every session — the dispatch gate
482
+ (below) is the dynamic authority, the description is the honest label.
470
483
 
471
484
  **Dispatch is gated to the session's own pool.** Materializing every pool
472
485
  host-wide is a vocabulary necessity (the task tool's agent list is
@@ -492,18 +505,23 @@ a host re-initialization in that workspace materializes it (or move the
492
505
  agents into the global layer).
493
506
 
494
507
  **Id naming**: use plain role words (`research`, `coder`, …) — the plugin
495
- materializes every id as `forge-<id>` itself (namespaced pools:
496
- `forge-<ns>-<id>`). A `forge-`-prefixed id in the file would double up
497
- (`forge-coder` → agent `forge-forge-coder`); the loader auto-strips the
508
+ materializes every id as `forge-<ns>-<id>` under its family's namespace
509
+ (never a plain `forge-<id>`; pool-less workspaces get the fixed `global`
510
+ namespace). A `forge-`-prefixed id in the file would double up
511
+ (`forge-coder` → agent `forge-<ns>-forge-coder`); the loader auto-strips the
498
512
  prefix (all repetitions) with a warning so existing files keep working, but
499
513
  rename the entries to silence it. The namespace infix is never parsed back
500
514
  out of a materialized id.
501
515
 
502
516
  ```jsonc
503
517
  {
504
- // non-primary pool files: namespace for this pool's ids (forge-<ns>-<id>).
518
+ // namespace for this pool's ids (forge-<ns>-<id> — EVERY pool is
519
+ // namespaced; declare it instead of riding the directory name).
505
520
  // Short, stable, [a-z0-9-] up to 24 chars — ids survive directory renames.
506
- // Invalid values degrade to a warning + the directory-derived namespace.
521
+ // REQUIRED for crew-governed dispatch (the /crew compliance gate: no
522
+ // valid declared `pool` → `crew_begin` refuses and crew `forge-*`
523
+ // dispatches are blocked until the field is added). Invalid values
524
+ // degrade to a warning + the directory-derived namespace.
507
525
  // "pool": "aa",
508
526
  "agents": {
509
527
  // PINNED worker: model + thoughtLevel form an ATOMIC PAIR — set BOTH
@@ -551,8 +569,11 @@ A rejected entry costs only itself — its siblings materialize normally.
551
569
  Findings surface in the plugin diagnostics log.
552
570
  - **`depths` is deprecated**: the array is ignored with a warning finding.
553
571
  Pin ONE `thoughtLevel` per agent instead.
554
- - Each agent materializes as `forge-<id>` — a subagent-mode agent visible in
555
- the task tool's vocabulary (never hidden), carrying the pinned `model`
572
+ - Each agent materializes as `forge-<ns>-<id>` — a subagent-mode agent
573
+ visible in the task tool's vocabulary (never hidden), whose description
574
+ names its **pool provenance** (namespace + origin forge.json path +
575
+ dispatchability scope; the global family names the global fallback
576
+ layer), carrying the pinned `model`
556
577
  (pinned workers) or NO model key at all (Auto workers — the description
557
578
  says "inherits the parent session's model" instead of naming a brain),
558
579
  the worker discipline (workspace-relative paths only, verbatim reporting
@@ -575,6 +596,28 @@ A rejected entry costs only itself — its siblings materialize normally.
575
596
  > error finding naming the missing half. Fix per entry: add the missing
576
597
  > `thoughtLevel`, or delete `model` to make it an Auto worker.
577
598
 
599
+ > **BREAKING — every materialized id is now namespaced.** The
600
+ > first-registered pool's plain-id privilege is gone: existing plain ids
601
+ > (`forge-coder`) no longer exist, and there are **no compat aliases** —
602
+ > deliberately, because an alias layer would resurrect the two-ids-one-agent
603
+ > ambiguity this change kills (crew records and docs referencing old ids
604
+ > are history artifacts). Migration:
605
+ >
606
+ > - **Single-pool workspaces**: add a short stable top-level `"pool"` field
607
+ > to your forge.json (`[a-z0-9-]`, 1–24 chars) and dispatch
608
+ > `forge-<pool>-<id>` from now on.
609
+ > - **Pool-less workspaces** (global fallback roster): dispatch
610
+ > `forge-global-<id>` — the global layer materializes under the fixed
611
+ > namespace `global`.
612
+ > - **New crew-governed pool-compliance gate**: `crew_begin` refuses to
613
+ > register, and every `task` dispatch of a `forge-*` worker from a
614
+ > crew-governed session is refused, while the workspace pool file lacks a
615
+ > valid declared top-level `pool` field. The fix is one line — add the
616
+ > field (the refusal suggests a namespace) — and the gate lifts
617
+ > hot-apply: the next resolve after the repair admits registration and
618
+ > dispatch with no host restart. Global-fallback sessions are exempt.
619
+ > Per-agent fail-soft findings never make the file non-compliant.
620
+
578
621
  **What applies when** — only a real materialization re-reads the pool
579
622
  files into the `forge-*` vocabulary. Materialization triggers (2026-10-01
580
623
  live finding): host boot (a freshly started CLI process reads the files as
@@ -586,7 +629,7 @@ opencode config file reloads. A NEW session on a long-lived shared host
586
629
  | --- | --- |
587
630
  | `thoughtLevel` on an existing agent | no (applies to NEW sessions of that agent; running sessions keep their frozen depth) — depth lookups resolve the pool files fresh at every dispatch, independent of materialization |
588
631
  | agent added / removed / edited, any pool file created or deleted | no restart — but not at the next session either: the materialized `forge-*` set rebuilds only at the next REAL materialization (host boot, opencode config reload, or a first config build); on a long-lived shared host pool changes wait for that rebuild or a restart. The crew gate, the session-domain dispatch gate, and depth lookups resolve the pool files fresh regardless — a DELETED pool file un-materializes at that next resolve |
589
- | a NEW directory joining the host (the first session of another project on the shared host) | no restart — that first config build itself re-initializes the plugin and registers that workspace and its pool (existing pools never flip; the first-seen workspace keeps plain ids) |
632
+ | a NEW directory joining the host (the first session of another project on the shared host) | no restart — that first config build itself re-initializes the plugin and registers that workspace and its pool (existing pools never flip — every family namespaces by its own `pool` field or directory name, so ids never depend on arrival order) |
590
633
 
591
634
  ### Dispatching
592
635
 
@@ -725,10 +768,18 @@ requires a plan. The flow:
725
768
  alongside the choice — so you can verify the record really landed on
726
769
  disk (an earlier output order buried the path line, and the model
727
770
  reliably drops what it buries — leading with it is the fix). The
728
- output then confirms the roster **grouped by pool with its origins**
729
- (which forge.json families the dispatchable set materialized from, the
730
- primary marked), presents the three execution-mode choices, and stops
731
- the turn. Reconnaissance task calls are free BEFORE registration.
771
+ output then confirms the roster **grouped by pool with its origins**
772
+ (which forge.json families the dispatchable set materialized from —
773
+ every family rendered as `forge-<ns>-*`, no plain-id family),
774
+ presents the three execution-mode choices, and stops
775
+ the turn. Registration pre-flights **pool compliance**: the workspace
776
+ pool file must declare a valid top-level `pool` namespace — a
777
+ non-compliant file refuses the registration outright (see the BREAKING
778
+ note above; global-fallback sessions are exempt), and while a crew
779
+ governs the session every `forge-*` task dispatch re-checks the same
780
+ compliance (a file degraded mid-crew blocks further crew dispatches
781
+ until repaired — the fix is the one `"pool": "<suggested>"` line, and
782
+ it lifts hot-apply). Reconnaissance task calls are free BEFORE registration.
732
783
  2. **The pause is mechanical**: while the crew pends, every `task` dispatch is
733
784
  refused by an interception belt. The user chooses:
734
785
  - **supervised waves now** — `crew_begin {execution: "waves"}` lifts the
package/dist/index.js CHANGED
@@ -12336,7 +12336,7 @@ tool.schema = exports_external;
12336
12336
  // plugin.ts
12337
12337
  import { execFileSync as execFileSync3 } from "node:child_process";
12338
12338
  import { appendFileSync, existsSync as existsSync3, mkdirSync as mkdirSync5, readdirSync as readdirSync4, readFileSync as readFileSync8, realpathSync, statSync as statSync5, writeFileSync as writeFileSync5 } from "node:fs";
12339
- import { isAbsolute, join as join6, relative } from "node:path";
12339
+ import { basename as basename2, dirname as dirname4, isAbsolute, join as join6, relative } from "node:path";
12340
12340
  import { tmpdir as tmpdir3, homedir } from "node:os";
12341
12341
 
12342
12342
  // src/plan-file.ts
@@ -15052,12 +15052,19 @@ function rolePromptFor(agentId, def) {
15052
15052
  return def.prompt;
15053
15053
  return ROLE_PROMPTS[agentId] ?? `You are a ${agentId} worker. Execute the assigned task exactly as scoped.`;
15054
15054
  }
15055
- function agentDescriptionFor(agentId, def) {
15055
+ function poolProvenanceLine(provenance) {
15056
+ if ("global" in provenance) {
15057
+ return `Pool 'global' — origin: the global fallback layer.`;
15058
+ }
15059
+ return `Pool '${provenance.ns}' — origin ${provenance.file}; dispatchable only from sessions whose workspace pool is this file.`;
15060
+ }
15061
+ function agentDescriptionFor(agentId, def, provenance) {
15056
15062
  const shape = def.shape === "readonly" ? "readonly" : "write";
15057
15063
  const brain = def.model !== undefined ? `pinned ${def.model}` : "auto — inherits the parent session's model at dispatch";
15058
- return `forge subagent "${agentId}" (${shape}, ${brain}) — dispatch via the task tool with subagent_type "forge-${agentId}".`;
15064
+ const base = `forge subagent "${agentId}" (${shape}, ${brain}) — dispatch via the task tool with subagent_type "forge-${agentId}".`;
15065
+ return provenance === undefined ? base : `${base} ${poolProvenanceLine(provenance)}`;
15059
15066
  }
15060
- function forgeAgentDef(agentId, def) {
15067
+ function forgeAgentDef(agentId, def, provenance) {
15061
15068
  const permission = { task: "deny" };
15062
15069
  const shape = def.shape ?? "write";
15063
15070
  if (shape === "readonly") {
@@ -15067,7 +15074,7 @@ function forgeAgentDef(agentId, def) {
15067
15074
  Object.assign(permission, def.permission ?? {});
15068
15075
  permission.task = "deny";
15069
15076
  const entry = {
15070
- description: agentDescriptionFor(agentId, def),
15077
+ description: agentDescriptionFor(agentId, def, provenance),
15071
15078
  mode: "subagent",
15072
15079
  prompt: materializeAgentPrompt({
15073
15080
  agent: agentId,
@@ -15801,6 +15808,16 @@ function validatePoolField(v) {
15801
15808
  return { ok: false };
15802
15809
  return { ok: true, ns: trimmed };
15803
15810
  }
15811
+ function poolFileCompliance(top) {
15812
+ const pool = isRecord(top) ? top.pool : undefined;
15813
+ if (pool === undefined || pool === null) {
15814
+ return { compliant: false, violation: 'missing top-level pool field — declare "pool": "<namespace>" ([a-z0-9-], 1-24 chars) to make this file crew-compliant' };
15815
+ }
15816
+ if (!validatePoolField(pool).ok) {
15817
+ return { compliant: false, violation: `invalid top-level pool field ${JSON.stringify(String(pool))} — expected a string of [a-z0-9-], 1-24 chars after trim` };
15818
+ }
15819
+ return { compliant: true, violation: "" };
15820
+ }
15804
15821
  function dirBasename(dir) {
15805
15822
  let s = dir;
15806
15823
  while (s.length > 1 && (s.endsWith("/") || s.endsWith("\\")))
@@ -15882,30 +15899,32 @@ function createPoolResolver(deps) {
15882
15899
  if (f)
15883
15900
  uniqueFiles.add(f);
15884
15901
  }
15885
- const anyWorkspacePool = uniqueFiles.size > 0;
15886
15902
  if (globalPath && globalParsed)
15887
15903
  uniqueFiles.add(globalPath);
15888
- const primaryFile = anchors.length > 0 ? workspacePoolOf(anchors[0]) : null;
15889
- const plainPoolFile = primaryFile ?? (!anyWorkspacePool && globalParsed !== null && globalPath ? globalPath : null);
15890
- const orderedFiles = [plainPoolFile, ...[...uniqueFiles].filter((f) => f !== plainPoolFile).sort(codepointCompare)].filter((f) => f !== null);
15904
+ const orderedFiles = [...uniqueFiles].sort(codepointCompare);
15891
15905
  const families = [];
15892
15906
  for (const file2 of orderedFiles) {
15893
15907
  const parsed = readPool(file2);
15894
15908
  if (parsed && file2 !== globalPath)
15895
15909
  findings.push(...parsed.findings);
15896
- const isPrimary = plainPoolFile !== null && file2 === plainPoolFile;
15897
15910
  let declaredPool = null;
15898
- if (!isPrimary && parsed) {
15899
- const v = validatePoolField(parsed.declaredPool);
15900
- if (parsed.declaredPool !== undefined && parsed.declaredPool !== null) {
15901
- if (v.ok) {
15902
- declaredPool = v.ns;
15903
- } else {
15904
- findings.push({ level: "warn", code: "pool-field-invalid", message: `${file2}: invalid "pool" field ${JSON.stringify(String(parsed.declaredPool))} (expected [a-z0-9-], 1-24 chars) — falling back to the directory-derived namespace` });
15911
+ if (parsed) {
15912
+ if (file2 === globalPath) {
15913
+ if (parsed.declaredPool !== undefined && parsed.declaredPool !== null) {
15914
+ findings.push({ level: "warn", code: "pool-field-ignored-global", message: `${file2}: the declared "pool" field ${JSON.stringify(String(parsed.declaredPool))} is ignored — the global fallback namespace is fixed to "global"` });
15915
+ }
15916
+ } else {
15917
+ const v = validatePoolField(parsed.declaredPool);
15918
+ if (parsed.declaredPool !== undefined && parsed.declaredPool !== null) {
15919
+ if (v.ok) {
15920
+ declaredPool = v.ns;
15921
+ } else {
15922
+ findings.push({ level: "warn", code: "pool-field-invalid", message: `${file2}: invalid "pool" field ${JSON.stringify(String(parsed.declaredPool))} (expected [a-z0-9-], 1-24 chars) — falling back to the directory-derived namespace` });
15923
+ }
15905
15924
  }
15906
15925
  }
15907
15926
  }
15908
- families.push({ file: file2, parsed, ns: null, declaredPool, primary: isPrimary });
15927
+ families.push({ file: file2, parsed, ns: "", declaredPool });
15909
15928
  }
15910
15929
  const allDeclared = new Set;
15911
15930
  for (const f of families)
@@ -15913,9 +15932,7 @@ function createPoolResolver(deps) {
15913
15932
  allDeclared.add(f.declaredPool);
15914
15933
  const usedNs = new Map;
15915
15934
  for (const f of families) {
15916
- if (f.primary)
15917
- continue;
15918
- const base = f.declaredPool ?? (f.file === globalPath ? "global" : sanitizeNamespace(dirBasename(parentDirLabel(parentDirLabel(f.file)))));
15935
+ const base = f.file === globalPath ? "global" : f.declaredPool ?? sanitizeNamespace(dirBasename(parentDirLabel(parentDirLabel(f.file))));
15919
15936
  let ns = base;
15920
15937
  let suffix = 2;
15921
15938
  while ((usedNs.has(ns) || ns !== base && allDeclared.has(ns)) && suffix < 100) {
@@ -15934,7 +15951,7 @@ function createPoolResolver(deps) {
15934
15951
  const own = f.parsed?.agents ?? {};
15935
15952
  const ids = [];
15936
15953
  for (const rawId of Object.keys(own)) {
15937
- const matId = f.ns === null ? rawId : `${f.ns}-${rawId}`;
15954
+ const matId = `${f.ns}-${rawId}`;
15938
15955
  const existing = agents[matId];
15939
15956
  if (existing !== undefined) {
15940
15957
  findings.push({ level: "error", code: "materialized-id-collision", message: `materialized agent forge-${matId} from ${f.file} collides with the one from ${existing.file} — the earlier pool in resolution order wins, this agent is skipped` });
@@ -15943,7 +15960,7 @@ function createPoolResolver(deps) {
15943
15960
  agents[matId] = { materializedId: matId, def: own[rawId], ns: f.ns, file: f.file };
15944
15961
  ids.push(matId);
15945
15962
  }
15946
- origins.push({ file: f.file, ns: f.ns, declaredPool: f.declaredPool, primary: f.primary, materializedIds: ids });
15963
+ origins.push({ file: f.file, ns: f.ns, declaredPool: f.declaredPool, materializedIds: ids });
15947
15964
  }
15948
15965
  return { families: origins, agents, findings };
15949
15966
  }
@@ -16126,6 +16143,28 @@ function sessionPoolFileOf(anchor) {
16126
16143
  return poolResolver.workspacePoolOf(anchor);
16127
16144
  return loaderForAnchor(anchor).load().projectPath;
16128
16145
  }
16146
+ function crewPoolComplianceRefusal(anchor) {
16147
+ const poolFile = sessionPoolFileOf(anchor);
16148
+ if (poolFile === null)
16149
+ return null;
16150
+ let text;
16151
+ try {
16152
+ text = readFileSync8(poolFile, "utf8");
16153
+ } catch (err) {
16154
+ const reason = err instanceof Error ? err.message : String(err);
16155
+ return `[forge:crew] Pool compliance refused: this session's workspace pool file (${poolFile}) could not be read (${reason}). ` + `Repair the file so it is readable JSONC declaring a valid top-level "pool" namespace ([a-z0-9-], 1-24 chars) — REQUIRED for crew-governed dispatch. ` + `The gate lifts hot-apply: the next resolve after the repair admits registration/dispatch; no host restart.`;
16156
+ }
16157
+ const parsed = parseForgeJsonc(text);
16158
+ if (!parsed.ok) {
16159
+ return `[forge:crew] Pool compliance refused: this session's workspace pool file (${poolFile}) does not parse (${parsed.error.message} at line ${parsed.error.line}). ` + `Repair the file so it parses and declares a valid top-level "pool" namespace ([a-z0-9-], 1-24 chars) — REQUIRED for crew-governed dispatch. ` + `The gate lifts hot-apply: the next resolve after the repair admits registration/dispatch; no host restart.`;
16160
+ }
16161
+ const verdict = poolFileCompliance(parsed.config);
16162
+ if (verdict.compliant)
16163
+ return null;
16164
+ const fam = resolvePools()?.families.find((f) => foldPath(f.file) === foldPath(poolFile));
16165
+ const suggested = fam?.ns ?? sanitizeNamespace(basename2(dirname4(dirname4(poolFile))));
16166
+ return `[forge:crew] Pool compliance refused: this session's workspace pool file (${poolFile}) is non-compliant — ${verdict.violation}. ` + `Minimal fix: add or repair the top-level "pool": "${suggested}" field (a [a-z0-9-] string, 1-24 chars — REQUIRED for crew-governed dispatch). ` + `The gate lifts hot-apply: the next resolve after the repair admits registration/dispatch; no host restart.`;
16167
+ }
16129
16168
  var crews = new Map;
16130
16169
  function crewRecordPathFor(worktree, objective) {
16131
16170
  const dir = join6(worktree, ".opencode", "crew");
@@ -16352,7 +16391,8 @@ function readPlanDir(worktree) {
16352
16391
  function ensureSession(sessionID, worktree) {
16353
16392
  const existing = sessions.get(sessionID);
16354
16393
  if (existing) {
16355
- existing.worktree = worktree;
16394
+ if (!isRootish(worktree))
16395
+ existing.worktree = worktree;
16356
16396
  return existing;
16357
16397
  }
16358
16398
  const state = { sessionID, worktree };
@@ -17038,9 +17078,9 @@ var CREW_INIT_TEMPLATE = [
17038
17078
  "",
17039
17079
  "Two axes, distinct: POOLS follow workspaces (sole ownership above — dispatch is gated to the session's own pool / the global fallback); SESSION ARTIFACTS (plan/goal/crew records) follow the session. A forge.json in another workspace is not dispatchable from this session.",
17040
17080
  "",
17041
- '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. When you configure it: name ids as PLAIN role words WITHOUT the `forge-` prefix — the plugin materializes every id as `forge-<id>` automatically (a `forge-`-prefixed id doubles up as `forge-forge-coder` and is auto-stripped with a warning). Give EVERY agent a SHORT prompt derived from the role the user asked for — one or two sentences describing its job (the user trims or extends from there); write a long prompt ONLY when the user explicitly asks for that agent. Give EVERY agent an EXPLICIT `shape`: `readonly` for read-only roles (reviewer/scout/planner — denies write/edit/bash and the patch family), `write` for implementers and build drivers. ALSO include a proposed short stable `pool` namespace (a [a-z0-9-] word up to 24 chars — e.g. "aa" for a specialist pool) so the materialized ids survive directory renames.',
17081
+ '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. When you configure it: name ids as PLAIN role words WITHOUT the `forge-` prefix (a `forge-`-prefixed id doubles up and is auto-stripped with a warning) — every pool materializes as `forge-<ns>-<id>` under its `pool` namespace (the global fallback materializes as `forge-global-<id>`). Give EVERY agent a SHORT prompt derived from the role the user asked for — one or two sentences describing its job (the user trims or extends from there); write a long prompt ONLY when the user explicitly asks for that agent. Give EVERY agent an EXPLICIT `shape`: `readonly` for read-only roles (reviewer/scout/planner — denies write/edit/bash and the patch family), `write` for implementers and build drivers. ALWAYS include the top-level `pool` namespace (a [a-z0-9-] word up to 24 chars — e.g. "aa" for a specialist pool): it is REQUIRED for crew-governed dispatch (the compliance gate) and keeps the materialized ids stable across directory renames.',
17042
17082
  "",
17043
- "Template (JSONC — comments allowed): ids are plain role words (research, coder, ...) — the plugin adds the `forge-` prefix at materialization; `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. Optional per agent: `prompt` — a short role description; an explicit prompt FULLY overrides the built-in role (only the ids `research` and `review` carry built-ins; any other id without a prompt gets a generic one-liner and will not know its job). Optional per agent: `shape` — an ABSENT shape defaults to WRITE (open permissions — the compatibility-preserving default for existing pools); `shape: \"readonly\"` is the explicit seal that denies the mutating-tool family (write/edit/bash + patch/applypatch/apply/multiedit) for read-only roles. Optional top level: `pool` — the file's namespace.",
17083
+ 'Template (JSONC — comments allowed): ids are plain role words (research, coder, ...) — every pool materializes as `forge-<ns>-<id>` (the global fallback materializes as `forge-global-<id>`); `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. Optional per agent: `prompt` — a short role description; an explicit prompt FULLY overrides the built-in role (only the ids `research` and `review` carry built-ins; any other id without a prompt gets a generic one-liner and will not know its job). Optional per agent: `shape` — an ABSENT shape defaults to WRITE (open permissions — the compatibility-preserving default for existing pools); `shape: "readonly"` is the explicit seal that denies the mutating-tool family (write/edit/bash + patch/applypatch/apply/multiedit) for read-only roles. Top level: `pool` — the file\'s namespace, REQUIRED for crew-governed dispatch.',
17044
17084
  "{template}",
17045
17085
  "",
17046
17086
  "After the file is saved: file edits, creations, and deletions apply at the next config hook — NO host restart is needed, and a DELETED pool file un-materializes its agents (deletion is real — no zombie pools survive on disk truth)."
@@ -17050,7 +17090,7 @@ function crewOrchestrationTemplate(resolution) {
17050
17090
  const families = resolution?.families ?? [];
17051
17091
  const globalPath = forgeConfigPaths({ homeDir: process.env.FORGE_TEST_FORGE_HOME ?? homedir() }).global;
17052
17092
  const otherLines = families.map((f) => {
17053
- const label = f.primary ? "primary pool (plain forge-<id> ids)" : `pool ${f.ns} (forge-${f.ns}-*)`;
17093
+ const label = `pool ${f.ns} (forge-${f.ns}-*)`;
17054
17094
  return ` - ${label} — origin ${f.file} — NOT dispatchable from this session (unless this session's worktree is owned by that pool / it is the global fallback): ${f.materializedIds.map((id) => `forge-${id}`).join(", ") || "no agents"}`;
17055
17095
  });
17056
17096
  return [
@@ -17077,7 +17117,7 @@ function crewOrchestrationTemplate(resolution) {
17077
17117
  `);
17078
17118
  }
17079
17119
  var crewBeginTool = tool({
17080
- description: `Register this session's crew (from /crew) with its DECLARED subtask plan — the crew starts PENDING: the execution-mode choice (waves / convert to goal / standby) is the user's. Dual-purpose: called with {execution} ONLY (no objective/subtasks) it ARMS the pending crew — "waves" lifts the dispatch belt, "goal" ends the crew as a conversion record (the following goal_write arm dialog is the user's gate). One-call register+arm ({objective, subtasks, execution:"waves"}) is legal ONLY while a live active goal governs the session (born armed, never pends). Refuses: active crew, plan draft, missing/duplicated subtasks. Crew state is in-memory: a host restart ends it honestly.`,
17120
+ description: `Register this session's crew (from /crew) with its DECLARED subtask plan — the crew starts PENDING: the execution-mode choice (waves / convert to goal / standby) is the user's. Dual-purpose: called with {execution} ONLY (no objective/subtasks) it ARMS the pending crew — "waves" lifts the dispatch belt, "goal" ends the crew as a conversion record (the following goal_write arm dialog is the user's gate). One-call register+arm ({objective, subtasks, execution:"waves"}) is legal ONLY while a live active goal governs the session (born armed, never pends). Refuses: active crew, plan draft, missing/duplicated subtasks, a non-compliant workspace pool file (missing/invalid top-level "pool" namespace — required for crew-governed dispatch). Crew state is in-memory: a host restart ends it honestly.`,
17081
17121
  args: {
17082
17122
  objective: tool.schema.string().optional().describe("One-line crew objective (from /crew) — required on registration, ABSENT on the arm call"),
17083
17123
  execution: tool.schema.string().optional().describe('Arm action: "waves" (supervised waves — lifts the belt) or "goal" (convert to a goal contract). Alone = arm the pending crew; with objective+subtasks = one-call born-armed registration, legal only under a governing active goal.'),
@@ -17111,9 +17151,9 @@ var crewBeginTool = tool({
17111
17151
  `[forge:crew] CREW IS NOT INITIALIZED: no forge subagent is materialized for this session's roster (neither the workspace pool nor the global fallback yields an agent). Crew orchestration is unavailable until the user sets it up.`,
17112
17152
  `Two layers, SOLE OWNERSHIP: the workspace pool — the NEAREST .opencode/forge.json walking up from this session's worktree (${paths.project}) — IS the whole roster when it exists; the global layer (${paths.global}) serves only as the fallback when the workspace has none (a fallback, NOT a merge; forge.json files nested deeper in the workspace subtree are ignored — the same workspace-ownership strictness as opencode's native MCP config).`,
17113
17153
  "Two axes, distinct: POOLS follow workspaces (sole ownership above — dispatch is gated to the session's own pool / the global fallback); SESSION ARTIFACTS (plan/goal/crew records) follow the session. A forge.json in another workspace is not dispatchable from this session.",
17114
- `Template (JSONC): top-level optional "pool" names this file's namespace (short stable [a-z0-9-] up to 24 chars — non-primary pools materialize as forge-<ns>-<id>); {"agents": {"research": {"model": "provider/model", "thoughtLevel": "low", "shape": "readonly", "prompt": "short role description"}}} — an absent shape defaults to WRITE (open permissions); "shape": "readonly" is the explicit seal denying write/edit/bash + the patch family for read-only roles.`,
17115
- "Agent ids are plain role words WITHOUT the `forge-` prefix — the plugin materializes each id as `forge-<id>` (namespaced pools: `forge-<ns>-<id>`); a prefixed id is auto-stripped with a warning.",
17116
- "The user may configure it themselves, or ask you to write the file — only on their explicit go-ahead. When you write it: give every agent a SHORT prompt from the role the user asked for (one or two sentences; long only on explicit request), give every agent an EXPLICIT shape (readonly for read-only roles like reviewer/scout/planner; write for implementers and build drivers), and INCLUDE a proposed short stable `pool` namespace so ids survive directory renames. File edits, creations, and deletions apply at the next config hook — no host restart needed, and a DELETED pool file un-materializes its agents (deletion is real)."
17154
+ `Template (JSONC): the top-level "pool" field names this file's namespace (short stable [a-z0-9-] up to 24 chars — REQUIRED for crew-governed dispatch; every pool materializes as forge-<ns>-<id>, the global fallback materializes as forge-global-<id>); {"pool": "short-stable-namespace", "agents": {"research": {"model": "provider/model", "thoughtLevel": "low", "shape": "readonly", "prompt": "short role description"}}} — an absent shape defaults to WRITE (open permissions); "shape": "readonly" is the explicit seal denying write/edit/bash + the patch family for read-only roles.`,
17155
+ "Agent ids are plain role words WITHOUT the `forge-` prefix — every pool materializes as `forge-<ns>-<id>` under its `pool` namespace (the global fallback materializes as `forge-global-<id>`); a prefixed id is auto-stripped with a warning.",
17156
+ "The user may configure it themselves, or ask you to write the file — only on their explicit go-ahead. When you write it: give every agent a SHORT prompt from the role the user asked for (one or two sentences; long only on explicit request), give every agent an EXPLICIT shape (readonly for read-only roles like reviewer/scout/planner; write for implementers and build drivers), and INCLUDE the top-level `pool` namespace — it is REQUIRED for crew-governed dispatch (the compliance gate refuses a missing or invalid field) and keeps ids stable across directory renames. File edits, creations, and deletions apply at the next config hook — no host restart needed, and a DELETED pool file un-materializes its agents (deletion is real)."
17117
17157
  ];
17118
17158
  if (sessionLoaded && Object.keys(sessionLoaded.agents).length > 0 && sessionProject && !sessionCovered) {
17119
17159
  lines2.push(`NOTE: this session's workspace DOES have a forge.json (${sessionProject}) with usable agents — but this host has NOT materialized it (no initialization in that workspace). Re-initialize the host in that workspace, or move the agents into the global layer (${globalPoolPath}).`);
@@ -17195,6 +17235,11 @@ var crewBeginTool = tool({
17195
17235
  if (execution === "goal") {
17196
17236
  throw new Error('[forge:crew] execution:"goal" converts an already-registered PENDING crew — register the plan first (no execution), then convert. Under a governing goal, only "waves" one-call arming is legal (the loop already owns the goal).');
17197
17237
  }
17238
+ if (anchor) {
17239
+ const complianceRefusal = crewPoolComplianceRefusal(anchor);
17240
+ if (complianceRefusal)
17241
+ throw new Error(complianceRefusal);
17242
+ }
17198
17243
  const governed = execution === "waves";
17199
17244
  const lineage = String(args.lineage ?? "").trim();
17200
17245
  const anchorDir = anchor || hostWorktree;
@@ -17228,7 +17273,7 @@ var crewBeginTool = tool({
17228
17273
  for (const f of families.slice(0, 8)) {
17229
17274
  if (f === sessionFamily)
17230
17275
  continue;
17231
- const label = f.primary ? "primary pool (plain forge-<id> ids)" : `pool ${f.ns} (forge-${f.ns}-*)`;
17276
+ const label = `pool ${f.ns} (forge-${f.ns}-*)`;
17232
17277
  familyLines.push(` - NOT dispatchable from this session — ${label}: ${f.file} — ${f.materializedIds.map((id) => `forge-${id}`).join(", ") || "no agents"}`);
17233
17278
  }
17234
17279
  if (families.length > 8)
@@ -17565,6 +17610,17 @@ var server = async (input, options) => {
17565
17610
  hostWorktree = effectiveInitDir;
17566
17611
  rememberHostAnchor(effectiveInitDir);
17567
17612
  const client = input.client;
17613
+ const resolveSessionAnchorForPools = async (sessionID) => {
17614
+ if (!sessionID)
17615
+ return "";
17616
+ try {
17617
+ const rec = client?.session?.get ? await client.session.get({ path: { id: sessionID } }) : undefined;
17618
+ const anchor = effectiveWorktree(rec?.worktree, rec?.directory);
17619
+ if (anchor && !isRootish(anchor))
17620
+ return anchor;
17621
+ } catch {}
17622
+ return sessions.get(sessionID)?.worktree ?? "";
17623
+ };
17568
17624
  jobClient = input.client;
17569
17625
  const jobsOpts = options?.jobs;
17570
17626
  if (jobsOpts?.mode === "auto" || jobsOpts?.mode === "forge" || jobsOpts?.mode === "native")
@@ -17680,11 +17736,13 @@ var server = async (input, options) => {
17680
17736
  console.warn(`[forge:config] ${finding.level}: ${finding.message}`);
17681
17737
  }
17682
17738
  materializedSnapshot = {};
17739
+ const globalMaterializationPath = globalPoolPathOf();
17683
17740
  for (const [matId, agent] of Object.entries(resolution?.agents ?? {})) {
17684
17741
  const id = `forge-${matId}`;
17685
17742
  if (agentSection[id] !== undefined)
17686
17743
  continue;
17687
- const entry = forgeAgentDef(matId, agent.def);
17744
+ const provenance = foldPath(agent.file) === foldPath(globalMaterializationPath) ? { global: true } : { ns: agent.ns, file: agent.file };
17745
+ const entry = forgeAgentDef(matId, agent.def, provenance);
17688
17746
  agentSection[id] = entry;
17689
17747
  materializedSnapshot[id] = entry;
17690
17748
  }
@@ -17732,7 +17790,7 @@ var server = async (input, options) => {
17732
17790
  const agentCount = Object.keys(resolution?.agents ?? {}).length;
17733
17791
  const primaryAnchorDir = hostAnchors[0] ?? undefined;
17734
17792
  const template = agentCount > 0 ? crewOrchestrationTemplate(resolution ?? null) : CREW_INIT_TEMPLATE.replaceAll("{paths}", ` - ${forgeConfigPaths({ projectDir: primaryAnchorDir, homeDir: process.env.FORGE_TEST_FORGE_HOME ?? homedir() }).project}
17735
- - ${forgeConfigPaths({ projectDir: primaryAnchorDir, homeDir: process.env.FORGE_TEST_FORGE_HOME ?? homedir() }).global}`).replaceAll("{template}", JSON.stringify({ pool: "short-stable-namespace (optional, [a-z0-9-] up to 24 chars — non-primary pools materialize as forge-<ns>-<id>)", agents: { research: { model: "provider/model", thoughtLevel: "low", shape: "readonly", prompt: "short role description — what this agent does" }, coder: { model: "provider/model", thoughtLevel: "low", shape: "write", prompt: "short role description — what this agent does" } } }, null, 2).split(`
17793
+ - ${forgeConfigPaths({ projectDir: primaryAnchorDir, homeDir: process.env.FORGE_TEST_FORGE_HOME ?? homedir() }).global}`).replaceAll("{template}", JSON.stringify({ pool: "namespace (REQUIRED for crew-governed dispatch, [a-z0-9-] up to 24 chars — every pool materializes as forge-<ns>-<id>; the global fallback materializes as forge-global-<id>)", agents: { research: { model: "provider/model", thoughtLevel: "low", shape: "readonly", prompt: "short role description — what this agent does" }, coder: { model: "provider/model", thoughtLevel: "low", shape: "write", prompt: "short role description — what this agent does" } } }, null, 2).split(`
17736
17794
  `).map((l) => " " + l).join(`
17737
17795
  `));
17738
17796
  const description = agentCount > 0 ? "forge crew orchestration: register a declared subtask plan, execute it in waves of parallel native task calls, close with an evidence-checked report" : "forge crew orchestration — NOT INITIALIZED: no forge subagents configured; invoking it shows the one-time setup guidance";
@@ -17775,8 +17833,7 @@ var server = async (input, options) => {
17775
17833
  const agentId = target.slice("forge-".length);
17776
17834
  const entry = resolution?.agents[agentId];
17777
17835
  if (resolution && entry) {
17778
- const bound = sessions.get(input2.sessionID);
17779
- const anchor = bound?.worktree ?? "";
17836
+ const anchor = await resolveSessionAnchorForPools(input2.sessionID);
17780
17837
  if (!anchor) {
17781
17838
  if (!poolGateUnknownNotes.has(input2.sessionID)) {
17782
17839
  poolGateUnknownNotes.add(input2.sessionID);
@@ -17795,6 +17852,17 @@ var server = async (input, options) => {
17795
17852
  if (pendingCrew && pendingCrew.mode === "pending") {
17796
17853
  throw new Error(`[forge:crew] Dispatch refused: the crew "${pendingCrew.objective}" is PENDING (registered, not armed). Present the execution-mode choice to the user — supervised waves now / convert to a goal contract / standby — then arm per their choice with crew_begin {execution: "waves" | "goal"}. Reconnaissance task calls are free only BEFORE registration.`);
17797
17854
  }
17855
+ if (target && target.toLowerCase().startsWith("forge-") && input2.sessionID) {
17856
+ const crew = crews.get(input2.sessionID);
17857
+ if (crew && (crew.mode === "pending" || crew.mode === "executing")) {
17858
+ const anchor = await resolveSessionAnchorForPools(input2.sessionID);
17859
+ if (anchor) {
17860
+ const complianceRefusal = crewPoolComplianceRefusal(anchor);
17861
+ if (complianceRefusal)
17862
+ throw new Error(complianceRefusal);
17863
+ }
17864
+ }
17865
+ }
17798
17866
  }
17799
17867
  if ((input2.tool === "write" || input2.tool === "edit") && input2.sessionID) {
17800
17868
  const speaker = sessionAgents.get(input2.sessionID);
@@ -18129,11 +18197,13 @@ async function v2Setup(ctx) {
18129
18197
  try {
18130
18198
  const resolver = createPoolResolver({ homeDir: process.env.FORGE_TEST_FORGE_HOME ?? homedir() });
18131
18199
  const resolution = resolver.resolve([process.cwd()]);
18200
+ const v2GlobalPath = forgeConfigPaths({ homeDir: process.env.FORGE_TEST_FORGE_HOME ?? homedir() }).global;
18132
18201
  for (const [matId, agent] of Object.entries(resolution.agents)) {
18133
18202
  const id = `forge-${matId}`;
18134
18203
  if (draft.get(id) !== undefined)
18135
18204
  continue;
18136
- const v1def = forgeAgentDef(matId, agent.def);
18205
+ const provenance = foldPath(agent.file) === foldPath(v2GlobalPath) ? { global: true } : { ns: agent.ns, file: agent.file };
18206
+ const v1def = forgeAgentDef(matId, agent.def, provenance);
18137
18207
  draft.update(id, (agentDraft) => {
18138
18208
  agentDraft.description = v1def.description;
18139
18209
  agentDraft.system = v1def.prompt;
package/package.json CHANGED
@@ -1,64 +1,64 @@
1
- {
2
- "name": "@sorenllm/opencode-forge",
3
- "version": "0.17.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
- }
1
+ {
2
+ "name": "@sorenllm/opencode-forge",
3
+ "version": "0.18.1",
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
+ }