@sorenllm/opencode-forge 0.13.0 → 0.14.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 +44 -17
- package/dist/index.js +72 -27
- package/package.json +64 -64
package/README.md
CHANGED
|
@@ -31,7 +31,18 @@ OpenSpec spec workflows remain a third, separate lane.
|
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
- Plan files: `.opencode/plan/<date>-<slug>.md` in your project, frontmatter
|
|
34
|
-
state machine `draft → approved → done
|
|
34
|
+
state machine `draft → approved → done`, exits `abandoned` (plain discard) and
|
|
35
|
+
`superseded` (`plan_discard {reason, supersede: "<successor path>"}` —
|
|
36
|
+
the exit for work whose execution moves to another harness, e.g.
|
|
37
|
+
`.opencode/crew/2026-10-01-....md`, rather than ends). Every terminal
|
|
38
|
+
transition appends a dated narrative section to the file body in the same
|
|
39
|
+
write as the status flip: close = each acceptance criterion's
|
|
40
|
+
pass/evidence; discard/supersede = the reason verbatim (`(none given)`
|
|
41
|
+
when absent; a supersession also records the successor path). Appended
|
|
42
|
+
sections are parser-inert — status reads frontmatter only, tick
|
|
43
|
+
counts read the Task List only — and a superseded plan's Task List
|
|
44
|
+
stays the frozen decision snapshot: closure IS the supersession, and
|
|
45
|
+
crew/goal results never tick it.
|
|
35
46
|
- While a plan is in draft, `write` / `edit` / `bash` / `task` are **denied
|
|
36
47
|
at the permission layer** — including your own `allow` config, and for
|
|
37
48
|
every agent in that session (the draft protects shared session state; see
|
|
@@ -419,9 +430,10 @@ discovered by walking up:
|
|
|
419
430
|
The two layers **merge**: agents combine, and on an id collision the project
|
|
420
431
|
definition wins wholesale (one definition per id — no cross-layer field
|
|
421
432
|
blending). A broken file empties only its own layer/pool (error finding with
|
|
422
|
-
the parse location); everything else still applies. Both layers
|
|
423
|
-
|
|
424
|
-
|
|
433
|
+
the parse location); everything else still applies. Both layers re-read at
|
|
434
|
+
every real materialization — host boot, a first config build, or an
|
|
435
|
+
opencode config reload — without a restart; a new session alone does
|
|
436
|
+
not re-read them (see the apply-timing table below).
|
|
425
437
|
|
|
426
438
|
**Hierarchical pools on a shared host.** Agent frontends (paseo and friends)
|
|
427
439
|
routinely share ONE opencode host process across projects, re-initializing
|
|
@@ -531,14 +543,18 @@ A rejected entry costs only itself — its siblings materialize normally.
|
|
|
531
543
|
> error finding naming the missing half. Fix per entry: add the missing
|
|
532
544
|
> `thoughtLevel`, or delete `model` to make it an Auto worker.
|
|
533
545
|
|
|
534
|
-
**What applies when** —
|
|
535
|
-
|
|
546
|
+
**What applies when** — only a real materialization re-reads the pool
|
|
547
|
+
files into the `forge-*` vocabulary; the anchor set itself only ever grows.
|
|
548
|
+
Materialization triggers (2026-10-01 live finding): host boot (a freshly
|
|
549
|
+
started CLI process reads the files as they are), the FIRST config build
|
|
550
|
+
for a directory on this host, and opencode config file reloads. A NEW
|
|
551
|
+
session on a long-lived shared host (serve-type) does NOT re-materialize:
|
|
536
552
|
|
|
537
553
|
| change | needs a host restart? |
|
|
538
554
|
| --- | --- |
|
|
539
|
-
| `thoughtLevel` on an existing agent | no (applies to NEW sessions of that agent; running sessions keep their frozen depth) |
|
|
540
|
-
| agent added / removed / edited, any pool file created or deleted | no restart —
|
|
541
|
-
| a NEW directory joining the host (another project
|
|
555
|
+
| `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 |
|
|
556
|
+
| 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 and depth lookups resolve the pool files fresh regardless |
|
|
557
|
+
| 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 appends the anchor and its pools (append-only; existing pools never flip) |
|
|
542
558
|
|
|
543
559
|
### Dispatching
|
|
544
560
|
|
|
@@ -580,12 +596,21 @@ requires a plan. The flow:
|
|
|
580
596
|
|
|
581
597
|
1. **Register** the declared plan: `crew_begin {objective, subtasks}` — one
|
|
582
598
|
titled subtask each, optionally naming the intended `forge-*` agent.
|
|
599
|
+
When the macro contract descends from a plan artifact, pass its path as
|
|
600
|
+
the optional `lineage` argument — recorded as a first-class `lineage:`
|
|
601
|
+
line in the crew record header and disclosed in the registration output
|
|
602
|
+
(explicit argument only; never inferred from a recently discarded plan).
|
|
583
603
|
Registration enters a **PENDING** crew: execution has not started. The
|
|
584
|
-
registration output
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
604
|
+
registration output LEADS with the crew record path line (same
|
|
605
|
+
first-block prominence as the plan tools' `Plan created:`) and the
|
|
606
|
+
PENDING pause text instructs the model to relay that path to the user
|
|
607
|
+
alongside the choice — so you can verify the record really landed on
|
|
608
|
+
disk (an earlier output order buried the path line, and the model
|
|
609
|
+
reliably drops what it buries — leading with it is the fix). The
|
|
610
|
+
output then confirms the roster **grouped by pool with its origins**
|
|
611
|
+
(which forge.json families the dispatchable set materialized from, the
|
|
612
|
+
primary marked), presents the three execution-mode choices, and stops
|
|
613
|
+
the turn. Reconnaissance task calls are free BEFORE registration.
|
|
589
614
|
2. **The pause is mechanical**: while the crew pends, every `task` dispatch is
|
|
590
615
|
refused by an interception belt. The user chooses:
|
|
591
616
|
- **supervised waves now** — `crew_begin {execution: "waves"}` lifts the
|
|
@@ -645,9 +670,11 @@ session AI do it — the latter only on your explicit go-ahead in that
|
|
|
645
670
|
conversation, through the normal visible write path (when the AI writes a
|
|
646
671
|
sub-pool file it also proposes a short stable `pool` namespace so ids
|
|
647
672
|
survive directory renames). This is the plugin's only configuration
|
|
648
|
-
invitation. Saved files apply at the next
|
|
649
|
-
on a shared host,
|
|
650
|
-
its pools automatically
|
|
673
|
+
invitation. Saved files apply at the next real materialization — no
|
|
674
|
+
restart needed; on a shared host, the first session in a NEW workspace adds
|
|
675
|
+
that anchor and its pools automatically (a new session in an existing
|
|
676
|
+
workspace does not re-read the files — see the apply-timing table
|
|
677
|
+
above).
|
|
651
678
|
|
|
652
679
|
### Debris note
|
|
653
680
|
|
package/dist/index.js
CHANGED
|
@@ -12346,12 +12346,13 @@ class PlanError extends Error {
|
|
|
12346
12346
|
this.name = "PlanError";
|
|
12347
12347
|
}
|
|
12348
12348
|
}
|
|
12349
|
-
var TERMINAL = ["done", "abandoned"];
|
|
12349
|
+
var TERMINAL = ["done", "abandoned", "superseded"];
|
|
12350
12350
|
var LEGAL_TRANSITIONS = {
|
|
12351
|
-
draft: ["approved", "abandoned"],
|
|
12352
|
-
approved: ["done", "abandoned"],
|
|
12351
|
+
draft: ["approved", "abandoned", "superseded"],
|
|
12352
|
+
approved: ["done", "abandoned", "superseded"],
|
|
12353
12353
|
done: [],
|
|
12354
|
-
abandoned: []
|
|
12354
|
+
abandoned: [],
|
|
12355
|
+
superseded: []
|
|
12355
12356
|
};
|
|
12356
12357
|
var SECTION_ORDER = [
|
|
12357
12358
|
"Goal",
|
|
@@ -12499,6 +12500,7 @@ function matchSectionHeader(line) {
|
|
|
12499
12500
|
}
|
|
12500
12501
|
function splitSections(body) {
|
|
12501
12502
|
const sections = new Map;
|
|
12503
|
+
const claimed = new Set;
|
|
12502
12504
|
let current = null;
|
|
12503
12505
|
const buf = [];
|
|
12504
12506
|
const flush = () => {
|
|
@@ -12509,12 +12511,14 @@ function splitSections(body) {
|
|
|
12509
12511
|
};
|
|
12510
12512
|
for (const line of body.split(/\r?\n/)) {
|
|
12511
12513
|
const header = matchSectionHeader(line);
|
|
12512
|
-
if (header
|
|
12513
|
-
|
|
12514
|
-
|
|
12515
|
-
|
|
12516
|
-
buf.push(line);
|
|
12514
|
+
if (header === null || claimed.has(header)) {
|
|
12515
|
+
if (current !== null)
|
|
12516
|
+
buf.push(line);
|
|
12517
|
+
continue;
|
|
12517
12518
|
}
|
|
12519
|
+
flush();
|
|
12520
|
+
claimed.add(header);
|
|
12521
|
+
current = header;
|
|
12518
12522
|
}
|
|
12519
12523
|
flush();
|
|
12520
12524
|
return sections;
|
|
@@ -12525,7 +12529,7 @@ var ACCEPT_RE = /^\s*(\d+)[.、)]\s*(.+)$/;
|
|
|
12525
12529
|
function parsePlan(text) {
|
|
12526
12530
|
const { fm, body } = parseFrontmatter(text);
|
|
12527
12531
|
const status = fm.status ?? "draft";
|
|
12528
|
-
if (!["draft", "approved", "done", "abandoned"].includes(status)) {
|
|
12532
|
+
if (!["draft", "approved", "done", "abandoned", "superseded"].includes(status)) {
|
|
12529
12533
|
throw new PlanError(`Unknown plan status: ${fm.status}`);
|
|
12530
12534
|
}
|
|
12531
12535
|
const sections = splitSections(body);
|
|
@@ -12612,11 +12616,35 @@ function transitionStatus(text, to, now) {
|
|
|
12612
12616
|
if (from === to)
|
|
12613
12617
|
throw new PlanError(`Plan is already ${to}`);
|
|
12614
12618
|
if (!canTransition(from, to)) {
|
|
12615
|
-
throw new PlanError(`Illegal status transition: ${from} -> ${to} (legal path: draft -> approved -> done; draft/approved -> abandoned)`);
|
|
12619
|
+
throw new PlanError(`Illegal status transition: ${from} -> ${to} (legal path: draft -> approved -> done; draft/approved -> abandoned/superseded)`);
|
|
12616
12620
|
}
|
|
12617
12621
|
const next = text.replace(/^status:.*$/m, `status: ${to}`);
|
|
12618
12622
|
return setUpdated(next, now);
|
|
12619
12623
|
}
|
|
12624
|
+
function appendTerminalSection(text, kind, opts = {}, now = new Date().toISOString()) {
|
|
12625
|
+
if (!["closed", "abandoned", "superseded"].includes(kind)) {
|
|
12626
|
+
throw new PlanError(`Unknown terminal section kind: ${kind}`);
|
|
12627
|
+
}
|
|
12628
|
+
const lines = [`## ${now} — ${kind}`, ""];
|
|
12629
|
+
const reason = (opts.reason ?? "").trim();
|
|
12630
|
+
lines.push(`reason: ${reason.length > 0 ? reason : "(none given)"}`);
|
|
12631
|
+
const successor = (opts.successor ?? "").trim();
|
|
12632
|
+
if (successor.length > 0)
|
|
12633
|
+
lines.push(`successor: ${successor}`);
|
|
12634
|
+
if (opts.checks && opts.checks.length > 0) {
|
|
12635
|
+
lines.push("checks:");
|
|
12636
|
+
for (const c of opts.checks) {
|
|
12637
|
+
lines.push(`- ${c.pass ? "PASS" : "FAIL"} — ${c.criterion.trim()}`);
|
|
12638
|
+
lines.push(` evidence: ${c.evidence.trim() || "(none)"}`);
|
|
12639
|
+
}
|
|
12640
|
+
}
|
|
12641
|
+
const base = text.replace(/\r?\n$/, "");
|
|
12642
|
+
return `${base}
|
|
12643
|
+
|
|
12644
|
+
${lines.join(`
|
|
12645
|
+
`)}
|
|
12646
|
+
`;
|
|
12647
|
+
}
|
|
12620
12648
|
function progressOf(doc2) {
|
|
12621
12649
|
return { total: doc2.tasks.length, done: doc2.tasks.filter((t) => t.done).length };
|
|
12622
12650
|
}
|
|
@@ -15819,7 +15847,7 @@ var PLAN_COMMAND_TEMPLATE = [
|
|
|
15819
15847
|
"Route on the argument above (decide silently; do not recite this routing text to the user):",
|
|
15820
15848
|
"- Argument empty: for every non-terminal plan (status draft or approved) in the directory above, read its frontmatter and task checkboxes, then report to the user: path, status, progress (x/y ticked). Ask whether to resume one or start something new.",
|
|
15821
15849
|
'- Argument "resume": pick the most recently updated non-terminal plan, summarize its remaining unticked tasks to the user in one short list, then continue executing it — tick each task the moment it is done (plan_tick). If none exists, say so.',
|
|
15822
|
-
'- Argument "discard": call the plan_discard tool, then tell the user the plan was abandoned and writes are restored.',
|
|
15850
|
+
'- Argument "discard": call the plan_discard tool with a short reason, then tell the user the plan was abandoned and writes are restored. If the work is NOT ending but moving to another harness — crew orchestration (/crew) or a goal contract (/goal) — pass supersede with the successor artifact path (e.g. ".opencode/crew/2026-10-01-....md") so the plan is marked superseded instead, with the successor recorded in the plan file.',
|
|
15823
15851
|
"- Any other argument: treat it as the task goal — you are now in planning mode for this goal. Follow the plan discipline below end to end.",
|
|
15824
15852
|
"",
|
|
15825
15853
|
"## Plan discipline",
|
|
@@ -15832,7 +15860,7 @@ var PLAN_COMMAND_TEMPLATE = [
|
|
|
15832
15860
|
"4. Execution (tick as you go): work tasks in order; call plan_tick with the task number immediately after EACH task's work is actually done — never batch ticks, never tick ahead of reality (tick timestamps are an audit trail). If the plan turns out wrong mid-execution, do not silently improvise: tell the user what changed and either finish the affected task or ask about revising (/plan with the same goal re-enters planning).",
|
|
15833
15861
|
"5. Completion (plan_close): when all tasks are ticked, self-check EVERY acceptance criterion with concrete evidence (file:line, command output, test result), then call plan_close with one check per criterion, pass/fail honest — a failing check refuses the close, and that is the design working, not an inconvenience. The user confirms closure in a dialog.",
|
|
15834
15862
|
"",
|
|
15835
|
-
"Boundaries: /plan discard abandons the plan (terminal, file kept as history, writes restored); /plan resume continues an unfinished plan from its remaining tasks. Work expected to span multiple sessions or days of multi-file change is spec work (e.g. OpenSpec), not plan work — say so once and let the user choose.",
|
|
15863
|
+
"Boundaries: /plan discard abandons the plan (terminal, file kept as history, writes restored) — pass supersede (the successor artifact path) when the work moves to crew/goal rather than ends, marking it superseded; /plan resume continues an unfinished plan from its remaining tasks. Work expected to span multiple sessions or days of multi-file change is spec work (e.g. OpenSpec), not plan work — say so once and let the user choose.",
|
|
15836
15864
|
""
|
|
15837
15865
|
].join(`
|
|
15838
15866
|
`);
|
|
@@ -15943,13 +15971,14 @@ function crewRecordPathFor(worktree, objective) {
|
|
|
15943
15971
|
name = `${date5}-${slug}-${n++}.md`;
|
|
15944
15972
|
return join6(dir, name);
|
|
15945
15973
|
}
|
|
15946
|
-
function writeCrewRecord(path, objective, anchor, subtasks) {
|
|
15974
|
+
function writeCrewRecord(path, objective, anchor, subtasks, lineage) {
|
|
15947
15975
|
mkdirSync5(join6(path, ".."), { recursive: true });
|
|
15948
15976
|
writeFileSync5(path, [
|
|
15949
15977
|
`# Crew Record: ${objective}`,
|
|
15950
15978
|
"",
|
|
15951
15979
|
`- registered: ${nowIso()}`,
|
|
15952
15980
|
`- anchor: ${anchor}`,
|
|
15981
|
+
...lineage ? [`- lineage: ${lineage}`] : [],
|
|
15953
15982
|
"",
|
|
15954
15983
|
`## Declared plan (${subtasks.length})`,
|
|
15955
15984
|
...subtasks.map((s, i) => `${i + 1}. ${s.title}${s.agent ? ` → ${s.agent}` : ""}`),
|
|
@@ -15979,6 +16008,7 @@ function governingGoalFor(sessionID) {
|
|
|
15979
16008
|
function crewPendingChoiceLines() {
|
|
15980
16009
|
return [
|
|
15981
16010
|
"[forge:crew] The crew is PENDING — execution has NOT started. Present the execution-mode choice to the user and END YOUR TURN (every task dispatch is refused while the crew pends):",
|
|
16011
|
+
`RELAY FIRST: tell the user the crew record path (it leads this output) alongside this choice — the record is on disk from the registration moment, and the user must hear it.`,
|
|
15982
16012
|
` 1. supervised waves NOW — when the user says go, call crew_begin {execution: "waves"} and run the wave discipline;`,
|
|
15983
16013
|
` 2. convert to a goal contract — call crew_begin {execution: "goal"} (the crew ends as a conversion record; no extra dialog here), then draft goal_write with arm=true folding the declared subtasks into criteria/checks — the arm dialog is the user's gate;`,
|
|
15984
16014
|
` 3. standby — no call; the crew waits until armed or abandoned.`
|
|
@@ -16283,28 +16313,40 @@ var planCloseTool = tool({
|
|
|
16283
16313
|
Fix the implementation and retry, or revise the plan first.`);
|
|
16284
16314
|
}
|
|
16285
16315
|
await gate(context.ask, "plan_close", `Close plan: ${active.doc.goal}`);
|
|
16286
|
-
|
|
16316
|
+
const closedAt = nowIso();
|
|
16317
|
+
const closed = appendTerminalSection(transitionStatus(readFileSync8(active.path, "utf8"), "done", closedAt), "closed", { reason: "completion gate passed (user confirmed)", checks: args.checks }, closedAt);
|
|
16318
|
+
writeFileSync5(active.path, closed);
|
|
16287
16319
|
context.metadata({ title: `Plan done: ${active.doc.goal}` });
|
|
16288
16320
|
return {
|
|
16289
16321
|
title: "plan done",
|
|
16290
|
-
output: `Plan completed and closed (done): ${relFrom(state.worktree, active.path)}. All ${args.checks.length} self-checks passed.`
|
|
16322
|
+
output: `Plan completed and closed (done): ${relFrom(state.worktree, active.path)}. All ${args.checks.length} self-checks passed. Terminal section "## ${closedAt} — closed" appended with the per-criterion verdicts.`
|
|
16291
16323
|
};
|
|
16292
16324
|
}
|
|
16293
16325
|
});
|
|
16294
16326
|
var planDiscardTool = tool({
|
|
16295
|
-
description: "Abandon the session's active plan (draft/approved -> abandoned) and lift the draft write ban.
|
|
16327
|
+
description: "Abandon the session's active plan (draft/approved -> abandoned) and lift the draft write ban. Pass `supersede` (the successor artifact's path or identifier, e.g. a .opencode/crew/ or .opencode/goal/ file) when the work is not ending but moving to another harness (crew orchestration, a goal contract): the plan then becomes superseded instead of abandoned, with the successor recorded. Either way a dated terminal section carrying the reason verbatim (a missing reason lands as `reason: (none given)`) is appended to the file in the SAME write as the status flip, and the file stays in .opencode/plan/ as history. Invoke via /plan discard or directly when the user cancels the task.",
|
|
16296
16328
|
args: {
|
|
16297
|
-
reason: tool.schema.string().optional().describe("Short reason recorded in the reply")
|
|
16329
|
+
reason: tool.schema.string().optional().describe("Short reason recorded in the reply and in the appended terminal section, verbatim"),
|
|
16330
|
+
supersede: tool.schema.string().optional().describe("Successor artifact's path or identifier (crew record, goal contract, ...); when present the plan becomes superseded instead of abandoned and this value is persisted as the successor reference")
|
|
16298
16331
|
},
|
|
16299
|
-
execute: async (
|
|
16332
|
+
execute: async (args, context) => {
|
|
16300
16333
|
const state = ensureSession(context.sessionID, worktreeFor(context));
|
|
16301
16334
|
const active = resolveActivePlan(state);
|
|
16302
16335
|
if (!active)
|
|
16303
16336
|
throw new PlanError("No plan to abandon in this workspace.");
|
|
16304
|
-
|
|
16337
|
+
const successor = (args.supersede ?? "").trim();
|
|
16338
|
+
const superseding = successor.length > 0;
|
|
16339
|
+
const target = superseding ? "superseded" : "abandoned";
|
|
16340
|
+
const exitedAt = nowIso();
|
|
16341
|
+
const next = appendTerminalSection(transitionStatus(readFileSync8(active.path, "utf8"), target, exitedAt), superseding ? "superseded" : "abandoned", { reason: args.reason, ...superseding ? { successor } : {} }, exitedAt);
|
|
16342
|
+
writeFileSync5(active.path, next);
|
|
16305
16343
|
state.planPath = undefined;
|
|
16306
|
-
|
|
16307
|
-
|
|
16344
|
+
const section = `## ${exitedAt} — ${target}`;
|
|
16345
|
+
context.metadata({ title: superseding ? `Plan superseded: ${active.doc.goal}` : `Plan abandoned: ${active.doc.goal}` });
|
|
16346
|
+
return {
|
|
16347
|
+
title: superseding ? "plan superseded" : "plan abandoned",
|
|
16348
|
+
output: superseding ? `Plan superseded (not abandoned): ${relFrom(state.worktree, active.path)}. Terminal section "${section}" appended (successor: ${successor}). Write operations are restored.` : `Plan abandoned: ${relFrom(state.worktree, active.path)}. Terminal section "${section}" appended. Write operations are restored.`
|
|
16349
|
+
};
|
|
16308
16350
|
}
|
|
16309
16351
|
});
|
|
16310
16352
|
function goalDirOf(worktree) {
|
|
@@ -16826,7 +16868,7 @@ function crewOrchestrationTemplate(resolution) {
|
|
|
16826
16868
|
"",
|
|
16827
16869
|
"You are entering CREW ORCHESTRATION discipline for this session (single subject: do not switch agents). The macro contract above the crew may come from an approved plan, a spec change, or inline text — the discipline is identical in all three cases and never requires a plan. Follow it exactly:",
|
|
16828
16870
|
"",
|
|
16829
|
-
|
|
16871
|
+
'1. REGISTER: call `crew_begin` with the objective AND the declared subtask plan (subtasks[]: one {title} per subtask, optionally naming the intended forge-* agent). When the macro contract descends from a plan artifact, pass its path as the optional `lineage` argument (e.g. lineage: ".opencode/plan/2026-10-01-my-plan.md") — the descent is recorded as a first-class line in the crew record header; lineage is never inferred. Reconnaissance BEFORE registration is free (task calls included). If crew_begin refuses (a crew is already active here, or a plan draft is active), STOP and tell the user why — never start a second crew. A wrong plan is fixed by abandoning and re-crewing (crew_close {abandon: true, reason}), never by improvising outside the declared plan.',
|
|
16830
16872
|
`2. THE PAUSE: registration enters a PENDING crew — execution has NOT started. STOP and present the user the three execution-mode choices (supervised waves now / convert to a goal contract / standby), then END YOUR TURN. Arm per the user's choice: waves → crew_begin {execution: "waves"}; conversion → crew_begin {execution: "goal"} followed by goal_write (arm=true — its dialog is the user's gate) folding the declared subtasks into criteria/checks; standby → no call. EXCEPTION: while a live active goal governs this session, register with execution: "waves" in the SAME call — the crew is born armed (the goal contract already authorized orchestration) and crew_close / abandon stay ask-free under that goal.`,
|
|
16831
16873
|
"3. WAVES, NEVER FLOODS (armed crews only): execute subtasks in waves of parallel native `task` calls (subagent_type = the matching forge-* agent; a wave's results return within the calling turn). Launch the next wave ONLY after the previous wave's results are in. GUI / computer-use subtasks are mutually exclusive within a wave (the desktop is a singleton resource — at most ONE in flight; non-GUI subtasks still parallelize freely), and GUI walkthroughs use the `computer` tool rather than DIY shell screenshot pipelines.",
|
|
16832
16874
|
"4. MISSING ROLE: when a subtask needs a role no configured forge-* agent matches, run that subtask through a native task call anyway AND tell the user about the gap — suggest configuring the missing role in forge.json.",
|
|
@@ -16844,14 +16886,15 @@ var crewBeginTool = tool({
|
|
|
16844
16886
|
subtasks: tool.schema.array(tool.schema.object({
|
|
16845
16887
|
title: tool.schema.string().describe("Short unique subtask title — the crew_close report must use these exact titles"),
|
|
16846
16888
|
agent: tool.schema.string().optional().describe("Intended forge-* agent id for this subtask, when one matches")
|
|
16847
|
-
})).describe("The declared subtask plan — decompose the objective into these before any execution; crew_close cross-checks the report against exactly this list")
|
|
16889
|
+
})).describe("The declared subtask plan — decompose the objective into these before any execution; crew_close cross-checks the report against exactly this list"),
|
|
16890
|
+
lineage: tool.schema.string().optional().describe("Free-text origin of the macro contract — e.g. the plan artifact path this crew descends from (.opencode/plan/<date>-<slug>.md). Recorded as a first-class lineage: line in the crew record header and disclosed in the registration output. Explicit argument only — never inferred from recently-terminal plans.")
|
|
16848
16891
|
},
|
|
16849
16892
|
execute: async (args, context) => {
|
|
16850
16893
|
noteToolAgent(context);
|
|
16851
16894
|
const state = stateForBan(context.sessionID);
|
|
16852
16895
|
const active = state ? resolveActivePlan(state) : null;
|
|
16853
16896
|
if (active && active.doc.status === "draft") {
|
|
16854
|
-
throw new Error("[forge:crew] A plan is in draft; crew orchestration is denied during planning.
|
|
16897
|
+
throw new Error("[forge:crew] A plan is in draft; crew orchestration is denied during planning. Three exits: " + "plan_approve puts the plan to user approval (an APPROVED plan does not block /crew — only a draft does); " + "/plan discard abandons the plan outright; " + '/plan discard {supersede: "<successor path or id>"} moves the work into this crew (the plan becomes superseded, the successor recorded).');
|
|
16855
16898
|
}
|
|
16856
16899
|
const resolution = resolvePools();
|
|
16857
16900
|
const hostIds = resolution ? Object.keys(resolution.agents) : [];
|
|
@@ -16953,13 +16996,14 @@ var crewBeginTool = tool({
|
|
|
16953
16996
|
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).');
|
|
16954
16997
|
}
|
|
16955
16998
|
const governed = execution === "waves";
|
|
16999
|
+
const lineage = String(args.lineage ?? "").trim();
|
|
16956
17000
|
const anchorDir = anchor || hostWorktree;
|
|
16957
17001
|
let recordPath = null;
|
|
16958
17002
|
let recordNote = "";
|
|
16959
17003
|
if (anchorDir) {
|
|
16960
17004
|
try {
|
|
16961
17005
|
recordPath = crewRecordPathFor(anchorDir, objectiveText);
|
|
16962
|
-
writeCrewRecord(recordPath, objectiveText, anchorDir, subtasks);
|
|
17006
|
+
writeCrewRecord(recordPath, objectiveText, anchorDir, subtasks, lineage || undefined);
|
|
16963
17007
|
recordNote = `Crew record (inspectable history; NOT a resume mechanism): ${relFrom(anchorDir, recordPath)}`;
|
|
16964
17008
|
} catch {
|
|
16965
17009
|
recordPath = null;
|
|
@@ -16985,12 +17029,13 @@ var crewBeginTool = tool({
|
|
|
16985
17029
|
if (families.length > 8)
|
|
16986
17030
|
familyLines.push(` - …and ${families.length - 8} more pools`);
|
|
16987
17031
|
const lines = [
|
|
17032
|
+
...recordNote ? [recordNote] : [],
|
|
17033
|
+
...lineage ? [`Lineage: ${lineage}`] : [],
|
|
16988
17034
|
governed ? `[forge:crew] Crew registered UNDER THE GOVERNING GOAL — born armed, never pends. Objective: ${objectiveText}` : `[forge:crew] Crew registered PENDING for this session (execution has NOT started). Objective: ${objectiveText}`,
|
|
16989
17035
|
`Declared plan (${subtasks.length}):`,
|
|
16990
17036
|
...subtasks.map((s, i) => ` ${i + 1}. ${s.title}${s.agent ? ` → ${s.agent}` : ""}`),
|
|
16991
17037
|
`Dispatchable roster origin — ${families.length} pool${families.length === 1 ? "" : "s"} across the host anchor set${hostAnchors.length > 0 ? ` (anchors: ${hostAnchors.join(", ")})` : ""}:`,
|
|
16992
17038
|
...familyLines,
|
|
16993
|
-
...recordNote ? [recordNote] : [],
|
|
16994
17039
|
...disclosure ? [disclosure] : []
|
|
16995
17040
|
];
|
|
16996
17041
|
if (governed) {
|
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.14.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
|
+
}
|