@sorenllm/opencode-forge 0.5.0 → 0.7.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 +57 -23
- package/dist/index.js +68 -17
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -257,12 +257,12 @@ Subagents have it worse: the native task tool has no timeout at all.
|
|
|
257
257
|
exit condition is bound to the actual exit event (structural immunity to
|
|
258
258
|
the stdio-EOF bug):
|
|
259
259
|
|
|
260
|
-
| condition | knob | on trigger |
|
|
261
|
-
| --- | --- | --- |
|
|
262
|
-
| process exit | — | final; exit code + output tail returned |
|
|
263
|
-
| `success_pattern` regex matches new output | opt-in | completes as success; process kept alive by default (server semantics), `keep_alive: false` kills its tree |
|
|
264
|
-
| `idle_ms` with no new output | default 60000 | early return `still-running` + `jobId`; process stays alive |
|
|
265
|
-
| `max_wait_ms` hard cap | default 120000, max 600000 | early return `still-running`; never kills |
|
|
260
|
+
| condition | knob | on trigger | completion behavior |
|
|
261
|
+
| --- | --- | --- | --- |
|
|
262
|
+
| 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
|
+
| `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 |
|
|
265
|
+
| `max_wait_ms` hard cap | default 120000, max 600000 | early return `still-running`; never kills | same as idle |
|
|
266
266
|
|
|
267
267
|
`run_in_background: true` skips all waiting and returns
|
|
268
268
|
`{jobId, logPath}` immediately. Every run pops one permission dialog
|
|
@@ -274,14 +274,21 @@ disagrees with that line, the line wins.
|
|
|
274
274
|
**`forge_jobs`** manages the registry: `list` / `poll {jobId, waitMs≤30s}`
|
|
275
275
|
(bounded wait for new output or exit, drains it) / `log {jobId, offset?,
|
|
276
276
|
limit?}` (line paging over the on-disk log) / `kill` (whole process tree) /
|
|
277
|
-
`clear` (drop a finished entry
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
`
|
|
277
|
+
`clear` (drop a finished entry — only early-returned/background jobs need
|
|
278
|
+
this; synchronously consumed jobs self-clear at completion) / `handoff`
|
|
279
|
+
(rebind ownership to the root session so a subagent's job survives the
|
|
280
|
+
subagent). Delegated agents are instructed to poll their jobs before
|
|
281
|
+
yielding a conclusion.
|
|
282
|
+
|
|
283
|
+
**Exit wakes.** A job whose caller last saw it before completion —
|
|
284
|
+
`run_in_background` starts and `still-running`/kept-alive `success` early
|
|
285
|
+
returns — queues a single `[forge:job-complete]` message when it finishes,
|
|
286
|
+
delivered into the owning session via `promptAsync` the next time it goes
|
|
287
|
+
idle (exactly once; `notify: false` opts out per job). A foreground call
|
|
288
|
+
resolved on the exit event is terminal evidence: the caller already holds
|
|
289
|
+
the complete output, so no wake fires and the registry entry self-clears —
|
|
290
|
+
a stale `poll`/`kill`/`clear` on such an id answers "already consumed"
|
|
291
|
+
instead of "unknown job".
|
|
285
292
|
|
|
286
293
|
**Ownership.** Jobs belong to the session that created them. Session
|
|
287
294
|
deleted → its live session-scoped jobs are killed and the event is recorded
|
|
@@ -359,8 +366,10 @@ complete.
|
|
|
359
366
|
## Forge subagents (static worker agents in forge.json)
|
|
360
367
|
|
|
361
368
|
Forge subagents are **static, user-authored worker agents**: each forge.json
|
|
362
|
-
entry
|
|
363
|
-
|
|
369
|
+
entry is either **pinned** (a model + a reasoning depth, bound at
|
|
370
|
+
materialization) or an **Auto worker** (no model — it inherits the parent
|
|
371
|
+
session's model at dispatch), materializes as a native `forge-<id>`
|
|
372
|
+
subagent, and is dispatched through the host's **native `task`
|
|
364
373
|
tool** — which means every run is visible in the TUI, expandable and
|
|
365
374
|
monitorable, exactly like any native subagent. There is no dispatch tool, no
|
|
366
375
|
child-session engine, and no configuration invitation anywhere: the session
|
|
@@ -381,17 +390,33 @@ cascade with a single winning source (never merged):
|
|
|
381
390
|
{
|
|
382
391
|
// forge subagents — definitions; see the restart table below for apply timing
|
|
383
392
|
"agents": {
|
|
393
|
+
// PINNED worker: model + thoughtLevel form an ATOMIC PAIR — set BOTH
|
|
384
394
|
"research": {
|
|
385
|
-
"model": "zai-coding-plan/glm-5.3", // exact "provider/model" string
|
|
386
|
-
"thoughtLevel": "low", //
|
|
395
|
+
"model": "zai-coding-plan/glm-5.3", // exact "provider/model" string
|
|
396
|
+
"thoughtLevel": "low", // none/low/medium/high/max or a native level name
|
|
387
397
|
// "prompt": "optional role prompt; built-in research/review defaults exist",
|
|
388
398
|
// "shape": "write", // default readonly denies write/edit/bash
|
|
389
399
|
// "permission": { "bash": "deny" } // optional override; "task" is ALWAYS denied
|
|
390
|
-
}
|
|
400
|
+
},
|
|
401
|
+
// AUTO worker: set NEITHER — inherits the parent session's model at
|
|
402
|
+
// dispatch (snapshot semantics: later primary model switches affect only
|
|
403
|
+
// future dispatches), runs at the provider's default depth
|
|
404
|
+
"scout": {}
|
|
391
405
|
}
|
|
392
406
|
}
|
|
393
407
|
```
|
|
394
408
|
|
|
409
|
+
**The atomic pair — the one rule that matters:**
|
|
410
|
+
|
|
411
|
+
| `model` | `thoughtLevel` | result |
|
|
412
|
+
| --- | --- | --- |
|
|
413
|
+
| set | set | pinned worker (brain + depth bound at materialization) |
|
|
414
|
+
| — | — | **Auto worker** (inherits the parent session's model; provider-default depth) |
|
|
415
|
+
| set | — | ✗ rejected: entry skipped, error finding names the missing `thoughtLevel` |
|
|
416
|
+
| — | set | ✗ rejected: entry skipped, error finding names the missing `model` |
|
|
417
|
+
|
|
418
|
+
A rejected entry costs only itself — its siblings materialize normally.
|
|
419
|
+
|
|
395
420
|
- **The plugin only ever reads this file.** It never creates, writes, or
|
|
396
421
|
migrates it. The old AI-authored onboarding path (seed placeholder +
|
|
397
422
|
error recipe) is gone: an unconfigured host is inert and silent, and the
|
|
@@ -400,13 +425,17 @@ cascade with a single winning source (never merged):
|
|
|
400
425
|
offered to YOU and happens only on your explicit go-ahead.
|
|
401
426
|
- **Parsing is field-level fail-soft** (ZCode-style): a semantically bad
|
|
402
427
|
agent entry is skipped with an error finding while its siblings apply; a
|
|
403
|
-
mistyped optional field is ignored with a warning
|
|
404
|
-
|
|
405
|
-
|
|
428
|
+
mistyped optional field is ignored with a warning (and counts as absent
|
|
429
|
+
for the atomic pair — so a pinned `model` with a typo'd `thoughtLevel`
|
|
430
|
+
skips the whole entry); a syntactically broken file empties the agent set
|
|
431
|
+
with one parse-location error. There is no seed in any failure path.
|
|
432
|
+
Findings surface in the plugin diagnostics log.
|
|
406
433
|
- **`depths` is deprecated**: the array is ignored with a warning finding.
|
|
407
434
|
Pin ONE `thoughtLevel` per agent instead.
|
|
408
435
|
- Each agent materializes as `forge-<id>` — a subagent-mode agent visible in
|
|
409
|
-
the task tool's vocabulary (never hidden), carrying the pinned `model
|
|
436
|
+
the task tool's vocabulary (never hidden), carrying the pinned `model`
|
|
437
|
+
(pinned workers) or NO model key at all (Auto workers — the description
|
|
438
|
+
says "inherits the parent session's model" instead of naming a brain),
|
|
410
439
|
the worker discipline (workspace-relative paths only, verbatim reporting
|
|
411
440
|
of tool refusals, evidence-bearing conclusions) around its role prompt,
|
|
412
441
|
and a deny-style permission: `shape: "readonly"` (the default) denies
|
|
@@ -414,6 +443,11 @@ cascade with a single winning source (never merged):
|
|
|
414
443
|
`permission` map overrides the shape default — and `task` is ALWAYS
|
|
415
444
|
denied (recursive spawning stays physically impossible).
|
|
416
445
|
|
|
446
|
+
> **Upgrading from ≤ 0.5.0 (breaking):** agent entries that pinned `model`
|
|
447
|
+
> without `thoughtLevel` used to be accepted; they are now SKIPPED with an
|
|
448
|
+
> error finding naming the missing half. Fix per entry: add the missing
|
|
449
|
+
> `thoughtLevel`, or delete `model` to make it an Auto worker.
|
|
450
|
+
|
|
417
451
|
**What applies when** — edits take hold per field:
|
|
418
452
|
|
|
419
453
|
| change | needs a host restart? |
|
package/dist/index.js
CHANGED
|
@@ -13416,12 +13416,30 @@ function createJobManager(opts = {}) {
|
|
|
13416
13416
|
job.state = state;
|
|
13417
13417
|
job.exitCode = exitCode;
|
|
13418
13418
|
job.endedAt = now();
|
|
13419
|
-
if (job.notify && job.wakeState === "none") {
|
|
13419
|
+
if (job.notify && job.wakeState === "none" && !job.terminalEvidence) {
|
|
13420
13420
|
job.wakeState = "queued";
|
|
13421
13421
|
job.wakeQueuedAt = now();
|
|
13422
13422
|
}
|
|
13423
|
+
if (job.terminalEvidence) {
|
|
13424
|
+
jobs.delete(job.id);
|
|
13425
|
+
rememberConsumed(job.id);
|
|
13426
|
+
}
|
|
13423
13427
|
enforceCaps();
|
|
13424
13428
|
}
|
|
13429
|
+
const consumedIds = new Set;
|
|
13430
|
+
function rememberConsumed(id) {
|
|
13431
|
+
if (consumedIds.has(id))
|
|
13432
|
+
return;
|
|
13433
|
+
consumedIds.add(id);
|
|
13434
|
+
if (consumedIds.size > maxFinishedJobs) {
|
|
13435
|
+
const oldest = consumedIds.values().next().value;
|
|
13436
|
+
if (oldest !== undefined)
|
|
13437
|
+
consumedIds.delete(oldest);
|
|
13438
|
+
}
|
|
13439
|
+
}
|
|
13440
|
+
function consumed(id) {
|
|
13441
|
+
return consumedIds.has(id);
|
|
13442
|
+
}
|
|
13425
13443
|
function kill(job) {
|
|
13426
13444
|
if (isTerminal2(job))
|
|
13427
13445
|
return;
|
|
@@ -13533,6 +13551,7 @@ function createJobManager(opts = {}) {
|
|
|
13533
13551
|
deliverWakesFor,
|
|
13534
13552
|
abandonStaleWakes,
|
|
13535
13553
|
ledgerEntries,
|
|
13554
|
+
consumed,
|
|
13536
13555
|
size: () => jobs.size
|
|
13537
13556
|
};
|
|
13538
13557
|
}
|
|
@@ -13640,6 +13659,11 @@ function startJob(manager, opts) {
|
|
|
13640
13659
|
clearTimeout(maxWaitTimer);
|
|
13641
13660
|
};
|
|
13642
13661
|
const firstOutputHooks = [];
|
|
13662
|
+
const foregroundWatch = !opts.runInBackground;
|
|
13663
|
+
const stampTerminalEvidence = () => {
|
|
13664
|
+
if (foregroundWatch && !settled)
|
|
13665
|
+
job.terminalEvidence = true;
|
|
13666
|
+
};
|
|
13643
13667
|
function resolveStillRunning() {
|
|
13644
13668
|
if (settled)
|
|
13645
13669
|
return;
|
|
@@ -13675,6 +13699,7 @@ function startJob(manager, opts) {
|
|
|
13675
13699
|
closeSync(wfd);
|
|
13676
13700
|
} catch {}
|
|
13677
13701
|
}
|
|
13702
|
+
stampTerminalEvidence();
|
|
13678
13703
|
manager.markTerminal(job, "killed", null);
|
|
13679
13704
|
maxWaitTimer && clearTimeout(maxWaitTimer);
|
|
13680
13705
|
resolveSettle({ status: "exited", exitCode: null, outputTail: "", spawnError: String(err) });
|
|
@@ -13726,6 +13751,8 @@ function startJob(manager, opts) {
|
|
|
13726
13751
|
killTree(child);
|
|
13727
13752
|
if (opts.survive)
|
|
13728
13753
|
opts.registry?.remove(id);
|
|
13754
|
+
if (foregroundWatch)
|
|
13755
|
+
job.terminalEvidence = true;
|
|
13729
13756
|
manager.markTerminal(job, "succeeded", null);
|
|
13730
13757
|
}
|
|
13731
13758
|
resolveSettle({ status: "succeeded", matched: m[0], outputTail: job.tail, keptAlive });
|
|
@@ -13747,6 +13774,7 @@ function startJob(manager, opts) {
|
|
|
13747
13774
|
tail.flush();
|
|
13748
13775
|
tail.stop();
|
|
13749
13776
|
liveTails.delete(id);
|
|
13777
|
+
stampTerminalEvidence();
|
|
13750
13778
|
manager.markTerminal(job, "exited", code);
|
|
13751
13779
|
if (opts.survive)
|
|
13752
13780
|
opts.registry?.remove(id);
|
|
@@ -13765,6 +13793,7 @@ function startJob(manager, opts) {
|
|
|
13765
13793
|
if (opts.survive)
|
|
13766
13794
|
opts.registry?.remove(id);
|
|
13767
13795
|
if (!settled) {
|
|
13796
|
+
stampTerminalEvidence();
|
|
13768
13797
|
settled = true;
|
|
13769
13798
|
stopTimers();
|
|
13770
13799
|
manager.markTerminal(job, "killed", null);
|
|
@@ -14893,7 +14922,8 @@ function rolePromptFor(agentId, def) {
|
|
|
14893
14922
|
}
|
|
14894
14923
|
function agentDescriptionFor(agentId, def) {
|
|
14895
14924
|
const shape = def.shape === "write" ? "write" : "readonly";
|
|
14896
|
-
|
|
14925
|
+
const brain = def.model !== undefined ? `pinned ${def.model}` : "auto — inherits the parent session's model at dispatch";
|
|
14926
|
+
return `forge subagent "${agentId}" (${shape}, ${brain}) — dispatch via the task tool with subagent_type "forge-${agentId}".`;
|
|
14897
14927
|
}
|
|
14898
14928
|
function forgeAgentDef(agentId, def) {
|
|
14899
14929
|
const permission = { task: "deny" };
|
|
@@ -14904,10 +14934,9 @@ function forgeAgentDef(agentId, def) {
|
|
|
14904
14934
|
}
|
|
14905
14935
|
Object.assign(permission, def.permission ?? {});
|
|
14906
14936
|
permission.task = "deny";
|
|
14907
|
-
|
|
14937
|
+
const entry = {
|
|
14908
14938
|
description: agentDescriptionFor(agentId, def),
|
|
14909
14939
|
mode: "subagent",
|
|
14910
|
-
model: def.model,
|
|
14911
14940
|
prompt: materializeAgentPrompt({
|
|
14912
14941
|
agent: agentId,
|
|
14913
14942
|
shape,
|
|
@@ -14916,6 +14945,9 @@ function forgeAgentDef(agentId, def) {
|
|
|
14916
14945
|
permission,
|
|
14917
14946
|
tools: { shell: false, bash: false }
|
|
14918
14947
|
};
|
|
14948
|
+
if (def.model !== undefined)
|
|
14949
|
+
entry.model = def.model;
|
|
14950
|
+
return entry;
|
|
14919
14951
|
}
|
|
14920
14952
|
|
|
14921
14953
|
// src/dispatch-depth.ts
|
|
@@ -15250,21 +15282,36 @@ function validateAgentSet(raw) {
|
|
|
15250
15282
|
findings.push({ level: "error", code: "agent-not-object", message: `${label}: definition must be an object — entry skipped` });
|
|
15251
15283
|
continue;
|
|
15252
15284
|
}
|
|
15253
|
-
if (!nonEmptyString(entry.model)) {
|
|
15254
|
-
findings.push({ level: "error", code: "agent-model-missing", message: `${label}.model: a non-empty "provider/model" string is required — entry skipped, siblings still apply` });
|
|
15255
|
-
continue;
|
|
15256
|
-
}
|
|
15257
|
-
const def = { model: entry.model.trim() };
|
|
15258
15285
|
if (entry.depths !== undefined) {
|
|
15259
15286
|
findings.push({ level: "warn", code: "depths-deprecated", message: `${label}.depths is no longer used (static agents pin one thoughtLevel) — ignored; set "thoughtLevel" instead` });
|
|
15260
15287
|
}
|
|
15288
|
+
let model;
|
|
15289
|
+
if (entry.model !== undefined) {
|
|
15290
|
+
if (nonEmptyString(entry.model)) {
|
|
15291
|
+
model = entry.model.trim();
|
|
15292
|
+
} else {
|
|
15293
|
+
findings.push({ level: "warn", code: "model-invalid", message: `${label}.model must be a non-empty string — field ignored (counts as absent for the model/thoughtLevel pair)` });
|
|
15294
|
+
}
|
|
15295
|
+
}
|
|
15296
|
+
let thoughtLevel;
|
|
15261
15297
|
if (entry.thoughtLevel !== undefined) {
|
|
15262
15298
|
if (nonEmptyString(entry.thoughtLevel)) {
|
|
15263
|
-
|
|
15299
|
+
thoughtLevel = entry.thoughtLevel.trim();
|
|
15264
15300
|
} else {
|
|
15265
|
-
findings.push({ level: "warn", code: "thoughtlevel-invalid", message: `${label}.thoughtLevel must be a non-empty string — field ignored
|
|
15301
|
+
findings.push({ level: "warn", code: "thoughtlevel-invalid", message: `${label}.thoughtLevel must be a non-empty string — field ignored (counts as absent for the model/thoughtLevel pair)` });
|
|
15266
15302
|
}
|
|
15267
15303
|
}
|
|
15304
|
+
if (model === undefined !== (thoughtLevel === undefined)) {
|
|
15305
|
+
const presentHalf = model !== undefined ? "model" : "thoughtLevel";
|
|
15306
|
+
const missingHalf = model !== undefined ? "thoughtLevel" : "model";
|
|
15307
|
+
findings.push({ level: "error", code: "agent-half-configured", message: `${label}: model and thoughtLevel form an atomic pair — ${presentHalf} is set but ${missingHalf} is missing; set BOTH to pin the brain and depth, or NEITHER for an Auto worker — entry skipped, siblings still apply` });
|
|
15308
|
+
continue;
|
|
15309
|
+
}
|
|
15310
|
+
const def = {};
|
|
15311
|
+
if (model !== undefined) {
|
|
15312
|
+
def.model = model;
|
|
15313
|
+
def.thoughtLevel = thoughtLevel;
|
|
15314
|
+
}
|
|
15268
15315
|
if (entry.prompt !== undefined) {
|
|
15269
15316
|
if (nonEmptyString(entry.prompt)) {
|
|
15270
15317
|
def.prompt = entry.prompt;
|
|
@@ -16151,6 +16198,8 @@ var forgeShellTool = tool({
|
|
|
16151
16198
|
output: [
|
|
16152
16199
|
`[forge:job] Command finished (exit=${r.exitCode ?? "none"}).`,
|
|
16153
16200
|
`cwd: ${cwd}`,
|
|
16201
|
+
`jobId: ${started.job.id}`,
|
|
16202
|
+
`Output was delivered in full above — terminal evidence: the job self-cleared from the registry (no wake fires, no forge_jobs clear needed).`,
|
|
16154
16203
|
...r.spawnError ? [`spawn error: ${r.spawnError}`] : [],
|
|
16155
16204
|
`output:
|
|
16156
16205
|
${r.outputTail || "(none)"}`
|
|
@@ -16164,7 +16213,7 @@ ${r.outputTail || "(none)"}`
|
|
|
16164
16213
|
output: [
|
|
16165
16214
|
`[forge:job] Success pattern matched: "${r.matched}".`,
|
|
16166
16215
|
`cwd: ${cwd}`,
|
|
16167
|
-
r.keptAlive ? `The process was kept alive as job ${started.job.id} (stop it with forge_jobs kill when done).` :
|
|
16216
|
+
r.keptAlive ? `The process was kept alive as job ${started.job.id} (stop it with forge_jobs kill when done).` : `The process tree was terminated (keep_alive=false) — terminal evidence: job ${started.job.id} self-cleared from the registry (no wake, no clear needed).`,
|
|
16168
16217
|
`output:
|
|
16169
16218
|
${r.outputTail}`
|
|
16170
16219
|
].join(`
|
|
@@ -16187,7 +16236,7 @@ ${r.outputTail || "(none yet)"}`,
|
|
|
16187
16236
|
});
|
|
16188
16237
|
var FORGE_JOBS_ACTIONS = ["list", "poll", "log", "kill", "clear", "handoff"];
|
|
16189
16238
|
var forgeJobsTool = tool({
|
|
16190
|
-
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; 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.",
|
|
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.",
|
|
16191
16240
|
args: {
|
|
16192
16241
|
action: tool.schema.string().describe(`One of: ${FORGE_JOBS_ACTIONS.join(", ")}`),
|
|
16193
16242
|
jobId: tool.schema.string().optional().describe("Job id from forge_shell (required for every action except list)"),
|
|
@@ -16212,8 +16261,9 @@ var forgeJobsTool = tool({
|
|
|
16212
16261
|
if (!args.jobId)
|
|
16213
16262
|
throw new Error(`Action "${action}" requires jobId.`);
|
|
16214
16263
|
const job = jobManager.get(args.jobId);
|
|
16215
|
-
if (!job)
|
|
16216
|
-
throw new Error(`No job ${args.jobId} in the registry (finished jobs age out; the on-disk log may still exist).`);
|
|
16264
|
+
if (!job) {
|
|
16265
|
+
throw new Error(jobManager.consumed(args.jobId) ? `Job ${args.jobId} was already consumed: a terminal-evidence foreground return delivered its complete output inline, so it self-cleared from the registry — no wake fires and no clear is needed.` : `No job ${args.jobId} in the registry (finished jobs age out; the on-disk log may still exist).`);
|
|
16266
|
+
}
|
|
16217
16267
|
if (action === "poll") {
|
|
16218
16268
|
const p = await pollJob(jobManager, args.jobId, args.waitMs ?? 0);
|
|
16219
16269
|
if (!p)
|
|
@@ -16272,7 +16322,7 @@ var CREW_INIT_TEMPLATE = [
|
|
|
16272
16322
|
"",
|
|
16273
16323
|
"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.",
|
|
16274
16324
|
"",
|
|
16275
|
-
"Template (JSONC — comments allowed
|
|
16325
|
+
"Template (JSONC — comments allowed): `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.",
|
|
16276
16326
|
"{template}",
|
|
16277
16327
|
"",
|
|
16278
16328
|
"After the file is saved: NEW agents require a host restart to appear in the task tool; thoughtLevel edits on existing agents apply without restart. Until then, crew stays unavailable."
|
|
@@ -17112,7 +17162,8 @@ async function v2Setup(ctx) {
|
|
|
17112
17162
|
agent.description = v1def.description;
|
|
17113
17163
|
agent.system = v1def.prompt;
|
|
17114
17164
|
agent.mode = v1def.mode;
|
|
17115
|
-
|
|
17165
|
+
if (v1def.model !== undefined)
|
|
17166
|
+
agent.model = v1def.model;
|
|
17116
17167
|
agent.permission = v1def.permission;
|
|
17117
17168
|
});
|
|
17118
17169
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
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 +
|
|
3
|
+
"version": "0.7.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
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"exports": {
|