@sorenllm/opencode-forge 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +11 -3
  2. package/dist/index.js +14 -5
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -65,7 +65,11 @@ tool layer (verified against the host's `tools` filter):
65
65
 
66
66
  Escape hatches: `jobs.keepBuiltinShell: true` gives the forge family the
67
67
  builtin shell back; `jobs.mode: "native"` retires `forge_shell`/`forge_jobs`
68
- entirely (the hide is withdrawn in the same stroke). Mixed sessions (Tab
68
+ entirely (the hide is withdrawn in the same stroke). The forge family is
69
+ prompted to route every shell command through `forge_shell` — on hosts that
70
+ ignore injected tools maps the builtin tool stays visible but is refused at
71
+ call time, and the channel mandate is what keeps the model from trying it
72
+ first (the belt refusal remains the backstop). Mixed sessions (Tab
69
73
  between agents in one session): harness state (plans/goals) stays
70
74
  session-bound, tool surfaces follow the current speaker, and the goal loop
71
75
  parks instead of driving a non-forge turn. One deliberate exception: a
@@ -261,7 +265,7 @@ the stdio-EOF bug):
261
265
  | --- | --- | --- | --- |
262
266
  | process exit | — | final; exit code + output tail returned | **terminal evidence** — output delivered in full; the job self-clears from the registry (no wake, no clear needed) |
263
267
  | `success_pattern` regex matches new output | opt-in | completes as success; process kept alive by default (server semantics), `keep_alive: false` kills its tree | kept alive → later exit still wakes (the server dying is real news); killed inline → self-clears |
264
- | `idle_ms` with no new output | default 60000 | early return `still-running` + `jobId`; process stays alive | later exit queues a wake; entry stays listed until cleared |
268
+ | `idle_ms` with no new output | default 60000 | early return `still-running` + `jobId`; process stays alive | later exit queues a wake — unless a poll has already read the terminal state (a terminal poll consumes the entry); stays listed until then |
265
269
  | `max_wait_ms` hard cap | default 120000, max 600000 | early return `still-running`; never kills | same as idle |
266
270
 
267
271
  `run_in_background: true` skips all waiting and returns
@@ -288,7 +292,11 @@ idle (exactly once; `notify: false` opts out per job). A foreground call
288
292
  resolved on the exit event is terminal evidence: the caller already holds
289
293
  the complete output, so no wake fires and the registry entry self-clears —
290
294
  a stale `poll`/`kill`/`clear` on such an id answers "already consumed"
291
- instead of "unknown job".
295
+ instead of "unknown job". The same consumption applies to a poll that
296
+ observes a finished job: the poll itself delivered the exit status and the
297
+ drained output, so the queued wake is dropped and the entry self-clears —
298
+ no duplicate notification, no manual clear. Wakes fire only for
299
+ completions that were never read.
292
300
 
293
301
  **Ownership.** Jobs belong to the session that created them. Session
294
302
  deleted → its live session-scoped jobs are killed and the event is recorded
package/dist/index.js CHANGED
@@ -13449,8 +13449,11 @@ function createJobManager(opts = {}) {
13449
13449
  markTerminal(job, "killed", null);
13450
13450
  }
13451
13451
  function poll(job) {
13452
- if (isTerminal2(job))
13452
+ if (isTerminal2(job)) {
13453
13453
  job.readAfterEnd = true;
13454
+ jobs.delete(job.id);
13455
+ rememberConsumed(job.id);
13456
+ }
13454
13457
  const cursor = job.outLen;
13455
13458
  const start = job.pollCursor;
13456
13459
  const newOutput = sliceFromCursor(job, start);
@@ -15436,6 +15439,12 @@ var FORGE_PROMPT = `You are forge — the single general-purpose coding agent. Y
15436
15439
  Workflow modes are strictly user-initiated. Never enter plan or goal mode — and never call a plan_* or goal_* tool — unless the user ran /plan or /goal, unmistakably asked for that mode (e.g. "plan first", "set a goal"), or the session already carries a [forge:plan-notice] / [forge:goal-notice] for a mode they started. Ordinary task requests are normal work. Mode-specific rules arrive with those commands and notices; when a notice is present, follow it.
15437
15440
 
15438
15441
  Subagent dispatch (task tool): when the work suits a scoped worker, prefer a configured \`forge-*\` subagent whose description matches the job. If none matches, say so plainly to the user and dispatch through the native task channel instead.`;
15442
+ function forgePromptText(opts) {
15443
+ const mandate = opts.hardRefusal ? "Exec surface: every shell command — quick ones included — runs through the forge_shell tool. The builtin shell/bash tools are refused on this agent; never try them first, not even for trivial commands." : "Exec surface: prefer the forge_shell tool for every shell command — quick ones included; it is this agent's exec surface, and the builtin shell exists only as an explicit escape hatch.";
15444
+ return `${FORGE_PROMPT}
15445
+
15446
+ ${mandate} Long-running or non-exiting commands: forge_shell with run_in_background, then collect results via forge_jobs.`;
15447
+ }
15439
15448
  var PLAN_COMMAND_TEMPLATE = [
15440
15449
  '(forge plan harness routing. Argument: "$ARGUMENTS")',
15441
15450
  "",
@@ -16124,7 +16133,7 @@ var FORGE_SHELL_DESCRIPTION = [
16124
16133
  "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).",
16125
16134
  "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.",
16126
16135
  "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`.",
16127
- "Long-running or possibly non-exiting commands (dev servers, watchers, installers, anything spawning detached children) MUST use this tool instead of the builtin shell."
16136
+ "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."
16128
16137
  ].join(`
16129
16138
  `);
16130
16139
  var forgeShellTool = tool({
@@ -16236,7 +16245,7 @@ ${r.outputTail || "(none yet)"}`,
16236
16245
  });
16237
16246
  var FORGE_JOBS_ACTIONS = ["list", "poll", "log", "kill", "clear", "handoff"];
16238
16247
  var forgeJobsTool = tool({
16239
- description: "Manage forge_shell jobs. Actions: list (all jobs, newest first); poll {jobId, waitMs<=30000} — bounded wait for NEW output or exit, drains it; log {jobId, offset?, limit?} — line paging over the on-disk log (omitted offset = tail window, default 200 lines); kill {jobId} — terminate the job's whole process tree; clear {jobId} — drop a finished job from the registry (only early-returned/background jobs need this — synchronously consumed jobs self-clear at completion); handoff {jobId} — rebind ownership to the root session so the job survives this (sub)session's end. A delegated agent MUST poll its jobs before yielding its conclusion.",
16248
+ description: "Manage forge_shell jobs. Actions: list (all jobs, newest first); poll {jobId, waitMs<=30000} — bounded wait for NEW output or exit, drains it (polling a finished job consumes it: the entry self-clears and no wake fires); log {jobId, offset?, limit?} — line paging over the on-disk log (omitted offset = tail window, default 200 lines); kill {jobId} — terminate the job's whole process tree; clear {jobId} — drop a finished job from the registry (only un-polled early-returned/background jobs need this — synchronously consumed jobs self-clear at completion, and a terminal poll consumes the entry); handoff {jobId} — rebind ownership to the root session so the job survives this (sub)session's end. A delegated agent MUST poll its jobs before yielding its conclusion.",
16240
16249
  args: {
16241
16250
  action: tool.schema.string().describe(`One of: ${FORGE_JOBS_ACTIONS.join(", ")}`),
16242
16251
  jobId: tool.schema.string().optional().describe("Job id from forge_shell (required for every action except list)"),
@@ -16776,7 +16785,7 @@ var server = async (input, options) => {
16776
16785
  ...existing ?? {},
16777
16786
  description: existing?.description ?? "forge — the single general-purpose coding agent: takes implementation tasks directly; planning goes through /plan into the plan harness (plans land in .opencode/plan/, with approve/close confirmation gates and tick discipline enforced by tools and the permission layer).",
16778
16787
  mode: existing?.mode ?? "primary",
16779
- prompt: existing?.prompt ?? FORGE_PROMPT
16788
+ prompt: existing?.prompt ?? (jobStage() < 2 ? forgePromptText({ hardRefusal: !jobsKeepBuiltinShell }) : FORGE_PROMPT)
16780
16789
  };
16781
16790
  if (!jobsKeepBuiltinShell && jobStage() < 2) {
16782
16791
  const entry = agentSection[FORGE_AGENT];
@@ -17083,7 +17092,7 @@ var server = async (input, options) => {
17083
17092
  output.system.push(`[forge:crew-active] A crew is ACTIVE in this session: "${crewActive.objective}" (started ${crewActive.startedAt}). A second /crew must be refused; finish with crew_close when every subtask has a verdict.`);
17084
17093
  }
17085
17094
  if (!forgeDisabled && jobStage() < 2 && !output.system.some((s) => s.startsWith("[forge:job-guidance]"))) {
17086
- output.system.push("[forge:job-guidance] Long-running or possibly non-exiting shell commands (dev servers, watchers, installers, anything spawning detached children) go through forge_shell, never the builtin shell: it returns on idle/success/exit with a jobId instead of blocking indefinitely; manage jobs with forge_jobs. Delegated agents: collect your job results with forge_jobs poll before yielding your conclusion.");
17095
+ output.system.push(jobsKeepBuiltinShell ? "[forge:job-guidance] Long-running or possibly non-exiting shell commands (dev servers, watchers, installers, anything spawning detached children) go through forge_shell, never the builtin shell: it returns on idle/success/exit with a jobId instead of blocking indefinitely; manage jobs with forge_jobs. Delegated agents: collect your job results with forge_jobs poll before yielding your conclusion." : "[forge:job-guidance] Every shell command — quick ones included — goes through forge_shell: the builtin shell/bash tools are refused on this agent, so never try them first. forge_shell returns on exit/success/idle with a jobId instead of blocking indefinitely; long-running or non-exiting commands use run_in_background. Manage jobs with forge_jobs. Delegated agents: collect your job results with forge_jobs poll before yielding your conclusion.");
17087
17096
  }
17088
17097
  const state = sessions.get(input2.sessionID);
17089
17098
  if (!state)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sorenllm/opencode-forge",
3
- "version": "0.7.0",
3
+ "version": "0.9.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",