@sorenllm/opencode-forge 0.12.0 → 0.13.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.
- package/README.md +68 -28
- package/dist/index.js +355 -83
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -172,7 +172,7 @@ What this plugin touches, exhaustively:
|
|
|
172
172
|
| `<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
173
|
| `<tmp>/opencode-forge/watchdog/log.jsonl` | watchdog interventions ledger (bounded: 200 entries, oldest rotated) | runtime debris — delete freely, also after uninstall |
|
|
174
174
|
| `<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
|
-
|
|
|
175
|
+
| `.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
176
|
| `~/.cache/opencode/packages/...` | installed package copy | written by the `opencode plugin` installer, not the plugin |
|
|
177
177
|
| `~/.config/opencode/opencode.json` | `plugin` array entry | written by the installer |
|
|
178
178
|
|
|
@@ -418,26 +418,60 @@ discovered by walking up:
|
|
|
418
418
|
|
|
419
419
|
The two layers **merge**: agents combine, and on an id collision the project
|
|
420
420
|
definition wins wholesale (one definition per id — no cross-layer field
|
|
421
|
-
blending). A broken file empties only its own layer (error finding with
|
|
422
|
-
parse location);
|
|
423
|
-
edits, and files appearing or disappearing
|
|
424
|
-
|
|
425
|
-
|
|
421
|
+
blending). A broken file empties only its own layer/pool (error finding with
|
|
422
|
+
the parse location); everything else still applies. Both layers hot-apply —
|
|
423
|
+
edits, and files appearing or disappearing, take effect at the next config
|
|
424
|
+
hook without a host restart (see the apply-timing table below).
|
|
425
|
+
|
|
426
|
+
**Hierarchical pools on a shared host.** Agent frontends (paseo and friends)
|
|
427
|
+
routinely share ONE opencode host process across projects, re-initializing
|
|
428
|
+
plugins with different directories. The plugin therefore keeps an
|
|
429
|
+
**append-only anchor set**: every host initialization appends its directory
|
|
430
|
+
(first one = the *primary anchor*), and pools resolve from the union —
|
|
431
|
+
anchor flips can only ever ADD pools, never remove them. Per anchor the
|
|
432
|
+
family is:
|
|
433
|
+
|
|
434
|
+
- the **root pool** — the nearest forge.json walking up from that anchor
|
|
435
|
+
(merged over the global layer per agent id);
|
|
436
|
+
- every **sub-pool** — a `.opencode/forge.json` anywhere in the anchor's
|
|
437
|
+
subdirectories, discovered by a bounded breadth-first scan (lexicographic
|
|
438
|
+
sibling order; dependency dirs like `node_modules`/`.git`/`dist` are
|
|
439
|
+
skipped; 2,000-directory budget per anchor).
|
|
440
|
+
|
|
441
|
+
The **primary anchor's root pool keeps plain `forge-<id>` ids** (a
|
|
442
|
+
single-project host is unchanged); every other family materializes
|
|
443
|
+
**namespaced** as `forge-<ns>-<id>`. `ns` is the file's optional `pool`
|
|
444
|
+
field, else the sanitized directory name. Pool identity is the absolute file
|
|
445
|
+
path — the same file reached through two anchors materializes exactly once;
|
|
446
|
+
namespace and materialized-id collisions resolve deterministically with an
|
|
447
|
+
error finding, never by silent shadowing. The `/crew` roster and the
|
|
448
|
+
`crew_begin` registration output list the dispatchable agents **grouped by
|
|
449
|
+
pool** with each pool's origin path, the primary marked.
|
|
450
|
+
|
|
451
|
+
| axis | follows | survives restart? |
|
|
452
|
+
| --- | --- | --- |
|
|
453
|
+
| pools (the `forge-*` vocabulary) | host directories — the anchor set | the set re-seeds from the spawn anchor only |
|
|
454
|
+
| session artifacts (plan/goal/crew records) | the session's own workspace | files on disk are the source of truth |
|
|
426
455
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
456
|
+
A forge.json in a directory NO host anchor covers is NOT silently ignored:
|
|
457
|
+
`/crew` names it as the residual mismatch and tells you a host
|
|
458
|
+
initialization in that workspace adds its pools (or fold the agents into the
|
|
459
|
+
global layer).
|
|
431
460
|
|
|
432
461
|
**Id naming**: use plain role words (`research`, `coder`, …) — the plugin
|
|
433
|
-
materializes every id as `forge-<id>` itself
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
462
|
+
materializes every id as `forge-<id>` itself (namespaced pools:
|
|
463
|
+
`forge-<ns>-<id>`). A `forge-`-prefixed id in the file would double up
|
|
464
|
+
(`forge-coder` → agent `forge-forge-coder`); the loader auto-strips the
|
|
465
|
+
prefix (all repetitions) with a warning so existing files keep working, but
|
|
466
|
+
rename the entries to silence it. The namespace infix is never parsed back
|
|
467
|
+
out of a materialized id.
|
|
437
468
|
|
|
438
469
|
```jsonc
|
|
439
470
|
{
|
|
440
|
-
//
|
|
471
|
+
// sub-pool files only: namespace for this family's ids (forge-<ns>-<id>).
|
|
472
|
+
// Short, stable, [a-z0-9-] up to 24 chars — ids survive directory renames.
|
|
473
|
+
// Invalid values degrade to a warning + the directory-derived namespace.
|
|
474
|
+
// "pool": "aa",
|
|
441
475
|
"agents": {
|
|
442
476
|
// PINNED worker: model + thoughtLevel form an ATOMIC PAIR — set BOTH
|
|
443
477
|
"research": {
|
|
@@ -497,12 +531,14 @@ A rejected entry costs only itself — its siblings materialize normally.
|
|
|
497
531
|
> error finding naming the missing half. Fix per entry: add the missing
|
|
498
532
|
> `thoughtLevel`, or delete `model` to make it an Auto worker.
|
|
499
533
|
|
|
500
|
-
**What applies when** —
|
|
534
|
+
**What applies when** — file truth is hot; only the anchor set is frozen at
|
|
535
|
+
host start:
|
|
501
536
|
|
|
502
537
|
| change | needs a host restart? |
|
|
503
538
|
| --- | --- |
|
|
504
539
|
| `thoughtLevel` on an existing agent | no (applies to NEW sessions of that agent; running sessions keep their frozen depth) |
|
|
505
|
-
| agent added / removed, or
|
|
540
|
+
| agent added / removed / edited, any pool file created or deleted | no restart — applies at the next config hook (materialization re-runs there; the crew gate and depth lookups always resolve fresh) |
|
|
541
|
+
| a NEW directory joining the host (another project's session on the shared host) | no restart — the re-initialization itself adds the anchor and its pools |
|
|
506
542
|
|
|
507
543
|
### Dispatching
|
|
508
544
|
|
|
@@ -545,10 +581,11 @@ requires a plan. The flow:
|
|
|
545
581
|
1. **Register** the declared plan: `crew_begin {objective, subtasks}` — one
|
|
546
582
|
titled subtask each, optionally naming the intended `forge-*` agent.
|
|
547
583
|
Registration enters a **PENDING** crew: execution has not started. The
|
|
548
|
-
registration output confirms the roster **with its
|
|
549
|
-
forge.json
|
|
550
|
-
|
|
551
|
-
|
|
584
|
+
registration output confirms the roster **grouped by pool with its
|
|
585
|
+
origins** (which forge.json families the dispatchable set materialized
|
|
586
|
+
from, the primary marked), points at the crew record file, presents the
|
|
587
|
+
three execution-mode choices, and stops the turn. Reconnaissance task
|
|
588
|
+
calls are free BEFORE registration.
|
|
552
589
|
2. **The pause is mechanical**: while the crew pends, every `task` dispatch is
|
|
553
590
|
refused by an interception belt. The user chooses:
|
|
554
591
|
- **supervised waves now** — `crew_begin {execution: "waves"}` lifts the
|
|
@@ -601,13 +638,16 @@ itself (a rootish guard keeps degenerate global-project worktrees from
|
|
|
601
638
|
landing files on the drive root or the launch anchor). The anchor follows the
|
|
602
639
|
session's workspace, never the objective's scope.
|
|
603
640
|
|
|
604
|
-
**Not initialized?** `/crew` on a host with
|
|
605
|
-
with one-time setup guidance: the two file paths, a
|
|
606
|
-
the choice of configuring it yourself OR having the
|
|
607
|
-
latter only on your explicit go-ahead in that
|
|
608
|
-
normal visible write path
|
|
609
|
-
|
|
610
|
-
|
|
641
|
+
**Not initialized?** `/crew` on a host with ZERO pools across its whole
|
|
642
|
+
anchor set refuses with one-time setup guidance: the two file paths, a
|
|
643
|
+
copy-paste template, and the choice of configuring it yourself OR having the
|
|
644
|
+
session AI do it — the latter only on your explicit go-ahead in that
|
|
645
|
+
conversation, through the normal visible write path (when the AI writes a
|
|
646
|
+
sub-pool file it also proposes a short stable `pool` namespace so ids
|
|
647
|
+
survive directory renames). This is the plugin's only configuration
|
|
648
|
+
invitation. Saved files apply at the next config hook — no restart needed;
|
|
649
|
+
on a shared host, opening a session in a new workspace adds that anchor and
|
|
650
|
+
its pools automatically.
|
|
611
651
|
|
|
612
652
|
### Debris note
|
|
613
653
|
|
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
|
|
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
|
|
|
@@ -15104,7 +15104,7 @@ function resolvePinnedDepth(thoughtLevel, family, ladder) {
|
|
|
15104
15104
|
}
|
|
15105
15105
|
|
|
15106
15106
|
// src/forge-config.ts
|
|
15107
|
-
import { readFileSync as readFileSync7, statSync as statSync4 } from "node:fs";
|
|
15107
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync7, statSync as statSync4 } from "node:fs";
|
|
15108
15108
|
function sanitizeJsonc(text) {
|
|
15109
15109
|
const out = [];
|
|
15110
15110
|
let i = 0;
|
|
@@ -15456,6 +15456,25 @@ function parentDirOf(p) {
|
|
|
15456
15456
|
const parent = s.slice(0, i + 1);
|
|
15457
15457
|
return parent === "" ? null : parent;
|
|
15458
15458
|
}
|
|
15459
|
+
function forgeCandidateAt(dir) {
|
|
15460
|
+
const sep2 = dir.includes("\\") && !dir.includes("/") ? "\\" : "/";
|
|
15461
|
+
const base = dir.endsWith("/") || dir.endsWith("\\") ? dir : `${dir}${sep2}`;
|
|
15462
|
+
return `${base}.opencode${sep2}forge.json`;
|
|
15463
|
+
}
|
|
15464
|
+
function discoverProjectFileWith(stat, anchor) {
|
|
15465
|
+
let dir = (anchor ?? "").trim();
|
|
15466
|
+
if (!dir)
|
|
15467
|
+
return null;
|
|
15468
|
+
for (;; ) {
|
|
15469
|
+
const candidate = forgeCandidateAt(dir);
|
|
15470
|
+
if (stat(candidate) !== null)
|
|
15471
|
+
return candidate;
|
|
15472
|
+
const parent = parentDirOf(dir);
|
|
15473
|
+
if (parent === null || parent === dir)
|
|
15474
|
+
return null;
|
|
15475
|
+
dir = parent;
|
|
15476
|
+
}
|
|
15477
|
+
}
|
|
15459
15478
|
function createForgeConfigLoader(deps) {
|
|
15460
15479
|
const stat = deps.stat ?? ((path) => {
|
|
15461
15480
|
try {
|
|
@@ -15512,28 +15531,9 @@ function createForgeConfigLoader(deps) {
|
|
|
15512
15531
|
cache.set(path, { mtimeMs: st.mtimeMs, size: st.size, result });
|
|
15513
15532
|
return result;
|
|
15514
15533
|
};
|
|
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
15534
|
return {
|
|
15535
15535
|
load() {
|
|
15536
|
-
const projectPath =
|
|
15536
|
+
const projectPath = discoverProjectFileWith(stat, deps.projectDir ?? "");
|
|
15537
15537
|
const project = projectPath ? readCandidate(projectPath, "project") : null;
|
|
15538
15538
|
const global = globalPath ? readCandidate(globalPath, "global") : null;
|
|
15539
15539
|
if (project && global) {
|
|
@@ -15553,6 +15553,228 @@ function createForgeConfigLoader(deps) {
|
|
|
15553
15553
|
}
|
|
15554
15554
|
};
|
|
15555
15555
|
}
|
|
15556
|
+
var POOL_SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build", "out", "target", ".cache", "venv", ".venv", "__pycache__", "coverage"]);
|
|
15557
|
+
var POOL_SCAN_BUDGET = 2000;
|
|
15558
|
+
function sanitizeNamespace(name) {
|
|
15559
|
+
const collapsed = name.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
15560
|
+
return collapsed || "pool";
|
|
15561
|
+
}
|
|
15562
|
+
function validatePoolField(v) {
|
|
15563
|
+
if (typeof v !== "string")
|
|
15564
|
+
return { ok: false };
|
|
15565
|
+
const trimmed = v.trim();
|
|
15566
|
+
if (trimmed.length < 1 || trimmed.length > 24 || !/^[a-z0-9-]+$/.test(trimmed))
|
|
15567
|
+
return { ok: false };
|
|
15568
|
+
return { ok: true, ns: trimmed };
|
|
15569
|
+
}
|
|
15570
|
+
function dirBasename(dir) {
|
|
15571
|
+
let s = dir;
|
|
15572
|
+
while (s.length > 1 && (s.endsWith("/") || s.endsWith("\\")))
|
|
15573
|
+
s = s.slice(0, -1);
|
|
15574
|
+
const i = Math.max(s.lastIndexOf("/"), s.lastIndexOf("\\"));
|
|
15575
|
+
return i < 0 ? s : s.slice(i + 1);
|
|
15576
|
+
}
|
|
15577
|
+
function codepointCompare(a, b) {
|
|
15578
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
15579
|
+
}
|
|
15580
|
+
function createPoolResolver(deps) {
|
|
15581
|
+
const stat = deps.stat ?? ((path) => {
|
|
15582
|
+
try {
|
|
15583
|
+
const s = statSync4(path);
|
|
15584
|
+
return { mtimeMs: s.mtimeMs, size: s.size };
|
|
15585
|
+
} catch {
|
|
15586
|
+
return null;
|
|
15587
|
+
}
|
|
15588
|
+
});
|
|
15589
|
+
const readFile = deps.readFile ?? ((path) => readFileSync7(path, "utf8"));
|
|
15590
|
+
const readdir = deps.readdir ?? ((dir) => {
|
|
15591
|
+
try {
|
|
15592
|
+
return readdirSync4(dir, { withFileTypes: true }).map((d) => ({ name: d.name, dir: d.isDirectory() }));
|
|
15593
|
+
} catch {
|
|
15594
|
+
return null;
|
|
15595
|
+
}
|
|
15596
|
+
});
|
|
15597
|
+
const budget = typeof deps.scanBudget === "number" && deps.scanBudget > 0 ? deps.scanBudget : POOL_SCAN_BUDGET;
|
|
15598
|
+
const globalPath = forgeConfigPaths({ homeDir: deps.homeDir }).global;
|
|
15599
|
+
const fileCache = new Map;
|
|
15600
|
+
const readPool = (path) => {
|
|
15601
|
+
const st = stat(path);
|
|
15602
|
+
if (st === null) {
|
|
15603
|
+
fileCache.delete(path);
|
|
15604
|
+
return null;
|
|
15605
|
+
}
|
|
15606
|
+
const hit = fileCache.get(path);
|
|
15607
|
+
if (hit && hit.mtimeMs === st.mtimeMs && hit.size === st.size)
|
|
15608
|
+
return hit.parsed;
|
|
15609
|
+
let parsed = null;
|
|
15610
|
+
const text = readFile(path);
|
|
15611
|
+
const doc2 = parseForgeJsonc(text);
|
|
15612
|
+
if (!doc2.ok) {
|
|
15613
|
+
parsed = {
|
|
15614
|
+
agents: {},
|
|
15615
|
+
declaredPool: null,
|
|
15616
|
+
findings: [
|
|
15617
|
+
{
|
|
15618
|
+
level: "error",
|
|
15619
|
+
code: "config-parse-error",
|
|
15620
|
+
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`
|
|
15621
|
+
}
|
|
15622
|
+
]
|
|
15623
|
+
};
|
|
15624
|
+
} else if (doc2.config !== undefined && !isRecord(doc2.config)) {
|
|
15625
|
+
parsed = { agents: {}, declaredPool: null, findings: [{ level: "error", code: "config-not-object", message: `${path}: top level must be an object — this pool contributes no agents` }] };
|
|
15626
|
+
} else {
|
|
15627
|
+
const top = doc2.config === undefined ? undefined : doc2.config;
|
|
15628
|
+
const validated = validateAgentSet(top === undefined ? undefined : top.agents);
|
|
15629
|
+
parsed = { agents: validated.agents, declaredPool: top === undefined ? null : top.pool, findings: validated.findings };
|
|
15630
|
+
}
|
|
15631
|
+
fileCache.set(path, { mtimeMs: st.mtimeMs, size: st.size, parsed });
|
|
15632
|
+
return parsed;
|
|
15633
|
+
};
|
|
15634
|
+
const dirCache = new Map;
|
|
15635
|
+
const listDir = (dir) => {
|
|
15636
|
+
const st = stat(dir);
|
|
15637
|
+
if (st === null) {
|
|
15638
|
+
dirCache.delete(dir);
|
|
15639
|
+
return null;
|
|
15640
|
+
}
|
|
15641
|
+
const hit = dirCache.get(dir);
|
|
15642
|
+
if (hit && hit.mtimeMs === st.mtimeMs)
|
|
15643
|
+
return hit;
|
|
15644
|
+
const listing = readdir(dir);
|
|
15645
|
+
const children = listing === null ? null : [...listing].sort((a, b) => codepointCompare(a.name, b.name));
|
|
15646
|
+
let poolFile = null;
|
|
15647
|
+
const candidate = forgeCandidateAt(dir);
|
|
15648
|
+
if (stat(candidate) !== null)
|
|
15649
|
+
poolFile = candidate;
|
|
15650
|
+
const entry = { mtimeMs: st.mtimeMs, children, poolFile };
|
|
15651
|
+
dirCache.set(dir, entry);
|
|
15652
|
+
return entry;
|
|
15653
|
+
};
|
|
15654
|
+
const scanSubtree = (anchor) => {
|
|
15655
|
+
const files = [];
|
|
15656
|
+
const seen = new Set;
|
|
15657
|
+
let opened = 0;
|
|
15658
|
+
const sep2 = anchor.includes("\\") && !anchor.includes("/") ? "\\" : "/";
|
|
15659
|
+
const joinDir = (parent, name) => parent.endsWith("/") || parent.endsWith("\\") ? `${parent}${name}` : `${parent}${sep2}${name}`;
|
|
15660
|
+
const queue = [anchor];
|
|
15661
|
+
while (queue.length > 0 && opened < budget) {
|
|
15662
|
+
const dir = queue.shift();
|
|
15663
|
+
if (seen.has(dir))
|
|
15664
|
+
continue;
|
|
15665
|
+
seen.add(dir);
|
|
15666
|
+
const entry = listDir(dir);
|
|
15667
|
+
opened++;
|
|
15668
|
+
if (entry === null || entry.children === null)
|
|
15669
|
+
continue;
|
|
15670
|
+
const isAnchor = dir === anchor;
|
|
15671
|
+
if (!isAnchor && entry.poolFile !== null)
|
|
15672
|
+
files.push(entry.poolFile);
|
|
15673
|
+
for (const child of entry.children) {
|
|
15674
|
+
if (!child.dir)
|
|
15675
|
+
continue;
|
|
15676
|
+
if (POOL_SKIP_DIRS.has(child.name) || child.name === ".opencode")
|
|
15677
|
+
continue;
|
|
15678
|
+
queue.push(joinDir(dir, child.name));
|
|
15679
|
+
}
|
|
15680
|
+
}
|
|
15681
|
+
return files;
|
|
15682
|
+
};
|
|
15683
|
+
return {
|
|
15684
|
+
resolve(anchorsInput) {
|
|
15685
|
+
const anchors = [];
|
|
15686
|
+
for (const a of anchorsInput) {
|
|
15687
|
+
const t = (a ?? "").trim();
|
|
15688
|
+
if (t && !anchors.includes(t))
|
|
15689
|
+
anchors.push(t);
|
|
15690
|
+
}
|
|
15691
|
+
const findings = [];
|
|
15692
|
+
const globalParsed = globalPath ? readPool(globalPath) : null;
|
|
15693
|
+
if (globalParsed)
|
|
15694
|
+
findings.push(...globalParsed.findings);
|
|
15695
|
+
const primaryRootFile = anchors.length > 0 ? discoverProjectFileWith(stat, anchors[0]) : null;
|
|
15696
|
+
const uniqueFiles = new Set;
|
|
15697
|
+
if (primaryRootFile)
|
|
15698
|
+
uniqueFiles.add(primaryRootFile);
|
|
15699
|
+
for (const anchor of anchors) {
|
|
15700
|
+
for (const f of scanSubtree(anchor))
|
|
15701
|
+
uniqueFiles.add(f);
|
|
15702
|
+
if (anchor !== anchors[0]) {
|
|
15703
|
+
const rootFile = discoverProjectFileWith(stat, anchor);
|
|
15704
|
+
if (rootFile)
|
|
15705
|
+
uniqueFiles.add(rootFile);
|
|
15706
|
+
}
|
|
15707
|
+
}
|
|
15708
|
+
const plainPoolFile = primaryRootFile ?? (globalParsed !== null && globalPath ? globalPath : null);
|
|
15709
|
+
const orderedFiles = [plainPoolFile, ...[...uniqueFiles].filter((f) => f !== plainPoolFile).sort(codepointCompare)].filter((f) => f !== null);
|
|
15710
|
+
const families = [];
|
|
15711
|
+
for (const file2 of orderedFiles) {
|
|
15712
|
+
const parsed = readPool(file2);
|
|
15713
|
+
if (parsed && file2 !== globalPath)
|
|
15714
|
+
findings.push(...parsed.findings);
|
|
15715
|
+
const isPrimary = plainPoolFile !== null && file2 === plainPoolFile;
|
|
15716
|
+
let declaredPool = null;
|
|
15717
|
+
if (!isPrimary && parsed) {
|
|
15718
|
+
const v = validatePoolField(parsed.declaredPool);
|
|
15719
|
+
if (parsed.declaredPool !== undefined && parsed.declaredPool !== null) {
|
|
15720
|
+
if (v.ok) {
|
|
15721
|
+
declaredPool = v.ns;
|
|
15722
|
+
} else {
|
|
15723
|
+
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` });
|
|
15724
|
+
}
|
|
15725
|
+
}
|
|
15726
|
+
}
|
|
15727
|
+
families.push({ file: file2, parsed, ns: null, declaredPool, primary: isPrimary });
|
|
15728
|
+
}
|
|
15729
|
+
const allDeclared = new Set;
|
|
15730
|
+
for (const f of families)
|
|
15731
|
+
if (f.declaredPool)
|
|
15732
|
+
allDeclared.add(f.declaredPool);
|
|
15733
|
+
const usedNs = new Map;
|
|
15734
|
+
for (const f of families) {
|
|
15735
|
+
if (f.primary)
|
|
15736
|
+
continue;
|
|
15737
|
+
const base = f.declaredPool ?? sanitizeNamespace(dirBasename(parentDirLabel(parentDirLabel(f.file))));
|
|
15738
|
+
let ns = base;
|
|
15739
|
+
let suffix = 2;
|
|
15740
|
+
while ((usedNs.has(ns) || ns !== base && allDeclared.has(ns)) && suffix < 100) {
|
|
15741
|
+
ns = `${base}-${suffix++}`;
|
|
15742
|
+
}
|
|
15743
|
+
const owner = usedNs.get(base);
|
|
15744
|
+
if (owner !== undefined && owner !== f.file) {
|
|
15745
|
+
findings.push({ level: "error", code: "pool-namespace-collision", message: `namespace "${base}" is already used by ${owner} — ${f.file} materializes under "${ns}" instead` });
|
|
15746
|
+
}
|
|
15747
|
+
f.ns = ns;
|
|
15748
|
+
usedNs.set(ns, f.file);
|
|
15749
|
+
}
|
|
15750
|
+
const agents = {};
|
|
15751
|
+
const origins = [];
|
|
15752
|
+
for (const f of families) {
|
|
15753
|
+
const merged = { ...globalParsed?.agents ?? {}, ...f.parsed?.agents ?? {} };
|
|
15754
|
+
const ids = [];
|
|
15755
|
+
for (const rawId of Object.keys(merged)) {
|
|
15756
|
+
const matId = f.ns === null ? rawId : `${f.ns}-${rawId}`;
|
|
15757
|
+
const existing = agents[matId];
|
|
15758
|
+
if (existing !== undefined) {
|
|
15759
|
+
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` });
|
|
15760
|
+
continue;
|
|
15761
|
+
}
|
|
15762
|
+
agents[matId] = { materializedId: matId, def: merged[rawId], ns: f.ns, file: f.file };
|
|
15763
|
+
ids.push(matId);
|
|
15764
|
+
}
|
|
15765
|
+
origins.push({ file: f.file, ns: f.ns, declaredPool: f.declaredPool, primary: f.primary, materializedIds: ids });
|
|
15766
|
+
}
|
|
15767
|
+
return { families: origins, agents, findings };
|
|
15768
|
+
}
|
|
15769
|
+
};
|
|
15770
|
+
}
|
|
15771
|
+
function parentDirLabel(filePath) {
|
|
15772
|
+
let s = filePath;
|
|
15773
|
+
while (s.length > 1 && (s.endsWith("/") || s.endsWith("\\")))
|
|
15774
|
+
s = s.slice(0, -1);
|
|
15775
|
+
const i = Math.max(s.lastIndexOf("/"), s.lastIndexOf("\\"));
|
|
15776
|
+
return i < 0 ? s : s.slice(0, i);
|
|
15777
|
+
}
|
|
15556
15778
|
|
|
15557
15779
|
// plugin.ts
|
|
15558
15780
|
var FORGE_AGENT = "forge";
|
|
@@ -15665,8 +15887,35 @@ function noteToolAgent(context) {
|
|
|
15665
15887
|
}
|
|
15666
15888
|
}
|
|
15667
15889
|
var forgeDisabled = false;
|
|
15668
|
-
var
|
|
15669
|
-
var
|
|
15890
|
+
var hostAnchors = [];
|
|
15891
|
+
var poolResolver = null;
|
|
15892
|
+
var poolResolution = null;
|
|
15893
|
+
function normalizeAnchorDir(dir) {
|
|
15894
|
+
let p = (dir ?? "").trim();
|
|
15895
|
+
if (!p)
|
|
15896
|
+
return "";
|
|
15897
|
+
try {
|
|
15898
|
+
p = realpathSync(p);
|
|
15899
|
+
} catch {}
|
|
15900
|
+
if (process.platform === "win32")
|
|
15901
|
+
return p.replace(/\//g, "\\").toLowerCase();
|
|
15902
|
+
return p;
|
|
15903
|
+
}
|
|
15904
|
+
function rememberHostAnchor(dir) {
|
|
15905
|
+
const normalized = normalizeAnchorDir(dir);
|
|
15906
|
+
if (!normalized)
|
|
15907
|
+
return;
|
|
15908
|
+
if (!hostAnchors.includes(normalized))
|
|
15909
|
+
hostAnchors.push(normalized);
|
|
15910
|
+
if (!poolResolver) {
|
|
15911
|
+
poolResolver = createPoolResolver({ homeDir: process.env.FORGE_TEST_FORGE_HOME ?? homedir() });
|
|
15912
|
+
}
|
|
15913
|
+
}
|
|
15914
|
+
function resolvePools() {
|
|
15915
|
+
if (!poolResolver || hostAnchors.length === 0)
|
|
15916
|
+
return null;
|
|
15917
|
+
return poolResolver.resolve(hostAnchors);
|
|
15918
|
+
}
|
|
15670
15919
|
var sessionAnchorLoaders = new Map;
|
|
15671
15920
|
function loaderForAnchor(anchor) {
|
|
15672
15921
|
const key = anchor || "-";
|
|
@@ -15874,7 +16123,7 @@ function readPlanDir(worktree) {
|
|
|
15874
16123
|
const dir = planDirOf(worktree);
|
|
15875
16124
|
if (!existsSync3(dir))
|
|
15876
16125
|
return [];
|
|
15877
|
-
return
|
|
16126
|
+
return readdirSync5(dir).filter((f) => f.endsWith(".md")).map((name) => ({ name, text: readFileSync8(join6(dir, name), "utf8") }));
|
|
15878
16127
|
}
|
|
15879
16128
|
function ensureSession(sessionID, worktree) {
|
|
15880
16129
|
const existing = sessions.get(sessionID);
|
|
@@ -15941,7 +16190,7 @@ var planWriteTool = tool({
|
|
|
15941
16190
|
} else {
|
|
15942
16191
|
const dir = planDirOf(state.worktree);
|
|
15943
16192
|
mkdirSync5(dir, { recursive: true });
|
|
15944
|
-
path = join6(dir, planFileName(localDate(), slugify(args.goal),
|
|
16193
|
+
path = join6(dir, planFileName(localDate(), slugify(args.goal), readdirSync5(dir).filter((f) => f.endsWith(".md"))));
|
|
15945
16194
|
mode = "created";
|
|
15946
16195
|
}
|
|
15947
16196
|
const text = renderPlan(args, now, created);
|
|
@@ -16065,7 +16314,7 @@ function readGoalDir(worktree) {
|
|
|
16065
16314
|
const dir = goalDirOf(worktree);
|
|
16066
16315
|
if (!existsSync3(dir))
|
|
16067
16316
|
return [];
|
|
16068
|
-
return
|
|
16317
|
+
return readdirSync5(dir).filter((f) => f.endsWith(".md")).map((name) => ({ name, text: readFileSync8(join6(dir, name), "utf8") }));
|
|
16069
16318
|
}
|
|
16070
16319
|
function resolveSessionGoal(state) {
|
|
16071
16320
|
if (state.goalPath && existsSync3(state.goalPath)) {
|
|
@@ -16174,7 +16423,7 @@ var goalWriteTool = tool({
|
|
|
16174
16423
|
}
|
|
16175
16424
|
const dir = goalDirOf(state.worktree);
|
|
16176
16425
|
mkdirSync5(dir, { recursive: true });
|
|
16177
|
-
const name = goalFileName(localDateNow(), slugifyGoal(args.goal),
|
|
16426
|
+
const name = goalFileName(localDateNow(), slugifyGoal(args.goal), readdirSync5(dir).filter((f) => f.endsWith(".md")));
|
|
16178
16427
|
const path = join6(dir, name);
|
|
16179
16428
|
const text = renderGoal(input, {
|
|
16180
16429
|
now,
|
|
@@ -16546,26 +16795,34 @@ var CREW_INIT_TEMPLATE = [
|
|
|
16546
16795
|
"",
|
|
16547
16796
|
"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
16797
|
"",
|
|
16549
|
-
"CREW IS NOT INITIALIZED: no forge subagents are
|
|
16798
|
+
"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
16799
|
"",
|
|
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):",
|
|
16800
|
+
"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
16801
|
"{paths}",
|
|
16553
16802
|
"",
|
|
16554
|
-
"
|
|
16803
|
+
"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.",
|
|
16804
|
+
"",
|
|
16805
|
+
'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
16806
|
"",
|
|
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).",
|
|
16807
|
+
"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
16808
|
"{template}",
|
|
16558
16809
|
"",
|
|
16559
|
-
"After the file is saved:
|
|
16810
|
+
"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
16811
|
].join(`
|
|
16561
16812
|
`);
|
|
16562
|
-
function crewOrchestrationTemplate(
|
|
16813
|
+
function crewOrchestrationTemplate(resolution) {
|
|
16814
|
+
const families = resolution?.families ?? [];
|
|
16815
|
+
const familyLines = families.map((f) => {
|
|
16816
|
+
const label = f.primary ? "root pool (PRIMARY, plain ids)" : `pool ${f.ns} (forge-${f.ns}-*)`;
|
|
16817
|
+
return ` - ${label}: ${f.file} — ${f.materializedIds.map((id) => `forge-${id}`).join(", ") || "no agents"}`;
|
|
16818
|
+
});
|
|
16563
16819
|
return [
|
|
16564
16820
|
'(forge crew orchestration. Argument: "$ARGUMENTS")',
|
|
16565
16821
|
"",
|
|
16566
16822
|
"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
16823
|
"",
|
|
16568
|
-
|
|
16824
|
+
"Configured forge subagents (task-tool vocabulary), grouped by pool:",
|
|
16825
|
+
...familyLines.length > 0 ? familyLines : [" (none)"],
|
|
16569
16826
|
"",
|
|
16570
16827
|
"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
16828
|
"",
|
|
@@ -16596,23 +16853,27 @@ var crewBeginTool = tool({
|
|
|
16596
16853
|
if (active && active.doc.status === "draft") {
|
|
16597
16854
|
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.");
|
|
16598
16855
|
}
|
|
16599
|
-
const
|
|
16600
|
-
const hostIds =
|
|
16856
|
+
const resolution = resolvePools();
|
|
16857
|
+
const hostIds = resolution ? Object.keys(resolution.agents) : [];
|
|
16601
16858
|
const anchor = sessionAnchor(context);
|
|
16602
|
-
const
|
|
16603
|
-
const sessionLoaded = anchor && anchor !== hostAnchorDir ? loaderForAnchor(anchor).load() : hostLoaded;
|
|
16859
|
+
const sessionLoaded = anchor ? loaderForAnchor(anchor).load() : null;
|
|
16604
16860
|
const sessionProject = sessionLoaded?.projectPath ?? null;
|
|
16861
|
+
const foldPath = (p) => process.platform === "win32" ? p.replace(/\//g, "\\").toLowerCase() : p;
|
|
16862
|
+
const sessionCovered = sessionProject === null || (resolution?.families.some((f) => foldPath(f.file) === foldPath(sessionProject)) ?? false);
|
|
16605
16863
|
if (hostIds.length === 0) {
|
|
16606
|
-
const
|
|
16864
|
+
const primaryAnchorDir = hostAnchors[0] ?? hostWorktree;
|
|
16865
|
+
const anchorNote = hostAnchors.length > 1 ? `host anchors: ${hostAnchors.join(", ")}` : `launched from ${primaryAnchorDir || "an unresolvable directory"}`;
|
|
16866
|
+
const paths = forgeConfigPaths({ projectDir: primaryAnchorDir || undefined, homeDir: process.env.FORGE_TEST_FORGE_HOME ?? homedir() });
|
|
16607
16867
|
const lines2 = [
|
|
16608
|
-
|
|
16609
|
-
`Two layers, MERGED (the project layer overrides the global one per agent id): ${
|
|
16610
|
-
|
|
16611
|
-
"
|
|
16612
|
-
"
|
|
16868
|
+
`[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.`,
|
|
16869
|
+
`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.`,
|
|
16870
|
+
"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.",
|
|
16871
|
+
`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"}}}`,
|
|
16872
|
+
"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.",
|
|
16873
|
+
"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
16874
|
];
|
|
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
|
|
16875
|
+
if (sessionLoaded && Object.keys(sessionLoaded.agents).length > 0 && sessionProject && !sessionCovered) {
|
|
16876
|
+
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
16877
|
}
|
|
16617
16878
|
throw new Error(lines2.join(`
|
|
16618
16879
|
`));
|
|
@@ -16708,20 +16969,27 @@ var crewBeginTool = tool({
|
|
|
16708
16969
|
crews.set(context.sessionID, { objective: objectiveText, startedAt: nowIso(), subtasks, mode: governed ? "executing" : "pending", recordPath });
|
|
16709
16970
|
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
16971
|
let disclosure = "";
|
|
16711
|
-
if (sessionProject &&
|
|
16712
|
-
const localOnly = Object.keys(sessionLoaded?.agents ?? {})
|
|
16972
|
+
if (sessionProject && !sessionCovered) {
|
|
16973
|
+
const localOnly = Object.keys(sessionLoaded?.agents ?? {});
|
|
16713
16974
|
if (localOnly.length > 0) {
|
|
16714
16975
|
disclosure = `
|
|
16715
|
-
[forge:crew] NOTE: this session's workspace has its own forge.json (${sessionProject}) defining ${localOnly.map((id) => `forge
|
|
16976
|
+
[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
16977
|
}
|
|
16717
16978
|
}
|
|
16718
|
-
const
|
|
16719
|
-
const
|
|
16979
|
+
const familyLines = [];
|
|
16980
|
+
const families = resolution?.families ?? [];
|
|
16981
|
+
for (const f of families.slice(0, 8)) {
|
|
16982
|
+
const label = f.primary ? "root pool (PRIMARY, plain ids)" : `pool ${f.ns} (forge-${f.ns}-*)`;
|
|
16983
|
+
familyLines.push(` - ${label}: ${f.file} — ${f.materializedIds.map((id) => `forge-${id}`).join(", ") || "no agents"}`);
|
|
16984
|
+
}
|
|
16985
|
+
if (families.length > 8)
|
|
16986
|
+
familyLines.push(` - …and ${families.length - 8} more pools`);
|
|
16720
16987
|
const lines = [
|
|
16721
16988
|
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
16989
|
`Declared plan (${subtasks.length}):`,
|
|
16723
16990
|
...subtasks.map((s, i) => ` ${i + 1}. ${s.title}${s.agent ? ` → ${s.agent}` : ""}`),
|
|
16724
|
-
`Dispatchable roster origin
|
|
16991
|
+
`Dispatchable roster origin — ${families.length} pool${families.length === 1 ? "" : "s"} across the host anchor set${hostAnchors.length > 0 ? ` (anchors: ${hostAnchors.join(", ")})` : ""}:`,
|
|
16992
|
+
...familyLines,
|
|
16725
16993
|
...recordNote ? [recordNote] : [],
|
|
16726
16994
|
...disclosure ? [disclosure] : []
|
|
16727
16995
|
];
|
|
@@ -17040,7 +17308,9 @@ function scheduleIdleContinuation(client, sessionID) {
|
|
|
17040
17308
|
}, IDLE_DEBOUNCE_MS));
|
|
17041
17309
|
}
|
|
17042
17310
|
var server = async (input, options) => {
|
|
17043
|
-
|
|
17311
|
+
const effectiveInitDir = effectiveWorktree(input.worktree, input.directory) || input.directory || "";
|
|
17312
|
+
hostWorktree = effectiveInitDir;
|
|
17313
|
+
rememberHostAnchor(effectiveInitDir);
|
|
17044
17314
|
const client = input.client;
|
|
17045
17315
|
jobClient = input.client;
|
|
17046
17316
|
const jobsOpts = options?.jobs;
|
|
@@ -17151,23 +17421,17 @@ var server = async (input, options) => {
|
|
|
17151
17421
|
if (permSection[gateKey] !== "deny")
|
|
17152
17422
|
permSection[gateKey] = "ask";
|
|
17153
17423
|
}
|
|
17154
|
-
|
|
17155
|
-
|
|
17156
|
-
|
|
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) {
|
|
17424
|
+
const resolution = resolvePools();
|
|
17425
|
+
poolResolution = resolution;
|
|
17426
|
+
for (const finding of resolution?.findings ?? []) {
|
|
17163
17427
|
console.warn(`[forge:config] ${finding.level}: ${finding.message}`);
|
|
17164
17428
|
}
|
|
17165
17429
|
materializedSnapshot = {};
|
|
17166
|
-
for (const [
|
|
17167
|
-
const id = `forge-${
|
|
17430
|
+
for (const [matId, agent] of Object.entries(resolution?.agents ?? {})) {
|
|
17431
|
+
const id = `forge-${matId}`;
|
|
17168
17432
|
if (agentSection[id] !== undefined)
|
|
17169
17433
|
continue;
|
|
17170
|
-
const entry = forgeAgentDef(
|
|
17434
|
+
const entry = forgeAgentDef(matId, agent.def);
|
|
17171
17435
|
agentSection[id] = entry;
|
|
17172
17436
|
materializedSnapshot[id] = entry;
|
|
17173
17437
|
}
|
|
@@ -17212,12 +17476,13 @@ var server = async (input, options) => {
|
|
|
17212
17476
|
cfg.command ??= {};
|
|
17213
17477
|
const existingCrew = cfg.command["crew"];
|
|
17214
17478
|
if (!existingCrew || String(existingCrew.template ?? "").startsWith("(forge crew")) {
|
|
17215
|
-
const
|
|
17216
|
-
const
|
|
17217
|
-
|
|
17479
|
+
const agentCount = Object.keys(resolution?.agents ?? {}).length;
|
|
17480
|
+
const primaryAnchorDir = hostAnchors[0] ?? undefined;
|
|
17481
|
+
const template = agentCount > 0 ? crewOrchestrationTemplate(resolution ?? null) : CREW_INIT_TEMPLATE.replaceAll("{paths}", ` - ${forgeConfigPaths({ projectDir: primaryAnchorDir, homeDir: process.env.FORGE_TEST_FORGE_HOME ?? homedir() }).project}
|
|
17482
|
+
- ${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
17483
|
`).map((l) => " " + l).join(`
|
|
17219
17484
|
`));
|
|
17220
|
-
const description =
|
|
17485
|
+
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
17486
|
cfg.command["crew"] = { template, description };
|
|
17222
17487
|
crewCommandSnapshot = { template, description };
|
|
17223
17488
|
}
|
|
@@ -17320,7 +17585,7 @@ var server = async (input, options) => {
|
|
|
17320
17585
|
return;
|
|
17321
17586
|
}
|
|
17322
17587
|
const agentId = agentName.slice("forge-".length);
|
|
17323
|
-
const def =
|
|
17588
|
+
const def = resolvePools()?.agents[agentId]?.def;
|
|
17324
17589
|
if (!def?.thoughtLevel)
|
|
17325
17590
|
return;
|
|
17326
17591
|
const providerID = typeof raw.model?.providerID === "string" ? raw.model.providerID : "";
|
|
@@ -17348,7 +17613,14 @@ var server = async (input, options) => {
|
|
|
17348
17613
|
modelsDevCatalog = c;
|
|
17349
17614
|
},
|
|
17350
17615
|
crewCommand: () => crewCommandSnapshot,
|
|
17351
|
-
materialized: () => materializedSnapshot
|
|
17616
|
+
materialized: () => materializedSnapshot,
|
|
17617
|
+
resetAnchors: () => {
|
|
17618
|
+
hostAnchors.length = 0;
|
|
17619
|
+
poolResolver = null;
|
|
17620
|
+
poolResolution = null;
|
|
17621
|
+
},
|
|
17622
|
+
anchors: () => [...hostAnchors],
|
|
17623
|
+
resolveNow: () => resolvePools()
|
|
17352
17624
|
},
|
|
17353
17625
|
"permission.ask": async (input2, output) => {
|
|
17354
17626
|
const meta = input2.metadata ?? {};
|
|
@@ -17514,20 +17786,20 @@ async function v2Setup(ctx) {
|
|
|
17514
17786
|
});
|
|
17515
17787
|
}
|
|
17516
17788
|
try {
|
|
17517
|
-
const
|
|
17518
|
-
const
|
|
17519
|
-
for (const [
|
|
17520
|
-
const id = `forge-${
|
|
17789
|
+
const resolver = createPoolResolver({ homeDir: process.env.FORGE_TEST_FORGE_HOME ?? homedir() });
|
|
17790
|
+
const resolution = resolver.resolve([process.cwd()]);
|
|
17791
|
+
for (const [matId, agent] of Object.entries(resolution.agents)) {
|
|
17792
|
+
const id = `forge-${matId}`;
|
|
17521
17793
|
if (draft.get(id) !== undefined)
|
|
17522
17794
|
continue;
|
|
17523
|
-
const v1def = forgeAgentDef(
|
|
17524
|
-
draft.update(id, (
|
|
17525
|
-
|
|
17526
|
-
|
|
17527
|
-
|
|
17795
|
+
const v1def = forgeAgentDef(matId, agent.def);
|
|
17796
|
+
draft.update(id, (agentDraft) => {
|
|
17797
|
+
agentDraft.description = v1def.description;
|
|
17798
|
+
agentDraft.system = v1def.prompt;
|
|
17799
|
+
agentDraft.mode = v1def.mode;
|
|
17528
17800
|
if (v1def.model !== undefined)
|
|
17529
|
-
|
|
17530
|
-
|
|
17801
|
+
agentDraft.model = v1def.model;
|
|
17802
|
+
agentDraft.permission = v1def.permission;
|
|
17531
17803
|
});
|
|
17532
17804
|
}
|
|
17533
17805
|
} catch {}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sorenllm/opencode-forge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Single general-purpose forge agent for opencode: static worker subagents defined in forge.json (pinned model + thoughtLevel pair, or an Auto worker inheriting the parent model; dispatched through the native task tool — TUI-visible), a plan harness (file-backed plans with approve/close confirmation gates and a hard draft write-ban), a goal harness (autonomous, host-verified objectives), a /crew orchestration command (declared-plan waves with an evidence-checked close gate), a job supervisor (forge_shell with exit-bound completion, idle/success early return, background jobs with poll/log/kill/handoff), and a builtin-shell hang watchdog.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|