@litfamily/litopencode 1.0.34 → 1.0.36

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 (51) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README-Ko-KR.md +6 -6
  3. package/README.md +6 -6
  4. package/dist/activation-primary-prompts.js +7 -3
  5. package/dist/activation.js +5 -0
  6. package/dist/adhd-next.d.ts +1 -1
  7. package/dist/adhd-next.js +11 -0
  8. package/dist/cli/agent-file-command.d.ts +21 -0
  9. package/dist/cli/agent-file-command.js +99 -0
  10. package/dist/cli/agent-file.d.ts +46 -0
  11. package/dist/cli/agent-file.js +197 -0
  12. package/dist/cli/args.d.ts +1 -0
  13. package/dist/cli/args.js +25 -1
  14. package/dist/cli/doctor.js +10 -0
  15. package/dist/cli/install.d.ts +4 -1
  16. package/dist/cli/install.js +8 -3
  17. package/dist/cli/managed-block.d.ts +26 -0
  18. package/dist/cli/managed-block.js +80 -0
  19. package/dist/cli/model-routing.d.ts +5 -0
  20. package/dist/cli/model-routing.js +1 -1
  21. package/dist/cli/types.d.ts +10 -0
  22. package/dist/cli.js +14 -1
  23. package/dist/commands.js +17 -4
  24. package/dist/native-goal-ledger.d.ts +43 -0
  25. package/dist/native-goal-ledger.js +74 -0
  26. package/dist/v2/commands.d.ts +1 -0
  27. package/dist/v2/commands.js +2 -2
  28. package/dist/v2/context.d.ts +6 -1
  29. package/dist/v2/native-goal-runtime.d.ts +74 -0
  30. package/dist/v2/native-goal-runtime.js +193 -0
  31. package/dist/v2/native-goal.d.ts +49 -0
  32. package/dist/v2/native-goal.js +71 -0
  33. package/dist/v2/server.js +11 -1
  34. package/dist/v2/session-hooks.d.ts +4 -0
  35. package/dist/v2/session-hooks.js +13 -0
  36. package/docs/assets/readme/badge-version.svg +1 -1
  37. package/docs/migration.md +1 -1
  38. package/docs/reference-Ko-KR.md +27 -0
  39. package/docs/reference.md +33 -3
  40. package/package.json +2 -2
  41. package/skills/lit-adhd/SKILL.md +3 -3
  42. package/skills/lit-adhd/references/brief-format.md +1 -1
  43. package/skills/lit-adhd/references/status-taxonomy.md +1 -1
  44. package/skills/lit-adhd/scripts/rewind.mjs +73 -14
  45. package/skills/lit-init/SKILL.md +4 -0
  46. package/skills/litgoal/SKILL.md +2 -2
  47. package/skills/litwork/SKILL.md +4 -0
  48. package/skills/managed-skill-manifest.json +3 -3
  49. package/skills/native-goal-verdict/SKILL.md +34 -60
  50. package/skills/start-work/SKILL.md +4 -0
  51. package/skills/workflow-loop/SKILL.md +4 -0
@@ -265,7 +265,10 @@ function goalOf(raw) {
265
265
  };
266
266
  }
267
267
 
