@sorenllm/opencode-forge 0.12.0 → 0.14.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 +97 -30
  2. package/dist/index.js +427 -110
  3. package/package.json +64 -64
package/README.md CHANGED
@@ -31,7 +31,18 @@ OpenSpec spec workflows remain a third, separate lane.
31
31
  ```
32
32
 
33
33
  - Plan files: `.opencode/plan/<date>-<slug>.md` in your project, frontmatter
34
- state machine `draft → approved → done` (exit: `abandoned`).
34
+ state machine `draft → approved → done`, exits `abandoned` (plain discard) and
35
+ `superseded` (`plan_discard {reason, supersede: "<successor path>"}` —
36
+ the exit for work whose execution moves to another harness, e.g.
37
+ `.opencode/crew/2026-10-01-....md`, rather than ends). Every terminal
38
+ transition appends a dated narrative section to the file body in the same
39
+ write as the status flip: close = each acceptance criterion's
40
+ pass/evidence; discard/supersede = the reason verbatim (`(none given)`
41
+ when absent; a supersession also records the successor path). Appended
42
+ sections are parser-inert — status reads frontmatter only, tick
43
+ counts read the Task List only — and a superseded plan's Task List
44
+ stays the frozen decision snapshot: closure IS the supersession, and
45
+ crew/goal results never tick it.
35
46
  - While a plan is in draft, `write` / `edit` / `bash` / `task` are **denied
36
47
  at the permission layer** — including your own `allow` config, and for
37
48
  every agent in that session (the draft protects shared session state; see
@@ -172,7 +183,7 @@ What this plugin touches, exhaustively:
172
183
  | `<tmp>/opencode-forge/jobs/registry.json` | persistent survivor registry (bounded: 100 entries) | runtime debris — after uninstall, kill any still-running `survive` jobs yourself first |
173
184
  | `<tmp>/opencode-forge/watchdog/log.jsonl` | watchdog interventions ledger (bounded: 200 entries, oldest rotated) | runtime debris — delete freely, also after uninstall |
174
185
  | `<tmp>/opencode-forge/dispatch/ledger.jsonl` | dispatch ledger — **no longer written** (the dispatch engine was removed); only present as inert debris from older versions | runtime debris — delete freely |
175
- | the nearest `.opencode/forge.json` on the workspace's ancestor chain, `~/.config/opencode/forge.json` | your static subagent definitions (JSONC), two MERGED layers (project overrides global per agent id). **User data — the plugin only reads it, never writes or migrates it** | yours — version the project one, keep the global one out of sync tools if it holds machine-specific models |
186
+ | `.opencode/forge.json` files (the nearest on each host anchor's ancestor chain, every one in an anchor's subtree, and `~/.config/opencode/forge.json`) | your static subagent definitions (JSONC) — pool families MERGED over the global layer per agent id (primary anchor's root pool plain ids, others namespaced). **User data — the plugin only reads them, never writes or migrates them** | yours — version the project ones, keep the global one out of sync tools if it holds machine-specific models |
176
187
  | `~/.cache/opencode/packages/...` | installed package copy | written by the `opencode plugin` installer, not the plugin |
177
188
  | `~/.config/opencode/opencode.json` | `plugin` array entry | written by the installer |
178
189
 
@@ -418,26 +429,61 @@ discovered by walking up:
418
429
 
419
430
  The two layers **merge**: agents combine, and on an id collision the project
420
431
  definition wins wholesale (one definition per id — no cross-layer field
421
- blending). A broken file empties only its own layer (error finding with the
422
- parse location); the other layer still applies. Both layers hot-apply —
423
- edits, and files appearing or disappearing on the ancestor chain, take
424
- effect without a host restart (new agents still need the restart to enter
425
- the task vocabulary; see the apply-timing table below).
432
+ blending). A broken file empties only its own layer/pool (error finding with
433
+ the parse location); everything else still applies. Both layers re-read at
434
+ every real materialization — host boot, a first config build, or an
435
+ opencode config reload — without a restart; a new session alone does
436
+ not re-read them (see the apply-timing table below).
437
+
438
+ **Hierarchical pools on a shared host.** Agent frontends (paseo and friends)
439
+ routinely share ONE opencode host process across projects, re-initializing
440
+ plugins with different directories. The plugin therefore keeps an
441
+ **append-only anchor set**: every host initialization appends its directory
442
+ (first one = the *primary anchor*), and pools resolve from the union —
443
+ anchor flips can only ever ADD pools, never remove them. Per anchor the
444
+ family is:
445
+
446
+ - the **root pool** — the nearest forge.json walking up from that anchor
447
+ (merged over the global layer per agent id);
448
+ - every **sub-pool** — a `.opencode/forge.json` anywhere in the anchor's
449
+ subdirectories, discovered by a bounded breadth-first scan (lexicographic
450
+ sibling order; dependency dirs like `node_modules`/`.git`/`dist` are
451
+ skipped; 2,000-directory budget per anchor).
452
+
453
+ The **primary anchor's root pool keeps plain `forge-<id>` ids** (a
454
+ single-project host is unchanged); every other family materializes
455
+ **namespaced** as `forge-<ns>-<id>`. `ns` is the file's optional `pool`
456
+ field, else the sanitized directory name. Pool identity is the absolute file
457
+ path — the same file reached through two anchors materializes exactly once;
458
+ namespace and materialized-id collisions resolve deterministically with an
459
+ error finding, never by silent shadowing. The `/crew` roster and the
460
+ `crew_begin` registration output list the dispatchable agents **grouped by
461
+ pool** with each pool's origin path, the primary marked.
462
+
463
+ | axis | follows | survives restart? |
464
+ | --- | --- | --- |
465
+ | pools (the `forge-*` vocabulary) | host directories — the anchor set | the set re-seeds from the spawn anchor only |
466
+ | session artifacts (plan/goal/crew records) | the session's own workspace | files on disk are the source of truth |
426
467
 
427
- One host instance materializes one pool (from its launch anchor) — a
428
- workspace-local file that the host was not launched on is NOT silently
429
- ignored: `/crew` names it and tells you to relaunch there (or fold the
430
- agents into the global layer).
468
+ A forge.json in a directory NO host anchor covers is NOT silently ignored:
469
+ `/crew` names it as the residual mismatch and tells you a host
470
+ initialization in that workspace adds its pools (or fold the agents into the
471
+ global layer).
431
472
 
432
473
  **Id naming**: use plain role words (`research`, `coder`, …) — the plugin
433
- materializes every id as `forge-<id>` itself. A `forge-`-prefixed id in the
434
- file would double up (`forge-coder` → agent `forge-forge-coder`); the loader
435
- auto-strips the prefix (all repetitions) with a warning so existing files
436
- keep working, but rename the entries to silence it.
474
+ materializes every id as `forge-<id>` itself (namespaced pools:
475
+ `forge-<ns>-<id>`). A `forge-`-prefixed id in the file would double up
476
+ (`forge-coder` → agent `forge-forge-coder`); the loader auto-strips the
477
+ prefix (all repetitions) with a warning so existing files keep working, but
478
+ rename the entries to silence it. The namespace infix is never parsed back
479
+ out of a materialized id.
437
480
 
438
481
  ```jsonc
