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