268
- // The first list item or line of text under a `Next` or `Next Steps` heading, without its marker.
268
+ // An item someone already marked finished; the words are matched case-sensitively, so "done" in prose stays open.
269
+ const HANDOFF_DONE = /✅|✓|^\[[xX]\]|~~.+~~|\b(?:DONE|SHIPPED)\b/u;
270
+
271
+ // The first open list item or line of text under a `Next` or `Next Steps` heading, without its marker.
269
272
  function handoffNextOf(text) {
270
273
  const lines = text.split(/\r?\n/u);
271
274
  const start = lines.findIndex((line) => /^#{1,6}\s+Next(?:\s+Steps)?\s*$/iu.test(line.trim()));
@@ -273,7 +276,7 @@ function handoffNextOf(text) {
273
276
  for (const line of lines.slice(start + 1)) {
274
277
  if (/^#{1,6}\s/u.test(line.trim())) return undefined;
275
278
  const item = line.trim().replace(/^(?:[-*+]|\d+[.)])\s+/u, "");
276
- if (item !== "") return item;
279
+ if (item !== "" && !HANDOFF_DONE.test(item)) return item;
277
280
  }
278
281
  return undefined;
279
282
  }
@@ -324,7 +327,7 @@ export function readLocalState(dir) {
324
327
  const record = readJson(join(livenessDir, name), 1024);
325
328
  const atMs = isRecord(record) ? Date.parse(str(record.at)) : Number.NaN;
326
329
  if (isRecord(record) && SAFE_SESSION.test(str(record.sessionId)) && ["alive", "idle", "ended"].includes(record.state) && Number.isFinite(atMs)) {
327
- state.liveness.set(record.sessionId, { state: record.state, atMs });
330
+ state.liveness.set(record.sessionId, { state: record.state, atMs, pane: typeof record.herdrPane === "string" ? clean(record.herdrPane, 32) : undefined });
328
331
  }
329
332
  }
330
333
  } catch {
@@ -339,8 +342,17 @@ export function readLocalState(dir) {
339
342
  } catch {
340
343
  // no stacks recorded for this worktree
341
344
  }
345
+ // The newest handoff file first: a lane's fresh HANDOFF beats an old root one.
342
346
  try {
343
- for (const name of readdirSync(dir).filter((entry) => /^HANDOFF.*\.md$/u.test(entry)).slice(0, 3)) {
347
+ const changed = (name) => {
348
+ try {
349
+ return lstatSync(join(dir, name)).mtimeMs;
350
+ } catch {
351
+ return 0;
352
+ }
353
+ };
354
+ const names = readdirSync(dir).filter((entry) => /^HANDOFF.*\.md$/u.test(entry)).slice(0, 40);
355
+ for (const name of names.map((entry) => [entry, changed(entry)]).sort((a, b) => b[1] - a[1]).slice(0, 3).map(([entry]) => entry)) {
344
356
  const text = readSmall(join(dir, name), FILE_CAP);
345
357
  if (text === undefined) continue;
346
358
  state.handoff.push(...[...text.matchAll(/^#{1,3}\s+(.+)$/gmu)].slice(0, 4).map((match) => clean(match[1], 80)));
@@ -607,6 +619,7 @@ export function buildCard({ row, detail, local, lanes, now, self = false }) {
607
619
  },
608
620
  updated: new Date(row.updated).toISOString(),
609
621
  boundary: local.work?.sessionID === row.id && local.work.boundary ? clean(local.work.boundary, 80) : undefined,
622
+ ...(local.liveness?.get(row.id)?.pane ? { pane: local.liveness.get(row.id).pane } : {}),
610
623
  };
611
624
  }
612
625
 
@@ -622,7 +635,37 @@ function roadmapOf(local) {
622
635
  return out;
623
636
  }
624
637
 
625
- export function recommend({ groups, sessionID }) {
638
+ /**
639
+ * Herdr agents at work beside this session, read only under HERDR_ENV=1: working or blocked, in this
640
+ * herdr workspace or with a cwd inside one of `dirs`, never the asking pane. A failed, slow or malformed
641
+ * `herdr agent list` reads "unknown" and counts no lane.
642
+ */
643
+ export function readHerdrLanes(dirs, env = process.env) {
644
+ if (env.HERDR_ENV !== "1") return { state: "off", agents: [] };
645
+ let doc;
646
+ try {
647
+ doc = JSON.parse(execFileSync("herdr", ["agent", "list"], { encoding: "utf8", timeout: 3000, env, stdio: ["ignore", "pipe", "ignore"], maxBuffer: 1024 * 1024 }));
648
+ } catch {
649
+ return { state: "unknown", agents: [] };
650
+ }
651
+ const list = isRecord(doc) && isRecord(doc.result) && Array.isArray(doc.result.agents) ? doc.result.agents : undefined;
652
+ if (list === undefined) return { state: "unknown", agents: [] };
653
+ const inside = (cwd) => isAbsolute(str(cwd)) && dirs.some((dir) => {
654
+ const rel = relative(dir, resolve(str(cwd)));
655
+ return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
656
+ });
657
+ const ownPane = str(env.HERDR_PANE_ID);
658
+ const workspace = str(env.HERDR_WORKSPACE_ID);
659
+ const agents = list
660
+ .filter((entry) => isRecord(entry) && (entry.agent_status === "working" || entry.agent_status === "blocked"))
661
+ .filter((entry) => ownPane === "" || str(entry.pane_id) !== ownPane)
662
+ .filter((entry) => (workspace !== "" && str(entry.workspace_id) === workspace) || inside(entry.cwd) || inside(entry.foreground_cwd))
663
+ .slice(0, 40)
664
+ .map((entry) => ({ pane: clean(str(entry.pane_id), 32), status: entry.agent_status, session: isRecord(entry.agent_session) ? str(entry.agent_session.value) : "", title: clean(str(entry.terminal_title_stripped), 60) }));
665
+ return { state: "read", agents };
666
+ }
667
+
668
+ export function recommend({ groups, sessionID, herdr = [] }) {
626
669
  const cards = groups.flatMap((group) => group.sessions.map((card) => ({ card, group })));
627
670
  const waiting = cards.find(({ card }) => card.status === "waits-on-you");
628
671
  if (waiting) {
@@ -631,19 +674,34 @@ export function recommend({ groups, sessionID }) {
631
674
  }
632
675
  const blocked = cards.find(({ card }) => card.status === "blocked");
633
676
  if (blocked) return { kind: "session", sessionID: blocked.card.id, text: `Open "${clip(blocked.card.goal, 50)}": its last step failed and nothing has run since.` };
634
- const paused = cards.find(({ card }) => card.status === "paused");
677
+ // Lanes: other sessions at work here, and herdr agents beside this one; one seen both ways counts once.
678
+ // They are counted before anything that resumes work here: while a lane runs, this session's own paused
679
+ // frame and its own run are held, and only an approval (above) stays the user's call.
680
+ const working = cards.filter(({ card }) => card.status === "working" && card.id !== sessionID);
681
+ const seen = new Set(working.flatMap(({ card }) => [card.id, card.pane].filter(Boolean)));
682
+ const agents = herdr.filter((agent) => agent.session === "" || (agent.session !== sessionID && !seen.has(agent.session))).filter((agent) => !seen.has(agent.pane));
683
+ const lanes = working.length + agents.length;
684
+ const paused = cards.find(({ card }) => card.status === "paused" && !(lanes > 0 && card.id === sessionID));
635
685
  if (paused) {
636
686
  return paused.card.id === sessionID
637
687
  ? { kind: "continue", sessionID, text: `Return to the paused discipline in "${clip(paused.card.goal, 50)}" where it stopped.` }
638
688
  : { kind: "session", sessionID: paused.card.id, text: `Open "${clip(paused.card.goal, 50)}" and say lit continue.` };
639
689
  }
640
- const working = cards.find(({ card }) => card.status === "working" && card.id !== sessionID);
641
- if (working) {
642
- const doing = working.card.facts.lastAction ? ` (now: ${working.card.facts.lastAction})` : "";
643
- return { kind: "session", sessionID: working.card.id, text: `"${clip(working.card.goal, 50)}" in ${working.group.worktree} is still running${doing}; check on it before starting more.` };
690
+ if (lanes > 0) {
691
+ const plural = `${lanes} lane${lanes === 1 ? "" : "s"}`;
692
+ const waiting = agents.find((agent) => agent.status === "blocked");
693
+ const first = working[0];
694
+ const doing = first?.card.facts.lastAction ? ` (now: ${first.card.facts.lastAction})` : "";
695
+ const what = first ? `"${clip(first.card.goal, 50)}" in ${first.group.worktree} is still running${doing}` : `${agents[0].title || "an agent"} in pane ${agents[0].pane}`;
696
+ const text = waiting
697
+ ? `A lane waits on you: answer the one waiting on you in pane ${waiting.pane}, then wait for ${plural} to finish before starting more.`
698
+ : `Wait for ${plural} to finish before starting more: ${what}.`;
699
+ return { kind: "lanes", sessionID: first?.card.id ?? sessionID, lanes, text, short: waiting ? `answer pane ${waiting.pane}, then wait for ${plural}` : `wait for ${plural} to finish` };
644
700
  }
645
701
  const run = groups.find((group) => group.work?.status === "active");
646
702
  if (sessionID !== undefined && run?.work.sessionID === sessionID) return { kind: "work", sessionID, text: "Keep the running start-work run going from its checked plan." };
703
+ // Another session's active run is followed from that session, never started again here.
704
+ if (run) return { kind: "session", sessionID: run.work.sessionID || undefined, text: `start-work on ${run.work.plan || "its plan"} is running in another session; open that session to follow it.` };
647
705
  const here = groups.find((group) => group.current);
648
706
  const open = here?.plans.filter((plan) => plan.done < plan.total) ?? [];
649
707
  if (open.length === 1) return { kind: "plan", sessionID, target: open[0].path, text: `Start work on ${open[0].path}: ${open[0].total - open[0].done} step${open[0].total - open[0].done === 1 ? "" : "s"} left.` };
@@ -701,7 +759,8 @@ export async function collect(options = {}) {
701
759
  const legacy = openLegacy(options.storage ?? join(dataHome, "opencode", "storage"));
702
760
  store = legacy.error ? { ...store, error: `${store.error}; ${legacy.error}` } : legacy;
703
761
  }
704
- const report = { schema: REWIND_SCHEMA, generatedAt: new Date(now).toISOString(), root: clean(root, 200), source: { store: store.kind ?? "none", ...(store.error ? { note: store.error } : {}) }, groups: [] };
762
+ const herdr = readHerdrLanes(dirs.length > 0 ? dirs : [root], options.env ?? process.env);
763
+ const report = { schema: REWIND_SCHEMA, generatedAt: new Date(now).toISOString(), root: clean(root, 200), source: { store: store.kind ?? "none", ...(store.error ? { note: store.error } : {}), ...(herdr.state === "off" ? {} : { herdr: herdr.state }) }, groups: [] };
705
764
  const rows = store.kind ? store.sessions(dirs, limit) : [];
706
765
  const lanes = store.kind ? store.lanes(rows.map((row) => row.id)) : new Map();
707
766
  for (const { entry, dir, local } of locals) {
@@ -728,7 +787,7 @@ export async function collect(options = {}) {
728
787
  });
729
788
  }
730
789
  report.plansSkipped = locals.reduce((total, { local }) => total + local.plansSkipped, 0);
731
- report.recommended = recommend({ groups: report.groups, sessionID: options.session });
790
+ report.recommended = recommend({ groups: report.groups, sessionID: options.session, herdr: herdr.agents });
732
791
  const all = report.groups.flatMap((group) => group.sessions);
733
792
  const waits = all.filter((card) => card.status === "waits-on-you").length;
734
793
  report.headline = all.length === 0
@@ -740,7 +799,7 @@ export async function collect(options = {}) {
740
799
  if (options.writeNext && forThisSession && (kind === "continue" || kind === "work" || kind === "plan")) {
741
800
  const record = { kind, target: report.recommended.target ?? kind, at: now, sessionID: options.session };
742
801
  if (writeNext(root, record)) report.nextRecord = { kind, target: record.target };
743
- } else if (options.writeNext && options.session !== undefined && kind !== "none" && kind !== "handoff") {
802
+ } else if (options.writeNext && options.session !== undefined && kind !== "none" && kind !== "handoff" && kind !== "lanes") {
744
803
  // The step belongs to another session or worktree: go here cannot run it, so it answers with where to go.
745
804
  const record = { kind: "elsewhere", target: clip(report.recommended.text, 300), at: now, sessionID: options.session };
746
805
  if (writeNext(root, record)) report.nextRecord = { kind: "elsewhere", target: record.target };
@@ -889,7 +948,7 @@ export function renderBrief(report, { ascii = false, width = 72, sessionID } = {
889
948
  for (const text of wrapCells(report.recommended.text, inner)) lines.push(`${glyph("│", "|")} ${padCells(text, inner)} ${glyph("│", "|")}`);
890
949
  if (runsHere) lines.push(`${glyph("│", "|")} ${padCells("type: go", inner)} ${glyph("│", "|")}`);
891
950
  lines.push(`${glyph("╰", "+")}${glyph("─", "-").repeat(inner + 2)}${glyph("╯", "+")}`, "```", "");
892
- lines.push(runsHere ? "Type **`go`** to do it." : report.nextRecord ? "Nothing runs on its own here: open that session, or say what to do (`go` says where)." : "Nothing runs on its own here: open that session, or say what to do.");
951
+ lines.push(runsHere ? "Type **`go`** to do it." : report.recommended.kind === "lanes" ? "Do not type `go` until they finish." : report.nextRecord ? "Nothing runs on its own here: open that session, or say what to do (`go` says where)." : "Nothing runs on its own here: open that session, or say what to do.");
893
952
  if (report.plansSkipped > 0) lines.push("", `*${report.plansSkipped} ${report.plansSkipped === 1 ? "plan was" : "plans were"} not read (read budget used up).*`);
894
953
  return lines.join("\n");
895
954
  }
@@ -197,6 +197,10 @@ Every lit-init run should try to prove itself wrong before completion. Pick at l
197
197
 
198
198
  If a claim cannot be verified quickly, mark it as uncertain or omit it. Future agents are better served by a small verified map than a large speculative one.
199
199
 
200
+ ## LitFamily Project Block
201
+
202
+ When the user has approved writing the root `AGENTS.md`, the short LitFamily block can go in with `litopencode agent-file project --dir <repo root>`. It writes only the lines between its own `litfamily:litopencode` markers at the end of the file, keeps every byte of the hand-written text, makes no backup copy next to tracked files, and reports `unchanged` on a repeat. A malformed marker pair or a symlinked `AGENTS.md` is refused and reported, never rewritten. Run `litopencode agent-file project --dir <repo root> --dry-run` first when the file already exists, and treat the block as one more thing the receipt lists.
203
+
200
204
  ## Completion Receipt
201
205
 
202
206
  The final receipt should list files created, files updated, directories intentionally skipped, key evidence sources, tests or checks run, and cleanup. If no files were written because the run was discovery-only, say so. If local handoff or ledger state influenced the map, state that it was cross-checked. If any temp files or exploration artifacts were created, remove them or identify them as ignored evidence. Do not commit the guidance unless the user explicitly asks for a commit.
@@ -128,7 +128,7 @@ litgoal sets and keeps the finish line; lit-loop and start-work run toward it.
128
128
  - **Keeps.** The goal survives context loss, compaction, and a new session. Status reads the ledger, not chat memory; an entry is a lead until the evidence is replayed.
129
129
  - **Does not run.** litgoal never edits product files and never executes the work. `lit-loop` pursues the criteria turn by turn, and `start-work` executes an approved plan against them under the schema-3 bounded-authority record. Both keep their progress and verification receipts in the same `.litopencode/litgoal` state.
130
130
  - **Does not decide completion.** A criterion is met when its evidence is replayed on the real surface, not when a turn says so. `review-work` challenges the claim before it stands.
131
- - **Is not the host's goal feature.** The verified OpenCode host exposes no native goal primitive, so litgoal is package-owned durable state. See `native-goal-verdict` before promising a host-side objective.
131
+ - **Is not the host's goal feature.** litgoal is package-owned durable state on every host. OpenCode 2.2 and later also has a native `/goal`, which LitOpenCode sets for lit-loop, start-work and litwork and mirrors from this ledger; done needs both. See `native-goal-verdict` before promising a host-side objective.
132
132
 
133
133
  The route name, the command, and the skill are one word: `litgoal`. `/lit-goal` is the hyphenated alias.
134
134
 
@@ -188,7 +188,7 @@ remains denied.
188
188
 
189
189
  ## Ledger Operating Model
190
190
 
191
- The litgoal skill explains the package-owned state model that LitOpenCode uses when the verified OpenCode host does not expose a native goal primitive. The state root is `.litopencode/litgoal`, and the primary loop ledger is `.litopencode/litgoal/lit-loop/ledger.jsonl`. This path is local workflow state, not package source. It should never be treated as a release payload, a benchmark fixture, or a substitute for current tests. It exists so the loop can survive context loss, command interruption, and cross-session continuation without storing raw secrets or pretending that chat memory is durable.
191
+ The litgoal skill explains the package-owned state model that LitOpenCode uses on every host, including OpenCode 2.2 and later where a native `/goal` also exists and the ledger stays the second half of done. The state root is `.litopencode/litgoal`, and the primary loop ledger is `.litopencode/litgoal/lit-loop/ledger.jsonl`. This path is local workflow state, not package source. It should never be treated as a release payload, a benchmark fixture, or a substitute for current tests. It exists so the loop can survive context loss, command interruption, and cross-session continuation without storing raw secrets or pretending that chat memory is durable.
192
192
 
193
193
  Each durable event should be understandable as a small fact about the work loop. Good events identify the phase, the goal or slice identifier, the relevant files or commands, the result, and a redacted evidence pointer. Bad events paste raw user data, full command output containing credentials, large source files, private URLs, or unreviewed external text. The ledger is evidence metadata, not a data lake. When in doubt, store a short summary and a path to a local evidence file whose contents were intentionally created for that purpose.
194
194
 
@@ -225,3 +225,7 @@ For any activation change, collect evidence by layer: source registration, runti
225
225
  ## Safe Defaults
226
226
 
227
227
  Default activation should be conservative. It should prefer planning or status over mutation when intent is unclear. It should ask for an approved plan before execution. It should preserve `lit-plan` boundaries even when permission mode is relaxed. It should keep recap read-only. It should avoid writing durable raw arguments. These defaults make accidental activation recoverable.
228
+
229
+ ## Native goal on OpenCode 2
230
+
231
+ On OpenCode 2.2 and later, a `litwork` request gets a native `/goal` from LitOpenCode itself when the session has none; bare `lit` and the read-only routes do not. LitOpenCode resumes a goal it created only after a server restart, clears it only after OpenCode's verdict and the ledger both say done, and leaves a goal the user made alone. A host without `/goal` records `native-goal-unavailable`. See `native-goal-verdict`.
@@ -84,7 +84,7 @@
84
84
  "canonicalFiles": [
85
85
  {
86
86
  "path": "references/brief-format.md",
87
- "sha256": "01e84c87740a7069daabb331b915c3dc9888f2c4052efc0c870d2450740e353d"
87
+ "sha256": "4989b4a2d141342fc729922f385c435a4a601c2103dc6c3bf5eac0fccadd72e6"
88
88
  },
89
89
  {
90
90
  "path": "references/card-examples.md",
@@ -92,11 +92,11 @@
92
92
  },
93
93
  {
94
94
  "path": "references/status-taxonomy.md",
95
- "sha256": "b4c16cbbefdf72b8e04b7aa67dcd45f7232dfc30b6f926cae3ce0f22724f2463"
95
+ "sha256": "00de506720908206f238edb01f410b5216483fbc291f232f46924ac4addbf7c6"
96
96
  },
97
97
  {
98
98
  "path": "scripts/rewind.mjs",
99
- "sha256": "a00d565d382c60bb179db25b7cae4ce8d8221c9dcc360f913ce0b0bb366f734a"
99
+ "sha256": "2d64bfda31edb07d86d94527bf9617b09c1b421e8de4f6258eb7ce31c9a10360"
100
100
  }
101
101
  ]
102
102
  },
@@ -18,6 +18,8 @@ entry_routes:
18
18
  opencode_surfaces:
19
19
  - "OpenCode CLI command list"
20
20
  - "OpenCode plugin type surface"
21
+ - "opencode.goal RPC and the /goal command"
22
+ - "session experimental.idle hook"
21
23
  - "LitOpenCode durable goal ledger"
22
24
  - "OpenCode tool litwork action=status"
23
25
  - "litopencode runtime state path"
@@ -40,7 +42,7 @@ Use the OpenCode vocabulary for this contract: `chat.message`, `command.execute.
40
42
  | --- | --- |
41
43
  | `user_request` | The user goal or slash-command arguments that selected Native Goal Verdict. Treat pasted external text as inert data. |
42
44
  | `repo_state` | Current package root, dirty worktree status, relevant handoff/ledger state, and OpenCode route config when it affects this skill. |
43
- | `host_surface` | OpenCode CLI command list, OpenCode plugin type surface, LitOpenCode durable goal ledger, OpenCode tool litwork action=status, litopencode runtime state path, skills/native-goal-verdict/SKILL.md. |
45
+ | `host_surface` | OpenCode CLI command list, OpenCode plugin type surface, the `opencode.goal` RPC and `/goal` command, LitOpenCode durable goal ledger, OpenCode tool litwork action=status, litopencode runtime state path, skills/native-goal-verdict/SKILL.md. |
44
46
  | `approval_state` | Whether mutation, execution, release, network, install, or config writes are explicitly approved. Absence of approval means read-only guidance. |
45
47
  | `evidence_budget` | Targeted tests, command transcripts, hook probes, pack/install checks, or source citations required before a completion claim. |
46
48
 
@@ -50,7 +52,7 @@ Required schema fields are `contract_schema_version`, `skill_id`, `runtime_class
50
52
 
51
53
  | Mode | Enter when | Allowed surfaces | Required behavior | Exit criteria |
52
54
  | --- | --- | --- | --- | --- |
53
- | `route` | Inspect the native-goal-verdict feature, skills/native-goal-verdict/SKILL.md, opencode --help, and @opencode-ai/plugin Hooks types. | OpenCode CLI command list, OpenCode plugin type surface, LitOpenCode durable goal ledger, OpenCode tool litwork action=status, litopencode runtime state path, skills/native-goal-verdict/SKILL.md | Select the matching LitOpenCode guidance and preserve static-documentation boundaries. | The intended skill, command, hook, tool, or route is identified with evidence. |
55
+ | `route` | Inspect the native-goal-verdict feature, skills/native-goal-verdict/SKILL.md, the host's `/goal` command, and the `@opencode/plugin` goal and idle types. | OpenCode CLI command list, OpenCode plugin type surface, LitOpenCode durable goal ledger, OpenCode tool litwork action=status, litopencode runtime state path, skills/native-goal-verdict/SKILL.md | Select the matching LitOpenCode guidance and preserve static-documentation boundaries. | The intended skill, command, hook, tool, or route is identified with evidence. |
54
56
  | `execute` | The user explicitly approved implementation or the surface is already an execution surface. | Approved OpenCode agents, tools, and repository commands. | Apply minimum-first changes, protect unrelated files, and keep evidence checkpoints. | Tests and real-surface probes pass or a precise blocker is reported. |
55
57
  | `review` | A DoneClaim, release claim, or completion claim is about to be made. | Review-work, targeted tests, scanners, CLI probes, package checks. | Challenge scope, outputs, evidence, safety, and cleanup. | Findings are resolved or listed as risks/limitations. |
56
58
  | `blocked` | Required approval, credentials, host capability, or evidence is missing. | Read-only reporting only. | Stop without inventing success and state the smallest unblocker. | User supplies the missing decision/evidence or scope changes. |
@@ -121,96 +123,68 @@ This is static documentation for LitOpenCode runtime discovery. Do not execute c
121
123
 
122
124
  ## Verdict
123
125
 
124
- The verified OpenCode host surface does not currently expose a native goal primitive through the local CLI command list or the `@opencode-ai/plugin` Hooks type surface. LitOpenCode must not promise direct native goal binding unless a future OpenCode release exposes that primitive.
126
+ OpenCode 2.2 and later has a native `/goal`; OpenCode 1 and OpenCode 2 builds before 2.2 do not. The goal belongs to a session: a built-in plugin keeps `{ id, objective, status, reason, rounds }` for it, a reviewer decides after each idle turn whether the objective is met, and the user steers it with `/goal <objective> | pause | resume | clear`. A plugin reads the goal through the `opencode.goal` RPC (`get({ sessionID })`) and changes it only by sending the `/goal` command to the session. The record has no owner field.
125
127
 
126
- LitOpenCode therefore treats durable goal state as a package-owned capability under `.litopencode/litgoal`, with activation through `lit`, `litwork`, `start-work`, `review-work`, tools, commands, and plugin hooks.
128
+ LitOpenCode uses that surface in a narrow way. On `lit-loop`, `start-work` and `litwork` it checks the session's goal first and sets one only when none exists. The objective is the work plus the completion conditions: tests pass, a real-surface check ran, temporary files are cleaned up with a receipt, and the ledger marks the goal complete. Read-only routes (`lit-plan`, `lit-research`, `review-work`, `lit-recap`, `lit-adhd`, `lit-brainstorm` and the rest) never set a goal.
127
129
 
128
- ## Verification Route
130
+ The ledger under `.litopencode/litgoal` stays the evidence record on every host. A goal counts as done only when OpenCode's verdict and the ledger both say so. When OpenCode says the goal is met while the ledger is still open, LitOpenCode prints a conflict line and does not report done.
129
131
 
130
- - Check `opencode --help` for the current CLI command list.
131
- - Check the installed `@opencode-ai/plugin` Hooks type for plugin extension points.
132
- - Run `node --test test/runtime-skills.test.mjs`.
133
- - Run `node --test test/docs.test.mjs`.
132
+ ## What LitOpenCode may and may not touch
134
133
 
135
- ## Safety Notes
134
+ - It remembers the goal id it created (ledger event `native-goal.created`) and treats that goal as its own. Any other goal is the user's.
135
+ - Its own goal: it resumes one that the server paused on a restart (at most twice), and clears one after both verdicts agree. A goal paused for any other reason (the user, the round limit, no progress, a blocked reviewer) is reported, never resumed.
136
+ - A user's goal is reported once as `left yours alone` and is never resumed, cleared, paused or overwritten. The `/goal <objective>` command replaces an existing goal without asking, so LitOpenCode reads before every create and treats an unreadable state as a conflict, not as "no goal".
137
+ - Every change prints one line that starts with `LitOpenCode goal:` and is recorded in the ledger (`native-goal.created`, `.resumed`, `.cleared`, `.left-alone`, `.conflict`, `.failed`, `.unavailable`).
138
+ - A failed `/goal` call is recorded and the work continues on the ledger alone.
136
139
 
137
- - Do not infer host goal support from naming similarity alone.
138
- - Keep the public README aligned with the verified host capability.
139
- - If native support appears in a future host version, add a new feature binding and a migration note before changing runtime behavior.
140
+ ## Hosts without `/goal`
140
141
 
141
- ## Why the Verdict Matters
142
+ OpenCode 1 and OpenCode 2 builds without the `opencode.goal` RPC record `native-goal-unavailable` once per session and route, set nothing, and keep today's behaviour. Do not fake a goal, and do not tell the user one exists. The `litgoal` skill and its ledger work the same everywhere.
142
143
 
143
- Goal behavior is central to LitOpenCode’s identity, but the implementation must match the host surface that actually exists. If OpenCode exposes a native goal primitive in a future release, LitOpenCode can consider binding to it. Until then, the package should not promise that `/goal` or any host-owned goal state exists. Overclaiming native support would mislead users, break tests, and make durable state migration unsafe.
144
+ ## Verification Route
144
145
 
145
- The current verdict is intentionally conservative: local evidence has not shown a native OpenCode goal primitive through the CLI command list or the `@opencode-ai/plugin` hooks type surface. Therefore LitOpenCode owns its goal state under `.litopencode/litgoal`. This package-owned ledger is not a second-class fallback; it is the verified implementation path for resumable work, evidence checkpoints, and recap continuity.
146
+ - On OpenCode 2, run `opencode2` (it installs beside `opencode`) in a temporary project and watch the `LitOpenCode goal:` lines for `lit-loop`, a user-typed `/goal` first, and `lit plan`.
147
+ - On OpenCode 1, run `lit-loop` in the same project and expect `native-goal-unavailable` in `.litopencode/litgoal/lit-loop/ledger.jsonl`.
148
+ - Run `node --test test/v2-native-goal.test.mjs test/v2-native-goal-wiring.test.mjs test/native-goal-v1.test.mjs`.
149
+ - Run `node --test test/runtime-skills.test.mjs` and `node --test test/docs.test.mjs`.
146
150
 
147
151
  ## Evidence Standard for Changing the Verdict
148
152
 
149
- Changing the verdict requires current host evidence. Acceptable evidence includes official OpenCode documentation for a goal primitive, local CLI help that lists a goal command or goal API, plugin type definitions that expose goal hooks or goal storage, and working integration tests that prove the behavior. Naming similarity, user memory, old screenshots, or another product’s goal feature are not enough.
150
-
151
- The evidence should be captured in tests or docs. If a future OpenCode release adds native goals, add a feature binding, migration docs, runtime tests, and compatibility behavior before changing public claims. The migration should explain what happens to existing `.litopencode/litgoal` state, whether state is copied, read-only migrated, or left as historical evidence, and how users can verify the change.
153
+ The verdict rests on the merged OpenCode sources (the goal plugin, the `Goal` schema and `SessionApi.command`), the published `@opencode/plugin` 2.2.5 types, and a run on a real 2.2.5 host. A later host release changes the details, so record the OpenCode version, the package version and what the RPC returned before changing any claim. Naming similarity, memory, old screenshots or another product's goal feature are not evidence.
152
154
 
153
155
  ## Package-Owned Ledger Behavior
154
156
 
155
- Because native host support is not verified, LitOpenCode uses package-owned durable state. The state path `.litopencode/litgoal` is local to the project and should stay out of package payload and commits unless a future task explicitly changes that design. Tool and command surfaces such as `lit`, `litwork`, `start-work`, and `review-work` can read or write ledger events through the package’s own implementation. Recap can read ledger state without writing it. Static skills can document the state but do not execute it.
156
-
157
- This design keeps behavior testable. Ledger tests can cover JSONL append, recovery, malformed lines, redaction, and status reads. Docs tests can ensure the verdict remains visible. Pack payload tests can ensure local state is excluded. Reviewers can reason about the exact path and event model rather than relying on an unspecified host feature.
157
+ The state path `.litopencode/litgoal` is local to the project and stays out of the package payload and commits. `lit`, `litwork`, `start-work` and `review-work` read and write ledger events through the package's own implementation; recap reads without writing. The ledger tests cover JSONL append, recovery, malformed lines, redaction and status reads, and the native-goal tests cover the ownership records on top of them.
158
158
 
159
159
  ## Wording Rules
160
160
 
161
- Use precise language: “OpenCode host surface does not currently expose a native goal primitive” and “LitOpenCode uses `.litopencode/litgoal`.” Avoid phrases that imply a hidden binding, such as “uses OpenCode goals internally,” unless future evidence proves it. Avoid promising `/goal` behavior. Avoid saying “native goal support is impossible”; host capabilities can change. The right statement is current, evidence-bound, and revisable.
162
-
163
- Public docs should align with this verdict. If README, release checklist, runtime skill docs, and command prompts disagree, users will not know which state is authoritative. When updating one surface, search for related wording and run docs tests. Do not introduce old host terms or sibling product terms while explaining the verdict.
161
+ Say "OpenCode 2.2 and later has a native `/goal`" and "LitOpenCode sets it only for lit-loop, start-work and litwork". Say "OpenCode 1 has none" for older hosts. Do not say LitOpenCode "uses OpenCode goals internally" for read-only routes, do not promise `/goal` on OpenCode 1, and do not describe the ledger as a second-class fallback: it is the second half of "done" on every host.
164
162
 
165
163
  ## OpenCode Surface Checks
166
164
 
167
- The verification route should inspect the host from two directions. The CLI command list shows what a user can invoke. The plugin hooks type shows what a plugin can observe or extend. Both matter. A CLI command without a plugin API may not support LitOpenCode integration. A type name without a runtime command may not be user-visible. If either surface changes, tests should capture the new supported path.
168
-
169
- Be careful with installed versions. A global OpenCode CLI may differ from the package used in local tests. A lockfile may point at one plugin version while the user runtime uses another. Record versions when re-evaluating the verdict. If evidence is ambiguous, keep the conservative ledger path and state uncertainty.
165
+ The goal surface has three parts: the `opencode.goal` RPC to read, the `/goal` command through `session.command` to change, and the `experimental.idle` session hook, which runs when a session finishes its work and is where LitOpenCode checks a finished goal. `@opencode/plugin` 2.2.5 types all three; the older 2.0.x types do not. A global `opencode2` may differ from the version in the lockfile, so record both when re-checking.
170
166
 
171
167
  ## Review Checklist
172
168
 
173
- - Does the doc say current host surface lacks verified native goal support?
174
- - Does it name `.litopencode/litgoal` as package-owned state?
175
- - Does it avoid claiming direct host goal binding?
176
- - Do tests cover runtime skill visibility and docs wording?
177
- - Do package guards exclude local ledger state?
178
- - If future native support is claimed, is there official or local host evidence and a migration plan?
169
+ - Does a user-made goal stay untouched, including on idle?
170
+ - Is a goal created only when none exists, and only on the three routes?
171
+ - Is "done" reported only when OpenCode's verdict and the ledger agree?
172
+ - Does a host without `/goal` record `native-goal-unavailable` and nothing else?
173
+ - Is there a bounded number of resumes, and no resume of a goal the user or the round limit paused?
174
+ - Does every change print one `LitOpenCode goal:` line?
179
175
 
180
176
  ## Failure Modes
181
177
 
182
- One failure mode is optimistic wording after a host release note mentions goals but before plugin integration is tested. Another is copying goal language from a different product with a different host model. Another is treating durable ledger state as if it were a host feature, which can confuse users about where files live and what gets packed. Another is hiding the verdict in a release note while command prompts still imply native support. The static skill exists to keep the conservative evidence-bound verdict easy to find.
178
+ A create over a goal the user just made (the command overwrites). A resume of a goal the user paused, which fights the user. A clear when only OpenCode's reviewer said done, which hides an open ledger. Repeated notices on every idle turn. A read failure treated as "no goal". Treating `/lit` and `/lit-loop` as different when the installed command files share one body. Each has a test in `test/v2-native-goal.test.mjs` or `test/v2-native-goal-wiring.test.mjs`.
183
179
 
184
180
  ## Completion Claims Involving Goals
185
181
 
186
- When work touches goal behavior, DoneClaims should say whether the change affected package-owned ledger behavior, host command activation, static docs, or only wording. Include tests such as goal tests, runtime skills tests, docs tests, and pack payload checks when relevant. If no host native goal support was re-verified, do not make new host-goal claims. If local ledger files were created during tests, clean them up or state that they are ignored evidence.
187
-
188
- ## Migration Thought Experiment
189
-
190
- If OpenCode later exposes native goals, do not immediately delete the package ledger. First, design compatibility. Existing projects may have `.litopencode/litgoal` history that powers recap and review. Users may depend on local evidence paths. A migration might read both native and package state, export old events, or keep package state for historical sessions while new sessions use host storage. Each option needs tests, docs, and rollback.
191
-
192
- The migration plan should answer: what is the source of truth after upgrade; how are duplicate goals reconciled; how are secrets protected; what happens in older OpenCode versions; how does package payload avoid local state; how does `/lit-recap` read history; and how does `start-work` find approved plans? Until those answers exist, the conservative ledger path remains safer.
182
+ A DoneClaim for goal work says which host it ran on, which routes set a goal, what happened to a user-made goal, and that the ledger and OpenCode's verdict both closed. Include the native-goal tests, the docs and runtime-skills tests, the pack payload check, and a real-surface run. If only OpenCode 1 was available, say the OpenCode 2 path was not exercised.
193
183
 
194
184
  ## Support Triage
195
185
 
196
- When a user asks “where is my goal,” first clarify whether they mean LitOpenCode durable state or a host feature. Ask for package version, OpenCode version, project root, and whether `.litopencode/litgoal` exists. Do not tell them to run destructive cleanup. If state is missing, check whether they are in a different worktree or package root. If state is malformed, preserve the file before repair. If the user expects host-native behavior, explain the current verdict and package-owned path.
186
+ When someone asks "where is my goal", ask whether they mean OpenCode's `/goal` or LitOpenCode's ledger. `/goal` with no text prints the host goal; `lit adhd` and `litwork action=status` print the ledger. A `LitOpenCode goal: conflict` line means OpenCode and the ledger disagree: finish the ledger criteria, or clear the host goal yourself if the work is abandoned. Do not tell anyone to delete `.litopencode/litgoal`.
197
187
 
198
188
  ## Documentation Consistency
199
189
 
200
- Every public mention of goal behavior should match three facts: current host native support is not verified; LitOpenCode uses package-owned durable state; future host support would require a tested migration. If one document drifts, update it and run docs tests. Avoid mixing “goal,” “ledger,” and “recap” as if they were the same feature. The ledger stores durable events, recap summarizes evidence, and goal behavior coordinates work.
201
-
202
- ## Test Ideas for Future Host Changes
203
-
204
- If host support appears, add tests that prove both old and new paths. A compatibility test should read an existing `.litopencode/litgoal` fixture and verify it is not lost. A host-integration test should prove the OpenCode goal surface can be detected without guessing. A command test should prove `litwork`, `/start-work`, and `/review-work` use the correct source of truth. A pack test should prove local goal state still does not ship. A docs test should prove wording changed from conservative ledger language to the new verified integration language only after evidence exists.
205
-
206
- ## User Communication
207
-
208
- Users do not need an implementation lecture every time. For normal use, say where state lives and how to inspect it. For capability questions, say the current verdict and evidence. For migration questions, say that package-owned state remains authoritative until a tested migration exists. Avoid blaming the host or promising future behavior. The goal is clear expectations.
209
-
210
- ## Decision Log Guidance
211
-
212
- When re-evaluating native goal support, record the decision like an engineering verdict: date, OpenCode version, package version, CLI evidence, plugin type evidence, tests run, and conclusion. If the conclusion remains “not exposed,” keep runtime behavior unchanged. If the conclusion changes, open a plan for migration rather than mixing discovery and implementation. This avoids accidental drift where one doc claims native support while code still uses the package ledger.
213
-
214
- ## Compatibility Language
215
-
216
- Compatibility docs should say that `.litopencode/litgoal` is stable local state for current LitOpenCode workflows. If future host integration arrives, compatibility will be documented and tested. Do not tell users to delete ledger state to prepare for a hypothetical migration. Do not tell users that host goals and package goals are interchangeable until code proves synchronization or conversion.
190
+ Every public mention of goals matches four facts: OpenCode 2.2 and later has `/goal`; LitOpenCode sets one only for lit-loop, start-work and litwork; a user's goal is never touched; the ledger stays the evidence record and the second half of done. Keep README, `docs/reference.md`, the litgoal and lit-loop skills, the lit-loop activation prompt and the release checklist in step, and run the docs tests after any change.
@@ -340,3 +340,7 @@ in your first response and continue with the one that injected you.
340
340
  It says nothing about how to execute — the sections above own that. Its only job is to answer three
341
341
  questions a skill body cannot answer for itself: how was I reached, have I already been reached, and
342
342
  whose contract am I under.
343
+
344
+ ## Native goal on OpenCode 2
345
+
346
+ On OpenCode 2.2 and later, start-work gets a native `/goal` from LitOpenCode itself, set once when none exists and carrying the approved plan's completion conditions. A goal the user made is reported and left alone, and a host without `/goal` records `native-goal-unavailable`. The goal never widens the run's authority: scope roots, grants and the bounded-authority journal decide what the work may touch. Done needs OpenCode's verdict and the ledger together. See `native-goal-verdict`.
@@ -389,3 +389,7 @@ a smaller task only if the deliverable is still required.
389
389
 
390
390
  A child's own done-claim is a claim, not verification. Silence is not failure and it is not success;
391
391
  it is absence of signal, and it must be recorded as such.
392
+
393
+ ## Native goal on OpenCode 2
394
+
395
+ On OpenCode 2.2 and later, lit-loop gets a native `/goal` from LitOpenCode itself: it is set when none exists, carries the completion conditions of this ledger, and is resumed or cleared only when LitOpenCode created it. Do not type `/goal` for the user, and leave a goal the user made alone; the `LitOpenCode goal:` line reports what happened. A host without `/goal` records `native-goal-unavailable` and the ledger carries the loop. Done needs both OpenCode's verdict and the ledger, so never report done while a criterion, the real-surface check or the cleanup receipt is open. See `native-goal-verdict`.