@sorenllm/opencode-forge 0.9.1 → 0.10.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 +43 -0
- package/dist/index.js +140 -58
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -275,6 +275,23 @@ Every result form carries `cwd:` — the absolute directory the command
|
|
|
275
275
|
actually ran in. When the agent's narrative about the working directory
|
|
276
276
|
disagrees with that line, the line wins.
|
|
277
277
|
|
|
278
|
+
**Interpreter selection.** Commands run under the host-aligned shell —
|
|
279
|
+
the same interpreter the host's builtin shell tool uses — never under
|
|
280
|
+
Node's `shell: true` default (cmd.exe / plain sh). On Windows the chain
|
|
281
|
+
is `pwsh → powershell → git-bash → %COMSPEC%` (cmd.exe only as a
|
|
282
|
+
machine-level capability fallback); on POSIX the login shell with bash
|
|
283
|
+
preferred and `/bin/sh` as the fallback. PowerShell-family shells are
|
|
284
|
+
invoked directly (`<shell> -NoProfile -Command <command>` plus an
|
|
285
|
+
exit-code guard, so a native command's exit status propagates as the
|
|
286
|
+
job's exit status) — which means you write PowerShell syntax in
|
|
287
|
+
`forge_shell` on Windows: `echo $env:USERNAME` expands, quoted-exe
|
|
288
|
+
invocations use the call operator (`& "C:\path with spaces\tool.exe" args`).
|
|
289
|
+
The tool description states the interpreter family, and there is no
|
|
290
|
+
configuration knob: the interpreter follows the host, by design.
|
|
291
|
+
Goal-harness verification checks run through the same resolver, so a
|
|
292
|
+
check drafted under the model's shell expectations behaves identically
|
|
293
|
+
at the completion gate.
|
|
294
|
+
|
|
278
295
|
**`forge_jobs`** manages the registry: `list` / `poll {jobId, waitMs≤30s}`
|
|
279
296
|
(bounded wait for new output or exit, drains it) / `log {jobId, offset?,
|
|
280
297
|
limit?}` (line paging over the on-disk log) / `kill` (whole process tree) /
|
|
@@ -605,6 +622,32 @@ supervisor chapter above). The hide is unconditional: it covers a
|
|
|
605
622
|
`jobs.keepBuiltinShell: true` (builtin shell back on the forge family) and
|
|
606
623
|
`jobs.mode: "native"` (supervisor retired entirely).
|
|
607
624
|
|
|
625
|
+
### The exec channel is final for this plugin-API generation (2026-09-28)
|
|
626
|
+
|
|
627
|
+
The four layers — tools-map hide, prompt mandate, belt refusal, `forge_shell`
|
|
628
|
+
itself — are the ceiling of what the current opencode plugin API can express,
|
|
629
|
+
and the design is closed deliberately:
|
|
630
|
+
|
|
631
|
+
- **Not expressible**: `tool.execute.before` can only mutate `args` or throw.
|
|
632
|
+
There is no tool-name rewrite, no executor replacement, no result injection —
|
|
633
|
+
"transparently reroute a builtin bash call through the job runner" cannot be
|
|
634
|
+
written. Registering a plugin tool named `bash` collides globally: tool
|
|
635
|
+
registration is not per-agent, and the per-agent mechanism is exactly the
|
|
636
|
+
tools map some hosts ignore.
|
|
637
|
+
- **Prior art ships a subset**: OMO (oh-my-opencode-slim / oh-my-openagent,
|
|
638
|
+
the largest opencode plugin ecosystem) pairs a permission-deny boundary with
|
|
639
|
+
a prompt channel mandate and does not hide the builtin shell at plugin level
|
|
640
|
+
either — their models also see bash and pay one denial round.
|
|
641
|
+
- **The residual is bounded and accepted**: on hosts where the hide does not
|
|
642
|
+
hold, a first bash attempt costs one round-trip per session — the belt
|
|
643
|
+
refuses pre-execution (~2 ms, the command never runs), the message names
|
|
644
|
+
`forge_shell`, and models re-issue once and stay on-channel. Field data
|
|
645
|
+
(2026-09-28): zero duplicate executions attributable to the belt.
|
|
646
|
+
- **Reopen on upstream movement**: hosts honoring injected tools maps (the
|
|
647
|
+
belt becomes pure insurance), a tool-execute replacement hook (transparent
|
|
648
|
+
rerouting becomes expressible), or exit-based builtin-shell completion (the
|
|
649
|
+
watchdog retirement path above applies).
|
|
650
|
+
|
|
608
651
|
A process restart forgets the session binding: the write-ban soft-disables
|
|
609
652
|
(safety over strictness) and the next session's system notice + `/plan
|
|
610
653
|
resume` re-bind from the plan file on disk, which is the source of truth.
|
package/dist/index.js
CHANGED
|
@@ -12335,8 +12335,8 @@ function tool(input) {
|
|
|
12335
12335
|
tool.schema = exports_external;
|
|
12336
12336
|
// plugin.ts
|
|
12337
12337
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
12338
|
-
import { appendFileSync, existsSync as existsSync3, mkdirSync as mkdirSync5, readdirSync as readdirSync4, readFileSync as readFileSync8, statSync as
|
|
12339
|
-
import { isAbsolute, join as
|
|
12338
|
+
import { appendFileSync, existsSync as existsSync3, mkdirSync as mkdirSync5, readdirSync as readdirSync4, readFileSync as readFileSync8, statSync as statSync5, writeFileSync as writeFileSync5 } from "node:fs";
|
|
12339
|
+
import { 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
|
|
@@ -13118,13 +13118,97 @@ function completeCheckFailures(doc2, attestations) {
|
|
|
13118
13118
|
// src/run-check.ts
|
|
13119
13119
|
import { spawn as spawn2 } from "node:child_process";
|
|
13120
13120
|
import { readFileSync } from "node:fs";
|
|
13121
|
-
import { join, resolve, sep } from "node:path";
|
|
13121
|
+
import { join as join2, resolve, sep } from "node:path";
|
|
13122
|
+
|
|
13123
|
+
// src/proc.ts
|
|
13124
|
+
import { spawn, spawnSync as spawnSync2 } from "node:child_process";
|
|
13125
|
+
|
|
13126
|
+
// src/shell-select.ts
|
|
13127
|
+
import { spawnSync } from "node:child_process";
|
|
13128
|
+
import { statSync } from "node:fs";
|
|
13129
|
+
import { basename, dirname, join } from "node:path";
|
|
13130
|
+
var hostConfiguredShell;
|
|
13131
|
+
var memo;
|
|
13132
|
+
function setHostShell(shell) {
|
|
13133
|
+
hostConfiguredShell = shell && shell.trim() ? shell.trim() : undefined;
|
|
13134
|
+
memo = undefined;
|
|
13135
|
+
}
|
|
13136
|
+
var DENY = new Set(["fish", "nu"]);
|
|
13137
|
+
function shellName(bin) {
|
|
13138
|
+
return basename(bin).replace(/\.exe$/i, "").toLowerCase();
|
|
13139
|
+
}
|
|
13140
|
+
function classify(bin) {
|
|
13141
|
+
const name = shellName(bin);
|
|
13142
|
+
if (name === "pwsh" || name === "powershell")
|
|
13143
|
+
return { bin, family: "ps", login: false };
|
|
13144
|
+
if (name === "cmd")
|
|
13145
|
+
return { bin, family: "cmd", login: false };
|
|
13146
|
+
return { bin, family: "posix", login: name === "bash" || name === "zsh" };
|
|
13147
|
+
}
|
|
13148
|
+
function defaultWhich(platform, env) {
|
|
13149
|
+
return (bin) => {
|
|
13150
|
+
try {
|
|
13151
|
+
const r = spawnSync(platform === "win32" ? "where" : "which", [bin], {
|
|
13152
|
+
env,
|
|
13153
|
+
windowsHide: true,
|
|
13154
|
+
timeout: 5000
|
|
13155
|
+
});
|
|
13156
|
+
if (r.status !== 0 || !r.stdout)
|
|
13157
|
+
return;
|
|
13158
|
+
const first = r.stdout.toString().split(/\r?\n/).find((l) => l.trim());
|
|
13159
|
+
return first?.trim() || undefined;
|
|
13160
|
+
} catch {
|
|
13161
|
+
return;
|
|
13162
|
+
}
|
|
13163
|
+
};
|
|
13164
|
+
}
|
|
13165
|
+
function defaultStat(file2) {
|
|
13166
|
+
return statSync(file2, { throwIfNoEntry: false }) ?? undefined;
|
|
13167
|
+
}
|
|
13168
|
+
function gitBash(which, stat) {
|
|
13169
|
+
const git = which("git");
|
|
13170
|
+
if (!git)
|
|
13171
|
+
return;
|
|
13172
|
+
const candidate = join(dirname(dirname(git)), "bin", "bash.exe");
|
|
13173
|
+
return stat(candidate)?.isFile() ? candidate : undefined;
|
|
13174
|
+
}
|
|
13175
|
+
function resolveShell(deps = {}) {
|
|
13176
|
+
const hasDeps = Object.keys(deps).length > 0;
|
|
13177
|
+
if (!hasDeps && memo)
|
|
13178
|
+
return memo;
|
|
13179
|
+
const platform = deps.platform ?? process.platform;
|
|
13180
|
+
const env = deps.env ?? process.env;
|
|
13181
|
+
const which = deps.which ?? defaultWhich(platform, env);
|
|
13182
|
+
const stat = deps.stat ?? defaultStat;
|
|
13183
|
+
const pick2 = (candidate) => {
|
|
13184
|
+
if (!candidate)
|
|
13185
|
+
return;
|
|
13186
|
+
if (DENY.has(shellName(candidate)))
|
|
13187
|
+
return;
|
|
13188
|
+
const resolved = /^[a-z]:[\\/]/i.test(candidate) || candidate.includes("/") || candidate.includes("\\") ? stat(candidate)?.isFile() ? candidate : which(candidate) : which(candidate);
|
|
13189
|
+
return resolved ? classify(resolved) : undefined;
|
|
13190
|
+
};
|
|
13191
|
+
let result = pick2(hostConfiguredShell);
|
|
13192
|
+
if (!result && platform === "win32") {
|
|
13193
|
+
result = pick2("pwsh") ?? pick2("powershell") ?? pick2(gitBash(which, stat)) ?? pick2(env.COMSPEC || "cmd.exe");
|
|
13194
|
+
}
|
|
13195
|
+
if (!result && platform !== "win32") {
|
|
13196
|
+
result = pick2(env.SHELL) ?? pick2("bash") ?? pick2("/bin/sh");
|
|
13197
|
+
}
|
|
13198
|
+
if (!result) {
|
|
13199
|
+
result = platform === "win32" ? { bin: env.COMSPEC || "cmd.exe", family: "cmd", login: false } : { bin: "/bin/sh", family: "posix", login: false };
|
|
13200
|
+
}
|
|
13201
|
+
if (!hasDeps)
|
|
13202
|
+
memo = result;
|
|
13203
|
+
return result;
|
|
13204
|
+
}
|
|
13122
13205
|
|
|
13123
13206
|
// src/proc.ts
|
|
13124
|
-
import { spawn, spawnSync } from "node:child_process";
|
|
13125
13207
|
function shellSpawn(spawnFn, cmd, opts = {}) {
|
|
13126
|
-
|
|
13127
|
-
|
|
13208
|
+
const shell = opts.shell ?? resolveShell();
|
|
13209
|
+
const argv = shell.family === "ps" ? ["-NoProfile", "-Command", `${cmd}
|
|
13210
|
+
exit $LASTEXITCODE`] : shell.family === "cmd" ? ["/c", cmd] : shell.login ? ["-l", "-c", cmd] : ["-c", cmd];
|
|
13211
|
+
return spawnFn(shell.bin, argv, {
|
|
13128
13212
|
...opts.cwd !== undefined ? { cwd: opts.cwd } : {},
|
|
13129
13213
|
env: opts.env ? { ...process.env, ...opts.env } : process.env,
|
|
13130
13214
|
windowsHide: opts.windowsHide ?? true,
|
|
@@ -13171,7 +13255,7 @@ function pidAlive(pid) {
|
|
|
13171
13255
|
function terminateTreeSync(pid, opts = {}) {
|
|
13172
13256
|
const graceMs = Math.max(0, opts.graceMs ?? 0);
|
|
13173
13257
|
const platform = opts.platform ?? process.platform;
|
|
13174
|
-
const sync = opts.spawnSyncFn ??
|
|
13258
|
+
const sync = opts.spawnSyncFn ?? spawnSync2;
|
|
13175
13259
|
const wait = opts.wait ?? true;
|
|
13176
13260
|
const sleep = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
13177
13261
|
const run = (force) => {
|
|
@@ -13207,12 +13291,7 @@ var OUTPUT_LIMIT = 2048;
|
|
|
13207
13291
|
var defaultShellRunner = (cmd, opts) => new Promise((resolveRun) => {
|
|
13208
13292
|
let child;
|
|
13209
13293
|
try {
|
|
13210
|
-
child = spawn2
|
|
13211
|
-
shell: true,
|
|
13212
|
-
cwd: opts.cwd,
|
|
13213
|
-
windowsHide: true,
|
|
13214
|
-
...process.platform !== "win32" ? { detached: true } : {}
|
|
13215
|
-
});
|
|
13294
|
+
child = shellSpawn(spawn2, cmd, { cwd: opts.cwd });
|
|
13216
13295
|
} catch (err) {
|
|
13217
13296
|
resolveRun({ code: null, output: "", timedOut: false, spawnError: String(err) });
|
|
13218
13297
|
return;
|
|
@@ -13281,7 +13360,7 @@ function runContainsItem(item, index, worktree) {
|
|
|
13281
13360
|
if (!insideWorktree(worktree, item.file)) {
|
|
13282
13361
|
return { index, kind: "contains", label, ok: false, detail: "path escapes the workspace boundary", durationMs: Date.now() - started };
|
|
13283
13362
|
}
|
|
13284
|
-
const abs =
|
|
13363
|
+
const abs = join2(worktree, item.file);
|
|
13285
13364
|
let content;
|
|
13286
13365
|
try {
|
|
13287
13366
|
content = readFileSync(abs, "utf8");
|
|
@@ -13570,8 +13649,8 @@ function newJobId(now = Date.now) {
|
|
|
13570
13649
|
|
|
13571
13650
|
// src/job-runner.ts
|
|
13572
13651
|
import { spawn as spawn3 } from "node:child_process";
|
|
13573
|
-
import { closeSync, existsSync, mkdirSync, openSync, readdirSync, readSync, readFileSync as readFileSync2, statSync, unlinkSync } from "node:fs";
|
|
13574
|
-
import { join as
|
|
13652
|
+
import { closeSync, existsSync, mkdirSync, openSync, readdirSync, readSync, readFileSync as readFileSync2, statSync as statSync2, unlinkSync } from "node:fs";
|
|
13653
|
+
import { join as join3 } from "node:path";
|
|
13575
13654
|
import { tmpdir } from "node:os";
|
|
13576
13655
|
var DEFAULT_IDLE_MS = 60000;
|
|
13577
13656
|
var DEFAULT_MAX_WAIT_MS = 120000;
|
|
@@ -13582,17 +13661,17 @@ var JOB_ENV_MARKER = "FORGE_JOB_ID";
|
|
|
13582
13661
|
var TAIL_POLL_MS = 200;
|
|
13583
13662
|
var ADOPT_LIVENESS_MS = 1000;
|
|
13584
13663
|
function jobsLogDir(base) {
|
|
13585
|
-
return
|
|
13664
|
+
return join3(base ?? join3(tmpdir(), "opencode-forge"), "jobs");
|
|
13586
13665
|
}
|
|
13587
13666
|
var TAIL_CHUNK_BYTES = 4 * 1024 * 1024;
|
|
13588
13667
|
function createFileTail(logPath, onText, pollMs = TAIL_POLL_MS) {
|
|
13589
|
-
let pos = existsSync(logPath) ?
|
|
13668
|
+
let pos = existsSync(logPath) ? statSync2(logPath).size : 0;
|
|
13590
13669
|
let stopped = false;
|
|
13591
13670
|
const flush = () => {
|
|
13592
13671
|
if (stopped)
|
|
13593
13672
|
return;
|
|
13594
13673
|
try {
|
|
13595
|
-
let size = existsSync(logPath) ?
|
|
13674
|
+
let size = existsSync(logPath) ? statSync2(logPath).size : 0;
|
|
13596
13675
|
if (size < pos)
|
|
13597
13676
|
pos = size;
|
|
13598
13677
|
const fd = openSync(logPath, "r");
|
|
@@ -13611,7 +13690,7 @@ function createFileTail(logPath, onText, pollMs = TAIL_POLL_MS) {
|
|
|
13611
13690
|
break;
|
|
13612
13691
|
pos += read;
|
|
13613
13692
|
onText(buf.toString("utf8", 0, read));
|
|
13614
|
-
size = existsSync(logPath) ?
|
|
13693
|
+
size = existsSync(logPath) ? statSync2(logPath).size : size;
|
|
13615
13694
|
if (stopped)
|
|
13616
13695
|
break;
|
|
13617
13696
|
}
|
|
@@ -13639,7 +13718,7 @@ function startJob(manager, opts) {
|
|
|
13639
13718
|
const maxWaitMs = Math.min(Math.max(0, opts.maxWaitMs ?? DEFAULT_MAX_WAIT_MS), HARD_MAX_WAIT_MS);
|
|
13640
13719
|
mkdirSync(opts.logDir, { recursive: true });
|
|
13641
13720
|
const id = newJobId();
|
|
13642
|
-
const logPath =
|
|
13721
|
+
const logPath = join3(opts.logDir, `${id}.log`);
|
|
13643
13722
|
let child;
|
|
13644
13723
|
let settled = false;
|
|
13645
13724
|
let exiting = false;
|
|
@@ -13695,6 +13774,7 @@ function startJob(manager, opts) {
|
|
|
13695
13774
|
try {
|
|
13696
13775
|
wfd = openSync(logPath, "a");
|
|
13697
13776
|
child = shellSpawn(opts.spawnFn ?? spawn3, opts.cmd, {
|
|
13777
|
+
shell: opts.shell,
|
|
13698
13778
|
cwd: opts.cwd,
|
|
13699
13779
|
env: { ...opts.env ?? {}, [JOB_ENV_MARKER]: id },
|
|
13700
13780
|
stdio: ["ignore", wfd, wfd]
|
|
@@ -13872,7 +13952,7 @@ function readJobLog(logPath, opts = {}) {
|
|
|
13872
13952
|
let windowed = false;
|
|
13873
13953
|
try {
|
|
13874
13954
|
if (existsSync(logPath)) {
|
|
13875
|
-
const size =
|
|
13955
|
+
const size = statSync2(logPath).size;
|
|
13876
13956
|
if (size > LOG_READ_MAX_BYTES) {
|
|
13877
13957
|
const fd = openSync(logPath, "r");
|
|
13878
13958
|
try {
|
|
@@ -13910,7 +13990,7 @@ function rotateLogs(logDir, keep = DEFAULT_LOG_KEEP) {
|
|
|
13910
13990
|
try {
|
|
13911
13991
|
entries = readdirSync(logDir).filter((f) => f.endsWith(".log")).map((name) => {
|
|
13912
13992
|
try {
|
|
13913
|
-
return { name, mtime:
|
|
13993
|
+
return { name, mtime: statSync2(join3(logDir, name)).mtimeMs };
|
|
13914
13994
|
} catch {
|
|
13915
13995
|
return { name, mtime: 0 };
|
|
13916
13996
|
}
|
|
@@ -13924,7 +14004,7 @@ function rotateLogs(logDir, keep = DEFAULT_LOG_KEEP) {
|
|
|
13924
14004
|
entries.sort((a, b) => a.mtime - b.mtime);
|
|
13925
14005
|
for (const e of entries.slice(0, excess)) {
|
|
13926
14006
|
try {
|
|
13927
|
-
unlinkSync(
|
|
14007
|
+
unlinkSync(join3(logDir, e.name));
|
|
13928
14008
|
} catch {}
|
|
13929
14009
|
}
|
|
13930
14010
|
}
|
|
@@ -14144,9 +14224,9 @@ function createJobFence(opts = {}) {
|
|
|
14144
14224
|
}
|
|
14145
14225
|
|
|
14146
14226
|
// src/job-registry.ts
|
|
14147
|
-
import { closeSync as closeSync2, existsSync as existsSync2, mkdirSync as mkdirSync2, openSync as openSync2, readFileSync as readFileSync3, readdirSync as readdirSync2, statSync as
|
|
14227
|
+
import { closeSync as closeSync2, existsSync as existsSync2, mkdirSync as mkdirSync2, openSync as openSync2, readFileSync as readFileSync3, readdirSync as readdirSync2, statSync as statSync3, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
14148
14228
|
import { execFileSync, spawn as spawn5 } from "node:child_process";
|
|
14149
|
-
import { dirname, join as
|
|
14229
|
+
import { dirname as dirname2, join as join4 } from "node:path";
|
|
14150
14230
|
var REGISTRY_KEEP = 100;
|
|
14151
14231
|
function structuralRelocate(deadPid, platform = process.platform) {
|
|
14152
14232
|
if (deadPid <= 0)
|
|
@@ -14226,7 +14306,7 @@ function acquireLock(lockPath, timeoutMs = 2000) {
|
|
|
14226
14306
|
if (err.code !== "EEXIST")
|
|
14227
14307
|
throw err;
|
|
14228
14308
|
try {
|
|
14229
|
-
if (Date.now() -
|
|
14309
|
+
if (Date.now() - statSync3(lockPath).mtimeMs > 5000) {
|
|
14230
14310
|
unlinkSync2(lockPath);
|
|
14231
14311
|
continue;
|
|
14232
14312
|
}
|
|
@@ -14251,7 +14331,7 @@ function readRaw(path) {
|
|
|
14251
14331
|
}
|
|
14252
14332
|
}
|
|
14253
14333
|
function writeRaw(path, entries) {
|
|
14254
|
-
mkdirSync2(
|
|
14334
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
14255
14335
|
writeFileSync2(path, `${JSON.stringify({ version: 1, entries: entries.slice(0, REGISTRY_KEEP) }, null, 2)}
|
|
14256
14336
|
`);
|
|
14257
14337
|
}
|
|
@@ -14307,7 +14387,7 @@ function createJobRegistry(registryPath) {
|
|
|
14307
14387
|
};
|
|
14308
14388
|
}
|
|
14309
14389
|
function registryPathFor(jobsDir) {
|
|
14310
|
-
return
|
|
14390
|
+
return join4(jobsDir, "registry.json");
|
|
14311
14391
|
}
|
|
14312
14392
|
|
|
14313
14393
|
// src/host-exit.ts
|
|
@@ -14389,7 +14469,7 @@ function createExitCleanup(manager, opts = {}) {
|
|
|
14389
14469
|
|
|
14390
14470
|
// src/watchdog.ts
|
|
14391
14471
|
import { mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
|
|
14392
|
-
import { dirname as
|
|
14472
|
+
import { dirname as dirname3 } from "node:path";
|
|
14393
14473
|
var WATCHDOG_ENV_MARK = "FORGE_WATCHDOG_MARK";
|
|
14394
14474
|
var DEFAULT_STALL_MS = 600000;
|
|
14395
14475
|
var MIN_STALL_MS = 60000;
|
|
@@ -14607,7 +14687,7 @@ function watchdogLogDir(base) {
|
|
|
14607
14687
|
function createFileLedger(logPath, maxEntries = 200, maxBytes = 1e6) {
|
|
14608
14688
|
const append = (entry) => {
|
|
14609
14689
|
try {
|
|
14610
|
-
mkdirSync3(
|
|
14690
|
+
mkdirSync3(dirname3(logPath), { recursive: true });
|
|
14611
14691
|
let lines = [];
|
|
14612
14692
|
try {
|
|
14613
14693
|
lines = readFileSync4(logPath, "utf8").split(`
|
|
@@ -14760,7 +14840,7 @@ function createLocator(platform = process.platform, posix = {}, win = {}) {
|
|
|
14760
14840
|
|
|
14761
14841
|
// src/models-dev.ts
|
|
14762
14842
|
import { mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
|
|
14763
|
-
import { join as
|
|
14843
|
+
import { join as join5 } from "node:path";
|
|
14764
14844
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
14765
14845
|
var DEFAULT_MODELS_DEV_URL = "https://models.dev/api.json";
|
|
14766
14846
|
function parseIdentity(identity) {
|
|
@@ -14782,7 +14862,7 @@ function nativeLadder(catalog, identity) {
|
|
|
14782
14862
|
return Array.isArray(options) ? [...options] : null;
|
|
14783
14863
|
}
|
|
14784
14864
|
function modelsDevCacheDir(base) {
|
|
14785
|
-
return
|
|
14865
|
+
return join5(base ?? join5(tmpdir2(), "opencode-forge"), "models-dev");
|
|
14786
14866
|
}
|
|
14787
14867
|
function namedLevels(rows) {
|
|
14788
14868
|
const out = [];
|
|
@@ -14824,7 +14904,7 @@ function reduceModelsDev(raw) {
|
|
|
14824
14904
|
async function loadModelsDevSnapshot(opts = {}) {
|
|
14825
14905
|
const url2 = opts.url ?? process.env.OPENCODE_MODELS_URL ?? DEFAULT_MODELS_DEV_URL;
|
|
14826
14906
|
const cacheDir = modelsDevCacheDir(opts.cacheDir);
|
|
14827
|
-
const cacheFile =
|
|
14907
|
+
const cacheFile = join5(cacheDir, "snapshot.json");
|
|
14828
14908
|
const fetcher = opts.fetcher;
|
|
14829
14909
|
if (fetcher) {
|
|
14830
14910
|
try {
|
|
@@ -15024,7 +15104,7 @@ function resolvePinnedDepth(thoughtLevel, family, ladder) {
|
|
|
15024
15104
|
}
|
|
15025
15105
|
|
|
15026
15106
|
// src/forge-config.ts
|
|
15027
|
-
import { readFileSync as readFileSync7, statSync as
|
|
15107
|
+
import { readFileSync as readFileSync7, statSync as statSync4 } from "node:fs";
|
|
15028
15108
|
function sanitizeJsonc(text) {
|
|
15029
15109
|
const out = [];
|
|
15030
15110
|
let i = 0;
|
|
@@ -15352,7 +15432,7 @@ function forgeConfigPaths(opts) {
|
|
|
15352
15432
|
function createForgeConfigLoader(deps) {
|
|
15353
15433
|
const stat = deps.stat ?? ((path) => {
|
|
15354
15434
|
try {
|
|
15355
|
-
const s =
|
|
15435
|
+
const s = statSync4(path);
|
|
15356
15436
|
return { mtimeMs: s.mtimeMs, size: s.size };
|
|
15357
15437
|
} catch {
|
|
15358
15438
|
return null;
|
|
@@ -15596,11 +15676,11 @@ function jobStage() {
|
|
|
15596
15676
|
return 0;
|
|
15597
15677
|
}
|
|
15598
15678
|
var jobLogDir = jobsLogDir();
|
|
15599
|
-
var jobLedgerPath =
|
|
15679
|
+
var jobLedgerPath = join6(jobLogDir, "ledger.jsonl");
|
|
15600
15680
|
function jobLedgerSink(entry) {
|
|
15601
15681
|
try {
|
|
15602
15682
|
mkdirSync5(jobLogDir, { recursive: true });
|
|
15603
|
-
if (existsSync3(jobLedgerPath) &&
|
|
15683
|
+
if (existsSync3(jobLedgerPath) && statSync5(jobLedgerPath).size > 1e6)
|
|
15604
15684
|
writeFileSync5(jobLedgerPath, "");
|
|
15605
15685
|
appendFileSync(jobLedgerPath, `${JSON.stringify(entry)}
|
|
15606
15686
|
`);
|
|
@@ -15663,13 +15743,13 @@ function nowIso() {
|
|
|
15663
15743
|
return new Date().toISOString();
|
|
15664
15744
|
}
|
|
15665
15745
|
function planDirOf(worktree) {
|
|
15666
|
-
return
|
|
15746
|
+
return join6(worktree, ".opencode", "plan");
|
|
15667
15747
|
}
|
|
15668
15748
|
function readPlanDir(worktree) {
|
|
15669
15749
|
const dir = planDirOf(worktree);
|
|
15670
15750
|
if (!existsSync3(dir))
|
|
15671
15751
|
return [];
|
|
15672
|
-
return readdirSync4(dir).filter((f) => f.endsWith(".md")).map((name) => ({ name, text: readFileSync8(
|
|
15752
|
+
return readdirSync4(dir).filter((f) => f.endsWith(".md")).map((name) => ({ name, text: readFileSync8(join6(dir, name), "utf8") }));
|
|
15673
15753
|
}
|
|
15674
15754
|
function ensureSession(sessionID, worktree) {
|
|
15675
15755
|
const existing = sessions.get(sessionID);
|
|
@@ -15695,7 +15775,7 @@ function resolveActivePlan(state) {
|
|
|
15695
15775
|
const ranked = rankActivePlans(readPlanDir(state.worktree));
|
|
15696
15776
|
if (ranked.length === 0)
|
|
15697
15777
|
return null;
|
|
15698
|
-
const path =
|
|
15778
|
+
const path = join6(planDirOf(state.worktree), ranked[0].name);
|
|
15699
15779
|
state.planPath = path;
|
|
15700
15780
|
return { path, doc: ranked[0].doc };
|
|
15701
15781
|
}
|
|
@@ -15730,7 +15810,7 @@ var planWriteTool = tool({
|
|
|
15730
15810
|
} else {
|
|
15731
15811
|
const dir = planDirOf(state.worktree);
|
|
15732
15812
|
mkdirSync5(dir, { recursive: true });
|
|
15733
|
-
path =
|
|
15813
|
+
path = join6(dir, planFileName(localDate(), slugify(args.goal), readdirSync4(dir).filter((f) => f.endsWith(".md"))));
|
|
15734
15814
|
mode = "created";
|
|
15735
15815
|
}
|
|
15736
15816
|
const text = renderPlan(args, now, created);
|
|
@@ -15848,13 +15928,13 @@ var planDiscardTool = tool({
|
|
|
15848
15928
|
}
|
|
15849
15929
|
});
|
|
15850
15930
|
function goalDirOf(worktree) {
|
|
15851
|
-
return
|
|
15931
|
+
return join6(worktree, ".opencode", "goal");
|
|
15852
15932
|
}
|
|
15853
15933
|
function readGoalDir(worktree) {
|
|
15854
15934
|
const dir = goalDirOf(worktree);
|
|
15855
15935
|
if (!existsSync3(dir))
|
|
15856
15936
|
return [];
|
|
15857
|
-
return readdirSync4(dir).filter((f) => f.endsWith(".md")).map((name) => ({ name, text: readFileSync8(
|
|
15937
|
+
return readdirSync4(dir).filter((f) => f.endsWith(".md")).map((name) => ({ name, text: readFileSync8(join6(dir, name), "utf8") }));
|
|
15858
15938
|
}
|
|
15859
15939
|
function resolveSessionGoal(state) {
|
|
15860
15940
|
if (state.goalPath && existsSync3(state.goalPath)) {
|
|
@@ -15865,7 +15945,7 @@ function resolveSessionGoal(state) {
|
|
|
15865
15945
|
}
|
|
15866
15946
|
const live = rankLiveGoals(readGoalDir(state.worktree))[0];
|
|
15867
15947
|
if (live) {
|
|
15868
|
-
const path =
|
|
15948
|
+
const path = join6(goalDirOf(state.worktree), live.name);
|
|
15869
15949
|
state.goalPath = path;
|
|
15870
15950
|
return { path, doc: live.doc };
|
|
15871
15951
|
}
|
|
@@ -15964,7 +16044,7 @@ var goalWriteTool = tool({
|
|
|
15964
16044
|
const dir = goalDirOf(state.worktree);
|
|
15965
16045
|
mkdirSync5(dir, { recursive: true });
|
|
15966
16046
|
const name = goalFileName(localDateNow(), slugifyGoal(args.goal), readdirSync4(dir).filter((f) => f.endsWith(".md")));
|
|
15967
|
-
const path =
|
|
16047
|
+
const path = join6(dir, name);
|
|
15968
16048
|
const text = renderGoal(input, {
|
|
15969
16049
|
now,
|
|
15970
16050
|
status: arm ? "active" : "queued",
|
|
@@ -16093,7 +16173,7 @@ var goalResumeTool = tool({
|
|
|
16093
16173
|
if (!goal || goal.doc.status === "queued") {
|
|
16094
16174
|
const oldest = rankQueuedGoals(readGoalDir(state.worktree))[0];
|
|
16095
16175
|
if (oldest) {
|
|
16096
|
-
const path =
|
|
16176
|
+
const path = join6(goalDirOf(state.worktree), oldest.name);
|
|
16097
16177
|
state.goalPath = path;
|
|
16098
16178
|
goal = { path, doc: oldest.doc };
|
|
16099
16179
|
}
|
|
@@ -16143,7 +16223,8 @@ var FORGE_SHELL_DESCRIPTION = [
|
|
|
16143
16223
|
"Run a shell command without ever blocking the session indefinitely. The call completes on the FIRST of: process exit (bound to the exit event — a detached grandchild holding the stdio pipes cannot suspend the call); success_pattern matching new output (opt-in regex); idle_ms with no new output (default 60000); max_wait_ms hard cap (default 120000, max 600000 — returns still-running, never kills).",
|
|
16144
16224
|
"Idle/max-wait return `still-running` with a jobId — the process stays alive; keep watching with forge_jobs poll / log, stop it with forge_jobs kill. run_in_background returns {jobId, logPath} immediately.",
|
|
16145
16225
|
"success_pattern semantics: a match completes the call as success; the process is kept alive by default (server semantics — the thing you just verified keeps running); pass keep_alive=false to kill its tree on match. Common patterns: dev servers `listening on|ready in|Local:`, builds `Compiled successfully|Done in`, test suites `passed|all tests`.",
|
|
16146
|
-
"This tool is the forge agent's exec surface for every shell command — quick ones included. Long-running or possibly non-exiting commands (dev servers, watchers, installers, anything spawning detached children) should start with run_in_background and collect results via forge_jobs."
|
|
16226
|
+
"This tool is the forge agent's exec surface for every shell command — quick ones included. Long-running or possibly non-exiting commands (dev servers, watchers, installers, anything spawning detached children) should start with run_in_background and collect results via forge_jobs.",
|
|
16227
|
+
"Interpreter: commands run under the host-preferred shell — the PowerShell family on Windows (git-bash only on machines without PowerShell), the login shell with bash preferred on POSIX — the same interpreter the host's builtin shell tool uses."
|
|
16147
16228
|
].join(`
|
|
16148
16229
|
`);
|
|
16149
16230
|
var forgeShellTool = tool({
|
|
@@ -16175,7 +16256,7 @@ var forgeShellTool = tool({
|
|
|
16175
16256
|
}
|
|
16176
16257
|
}
|
|
16177
16258
|
const { registry: registry2, fence } = ensureJobLifecycle();
|
|
16178
|
-
const cwd = args.workdir ? isAbsolute(args.workdir) ? args.workdir :
|
|
16259
|
+
const cwd = args.workdir ? isAbsolute(args.workdir) ? args.workdir : join6(state.worktree, args.workdir) : state.worktree;
|
|
16179
16260
|
const started = startJob(jobManager, {
|
|
16180
16261
|
cmd: args.command,
|
|
16181
16262
|
cwd,
|
|
@@ -16511,7 +16592,7 @@ var pendingContinuationTurn = new Set;
|
|
|
16511
16592
|
function goalProbe(line) {
|
|
16512
16593
|
if (process.env.FORGE_GOAL_PROBE) {
|
|
16513
16594
|
try {
|
|
16514
|
-
appendFileSync(
|
|
16595
|
+
appendFileSync(join6(tmpdir3(), "forge-goal-probe.log"), `${new Date().toISOString()} ${line}
|
|
16515
16596
|
`);
|
|
16516
16597
|
} catch {}
|
|
16517
16598
|
}
|
|
@@ -16726,14 +16807,14 @@ var server = async (input, options) => {
|
|
|
16726
16807
|
if (wdStallRaw !== undefined && wdStallRaw !== wdStall) {
|
|
16727
16808
|
wdFallbacks.push(`watchdog.stallMs ${JSON.stringify(String(wdStallRaw))} adjusted to ${wdStall} (floor/default applied)`);
|
|
16728
16809
|
}
|
|
16729
|
-
const watchdogLedger = createFileLedger(
|
|
16810
|
+
const watchdogLedger = createFileLedger(join6(watchdogLogDir(), "log.jsonl"));
|
|
16730
16811
|
const rawLocator = createLocator();
|
|
16731
16812
|
const probing = () => process.env.FORGE_WATCHDOG_PROBE === "1";
|
|
16732
16813
|
const probeLine = (text) => {
|
|
16733
16814
|
if (!probing())
|
|
16734
16815
|
return;
|
|
16735
16816
|
try {
|
|
16736
|
-
appendFileSync(
|
|
16817
|
+
appendFileSync(join6(tmpdir3(), "forge-watchdog-probe.log"), `${new Date().toISOString()} ${text}
|
|
16737
16818
|
`);
|
|
16738
16819
|
} catch {}
|
|
16739
16820
|
};
|
|
@@ -16789,6 +16870,7 @@ var server = async (input, options) => {
|
|
|
16789
16870
|
forgeDisabled = forgeUserCfg?.disable === true;
|
|
16790
16871
|
if (forgeDisabled)
|
|
16791
16872
|
return;
|
|
16873
|
+
setHostShell(typeof cfg.shell === "string" ? cfg.shell : undefined);
|
|
16792
16874
|
cfg.default_agent ??= FORGE_AGENT;
|
|
16793
16875
|
const existing = agentSection[FORGE_AGENT];
|
|
16794
16876
|
agentSection[FORGE_AGENT] = {
|
|
@@ -16893,7 +16975,7 @@ var server = async (input, options) => {
|
|
|
16893
16975
|
"tool.execute.before": async (input2, output) => {
|
|
16894
16976
|
if (process.env.FORGE_PERM_PROBE) {
|
|
16895
16977
|
try {
|
|
16896
|
-
appendFileSync(
|
|
16978
|
+
appendFileSync(join6(tmpdir3(), "forge-perm-probe.log"), `${new Date().toISOString()} before tool=${JSON.stringify(input2.tool)} session=${input2.sessionID}
|
|
16897
16979
|
`);
|
|
16898
16980
|
} catch {}
|
|
16899
16981
|
}
|
|
@@ -16903,7 +16985,7 @@ var server = async (input, options) => {
|
|
|
16903
16985
|
watchdog.track(input2.callID, input2.sessionID, input2.tool, undefined, cmdText !== undefined ? commandNeedle(cmdText) : undefined);
|
|
16904
16986
|
if (process.env.FORGE_WATCHDOG_PROBE) {
|
|
16905
16987
|
try {
|
|
16906
|
-
appendFileSync(
|
|
16988
|
+
appendFileSync(join6(tmpdir3(), "forge-watchdog-probe.log"), `${new Date().toISOString()} track ${input2.callID} needle=${JSON.stringify(cmdText !== undefined ? commandNeedle(cmdText) : undefined)} rawArgs=${JSON.stringify(output?.args ?? null).slice(0, 200)}
|
|
16907
16989
|
`);
|
|
16908
16990
|
} catch {}
|
|
16909
16991
|
}
|
|
@@ -16926,7 +17008,7 @@ var server = async (input, options) => {
|
|
|
16926
17008
|
if (raw) {
|
|
16927
17009
|
const state = stateForBan(input2.sessionID);
|
|
16928
17010
|
const base = state?.worktree ?? hostWorktree ?? "";
|
|
16929
|
-
const abs = (isAbsolute(raw) ? raw :
|
|
17011
|
+
const abs = (isAbsolute(raw) ? raw : join6(base, raw)).replaceAll("\\", "/").toLowerCase();
|
|
16930
17012
|
if (abs.includes("/.opencode/plan/") || abs.includes("/.opencode/goal/")) {
|
|
16931
17013
|
throw new Error(`[forge:partition] Write refused: "${raw}" is inside the forge state directories (.opencode/plan, .opencode/goal), which belong to the forge agent in this workspace.`);
|
|
16932
17014
|
}
|
|
@@ -17017,7 +17099,7 @@ var server = async (input, options) => {
|
|
|
17017
17099
|
const name = (typeof meta.tool === "string" ? meta.tool : undefined) ?? (typeof permissionField === "string" ? permissionField : undefined) ?? input2.id ?? input2.type;
|
|
17018
17100
|
if (process.env.FORGE_PERM_PROBE) {
|
|
17019
17101
|
try {
|
|
17020
|
-
appendFileSync(
|
|
17102
|
+
appendFileSync(join6(tmpdir3(), "forge-perm-probe.log"), `${new Date().toISOString()} ask name=${JSON.stringify(name)} in_status=${output.status} id=${JSON.stringify(input2.id)} type=${JSON.stringify(input2.type)} meta=${JSON.stringify(input2.metadata)}
|
|
17021
17103
|
`);
|
|
17022
17104
|
} catch {}
|
|
17023
17105
|
}
|
|
@@ -17071,7 +17153,7 @@ var server = async (input, options) => {
|
|
|
17071
17153
|
watchdog.markSeen(input2.callID);
|
|
17072
17154
|
if (process.env.FORGE_WATCHDOG_PROBE) {
|
|
17073
17155
|
try {
|
|
17074
|
-
appendFileSync(
|
|
17156
|
+
appendFileSync(join6(tmpdir3(), "forge-watchdog-probe.log"), `${new Date().toISOString()} mark ${input2.callID} hostpid=${process.pid}
|
|
17075
17157
|
`);
|
|
17076
17158
|
} catch {}
|
|
17077
17159
|
}
|
|
@@ -17081,7 +17163,7 @@ var server = async (input, options) => {
|
|
|
17081
17163
|
watchdog.untrack(input2.callID);
|
|
17082
17164
|
if (process.env.FORGE_WATCHDOG_PROBE) {
|
|
17083
17165
|
try {
|
|
17084
|
-
appendFileSync(
|
|
17166
|
+
appendFileSync(join6(tmpdir3(), "forge-watchdog-probe.log"), `${new Date().toISOString()} untrack ${input2.callID}
|
|
17085
17167
|
`);
|
|
17086
17168
|
} catch {}
|
|
17087
17169
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sorenllm/opencode-forge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.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",
|