@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.
Files changed (3) hide show
  1. package/README.md +44 -17
  2. package/dist/index.js +72 -27
  3. 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` (exit: `abandoned`).
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 hot-apply —
423
- edits, and files appearing or disappearing, take effect at the next config
424
- hook without a host restart (see the apply-timing table below).
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** — file truth is hot; only the anchor set is frozen at
535
- host start:
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 — applies at the next config hook (materialization re-runs there; the crew gate and depth lookups always resolve fresh) |
541
- | a NEW directory joining the host (another project's session on the shared host) | no restart — the re-initialization itself adds the anchor and its pools |
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 confirms the roster **grouped by pool with its
585
- origins** (which forge.json families the dispatchable set materialized
586
- from, the primary marked), points at the crew record file, presents the
587
- three execution-mode choices, and stops the turn. Reconnaissance task
588
- calls are free BEFORE registration.
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 config hook — no restart needed;
649
- on a shared host, opening a session in a new workspace adds that anchor and
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 !== null) {
12513
- flush();
12514
- current = header;
12515
- } else if (current !== null) {
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
- writeFileSync5(active.path, transitionStatus(readFileSync8(active.path, "utf8"), "done", nowIso()));
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. Terminal: the file stays in .opencode/plan/ as history. Invoke via /plan discard or directly when the user cancels the task.",
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 (_args, context) => {
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
- writeFileSync5(active.path, transitionStatus(readFileSync8(active.path, "utf8"), "abandoned", nowIso()));
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
- context.metadata({ title: `Plan abandoned: ${active.doc.goal}` });
16307
- return { title: "plan abandoned", output: `Plan abandoned: ${relFrom(state.worktree, active.path)}. Write operations are restored.` };
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
- "1. REGISTER: call `crew_begin` with the objective AND the declared subtask plan (subtasks[]: one {title} per subtask, optionally naming the intended forge-* agent). 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.",
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. Call plan_approve for user approval, or /plan discard to abandon the plan.");
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.13.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
+ }