439
482
  {
440
- // forge subagents — definitions; see the restart table below for apply timing
483
+ // sub-pool files only: namespace for this family's ids (forge-<ns>-<id>).
484
+ // Short, stable, [a-z0-9-] up to 24 chars — ids survive directory renames.
485
+ // Invalid values degrade to a warning + the directory-derived namespace.
486
+ // "pool": "aa",
441
487
  "agents": {
442
488
  // PINNED worker: model + thoughtLevel form an ATOMIC PAIR — set BOTH
443
489
  "research": {
@@ -497,12 +543,18 @@ A rejected entry costs only itself — its siblings materialize normally.
497
543
  > error finding naming the missing half. Fix per entry: add the missing
498
544
  > `thoughtLevel`, or delete `model` to make it an Auto worker.
499
545
 
500
- **What applies when** — edits take hold per field:
546
+ **What applies when** — only a real materialization re-reads the pool
547
+ files into the `forge-*` vocabulary; the anchor set itself only ever grows.
548
+ Materialization triggers (2026-10-01 live finding): host boot (a freshly
549
+ started CLI process reads the files as they are), the FIRST config build
550
+ for a directory on this host, and opencode config file reloads. A NEW
551
+ session on a long-lived shared host (serve-type) does NOT re-materialize:
501
552
 
502
553
  | change | needs a host restart? |
503
554
  | --- | --- |
504
- | `thoughtLevel` on an existing agent | no (applies to NEW sessions of that agent; running sessions keep their frozen depth) |
505
- | agent added / removed, or `model` / `prompt` / `permission` changed | yes (materialization runs on the config hook) |
555
+ | `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 |
556
+ | 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 and depth lookups resolve the pool files fresh regardless |
557
+ | 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 appends the anchor and its pools (append-only; existing pools never flip) |
506
558
 
507
559
  ### Dispatching
508
560
 
@@ -544,11 +596,21 @@ requires a plan. The flow:
544
596
 
545
597
  1. **Register** the declared plan: `crew_begin {objective, subtasks}` — one
546
598
  titled subtask each, optionally naming the intended `forge-*` agent.
599
+ When the macro contract descends from a plan artifact, pass its path as
600
+ the optional `lineage` argument — recorded as a first-class `lineage:`
601
+ line in the crew record header and disclosed in the registration output
602
+ (explicit argument only; never inferred from a recently discarded plan).
547
603
  Registration enters a **PENDING** crew: execution has not started. The
548
- registration output confirms the roster **with its origin** (which
549
- forge.json layer the dispatchable set materialized from), points at the
550
- crew record file, presents the three execution-mode choices, and stops the
551
- turn. Reconnaissance task calls are free BEFORE registration.
604
+ registration output LEADS with the crew record path line (same
605
+ first-block prominence as the plan tools' `Plan created:`) and the
606
+ PENDING pause text instructs the model to relay that path to the user
607
+ alongside the choice — so you can verify the record really landed on
608
+ disk (an earlier output order buried the path line, and the model
609
+ reliably drops what it buries — leading with it is the fix). The
610
+ output then confirms the roster **grouped by pool with its origins**
611
+ (which forge.json families the dispatchable set materialized from, the
612
+ primary marked), presents the three execution-mode choices, and stops
613
+ the turn. Reconnaissance task calls are free BEFORE registration.
552
614
  2. **The pause is mechanical**: while the crew pends, every `task` dispatch is
553
615
  refused by an interception belt. The user chooses:
554
616
  - **supervised waves now** — `crew_begin {execution: "waves"}` lifts the
@@ -601,13 +663,18 @@ itself (a rootish guard keeps degenerate global-project worktrees from
601
663
  landing files on the drive root or the launch anchor). The anchor follows the
602
664
  session's workspace, never the objective's scope.
603
665
 
604
- **Not initialized?** `/crew` on a host with zero usable forge agents refuses
605
- with one-time setup guidance: the two file paths, a copy-paste template, and
606
- the choice of configuring it yourself OR having the session AI do it — the
607
- latter only on your explicit go-ahead in that conversation, through the
608
- normal visible write path. This is the plugin's only configuration
609
- invitation. After saving the file, restart opencode to materialize the
610
- agents.
666
+ **Not initialized?** `/crew` on a host with ZERO pools across its whole
667
+ anchor set refuses with one-time setup guidance: the two file paths, a
668
+ copy-paste template, and the choice of configuring it yourself OR having the
669
+ session AI do it — the latter only on your explicit go-ahead in that
670
+ conversation, through the normal visible write path (when the AI writes a
671
+ sub-pool file it also proposes a short stable `pool` namespace so ids
672
+ survive directory renames). This is the plugin's only configuration
673
+ invitation. Saved files apply at the next real materialization — no
674
+ restart needed; on a shared host, the first session in a NEW workspace adds
675
+ that anchor and its pools automatically (a new session in an existing
676
+ workspace does not re-read the files — see the apply-timing table
677
+ above).
611
678
 
612
679
  ### Debris note
613
680
 
package/dist/index.js CHANGED
@@ -12335,7 +12335,7 @@ function tool(input) {
12335
12335
  tool.schema = exports_external;
12336
12336
  // plugin.ts
12337
12337
  import { execFileSync as execFileSync3 } from "node:child_process";
12338
- import { appendFileSync, existsSync as existsSync3, mkdirSync as mkdirSync5, readdirSync as readdirSync4, readFileSync as readFileSync8, statSync as statSync5, writeFileSync as writeFileSync5 } from "node:fs";
12338
+ import { appendFileSync, existsSync as existsSync3, mkdirSync as mkdirSync5, readdirSync as readdirSync5, readFileSync as readFileSync8, realpathSync, statSync as statSync5, writeFileSync as writeFileSync5 } from "node:fs";
12339
12339
  import { isAbsolute, join as join6, relative } from "node:path";
12340
12340
  import { tmpdir as tmpdir3, homedir } from "node:os";
12341
12341
 
@@ -12346,12 +12346,13 @@ class PlanError extends Error {
12346
12346
  this.name = "PlanError";
12347
12347
  }
12348
12348
  }
12349
- var TERMINAL = ["done", "abandoned"];
12349
+ var TERMINAL = ["done", "abandoned", "superseded"];
12350
12350
  var LEGAL_TRANSITIONS = {
12351
- draft: ["approved", "abandoned"],
12352
- approved: ["done", "abandoned"],
12351
+ draft: ["approved", "abandoned", "superseded"],
12352
+ approved: ["done", "abandoned", "superseded"],
12353
12353
  done: [],
12354
- abandoned: []
12354
+ abandoned: [],
12355
+ superseded: []
12355
12356
  };
12356
12357
  var SECTION_ORDER = [
12357
12358
  "Goal",
@@ -12499,6 +12500,7 @@ function matchSectionHeader(line) {
12499
12500
  }
12500
12501
  function splitSections(body) {
12501
12502
  const sections = new Map;
12503
+ const claimed = new Set;
12502
12504
  let current = null;
12503
12505
  const buf = [];
12504
12506
  const flush = () => {
@@ -12509,12 +12511,14 @@ function splitSections(body) {
12509
12511
  };
12510
12512
  for (const line of body.split(/\r?\n/)) {
12511
12513
  const header = matchSectionHeader(line);
12512
- if (header !== null) {
12513
- flush();
12514
- current = header;
12515
- } else if (current !== null) {
12516
- buf.push(line);
12514
+ if (header === null || claimed.has(header)) {
12515
+ if (current !== null)
12516
+ buf.push(line);
12517
+ continue;
12517
12518
  }
12519
+ flush();
12520
+ claimed.add(header);
12521
+ current = header;
12518
12522
  }
12519
12523
  flush();
12520
12524
  return sections;
@@ -12525,7 +12529,7 @@ var ACCEPT_RE = /^\s*(\d+)[.、)]\s*(.+)$/;
12525
12529
  function parsePlan(text) {
12526
12530
  const { fm, body } = parseFrontmatter(text);
12527
12531
  const status = fm.status ?? "draft";
12528
- if (!["draft", "approved", "done", "abandoned"].includes(status)) {
12532
+ if (!["draft", "approved", "done", "abandoned", "superseded"].includes(status)) {
12529
12533
  throw new PlanError(`Unknown plan status: ${fm.status}`);
12530
12534
  }
12531
12535
  const sections = splitSections(body);
@@ -12612,11 +12616,35 @@ function transitionStatus(text, to, now) {
12612
12616
  if (from === to)
12613
12617
  throw new PlanError(`Plan is already ${to}`);
12614
12618
  if (!canTransition(from, to)) {
12615
- throw new PlanError(`Illegal status transition: ${from} -> ${to} (legal path: draft -> approved -> done; draft/approved -> abandoned)`);
12619
+ throw new PlanError(`Illegal status transition: ${from} -> ${to} (legal path: draft -> approved -> done; draft/approved -> abandoned/superseded)`);
12616
12620
  }
12617
12621
  const next = text.replace(/^status:.*$/m, `status: ${to}`);
12618
12622
  return setUpdated(next, now);
12619
12623
  }
12624
+ function appendTerminalSection(text, kind, opts = {}, now = new Date().toISOString()) {
12625
+ if (!["closed", "abandoned", "superseded"].includes(kind)) {
12626
+ throw new PlanError(`Unknown terminal section kind: ${kind}`);
12627
+ }
12628
+ const lines = [`## ${now} — ${kind}`, ""];
12629
+ const reason = (opts.reason ?? "").trim();
12630
+ lines.push(`reason: ${reason.length > 0 ? reason : "(none given)"}`);
12631
+ const successor = (opts.successor ?? "").trim();
12632
+ if (successor.length > 0)
12633
+ lines.push(`successor: ${successor}`);
12634
+ if (opts.checks && opts.checks.length > 0) {
12635
+ lines.push("checks:");
12636
+ for (const c of opts.checks) {
12637
+ lines.push(`- ${c.pass ? "PASS" : "FAIL"} — ${c.criterion.trim()}`);
12638
+ lines.push(` evidence: ${c.evidence.trim() || "(none)"}`);
12639
+ }
12640
+ }
12641
+ const base = text.replace(/\r?\n$/, "");
12642
+ return `${base}
12643
+
12644
+ ${lines.join(`
12645
+ `)}
12646
+ `;
12647
+ }
12620
12648
  function progressOf(doc2) {
12621
12649
  return { total: doc2.tasks.length, done: doc2.tasks.filter((t) => t.done).length };
12622
12650
  }
@@ -15104,7 +15132,7 @@ function resolvePinnedDepth(thoughtLevel, family, ladder) {
15104
15132
  }
15105
15133
 
15106
15134
  // src/forge-config.ts
15107
- import { readFileSync as readFileSync7, statSync as statSync4 } from "node:fs";
15135
+ import { readdirSync as readdirSync4, readFileSync as readFileSync7, statSync as statSync4 } from "node:fs";
15108
15136
  function sanitizeJsonc(text) {
15109
15137
  const out = [];
15110
15138
  let i = 0;
@@ -15456,6 +15484,25 @@ function parentDirOf(p) {
15456
15484
  const parent = s.slice(0, i + 1);
15457
15485
  return parent === "" ? null : parent;
15458
15486
  }
15487
+ function forgeCandidateAt(dir) {
15488
+ const sep2 = dir.includes("\\") && !dir.includes("/") ? "\\" : "/";
15489
+ const base = dir.endsWith("/") || dir.endsWith("\\") ? dir : `${dir}${sep2}`;
15490
+ return `${base}.opencode${sep2}forge.json`;
15491
+ }
15492
+ function discoverProjectFileWith(stat, anchor) {
15493
+ let dir = (anchor ?? "").trim();
15494
+ if (!dir)
15495
+ return null;
15496
+ for (;; ) {
15497
+ const candidate = forgeCandidateAt(dir);
15498
+ if (stat(candidate) !== null)
15499
+ return candidate;
15500
+ const parent = parentDirOf(dir);
15501
+ if (parent === null || parent === dir)
15502
+ return null;
15503
+ dir = parent;
15504
+ }
15505
+ }
15459
15506
  function createForgeConfigLoader(deps) {
15460
15507
  const stat = deps.stat ?? ((path) => {
15461
15508
  try {
@@ -15512,28 +15559,9 @@ function createForgeConfigLoader(deps) {
15512
15559
  cache.set(path, { mtimeMs: st.mtimeMs, size: st.size, result });
15513
15560
  return result;
15514
15561
  };
15515
- const candidateAt = (dir) => {
15516
- const sep2 = dir.includes("\\") && !dir.includes("/") ? "\\" : "/";
15517
- const base = dir.endsWith("/") || dir.endsWith("\\") ? dir : `${dir}${sep2}`;
15518
- return `${base}.opencode${sep2}forge.json`;
15519
- };
15520
- const discoverProjectFile = (anchor) => {
15521
- let dir = (anchor ?? "").trim();
15522
- if (!dir)
15523
- return null;
15524
- for (;; ) {
15525
- const candidate = candidateAt(dir);
15526
- if (stat(candidate) !== null)
15527
- return candidate;
15528
- const parent = parentDirOf(dir);
15529
- if (parent === null || parent === dir)
15530
- return null;
15531
- dir = parent;
15532
- }
15533
- };
15534
15562
  return {
15535
15563
  load() {
15536
- const projectPath = discoverProjectFile(deps.projectDir ?? "");
15564
+ const projectPath = discoverProjectFileWith(stat, deps.projectDir ?? "");
15537
15565
  const project = projectPath ? readCandidate(projectPath, "project") : null;
15538
15566
  const global = globalPath ? readCandidate(globalPath, "global") : null;
15539
15567
  if (project && global) {
@@ -15553,6 +15581,228 @@ function createForgeConfigLoader(deps) {
15553
15581
  }
15554
15582
  };
15555
15583
  }
15584
+ var POOL_SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build", "out", "target", ".cache", "venv", ".venv", "__pycache__", "coverage"]);
15585
+ var POOL_SCAN_BUDGET = 2000;
15586
+ function sanitizeNamespace(name) {
15587
+ const collapsed = name.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
15588
+ return collapsed || "pool";
15589
+ }
15590
+ function validatePoolField(v) {
15591
+ if (typeof v !== "string")
15592
+ return { ok: false };
15593
+ const trimmed = v.trim();
15594
+ if (trimmed.length < 1 || trimmed.length > 24 || !/^[a-z0-9-]+$/.test(trimmed))
15595
+ return { ok: false };
15596
+ return { ok: true, ns: trimmed };
15597
+ }
15598
+ function dirBasename(dir) {
15599
+ let s = dir;
15600
+ while (s.length > 1 && (s.endsWith("/") || s.endsWith("\\")))
15601
+ s = s.slice(0, -1);
15602
+ const i = Math.max(s.lastIndexOf("/"), s.lastIndexOf("\\"));
15603
+ return i < 0 ? s : s.slice(i + 1);
15604
+ }
15605
+ function codepointCompare(a, b) {
15606
+ return a < b ? -1 : a > b ? 1 : 0;
15607
+ }
15608
+ function createPoolResolver(deps) {
15609
+ const stat = deps.stat ?? ((path) => {
15610
+ try {
15611
+ const s = statSync4(path);
15612
+ return { mtimeMs: s.mtimeMs, size: s.size };
15613
+ } catch {
15614
+ return null;
15615
+ }
15616
+ });
15617
+ const readFile = deps.readFile ?? ((path) => readFileSync7(path, "utf8"));
15618
+ const readdir = deps.readdir ?? ((dir) => {
15619
+ try {
15620
+ return readdirSync4(dir, { withFileTypes: true }).map((d) => ({ name: d.name, dir: d.isDirectory() }));
15621
+ } catch {
15622
+ return null;
15623
+ }
15624
+ });
15625
+ const budget = typeof deps.scanBudget === "number" && deps.scanBudget > 0 ? deps.scanBudget : POOL_SCAN_BUDGET;
15626
+ const globalPath = forgeConfigPaths({ homeDir: deps.homeDir }).global;
15627
+ const fileCache = new Map;
15628
+ const readPool = (path) => {
15629
+ const st = stat(path);
15630
+ if (st === null) {
15631
+ fileCache.delete(path);
15632
+ return null;
15633
+ }
15634
+ const hit = fileCache.get(path);
15635
+ if (hit && hit.mtimeMs === st.mtimeMs && hit.size === st.size)
15636
+ return hit.parsed;
15637
+ let parsed = null;
15638
+ const text = readFile(path);
15639
+ const doc2 = parseForgeJsonc(text);
15640
+ if (!doc2.ok) {
15641
+ parsed = {
15642
+ agents: {},
15643
+ declaredPool: null,
15644
+ findings: [
15645
+ {
15646
+ level: "error",
15647
+ code: "config-parse-error",
15648
+ message: `${path}: ${doc2.error.message}${doc2.error.line !== undefined ? ` (line ${doc2.error.line}, column ${doc2.error.column})` : ""} — this pool contributes no agents until the file parses`
15649
+ }
15650
+ ]
15651
+ };
15652
+ } else if (doc2.config !== undefined && !isRecord(doc2.config)) {
15653
+ parsed = { agents: {}, declaredPool: null, findings: [{ level: "error", code: "config-not-object", message: `${path}: top level must be an object — this pool contributes no agents` }] };
15654
+ } else {
15655
+ const top = doc2.config === undefined ? undefined : doc2.config;
15656
+ const validated = validateAgentSet(top === undefined ? undefined : top.agents);
15657
+ parsed = { agents: validated.agents, declaredPool: top === undefined ? null : top.pool, findings: validated.findings };
15658
+ }
15659
+ fileCache.set(path, { mtimeMs: st.mtimeMs, size: st.size, parsed });
15660
+ return parsed;
15661
+ };
15662
+ const dirCache = new Map;
15663
+ const listDir = (dir) => {
15664
+ const st = stat(dir);
15665
+ if (st === null) {
15666
+ dirCache.delete(dir);
15667
+ return null;
15668
+ }
15669
+ const hit = dirCache.get(dir);
15670
+ if (hit && hit.mtimeMs === st.mtimeMs)
15671
+ return hit;
15672
+ const listing = readdir(dir);
15673
+ const children = listing === null ? null : [...listing].sort((a, b) => codepointCompare(a.name, b.name));
15674
+ let poolFile = null;
15675
+ const candidate = forgeCandidateAt(dir);
15676
+ if (stat(candidate) !== null)
15677
+ poolFile = candidate;
15678
+ const entry = { mtimeMs: st.mtimeMs, children, poolFile };
15679
+ dirCache.set(dir, entry);
15680
+ return entry;
15681
+ };
15682
+ const scanSubtree = (anchor) => {
15683
+ const files = [];
15684
+ const seen = new Set;
15685
+ let opened = 0;
15686
+ const sep2 = anchor.includes("\\") && !anchor.includes("/") ? "\\" : "/";
15687
+ const joinDir = (parent, name) => parent.endsWith("/") || parent.endsWith("\\") ? `${parent}${name}` : `${parent}${sep2}${name}`;
15688
+ const queue = [anchor];
15689
+ while (queue.length > 0 && opened < budget) {
15690
+ const dir = queue.shift();
15691
+ if (seen.has(dir))
15692
+ continue;
15693
+ seen.add(dir);
15694
+ const entry = listDir(dir);
15695
+ opened++;
15696
+ if (entry === null || entry.children === null)
15697
+ continue;
15698
+ const isAnchor = dir === anchor;
15699
+ if (!isAnchor && entry.poolFile !== null)
15700
+ files.push(entry.poolFile);
15701
+ for (const child of entry.children) {
15702
+ if (!child.dir)
15703
+ continue;
15704
+ if (POOL_SKIP_DIRS.has(child.name) || child.name === ".opencode")
15705
+ continue;
15706
+ queue.push(joinDir(dir, child.name));
15707
+ }
15708
+ }
15709
+ return files;
15710
+ };
15711
+ return {
15712
+ resolve(anchorsInput) {
15713
+ const anchors = [];
15714
+ for (const a of anchorsInput) {
15715
+ const t = (a ?? "").trim();
15716
+ if (t && !anchors.includes(t))
15717
+ anchors.push(t);
15718
+ }
15719
+ const findings = [];
15720
+ const globalParsed = globalPath ? readPool(globalPath) : null;
15721
+ if (globalParsed)
15722
+ findings.push(...globalParsed.findings);
15723
+ const primaryRootFile = anchors.length > 0 ? discoverProjectFileWith(stat, anchors[0]) : null;
15724
+ const uniqueFiles = new Set;
15725
+ if (primaryRootFile)
15726
+ uniqueFiles.add(primaryRootFile);
15727
+ for (const anchor of anchors) {
15728
+ for (const f of scanSubtree(anchor))
15729
+ uniqueFiles.add(f);
15730
+ if (anchor !== anchors[0]) {
15731
+ const rootFile = discoverProjectFileWith(stat, anchor);
15732
+ if (rootFile)
15733
+ uniqueFiles.add(rootFile);
15734
+ }
15735
+ }
15736
+ const plainPoolFile = primaryRootFile ?? (globalParsed !== null && globalPath ? globalPath : null);
15737
+ const orderedFiles = [plainPoolFile, ...[...uniqueFiles].filter((f) => f !== plainPoolFile).sort(codepointCompare)].filter((f) => f !== null);
15738
+ const families = [];
15739
+ for (const file2 of orderedFiles) {
15740
+ const parsed = readPool(file2);
15741
+ if (parsed && file2 !== globalPath)
15742
+ findings.push(...parsed.findings);
15743
+ const isPrimary = plainPoolFile !== null && file2 === plainPoolFile;
15744
+ let declaredPool = null;
15745
+ if (!isPrimary && parsed) {
15746
+ const v = validatePoolField(parsed.declaredPool);
15747
+ if (parsed.declaredPool !== undefined && parsed.declaredPool !== null) {
15748
+ if (v.ok) {
15749
+ declaredPool = v.ns;
15750
+ } else {
15751
+ 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` });
15752
+ }
15753
+ }
15754
+ }
15755
+ families.push({ file: file2, parsed, ns: null, declaredPool, primary: isPrimary });
15756
+ }
15757
+ const allDeclared = new Set;
15758
+ for (const f of families)
15759
+ if (f.declaredPool)
15760
+ allDeclared.add(f.declaredPool);
15761
+ const usedNs = new Map;
15762
+ for (const f of families) {
15763
+ if (f.primary)
15764
+ continue;
15765
+ const base = f.declaredPool ?? sanitizeNamespace(dirBasename(parentDirLabel(parentDirLabel(f.file))));
15766
+ let ns = base;
15767
+ let suffix = 2;
15768
+ while ((usedNs.has(ns) || ns !== base && allDeclared.has(ns)) && suffix < 100) {
15769
+ ns = `${base}-${suffix++}`;
15770
+ }
15771
+ const owner = usedNs.get(base);
15772
+ if (owner !== undefined && owner !== f.file) {
15773
+ findings.push({ level: "error", code: "pool-namespace-collision", message: `namespace "${base}" is already used by ${owner} — ${f.file} materializes under "${ns}" instead` });
15774
+ }
15775
+ f.ns = ns;
15776
+ usedNs.set(ns, f.file);
15777
+ }
15778
+ const agents = {};
15779
+ const origins = [];
15780
+ for (const f of families) {
15781
+ const merged = { ...globalParsed?.agents ?? {}, ...f.parsed?.agents ?? {} };
15782
+ const ids = [];
15783
+ for (const rawId of Object.keys(merged)) {
15784
+ const matId = f.ns === null ? rawId : `${f.ns}-${rawId}`;
15785
+ const existing = agents[matId];
15786
+ if (existing !== undefined) {
15787
+ 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` });
15788
+ continue;
15789
+ }
15790
+ agents[matId] = { materializedId: matId, def: merged[rawId], ns: f.ns, file: f.file };
15791
+ ids.push(matId);
15792
+ }
15793
+ origins.push({ file: f.file, ns: f.ns, declaredPool: f.declaredPool, primary: f.primary, materializedIds: ids });
15794
+ }
15795
+ return { families: origins, agents, findings };
15796
+ }
15797
+ };
15798
+ }
15799
+ function parentDirLabel(filePath) {
15800
+ let s = filePath;
15801
+ while (s.length > 1 && (s.endsWith("/") || s.endsWith("\\")))
15802
+ s = s.slice(0, -1);
15803
+ const i = Math.max(s.lastIndexOf("/"), s.lastIndexOf("\\"));
15804
+ return i < 0 ? s : s.slice(0, i);
15805
+ }
15556
15806
 
15557
15807
  // plugin.ts
15558
15808
  var FORGE_AGENT = "forge";
@@ -15597,7 +15847,7 @@ var PLAN_COMMAND_TEMPLATE = [
15597
15847
  "Route on the argument above (decide silently; do not recite this routing text to the user):",
15598
15848
  "- Argument empty: for every non-terminal plan (status draft or approved) in the directory above, read its frontmatter and task checkboxes, then report to the user: path, status, progress (x/y ticked). Ask whether to resume one or start something new.",
15599
15849
  '- Argument "resume": pick the most recently updated non-terminal plan, summarize its remaining unticked tasks to the user in one short list, then continue executing it — tick each task the moment it is done (plan_tick). If none exists, say so.',
15600
- '- Argument "discard": call the plan_discard tool, then tell the user the plan was abandoned and writes are restored.',
15850
+ '- Argument "discard": call the plan_discard tool with a short reason, then tell the user the plan was abandoned and writes are restored. If the work is NOT ending but moving to another harness — crew orchestration (/crew) or a goal contract (/goal) — pass supersede with the successor artifact path (e.g. ".opencode/crew/2026-10-01-....md") so the plan is marked superseded instead, with the successor recorded in the plan file.',
15601
15851
  "- Any other argument: treat it as the task goal — you are now in planning mode for this goal. Follow the plan discipline below end to end.",
15602
15852
  "",
15603
15853
  "## Plan discipline",
@@ -15610,7 +15860,7 @@ var PLAN_COMMAND_TEMPLATE = [
15610
15860
  "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).",
15611
15861
  "5. Completion (plan_close): when all tasks are ticked, self-check EVERY acceptance criterion with concrete evidence (file:line, command output, test result), then call plan_close with one check per criterion, pass/fail honest — a failing check refuses the close, and that is the design working, not an inconvenience. The user confirms closure in a dialog.",
15612
15862
  "",
15613
- "Boundaries: /plan discard abandons the plan (terminal, file kept as history, writes restored); /plan resume continues an unfinished plan from its remaining tasks. Work expected to span multiple sessions or days of multi-file change is spec work (e.g. OpenSpec), not plan work — say so once and let the user choose.",
15863
+ "Boundaries: /plan discard abandons the plan (terminal, file kept as history, writes restored) — pass supersede (the successor artifact path) when the work moves to crew/goal rather than ends, marking it superseded; /plan resume continues an unfinished plan from its remaining tasks. Work expected to span multiple sessions or days of multi-file change is spec work (e.g. OpenSpec), not plan work — say so once and let the user choose.",
15614
15864
  ""
15615
15865
  ].join(`
15616
15866
  `);
@@ -15665,8 +15915,35 @@ function noteToolAgent(context) {
15665
15915
  }
15666
15916
  }
15667
15917
  var forgeDisabled = false;
15668
- var forgeLoader = null;
15669
- var forgeLoaderDir = null;
15918
+ var hostAnchors = [];
15919
+ var poolResolver = null;
15920
+ var poolResolution = null;
15921
+ function normalizeAnchorDir(dir) {
15922
+ let p = (dir ?? "").trim();
15923
+ if (!p)
15924
+ return "";
15925
+ try {
15926
+ p = realpathSync(p);
15927
+ } catch {}
15928
+ if (process.platform === "win32")
15929
+ return p.replace(/\//g, "\\").toLowerCase();
15930
+ return p;
15931
+ }
15932
+ function rememberHostAnchor(dir) {
15933
+ const normalized = normalizeAnchorDir(dir);
15934
+ if (!normalized)
15935
+ return;
15936
+ if (!hostAnchors.includes(normalized))
15937
+ hostAnchors.push(normalized);
15938
+ if (!poolResolver) {
15939
+ poolResolver = createPoolResolver({ homeDir: process.env.FORGE_TEST_FORGE_HOME ?? homedir() });
15940
+ }
15941
+ }
15942
+ function resolvePools() {
15943
+ if (!poolResolver || hostAnchors.length === 0)
15944
+ return null;
15945
+ return poolResolver.resolve(hostAnchors);
15946
+ }
15670
15947
  var sessionAnchorLoaders = new Map;
15671
15948
  function loaderForAnchor(anchor) {
15672
15949
  const key = anchor || "-";
@@ -15694,13 +15971,14 @@ function crewRecordPathFor(worktree, objective) {
15694
15971
  name = `${date5}-${slug}-${n++}.md`;
15695
15972
  return join6(dir, name);
15696
15973
  }
15697
- function writeCrewRecord(path, objective, anchor, subtasks) {
15974
+ function writeCrewRecord(path, objective, anchor, subtasks, lineage) {
15698
15975
  mkdirSync5(join6(path, ".."), { recursive: true });
15699
15976
  writeFileSync5(path, [
15700
15977
  `# Crew Record: ${objective}`,
15701
15978
  "",
15702
15979
  `- registered: ${nowIso()}`,
15703
15980
  `- anchor: ${anchor}`,
15981
+ ...lineage ? [`- lineage: ${lineage}`] : [],
15704
15982
  "",
15705
15983
  `## Declared plan (${subtasks.length})`,
15706
15984
  ...subtasks.map((s, i) => `${i + 1}. ${s.title}${s.agent ? ` → ${s.agent}` : ""}`),
@@ -15730,6 +16008,7 @@ function governingGoalFor(sessionID) {
15730
16008
  function crewPendingChoiceLines() {
15731
16009
  return [
15732
16010
  "[forge:crew] The crew is PENDING — execution has NOT started. Present the execution-mode choice to the user and END YOUR TURN (every task dispatch is refused while the crew pends):",
16011
+ `RELAY FIRST: tell the user the crew record path (it leads this output) alongside this choice — the record is on disk from the registration moment, and the user must hear it.`,
15733
16012
  ` 1. supervised waves NOW — when the user says go, call crew_begin {execution: "waves"} and run the wave discipline;`,
15734
16013
  ` 2. convert to a goal contract — call crew_begin {execution: "goal"} (the crew ends as a conversion record; no extra dialog here), then draft goal_write with arm=true folding the declared subtasks into criteria/checks — the arm dialog is the user's gate;`,
15735
16014
  ` 3. standby — no call; the crew waits until armed or abandoned.`
@@ -15874,7 +16153,7 @@ function readPlanDir(worktree) {
15874
16153
  const dir = planDirOf(worktree);
15875
16154
  if (!existsSync3(dir))
15876
16155
  return [];
15877
- return readdirSync4(dir).filter((f) => f.endsWith(".md")).map((name) => ({ name, text: readFileSync8(join6(dir, name), "utf8") }));
16156
+ return readdirSync5(dir).filter((f) => f.endsWith(".md")).map((name) => ({ name, text: readFileSync8(join6(dir, name), "utf8") }));
15878
16157
  }
15879
16158
  function ensureSession(sessionID, worktree) {
15880
16159
  const existing = sessions.get(sessionID);
@@ -15941,7 +16220,7 @@ var planWriteTool = tool({
15941
16220
  } else {
15942
16221
  const dir = planDirOf(state.worktree);
15943
16222
  mkdirSync5(dir, { recursive: true });
15944
- path = join6(dir, planFileName(localDate(), slugify(args.goal), readdirSync4(dir).filter((f) => f.endsWith(".md"))));
16223
+ path = join6(dir, planFileName(localDate(), slugify(args.goal), readdirSync5(dir).filter((f) => f.endsWith(".md"))));
15945
16224
  mode = "created";
15946
16225
  }
15947
16226
  const text = renderPlan(args, now, created);
@@ -16034,28 +16313,40 @@ var planCloseTool = tool({
16034
16313
  Fix the implementation and retry, or revise the plan first.`);
16035
16314
  }
16036
16315
  await gate(context.ask, "plan_close", `Close plan: ${active.doc.goal}`);
16037
- writeFileSync5(active.path, transitionStatus(readFileSync8(active.path, "utf8"), "done", nowIso()));
16316
+ const closedAt = nowIso();
16317
+ const closed = appendTerminalSection(transitionStatus(readFileSync8(active.path, "utf8"), "done", closedAt), "closed", { reason: "completion gate passed (user confirmed)", checks: args.checks }, closedAt);
16318
+ writeFileSync5(active.path, closed);
16038
16319
  context.metadata({ title: `Plan done: ${active.doc.goal}` });
16039
16320
  return {
16040
16321
  title: "plan done",
16041
- output: `Plan completed and closed (done): ${relFrom(state.worktree, active.path)}. All ${args.checks.length} self-checks passed.`
16322
+ output: `Plan completed and closed (done): ${relFrom(state.worktree, active.path)}. All ${args.checks.length} self-checks passed. Terminal section "## ${closedAt} — closed" appended with the per-criterion verdicts.`
16042
16323
  };
16043
16324
  }
16044
16325
  });
16045
16326
  var planDiscardTool = tool({
16046
- description: "Abandon the session's active plan (draft/approved -> abandoned) and lift the draft write ban. Terminal: the file stays in .opencode/plan/ as history. Invoke via /plan discard or directly when the user cancels the task.",
16327
+ description: "Abandon the session's active plan (draft/approved -> abandoned) and lift the draft write ban. Pass `supersede` (the successor artifact's path or identifier, e.g. a .opencode/crew/ or .opencode/goal/ file) when the work is not ending but moving to another harness (crew orchestration, a goal contract): the plan then becomes superseded instead of abandoned, with the successor recorded. Either way a dated terminal section carrying the reason verbatim (a missing reason lands as `reason: (none given)`) is appended to the file in the SAME write as the status flip, and the file stays in .opencode/plan/ as history. Invoke via /plan discard or directly when the user cancels the task.",
16047
16328
  args: {
16048
- reason: tool.schema.string().optional().describe("Short reason recorded in the reply")
16329
+ reason: tool.schema.string().optional().describe("Short reason recorded in the reply and in the appended terminal section, verbatim"),
16330
+ supersede: tool.schema.string().optional().describe("Successor artifact's path or identifier (crew record, goal contract, ...); when present the plan becomes superseded instead of abandoned and this value is persisted as the successor reference")
16049
16331
  },
16050
- execute: async (_args, context) => {
16332
+ execute: async (args, context) => {
16051
16333
  const state = ensureSession(context.sessionID, worktreeFor(context));
16052
16334
  const active = resolveActivePlan(state);
16053
16335
  if (!active)
16054
16336
  throw new PlanError("No plan to abandon in this workspace.");
16055
- writeFileSync5(active.path, transitionStatus(readFileSync8(active.path, "utf8"), "abandoned", nowIso()));
16337
+ const successor = (args.supersede ?? "").trim();
16338
+ const superseding = successor.length > 0;
16339
+ const target = superseding ? "superseded" : "abandoned";
16340
+ const exitedAt = nowIso();
16341
+ const next = appendTerminalSection(transitionStatus(readFileSync8(active.path, "utf8"), target, exitedAt), superseding ? "superseded" : "abandoned", { reason: args.reason, ...superseding ? { successor } : {} }, exitedAt);
16342
+ writeFileSync5(active.path, next);
16056
16343
  state.planPath = undefined;
16057
- context.metadata({ title: `Plan abandoned: ${active.doc.goal}` });
16058
- return { title: "plan abandoned", output: `Plan abandoned: ${relFrom(state.worktree, active.path)}. Write operations are restored.` };
16344
+ const section = `## ${exitedAt} — ${target}`;
16345
+ context.metadata({ title: superseding ? `Plan superseded: ${active.doc.goal}` : `Plan abandoned: ${active.doc.goal}` });
16346
+ return {
16347
+ title: superseding ? "plan superseded" : "plan abandoned",
16348
+ output: superseding ? `Plan superseded (not abandoned): ${relFrom(state.worktree, active.path)}. Terminal section "${section}" appended (successor: ${successor}). Write operations are restored.` : `Plan abandoned: ${relFrom(state.worktree, active.path)}. Terminal section "${section}" appended. Write operations are restored.`
16349
+ };
16059
16350
  }
16060
16351
  });
16061
16352
  function goalDirOf(worktree) {
@@ -16065,7 +16356,7 @@ function readGoalDir(worktree) {
16065
16356
  const dir = goalDirOf(worktree);
16066
16357
  if (!existsSync3(dir))
16067
16358
  return [];
16068
- return readdirSync4(dir).filter((f) => f.endsWith(".md")).map((name) => ({ name, text: readFileSync8(join6(dir, name), "utf8") }));
16359
+ return readdirSync5(dir).filter((f) => f.endsWith(".md")).map((name) => ({ name, text: readFileSync8(join6(dir, name), "utf8") }));
16069
16360
  }
16070
16361
  function resolveSessionGoal(state) {
16071
16362
  if (state.goalPath && existsSync3(state.goalPath)) {
@@ -16174,7 +16465,7 @@ var goalWriteTool = tool({
16174
16465
  }
16175
16466
  const dir = goalDirOf(state.worktree);
16176
16467
  mkdirSync5(dir, { recursive: true });
16177
- const name = goalFileName(localDateNow(), slugifyGoal(args.goal), readdirSync4(dir).filter((f) => f.endsWith(".md")));
16468
+ const name = goalFileName(localDateNow(), slugifyGoal(args.goal), readdirSync5(dir).filter((f) => f.endsWith(".md")));
16178
16469
  const path = join6(dir, name);
16179
16470
  const text = renderGoal(input, {
16180
16471
  now,
@@ -16546,30 +16837,38 @@ var CREW_INIT_TEMPLATE = [
16546
16837
  "",
16547
16838
  "FAMILY GUARD (step 0, before anything else): if you do not have the crew_begin tool in your available tools, you are not in a forge session — STOP now and tell the user that /crew belongs to the forge agent, who should switch to forge (Tab) and rerun it there.",
16548
16839
  "",
16549
- "CREW IS NOT INITIALIZED: no forge subagents are configured (no usable forge.json agents). Refuse to enter orchestration and tell the user this feature needs a one-time setup, offering BOTH options:",
16840
+ "CREW IS NOT INITIALIZED: no forge subagents are materialized across the whole host anchor set (zero pools). Refuse to enter orchestration and tell the user this feature needs a one-time setup, offering BOTH options:",
16550
16841
  "",
16551
- "1. The user configures it themselves: create either layer — MERGED, the project layer overrides the global one per agent id (the project file is the nearest .opencode/forge.json walking up from the workspace root):",
16842
+ "1. The user configures it themselves: create either layer — MERGED, the project layer overrides the global one per agent id (the project file is the nearest .opencode/forge.json walking up from the workspace root; a .opencode/forge.json in any anchor subdirectory additionally forms a namespaced sub-pool family, its ids materializing as forge-<ns>-<id>):",
16552
16843
  "{paths}",
16553
16844
  "",
16554
- "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.",
16845
+ "Two axes, distinct: POOLS follow host directories (the host's anchor set); SESSION ARTIFACTS (plan/goal/crew records) follow the session. A forge.json in a directory no host anchor covers is not dispatchable on this host.",
16846
+ "",
16847
+ '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. For sub-pool files, ALSO include a proposed short stable `pool` namespace (a [a-z0-9-] word up to 24 chars — e.g. "aa" for a devAa specialist pool) so the materialized ids survive directory renames.',
16555
16848
  "",
16556
- "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).",
16849
+ "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 top level: `pool` — the file's namespace.",
16557
16850
  "{template}",
16558
16851
  "",
16559
- "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."
16852
+ "After the file is saved: file edits, creations, and deletions apply at the next config hook — NO host restart is needed. Only the anchor set itself is fixed at host start: host re-initializations add anchors and pools, they never remove them."
16560
16853
  ].join(`
16561
16854
  `);
16562
- function crewOrchestrationTemplate(agentIds) {
16855
+ function crewOrchestrationTemplate(resolution) {
16856
+ const families = resolution?.families ?? [];
16857
+ const familyLines = families.map((f) => {
16858
+ const label = f.primary ? "root pool (PRIMARY, plain ids)" : `pool ${f.ns} (forge-${f.ns}-*)`;
16859
+ return ` - ${label}: ${f.file} — ${f.materializedIds.map((id) => `forge-${id}`).join(", ") || "no agents"}`;
16860
+ });
16563
16861
  return [
16564
16862
  '(forge crew orchestration. Argument: "$ARGUMENTS")',
16565
16863
  "",
16566
16864
  "FAMILY GUARD (step 0, before anything else): if you do not have the crew_begin tool in your available tools, you are not in a forge session — STOP now and tell the user that /crew belongs to the forge agent, who should switch to forge (Tab) and rerun it there.",
16567
16865
  "",
16568
- `Configured forge subagents (task-tool vocabulary): ${agentIds.map((id) => `forge-${id}`).join(", ")}`,
16866
+ "Configured forge subagents (task-tool vocabulary), grouped by pool:",
16867
+ ...familyLines.length > 0 ? familyLines : [" (none)"],
16569
16868
  "",
16570
16869
  "You are entering CREW ORCHESTRATION discipline for this session (single subject: do not switch agents). The macro contract above the crew may come from an approved plan, a spec change, or inline text — the discipline is identical in all three cases and never requires a plan. Follow it exactly:",
16571
16870
  "",
16572
- "1. REGISTER: call `crew_begin` with the objective AND the declared subtask plan (subtasks[]: one {title} per subtask, optionally naming the intended forge-* agent). Reconnaissance BEFORE registration is free (task calls included). If crew_begin refuses (a crew is already active here, or a plan draft is active), STOP and tell the user why — never start a second crew. A wrong plan is fixed by abandoning and re-crewing (crew_close {abandon: true, reason}), never by improvising outside the declared plan.",
16871
+ '1. REGISTER: call `crew_begin` with the objective AND the declared subtask plan (subtasks[]: one {title} per subtask, optionally naming the intended forge-* agent). When the macro contract descends from a plan artifact, pass its path as the optional `lineage` argument (e.g. lineage: ".opencode/plan/2026-10-01-my-plan.md") — the descent is recorded as a first-class line in the crew record header; lineage is never inferred. Reconnaissance BEFORE registration is free (task calls included). If crew_begin refuses (a crew is already active here, or a plan draft is active), STOP and tell the user why — never start a second crew. A wrong plan is fixed by abandoning and re-crewing (crew_close {abandon: true, reason}), never by improvising outside the declared plan.',
16573
16872
  `2. THE PAUSE: registration enters a PENDING crew — execution has NOT started. STOP and present the user the three execution-mode choices (supervised waves now / convert to a goal contract / standby), then END YOUR TURN. Arm per the user's choice: waves → crew_begin {execution: "waves"}; conversion → crew_begin {execution: "goal"} followed by goal_write (arm=true — its dialog is the user's gate) folding the declared subtasks into criteria/checks; standby → no call. EXCEPTION: while a live active goal governs this session, register with execution: "waves" in the SAME call — the crew is born armed (the goal contract already authorized orchestration) and crew_close / abandon stay ask-free under that goal.`,
16574
16873
  "3. WAVES, NEVER FLOODS (armed crews only): execute subtasks in waves of parallel native `task` calls (subagent_type = the matching forge-* agent; a wave's results return within the calling turn). Launch the next wave ONLY after the previous wave's results are in. GUI / computer-use subtasks are mutually exclusive within a wave (the desktop is a singleton resource — at most ONE in flight; non-GUI subtasks still parallelize freely), and GUI walkthroughs use the `computer` tool rather than DIY shell screenshot pipelines.",
16575
16874
  "4. MISSING ROLE: when a subtask needs a role no configured forge-* agent matches, run that subtask through a native task call anyway AND tell the user about the gap — suggest configuring the missing role in forge.json.",
@@ -16587,32 +16886,37 @@ var crewBeginTool = tool({
16587
16886
  subtasks: tool.schema.array(tool.schema.object({
16588
16887
  title: tool.schema.string().describe("Short unique subtask title — the crew_close report must use these exact titles"),
16589
16888
  agent: tool.schema.string().optional().describe("Intended forge-* agent id for this subtask, when one matches")
16590
- })).describe("The declared subtask plan — decompose the objective into these before any execution; crew_close cross-checks the report against exactly this list")
16889
+ })).describe("The declared subtask plan — decompose the objective into these before any execution; crew_close cross-checks the report against exactly this list"),
16890
+ lineage: tool.schema.string().optional().describe("Free-text origin of the macro contract — e.g. the plan artifact path this crew descends from (.opencode/plan/<date>-<slug>.md). Recorded as a first-class lineage: line in the crew record header and disclosed in the registration output. Explicit argument only — never inferred from recently-terminal plans.")
16591
16891
  },
16592
16892
  execute: async (args, context) => {
16593
16893
  noteToolAgent(context);
16594
16894
  const state = stateForBan(context.sessionID);
16595
16895
  const active = state ? resolveActivePlan(state) : null;
16596
16896
  if (active && active.doc.status === "draft") {
16597
- throw new Error("[forge:crew] A plan is in draft; crew orchestration is denied during planning. Call plan_approve for user approval, or /plan discard to abandon the plan.");
16897
+ throw new Error("[forge:crew] A plan is in draft; crew orchestration is denied during planning. Three exits: " + "plan_approve puts the plan to user approval (an APPROVED plan does not block /crew — only a draft does); " + "/plan discard abandons the plan outright; " + '/plan discard {supersede: "<successor path or id>"} moves the work into this crew (the plan becomes superseded, the successor recorded).');
16598
16898
  }
16599
- const hostLoaded = forgeLoader?.load();
16600
- const hostIds = hostLoaded ? Object.keys(hostLoaded.agents) : [];
16899
+ const resolution = resolvePools();
16900
+ const hostIds = resolution ? Object.keys(resolution.agents) : [];
16601
16901
  const anchor = sessionAnchor(context);
16602
- const hostAnchorDir = forgeLoaderDir ?? "";
16603
- const sessionLoaded = anchor && anchor !== hostAnchorDir ? loaderForAnchor(anchor).load() : hostLoaded;
16902
+ const sessionLoaded = anchor ? loaderForAnchor(anchor).load() : null;
16604
16903
  const sessionProject = sessionLoaded?.projectPath ?? null;
16904
+ const foldPath = (p) => process.platform === "win32" ? p.replace(/\//g, "\\").toLowerCase() : p;
16905
+ const sessionCovered = sessionProject === null || (resolution?.families.some((f) => foldPath(f.file) === foldPath(sessionProject)) ?? false);
16605
16906
  if (hostIds.length === 0) {
16606
- const paths2 = forgeConfigPaths({ projectDir: hostAnchorDir || undefined, homeDir: process.env.FORGE_TEST_FORGE_HOME ?? homedir() });
16907
+ const primaryAnchorDir = hostAnchors[0] ?? hostWorktree;
16908
+ const anchorNote = hostAnchors.length > 1 ? `host anchors: ${hostAnchors.join(", ")}` : `launched from ${primaryAnchorDir || "an unresolvable directory"}`;
16909
+ const paths = forgeConfigPaths({ projectDir: primaryAnchorDir || undefined, homeDir: process.env.FORGE_TEST_FORGE_HOME ?? homedir() });
16607
16910
  const lines2 = [
16608
- "[forge:crew] CREW IS NOT INITIALIZED on this host: no forge subagents are materialized. Crew orchestration is unavailable until the user sets it up.",
16609
- `Two layers, MERGED (the project layer overrides the global one per agent id): ${paths2.project} (nearest .opencode/forge.json walking up from the workspace) OR ${paths2.global}.`,
16610
- 'Template (JSONC): {"agents": {"research": {"model": "provider/model", "thoughtLevel": "low", "prompt": "short role description"}}}',
16611
- "Agent ids are plain role words WITHOUT the `forge-` prefix — the plugin materializes each id as `forge-<id>` (a prefixed id is auto-stripped with a warning).",
16612
- "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) — without a prompt, custom ids get a generic one-liner and will not know their job. Newly added agents need a host restart; thoughtLevel edits apply without one."
16911
+ `[forge:crew] CREW IS NOT INITIALIZED on this host: no forge subagents are materialized across the whole anchor set (${anchorNote}). Crew orchestration is unavailable until the user sets it up.`,
16912
+ `Two layers, MERGED (the project layer overrides the global one per agent id): ${paths.project} (nearest .opencode/forge.json walking up from the workspace) OR ${paths.global}. Sub-pool files (.opencode/forge.json in an anchor's subdirectories) contribute additional namespaced families.`,
16913
+ "Two axes, distinct: POOLS follow host directories (the anchor set above); SESSION ARTIFACTS (plan/goal/crew records) follow the session. A forge.json in an unanchored directory is not dispatchable on this host.",
16914
+ `Template (JSONC): top-level optional "pool" names this file's namespace (short stable [a-z0-9-] up to 24 chars — sub-pool ids materialize as forge-<ns>-<id>); {"agents": {"research": {"model": "provider/model", "thoughtLevel": "low", "prompt": "short role description"}}}`,
16915
+ "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.",
16916
+ "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), and INCLUDE a proposed short stable `pool` namespace for sub-pool files so ids survive directory renames. File edits, creations, and deletions apply at the next config hook — no host restart; only the anchor set itself is fixed at host start (re-initializations add pools, never remove)."
16613
16917
  ];
16614
- if (sessionLoaded && Object.keys(sessionLoaded.agents).length > 0 && sessionProject) {
16615
- lines2.push(`NOTE: this session's workspace DOES have a forge.json (${sessionProject}) with usable agents — but this host instance was launched from ${hostAnchorDir || "a different directory"}, so they are not in the task vocabulary. Relaunch opencode in that workspace, or add the agents to the global layer.`);
16918
+ if (sessionLoaded && Object.keys(sessionLoaded.agents).length > 0 && sessionProject && !sessionCovered) {
16919
+ lines2.push(`NOTE: this session's workspace DOES have a forge.json (${sessionProject}) with usable agents — but NO host anchor covers it (${anchorNote}). A host initialization in that workspace adds its pools; or fold the agents into the global layer.`);
16616
16920
  }
16617
16921
  throw new Error(lines2.join(`
16618
16922
  `));
@@ -16692,13 +16996,14 @@ var crewBeginTool = tool({
16692
16996
  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).');
16693
16997
  }
16694
16998
  const governed = execution === "waves";
16999
+ const lineage = String(args.lineage ?? "").trim();
16695
17000
  const anchorDir = anchor || hostWorktree;
16696
17001
  let recordPath = null;
16697
17002
  let recordNote = "";
16698
17003
  if (anchorDir) {
16699
17004
  try {
16700
17005
  recordPath = crewRecordPathFor(anchorDir, objectiveText);
16701
- writeCrewRecord(recordPath, objectiveText, anchorDir, subtasks);
17006
+ writeCrewRecord(recordPath, objectiveText, anchorDir, subtasks, lineage || undefined);
16702
17007
  recordNote = `Crew record (inspectable history; NOT a resume mechanism): ${relFrom(anchorDir, recordPath)}`;
16703
17008
  } catch {
16704
17009
  recordPath = null;
@@ -16708,21 +17013,29 @@ var crewBeginTool = tool({
16708
17013
  crews.set(context.sessionID, { objective: objectiveText, startedAt: nowIso(), subtasks, mode: governed ? "executing" : "pending", recordPath });
16709
17014
  appendCrewRecord(recordPath, governed ? "registered (born armed under a governing goal)" : "registered (pending)", subtasks.map((s, i) => `${i + 1}. ${s.title}${s.agent ? ` → ${s.agent}` : ""}`));
16710
17015
  let disclosure = "";
16711
- if (sessionProject && sessionProject !== (hostLoaded?.projectPath ?? null)) {
16712
- const localOnly = Object.keys(sessionLoaded?.agents ?? {}).filter((id) => !hostIds.includes(id));
17016
+ if (sessionProject && !sessionCovered) {
17017
+ const localOnly = Object.keys(sessionLoaded?.agents ?? {});
16713
17018
  if (localOnly.length > 0) {
16714
17019
  disclosure = `
16715
- [forge:crew] NOTE: this session's workspace has its own forge.json (${sessionProject}) defining ${localOnly.map((id) => `forge-${id}`).join(", ")} — NOT dispatchable on this host (launched from ${hostAnchorDir || "a different directory"}). Relaunch opencode in that workspace to use them, or fold them into the global layer.`;
17020
+ [forge:crew] NOTE: this session's workspace has its own forge.json (${sessionProject}) defining ${localOnly.map((id) => `forge-<ns>-${id} (namespaced by its pool)`).join(", ")} — NOT dispatchable on this host (no anchor covers it; anchors: ${hostAnchors.join(", ") || "none"}). A host initialization in that workspace adds its pools, or fold the agents into the global layer.`;
16716
17021
  }
16717
17022
  }
16718
- const paths = forgeConfigPaths({ projectDir: hostAnchorDir || undefined, homeDir: process.env.FORGE_TEST_FORGE_HOME ?? homedir() });
16719
- const originDesc = hostLoaded?.projectPath ? `the project layer ${hostLoaded.projectPath}${hostLoaded.source === "project+global" ? ` merged with the global layer (${paths.global})` : ""}` : `the global layer (${paths.global})`;
17023
+ const familyLines = [];
17024
+ const families = resolution?.families ?? [];
17025
+ for (const f of families.slice(0, 8)) {
17026
+ const label = f.primary ? "root pool (PRIMARY, plain ids)" : `pool ${f.ns} (forge-${f.ns}-*)`;
17027
+ familyLines.push(` - ${label}: ${f.file} — ${f.materializedIds.map((id) => `forge-${id}`).join(", ") || "no agents"}`);
17028
+ }
17029
+ if (families.length > 8)
17030
+ familyLines.push(` - …and ${families.length - 8} more pools`);
16720
17031
  const lines = [
17032
+ ...recordNote ? [recordNote] : [],
17033
+ ...lineage ? [`Lineage: ${lineage}`] : [],
16721
17034
  governed ? `[forge:crew] Crew registered UNDER THE GOVERNING GOAL — born armed, never pends. Objective: ${objectiveText}` : `[forge:crew] Crew registered PENDING for this session (execution has NOT started). Objective: ${objectiveText}`,
16722
17035
  `Declared plan (${subtasks.length}):`,
16723
17036
  ...subtasks.map((s, i) => ` ${i + 1}. ${s.title}${s.agent ? ` → ${s.agent}` : ""}`),
16724
- `Dispatchable roster origin: ${originDesc} — materialized by this host instance${hostAnchorDir ? ` (launched from ${hostAnchorDir})` : ""}.`,
16725
- ...recordNote ? [recordNote] : [],
17037
+ `Dispatchable roster origin — ${families.length} pool${families.length === 1 ? "" : "s"} across the host anchor set${hostAnchors.length > 0 ? ` (anchors: ${hostAnchors.join(", ")})` : ""}:`,
17038
+ ...familyLines,
16726
17039
  ...disclosure ? [disclosure] : []
16727
17040
  ];
16728
17041
  if (governed) {
@@ -17040,7 +17353,9 @@ function scheduleIdleContinuation(client, sessionID) {
17040
17353
  }, IDLE_DEBOUNCE_MS));
17041
17354
  }
17042
17355
  var server = async (input, options) => {
17043
- hostWorktree = effectiveWorktree(input.worktree, input.directory) || input.directory || "";
17356
+ const effectiveInitDir = effectiveWorktree(input.worktree, input.directory) || input.directory || "";
17357
+ hostWorktree = effectiveInitDir;
17358
+ rememberHostAnchor(effectiveInitDir);
17044
17359
  const client = input.client;
17045
17360
  jobClient = input.client;
17046
17361
  const jobsOpts = options?.jobs;
@@ -17151,23 +17466,17 @@ var server = async (input, options) => {
17151
17466
  if (permSection[gateKey] !== "deny")
17152
17467
  permSection[gateKey] = "ask";
17153
17468
  }
17154
- if (!forgeLoader || forgeLoaderDir !== hostWorktree) {
17155
- forgeLoader = createForgeConfigLoader({
17156
- projectDir: hostWorktree,
17157
- homeDir: process.env.FORGE_TEST_FORGE_HOME ?? homedir()
17158
- });
17159
- forgeLoaderDir = hostWorktree;
17160
- }
17161
- const loaded = forgeLoader.load();
17162
- for (const finding of loaded.findings) {
17469
+ const resolution = resolvePools();
17470
+ poolResolution = resolution;
17471
+ for (const finding of resolution?.findings ?? []) {
17163
17472
  console.warn(`[forge:config] ${finding.level}: ${finding.message}`);
17164
17473
  }
17165
17474
  materializedSnapshot = {};
17166
- for (const [agentId, def] of Object.entries(loaded.agents)) {
17167
- const id = `forge-${agentId}`;
17475
+ for (const [matId, agent] of Object.entries(resolution?.agents ?? {})) {
17476
+ const id = `forge-${matId}`;
17168
17477
  if (agentSection[id] !== undefined)
17169
17478
  continue;
17170
- const entry = forgeAgentDef(agentId, def);
17479
+ const entry = forgeAgentDef(matId, agent.def);
17171
17480
  agentSection[id] = entry;
17172
17481
  materializedSnapshot[id] = entry;
17173
17482
  }
@@ -17212,12 +17521,13 @@ var server = async (input, options) => {
17212
17521
  cfg.command ??= {};
17213
17522
  const existingCrew = cfg.command["crew"];
17214
17523
  if (!existingCrew || String(existingCrew.template ?? "").startsWith("(forge crew")) {
17215
- const agentIds = Object.keys(loaded.agents);
17216
- const template = agentIds.length > 0 ? crewOrchestrationTemplate(agentIds) : CREW_INIT_TEMPLATE.replaceAll("{paths}", ` - ${forgeConfigPaths({ projectDir: forgeLoaderDir ?? undefined, homeDir: process.env.FORGE_TEST_FORGE_HOME ?? homedir() }).project}
17217
- - ${forgeConfigPaths({ projectDir: forgeLoaderDir ?? undefined, homeDir: process.env.FORGE_TEST_FORGE_HOME ?? homedir() }).global}`).replaceAll("{template}", JSON.stringify({ agents: { research: { model: "provider/model", thoughtLevel: "low", prompt: "short role description — what this agent does" } } }, null, 2).split(`
17524
+ const agentCount = Object.keys(resolution?.agents ?? {}).length;
17525
+ const primaryAnchorDir = hostAnchors[0] ?? undefined;
17526
+ const template = agentCount > 0 ? crewOrchestrationTemplate(resolution ?? null) : CREW_INIT_TEMPLATE.replaceAll("{paths}", ` - ${forgeConfigPaths({ projectDir: primaryAnchorDir, homeDir: process.env.FORGE_TEST_FORGE_HOME ?? homedir() }).project}
17527
+ - ${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 — only meaningful for sub-pool files)", agents: { research: { model: "provider/model", thoughtLevel: "low", prompt: "short role description — what this agent does" } } }, null, 2).split(`
17218
17528
  `).map((l) => " " + l).join(`
17219
17529
  `));
17220
- const description = agentIds.length > 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";
17530
+ 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";
17221
17531
  cfg.command["crew"] = { template, description };
17222
17532
  crewCommandSnapshot = { template, description };
17223
17533
  }
@@ -17320,7 +17630,7 @@ var server = async (input, options) => {
17320
17630
  return;
17321
17631
  }
17322
17632
  const agentId = agentName.slice("forge-".length);
17323
- const def = forgeLoader?.load().agents[agentId];
17633
+ const def = resolvePools()?.agents[agentId]?.def;
17324
17634
  if (!def?.thoughtLevel)
17325
17635
  return;
17326
17636
  const providerID = typeof raw.model?.providerID === "string" ? raw.model.providerID : "";
@@ -17348,7 +17658,14 @@ var server = async (input, options) => {
17348
17658
  modelsDevCatalog = c;
17349
17659
  },
17350
17660
  crewCommand: () => crewCommandSnapshot,
17351
- materialized: () => materializedSnapshot
17661
+ materialized: () => materializedSnapshot,
17662
+ resetAnchors: () => {
17663
+ hostAnchors.length = 0;
17664
+ poolResolver = null;
17665
+ poolResolution = null;
17666
+ },
17667
+ anchors: () => [...hostAnchors],
17668
+ resolveNow: () => resolvePools()
17352
17669
  },
17353
17670
  "permission.ask": async (input2, output) => {
17354
17671
  const meta = input2.metadata ?? {};
@@ -17514,20 +17831,20 @@ async function v2Setup(ctx) {
17514
17831
  });
17515
17832
  }
17516
17833
  try {
17517
- const loader = createForgeConfigLoader({ projectDir: process.cwd(), homeDir: process.env.FORGE_TEST_FORGE_HOME ?? homedir() });
17518
- const loaded = loader.load();
17519
- for (const [agentId, def] of Object.entries(loaded.agents)) {
17520
- const id = `forge-${agentId}`;
17834
+ const resolver = createPoolResolver({ homeDir: process.env.FORGE_TEST_FORGE_HOME ?? homedir() });
17835
+ const resolution = resolver.resolve([process.cwd()]);
17836
+ for (const [matId, agent] of Object.entries(resolution.agents)) {
17837
+ const id = `forge-${matId}`;
17521
17838
  if (draft.get(id) !== undefined)
17522
17839
  continue;
17523
- const v1def = forgeAgentDef(agentId, def);
17524
- draft.update(id, (agent) => {
17525
- agent.description = v1def.description;
17526
- agent.system = v1def.prompt;
17527
- agent.mode = v1def.mode;
17840
+ const v1def = forgeAgentDef(matId, agent.def);
17841
+ draft.update(id, (agentDraft) => {
17842
+ agentDraft.description = v1def.description;
17843
+ agentDraft.system = v1def.prompt;
17844
+ agentDraft.mode = v1def.mode;
17528
17845
  if (v1def.model !== undefined)
17529
- agent.model = v1def.model;
17530
- agent.permission = v1def.permission;
17846
+ agentDraft.model = v1def.model;
17847
+ agentDraft.permission = v1def.permission;
17531
17848
  });
17532
17849
  }
17533
17850
  } catch {}
package/package.json CHANGED
@@ -1,64 +1,64 @@
1
- {
2
- "name": "@sorenllm/opencode-forge",
3
- "version": "0.12.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.14.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
+ }