@litfamily/litopencode 1.0.35 → 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.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,10 @@ are tracked separately from this product history.
5
5
 
6
6
  ## Unreleased
7
7
 
8
+ ## 1.0.36 - 2026-10-31
9
+
10
+ - On OpenCode 2.2 and later, `lit loop`, `litwork` and `/start-work` set OpenCode's native `/goal` for you. The goal carries your task plus what done means here (tests pass, a real-surface check ran, temporary files are cleaned up, the ledger is closed), and each change prints one `LitOpenCode goal:` line. LitOpenCode manages only goals it created: it resumes one that a server restart paused, clears one after OpenCode's verdict and the ledger both say done, and reports a goal you made as left alone without touching it. When OpenCode says the goal is met but the ledger is open, you get a conflict line and no done. Planning, research, review and bare `lit` never set a goal. OpenCode 1 and older OpenCode 2 builds have no `/goal`, so the ledger carries the work and `native-goal-unavailable` is recorded once.
11
+
8
12
  ## 1.0.35 - 2026-10-31
9
13
 
10
14
  - `lit adhd` counts the other lanes before it offers to resume this session's own work. A session that had started herdr lanes (or sat beside other running sessions) was still told to continue its paused discipline with `go` while they worked; now the box says to wait for them, arms nothing and ends with "Do not type `go` until they finish." A pending approval is still offered, and without lanes the paused discipline is offered as before.
package/README-Ko-KR.md CHANGED
@@ -1,13 +1,13 @@
1
- <p align="center"><picture><source media="(prefers-reduced-motion: reduce)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.35/docs/assets/cover-motion-still.webp" /><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.35/docs/assets/cover-motion.webp" width="100%" alt="LitFamily 모션 커버: 일곱 로봇 패널이 차례로 켜지고, LitOpenCode 로봇의 눈과 테두리가 빛난 뒤 LITFAMILY와 KEEP THE WORK LIT. 문구가 밝아지는 영상" /></picture></p>
1
+ <p align="center"><picture><source media="(prefers-reduced-motion: reduce)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.36/docs/assets/cover-motion-still.webp" /><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.36/docs/assets/cover-motion.webp" width="100%" alt="LitFamily 모션 커버: 일곱 로봇 패널이 차례로 켜지고, LitOpenCode 로봇의 눈과 테두리가 빛난 뒤 LITFAMILY와 KEEP THE WORK LIT. 문구가 밝아지는 영상" /></picture></p>
2
2
 
3
3
  # LitOpenCode
4
4
 
5
5
  **LitOpenCode는 OpenCode용 워크플로 플러그인입니다. 프롬프트에 `lit`만 붙이면 계획하고, 만들고, 확인하고, 기록까지 남겨서 다음 세션이 어디서 멈췄는지 바로 압니다.**
6
6
 
7
7
  <p align="center">
8
- <a href="https://www.npmjs.com/package/@litfamily/litopencode"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.35/docs/assets/readme/badge-version.svg" alt="1.0.35" /></a>
8
+ <a href="https://www.npmjs.com/package/@litfamily/litopencode"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.36/docs/assets/readme/badge-version.svg" alt="1.0.36" /></a>
9
9
  <a href="https://www.npmjs.com/package/@litfamily/litopencode"><img src="https://img.shields.io/npm/dm/@litfamily/litopencode?label=downloads&color=D7F75B" alt="월간 npm 다운로드 수" /></a>
10
- <a href="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.35/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.35/docs/assets/readme/badge-license.svg" alt="MIT 라이선스" /></a>
10
+ <a href="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.36/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.36/docs/assets/readme/badge-license.svg" alt="MIT 라이선스" /></a>
11
11
  <a href="https://github.com/wjgoarxiv/litopencode"><img src="https://img.shields.io/github/stars/wjgoarxiv/litopencode?style=social" alt="GitHub 스타" /></a>
12
12
  </p>
13
13
 
@@ -57,9 +57,9 @@ npm uninstall -g @litfamily/litopencode
57
57
  ## 더 보기
58
58
 
59
59
  - [GitHub의 사용 안내](https://github.com/wjgoarxiv/litopencode/blob/main/docs/guide-Ko-KR.md): 설치 옵션, 자동 인수인계, 문제 해결, 제거
60
- - [워크플로 상세 안내](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.35/docs/reference-Ko-KR.md) · [English reference](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.35/docs/reference.md)
61
- - [마이그레이션 안내](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.35/docs/migration.md) · [Changelog](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.35/CHANGELOG.md) · [개인정보와 네트워크 동작](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.35/docs/privacy.md)
62
- - [LitFamily 허브](https://github.com/wjgoarxiv/litfamily) · [MIT 라이선스](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.35/LICENSE) · [상표 정책](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.35/TRADEMARK.md) · [ASCII 글꼴 라이선스](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.35/docs/assets/readme/JetBrainsMono-OFL.txt)
60
+ - [워크플로 상세 안내](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.36/docs/reference-Ko-KR.md) · [English reference](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.36/docs/reference.md)
61
+ - [마이그레이션 안내](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.36/docs/migration.md) · [Changelog](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.36/CHANGELOG.md) · [개인정보와 네트워크 동작](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.36/docs/privacy.md)
62
+ - [LitFamily 허브](https://github.com/wjgoarxiv/litfamily) · [MIT 라이선스](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.36/LICENSE) · [상표 정책](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.36/TRADEMARK.md) · [ASCII 글꼴 라이선스](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.36/docs/assets/readme/JetBrainsMono-OFL.txt)
63
63
 
64
64
  <details>
65
65
  <summary>ASCII 로고 복사</summary>
package/README.md CHANGED
@@ -1,13 +1,13 @@
1
- <p align="center"><picture><source media="(prefers-reduced-motion: reduce)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.35/docs/assets/cover-motion-still.webp" /><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.35/docs/assets/cover-motion.webp" width="100%" alt="LitFamily motion cover: seven armored robots power on one by one, the LitOpenCode robot wakes with glowing eyes and a lit frame, then LITFAMILY and KEEP THE WORK LIT. light up." /></picture></p>
1
+ <p align="center"><picture><source media="(prefers-reduced-motion: reduce)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.36/docs/assets/cover-motion-still.webp" /><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.36/docs/assets/cover-motion.webp" width="100%" alt="LitFamily motion cover: seven armored robots power on one by one, the LitOpenCode robot wakes with glowing eyes and a lit frame, then LITFAMILY and KEEP THE WORK LIT. light up." /></picture></p>
2
2
 
3
3
  # LitOpenCode
4
4
 
5
5
  **LitOpenCode is a workflow plugin for OpenCode. Add `lit` to a prompt and the work gets planned, built, checked and written down, so the next session knows where you stopped.**
6
6
 
7
7
  <p align="center">
8
- <a href="https://www.npmjs.com/package/@litfamily/litopencode"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.35/docs/assets/readme/badge-version.svg" alt="1.0.35" /></a>
8
+ <a href="https://www.npmjs.com/package/@litfamily/litopencode"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.36/docs/assets/readme/badge-version.svg" alt="1.0.36" /></a>
9
9
  <a href="https://www.npmjs.com/package/@litfamily/litopencode"><img src="https://img.shields.io/npm/dm/@litfamily/litopencode?label=downloads&color=D7F75B" alt="npm downloads per month" /></a>
10
- <a href="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.35/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.35/docs/assets/readme/badge-license.svg" alt="MIT license" /></a>
10
+ <a href="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.36/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.36/docs/assets/readme/badge-license.svg" alt="MIT license" /></a>
11
11
  <a href="https://github.com/wjgoarxiv/litopencode"><img src="https://img.shields.io/github/stars/wjgoarxiv/litopencode?style=social" alt="GitHub stars" /></a>
12
12
  </p>
13
13
 
@@ -57,9 +57,9 @@ npm uninstall -g @litfamily/litopencode
57
57
  ## More
58
58
 
59
59
  - [Guide on GitHub](https://github.com/wjgoarxiv/litopencode/blob/main/docs/guide.md): install options, automatic handoff, troubleshooting and removal.
60
- - [Workflow reference](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.35/docs/reference.md)
61
- - [Migration notes](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.35/docs/migration.md) · [Changelog](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.35/CHANGELOG.md) · [Privacy and network behavior](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.35/docs/privacy.md)
62
- - [LitFamily hub](https://github.com/wjgoarxiv/litfamily) · [MIT license](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.35/LICENSE) · [Trademark policy](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.35/TRADEMARK.md) · [ASCII font license](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.35/docs/assets/readme/JetBrainsMono-OFL.txt)
60
+ - [Workflow reference](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.36/docs/reference.md)
61
+ - [Migration notes](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.36/docs/migration.md) · [Changelog](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.36/CHANGELOG.md) · [Privacy and network behavior](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.36/docs/privacy.md)
62
+ - [LitFamily hub](https://github.com/wjgoarxiv/litfamily) · [MIT license](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.36/LICENSE) · [Trademark policy](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.36/TRADEMARK.md) · [ASCII font license](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.36/docs/assets/readme/JetBrainsMono-OFL.txt)
63
63
 
64
64
  <details>
65
65
  <summary>Copy ASCII logo</summary>
@@ -80,9 +80,13 @@ Checkpoint as you go so progress is durable and incremental — never batch it t
80
80
 
81
81
  # OpenCode goal handoff
82
82
 
83
- OpenCode does not expose a native goal primitive in the verified plugin or CLI surface.
84
- Use LitOpenCode-owned durable goal state under .litopencode/litgoal/lit-loop instead of
85
- claiming a host-native /goal integration. If a request refers to a Codex-native goal,
83
+ OpenCode 2.2 and later has a native /goal. On such a host LitOpenCode sets, resumes and clears
84
+ one for lit-loop, start-work and litwork by itself and prints a "LitOpenCode goal:" line for each
85
+ change. Do not type /goal yourself, and never touch a goal the user made. OpenCode 1 and older
86
+ OpenCode 2 hosts have none; the work then runs on the ledger alone (native-goal-unavailable).
87
+ The durable ledger under .litopencode/litgoal/lit-loop is the evidence record on every host:
88
+ a goal counts as done only when OpenCode's verdict and the ledger both say so, so never
89
+ report done while the ledger is open. If a request refers to a Codex-native goal,
86
90
  translate it into LitOpenCode's durable ledger semantics and make that adaptation
87
91
  explicit before execution.
88
92
 
@@ -12,6 +12,8 @@ import { typedPromptReply } from "./adhd-next.js";
12
12
  import { decideReentry, recordActivation } from "./activation-reentry.js";
13
13
  import { startTypedWork, typedStartArgument } from "./start-work-typed.js";
14
14
  import { micro, supportsMarkGlyphs } from "./lit-mark.js";
15
+ import { recordNativeGoalUnavailable } from "./native-goal-ledger.js";
16
+ import { nativeGoalRoute } from "./v2/native-goal.js";
15
17
  // Prompt modules preserve minimum-first, single-task or few-task planning, avoidable custom code review,
16
18
  // and external-source safety; this module only coordinates their unchanged activation surfaces.
17
19
  export { litHandoffPromptInjection, scientificVisualizationPromptInjection } from "./activation-managed-prompts.js";
@@ -228,6 +230,9 @@ export function createChatMessageActivationHook(projectRoot, options = {}) {
228
230
  return;
229
231
  }
230
232
  await options.onSkillActivation?.(input.sessionID, mode, appendedLit);
233
+ const goalRoute = nativeGoalRoute(mode, triggerText ?? "");
234
+ if (goalRoute !== undefined)
235
+ await recordNativeGoalUnavailable(projectRoot, input.sessionID, goalRoute, "opencode-1");
231
236
  if (mode === "lit-scientific-visualization") {
232
237
  options.onScientificVisualizationActivation?.(input.sessionID);
233
238
  }
package/dist/commands.js CHANGED
@@ -9,8 +9,18 @@ import { writeNext } from "./adhd-next.js";
9
9
  import { autoHandoffSettingText } from "./auto-handoff.js";
10
10
  import { formatResolvedPlanNotice, formatSavedPlanNotice, parsePlanSaveArgument, persistApprovedPlan, PlanScopeError, resolveLatestDurablePlan } from "./durable-plan.js";
11
11
  import { isWorkflowFamilyMode, workflowFamilyCommandDefinitions } from "./workflow-families.js";
12
+ import { recordNativeGoalUnavailable } from "./native-goal-ledger.js";
13
+ import { nativeGoalRoute } from "./v2/native-goal.js";
12
14
  import { captureKnowledgeEvent, inspectKnowledge, queryKnowledge, recoverKnowledge, reviewKnowledgeRecord } from "./knowledge.js";
13
15
  export { litActivationBanner, startWorkPromptInjection } from "./activation.js";
16
+ /**
17
+ * The four loop commands share one prompt, and an installed alias file hands the host exactly that text,
18
+ * so the prompt alone cannot say which command was typed. A last line names it; the plugin reads it to tell
19
+ * `/lit-loop` and `/litwork` (which set a native goal) from bare `/lit` (which does not).
20
+ */
21
+ function loopCommandText(id) {
22
+ return `${litLoopPromptInjection}\nCommand route: "${id}"`;
23
+ }
14
24
  export const litOpenCodeCommands = Object.freeze([
15
25
  ...workflowFamilyCommandDefinitions,
16
26
  {
@@ -35,7 +45,7 @@ export const litOpenCodeCommands = Object.freeze([
35
45
  title: "LitOpenCode",
36
46
  description: "Activate the LitOpenCode model probe line and durable goal loop.",
37
47
  banner: activationBanner("lit"),
38
- activationText: litLoopPromptInjection
48
+ activationText: loopCommandText("lit")
39
49
  },
40
50
  {
41
51
  id: "lit-loop",
@@ -43,7 +53,7 @@ export const litOpenCodeCommands = Object.freeze([
43
53
  title: "LitOpenCode Lit Loop",
44
54
  description: "Activate the full LitOpenCode durable lit-loop prompt.",
45
55
  banner: activationBanner("lit-loop"),
46
- activationText: litLoopPromptInjection
56
+ activationText: loopCommandText("lit-loop")
47
57
  },
48
58
  {
49
59
  id: "litwork",
@@ -51,7 +61,7 @@ export const litOpenCodeCommands = Object.freeze([
51
61
  title: "LitOpenCode Work Loop",
52
62
  description: "Resume a LitOpenCode work loop with durable ledger-backed progress.",
53
63
  banner: activationBanner("litwork"),
54
- activationText: litLoopPromptInjection
64
+ activationText: loopCommandText("litwork")
55
65
  },
56
66
  {
57
67
  id: "lit-work",
@@ -59,7 +69,7 @@ export const litOpenCodeCommands = Object.freeze([
59
69
  title: "LitOpenCode Work Loop",
60
70
  description: "Hyphenated alias for /litwork.",
61
71
  banner: activationBanner("lit-work"),
62
- activationText: litLoopPromptInjection
72
+ activationText: loopCommandText("lit-work")
63
73
  },
64
74
  {
65
75
  id: "start-work",
@@ -664,6 +674,9 @@ export function createCommandActivationHook(projectRoot, options = {}) {
664
674
  }
665
675
  });
666
676
  await options.onSkillActivation?.(input.sessionID, command.id);
677
+ const goalRoute = nativeGoalRoute(command.id, "");
678
+ if (goalRoute !== undefined)
679
+ await recordNativeGoalUnavailable(projectRoot, input.sessionID, goalRoute, "opencode-1");
667
680
  if (command.id === "lit-scientific-visualization") {
668
681
  options.onScientificVisualizationActivation?.(input.sessionID);
669
682
  }
@@ -0,0 +1,43 @@
1
+ export type NativeGoalLedgerEvent = {
2
+ readonly type: "native-goal.created";
3
+ readonly goalID: string;
4
+ } | {
5
+ readonly type: "native-goal.resumed";
6
+ readonly goalID: string;
7
+ } | {
8
+ readonly type: "native-goal.cleared";
9
+ readonly goalID: string;
10
+ } | {
11
+ readonly type: "native-goal.left-alone";
12
+ readonly goalID: string;
13
+ readonly status: string;
14
+ } | {
15
+ readonly type: "native-goal.conflict";
16
+ readonly goalID: string;
17
+ readonly reason: string;
18
+ } | {
19
+ readonly type: "native-goal.failed";
20
+ readonly step: string;
21
+ readonly detail: string;
22
+ } | {
23
+ readonly type: "native-goal.unavailable";
24
+ readonly host: string;
25
+ };
26
+ export type NativeGoalOwnership = {
27
+ readonly goalID: string | undefined;
28
+ readonly resumes: number;
29
+ readonly createdAt: string | undefined;
30
+ };
31
+ export declare function recordNativeGoal(projectRoot: string, sessionID: string, route: string | undefined, event: NativeGoalLedgerEvent): Promise<boolean>;
32
+ /** The goal id LitOpenCode created for this session and has not cleared, with its resume count. */
33
+ export declare function nativeGoalOwnership(projectRoot: string, sessionID: string): Promise<NativeGoalOwnership>;
34
+ /** OpenCode 1 and older OpenCode 2 hosts have no `/goal`: say so once per session and route. */
35
+ export declare function recordNativeGoalUnavailable(projectRoot: string, sessionID: string, route: string, host: string): Promise<void>;
36
+ /**
37
+ * True when the evidence ledger holds a goal for this session that is complete and was last written after
38
+ * the native goal was created. A session's first ledger goal stays complete for good, so an older one must
39
+ * not close a later native goal.
40
+ */
41
+ export declare function ledgerGoalDone(projectRoot: string, sessionID: string, since?: string): Promise<boolean>;
42
+ /** Whether the ledger already holds an event of this type for the session (used to say a thing once). */
43
+ export declare function nativeGoalRecorded(projectRoot: string, sessionID: string, type: NativeGoalLedgerEvent["type"], route?: string): Promise<boolean>;
@@ -0,0 +1,74 @@
1
+ // The native-goal records in LitOpenCode's own ledger (`.litopencode/litgoal`). OpenCode's goal has no
2
+ // owner field, so the ledger is where LitOpenCode remembers which goal id it created, how often it
3
+ // resumed it, and that a host had no `/goal`. Every write is best-effort: a ledger failure never blocks
4
+ // the work, and a read failure reads as "owns nothing", which only makes LitOpenCode do less.
5
+ import { createLitGoalOperations, readEvidenceLedger } from "./ledger.js";
6
+ export async function recordNativeGoal(projectRoot, sessionID, route, event) {
7
+ try {
8
+ const operations = createLitGoalOperations(projectRoot);
9
+ await operations.init();
10
+ await operations.append({ ...event, sessionID, ...(route === undefined ? {} : { route }), timestamp: new Date().toISOString() });
11
+ return true;
12
+ }
13
+ catch { // no-excuse-ok: catch -- a ledger failure must never block the user's work.
14
+ return false;
15
+ }
16
+ }
17
+ /** The goal id LitOpenCode created for this session and has not cleared, with its resume count. */
18
+ export async function nativeGoalOwnership(projectRoot, sessionID) {
19
+ let goalID;
20
+ let createdAt;
21
+ let resumes = 0;
22
+ try {
23
+ for (const event of await createLitGoalOperations(projectRoot).read()) {
24
+ if (event["sessionID"] !== sessionID || typeof event["goalID"] !== "string")
25
+ continue;
26
+ if (event.type === "native-goal.created") {
27
+ goalID = event["goalID"];
28
+ createdAt = typeof event["timestamp"] === "string" ? event["timestamp"] : undefined;
29
+ resumes = 0;
30
+ }
31
+ else if (event.type === "native-goal.cleared" && event["goalID"] === goalID) {
32
+ goalID = undefined;
33
+ createdAt = undefined;
34
+ }
35
+ else if (event.type === "native-goal.resumed" && event["goalID"] === goalID) {
36
+ resumes += 1;
37
+ }
38
+ }
39
+ }
40
+ catch { // no-excuse-ok: catch -- an unreadable ledger reads as "owns nothing", so LitOpenCode touches nothing.
41
+ return { goalID: undefined, resumes: 0, createdAt: undefined };
42
+ }
43
+ return { goalID, resumes, createdAt };
44
+ }
45
+ /** OpenCode 1 and older OpenCode 2 hosts have no `/goal`: say so once per session and route. */
46
+ export async function recordNativeGoalUnavailable(projectRoot, sessionID, route, host) {
47
+ if (await nativeGoalRecorded(projectRoot, sessionID, "native-goal.unavailable", route))
48
+ return;
49
+ await recordNativeGoal(projectRoot, sessionID, route, { type: "native-goal.unavailable", host });
50
+ }
51
+ /**
52
+ * True when the evidence ledger holds a goal for this session that is complete and was last written after
53
+ * the native goal was created. A session's first ledger goal stays complete for good, so an older one must
54
+ * not close a later native goal.
55
+ */
56
+ export async function ledgerGoalDone(projectRoot, sessionID, since) {
57
+ try {
58
+ const state = await readEvidenceLedger(projectRoot);
59
+ const floor = since === undefined ? Number.NaN : Date.parse(since);
60
+ return state.goals.some((goal) => goal.sessionId === sessionID && goal.status === "complete" && (Number.isNaN(floor) || Date.parse(goal.updatedAt) >= floor));
61
+ }
62
+ catch { // no-excuse-ok: catch -- an unreadable evidence ledger is not done.
63
+ return false;
64
+ }
65
+ }
66
+ /** Whether the ledger already holds an event of this type for the session (used to say a thing once). */
67
+ export async function nativeGoalRecorded(projectRoot, sessionID, type, route) {
68
+ try {
69
+ return (await createLitGoalOperations(projectRoot).read()).some((event) => event.type === type && event["sessionID"] === sessionID && (route === undefined || event["route"] === route));
70
+ }
71
+ catch { // no-excuse-ok: catch -- an unreadable ledger cannot prove the record exists.
72
+ return false;
73
+ }
74
+ }
@@ -36,6 +36,7 @@ export declare function pluginOwnedCommandIds(roots: readonly string[]): readonl
36
36
  export type CommandMark = {
37
37
  readonly command: string;
38
38
  readonly banner: string;
39
+ readonly work?: string;
39
40
  };
40
41
  export declare function createV2Commands(roots: readonly string[], session: CommandSessionLike): {
41
42
  definitions: CommandDefinitionLike[];
@@ -29,8 +29,8 @@ export function createV2Commands(roots, session) {
29
29
  async execute(input) {
30
30
  const nonce = randomUUID();
31
31
  const mark = { command: command.id, banner: command.banner };
32
- pending.set(input.sessionID, { nonce, mark });
33
32
  const argumentsText = input.prompt.text.trim();
33
+ pending.set(input.sessionID, { nonce, mark, work: argumentsText });
34
34
  if (command.agent !== undefined)
35
35
  await session.switchAgent({ sessionID: input.sessionID, agent: command.agent });
36
36
  await session.prompt({
@@ -52,7 +52,7 @@ export function createV2Commands(roots, session) {
52
52
  if (expected === undefined || marker.nonce !== expected.nonce)
53
53
  return undefined;
54
54
  pending.delete(sessionID);
55
- return expected.mark;
55
+ return { ...expected.mark, work: expected.work };
56
56
  }
57
57
  return {
58
58
  definitions,
@@ -3,6 +3,7 @@ import type { AgentEditorLike, ModelEditorLike, PermissionEvaluationLike, Sessio
3
3
  import type { CommandEditorLike, CommandSessionLike } from "./commands.ts";
4
4
  import type { EventDomainLike, EventModelLike, EventSessionLike } from "./events.ts";
5
5
  import type { ToolAfterEvent, ToolBeforeEvent } from "./hooks.ts";
6
+ import type { NativeGoalHostLike } from "./native-goal-runtime.ts";
6
7
  import type { ContextEvent, PromptEvent } from "./session-hooks.ts";
7
8
  import type { ToolEditorLike } from "./tools.ts";
8
9
  export type Registration = {
@@ -28,6 +29,9 @@ type SessionHook = {
28
29
  (name: "prompt", callback: Callback<PromptEvent>): Promise<Registration>;
29
30
  (name: "context", callback: Callback<ContextEvent>): Promise<Registration>;
30
31
  (name: "compaction", callback: Callback<ContextEvent>): Promise<Registration>;
32
+ (name: "experimental.idle", callback: Callback<{
33
+ readonly sessionID: string;
34
+ }>): Promise<Registration>;
31
35
  };
32
36
  export type SessionDomainLike = {
33
37
  readonly get: (input: {
@@ -49,7 +53,8 @@ export type ServerContextLike = {
49
53
  readonly command: {
50
54
  readonly transform: Transform<CommandEditorLike>;
51
55
  };
52
- readonly session: SessionDomainLike & SessionModelDomainLike & CommandSessionLike & EventSessionLike & {
56
+ readonly rpc?: NativeGoalHostLike["rpc"];
57
+ readonly session: SessionDomainLike & SessionModelDomainLike & CommandSessionLike & EventSessionLike & NativeGoalHostLike["session"] & {
53
58
  readonly hook: SessionHook;
54
59
  };
55
60
  readonly event: EventDomainLike;
@@ -0,0 +1,74 @@
1
+ import { type NativeGoalRoute } from "./native-goal.ts";
2
+ /** The `opencode.goal` RPC contract as JSON Schema, so the package needs no runtime import of the host's schema library. */
3
+ declare const goalDefinition: Readonly<{
4
+ id: "opencode.goal";
5
+ methods: Readonly<{
6
+ get: Readonly<{
7
+ input: Readonly<{
8
+ type: "object";
9
+ properties: Readonly<{
10
+ sessionID: Readonly<{
11
+ type: "string";
12
+ }>;
13
+ }>;
14
+ required: readonly string[];
15
+ }>;
16
+ output: Readonly<{}>;
17
+ }>;
18
+ }>;
19
+ events: Readonly<{
20
+ changed: Readonly<{
21
+ schema: Readonly<{
22
+ type: "object";
23
+ properties: Readonly<{
24
+ sessionID: Readonly<{
25
+ type: "string";
26
+ }>;
27
+ }>;
28
+ required: readonly string[];
29
+ }>;
30
+ }>;
31
+ }>;
32
+ }>;
33
+ export type GoalRpcClientLike = {
34
+ readonly get: (input: {
35
+ readonly sessionID: string;
36
+ }) => Promise<unknown>;
37
+ };
38
+ export type NativeGoalHostLike = {
39
+ /** `context.rpc`; absent on a host that has no RPC domain. */
40
+ readonly rpc?: (definition: typeof goalDefinition) => GoalRpcClientLike;
41
+ readonly session: {
42
+ readonly command: (input: {
43
+ readonly sessionID: string;
44
+ readonly name: string;
45
+ readonly text: string;
46
+ readonly delivery?: "steer" | "queue";
47
+ }) => Promise<unknown>;
48
+ readonly synthetic: (input: {
49
+ readonly sessionID: string;
50
+ readonly text: string;
51
+ readonly description?: string;
52
+ readonly metadata?: Readonly<Record<string, string>>;
53
+ readonly resume?: boolean;
54
+ }) => Promise<unknown>;
55
+ };
56
+ };
57
+ /** A prompt the host expanded from an installed command alias file arrives as the command body, unmarked. */
58
+ export declare function aliasGoalRoute(input: string): {
59
+ readonly route: NativeGoalRoute;
60
+ readonly work: string;
61
+ } | undefined;
62
+ export declare function createNativeGoal(options: {
63
+ readonly projectRoot: string;
64
+ readonly host: NativeGoalHostLike;
65
+ }): {
66
+ /** Starts the goal step for a qualifying activation without holding up the user's prompt. */
67
+ activate(sessionID: string, route: NativeGoalRoute, work: string): Promise<void>;
68
+ /** The session finished its model work: clear a finished owned goal or report a conflict. */
69
+ idle(sessionID: string): Promise<void>;
70
+ /** Resolves when every started goal step has finished. */
71
+ settled(): Promise<void>;
72
+ };
73
+ export type NativeGoal = ReturnType<typeof createNativeGoal>;
74
+ export {};
@@ -0,0 +1,193 @@
1
+ // Drives OpenCode 2's native `/goal` for lit-loop, start-work and litwork. The host has no goal setter:
2
+ // the goal is read through the `opencode.goal` RPC and changed by sending the `/goal` command to the
3
+ // session (`session.command`). Policy lives in native-goal.ts; this file reads the state, performs the one
4
+ // action it returns, prints one line per change as a notice row, and records it in the ledger. Nothing here
5
+ // throws: a failed step is recorded and the work carries on with the ledger alone.
6
+ import { ledgerGoalDone, nativeGoalOwnership, recordNativeGoal, recordNativeGoalUnavailable } from "../native-goal-ledger.js";
7
+ import { litOpenCodeCommands } from "../commands.js";
8
+ import { decideNativeGoal, nativeGoalLine, nativeGoalObjective, nativeGoalRoute } from "./native-goal.js";
9
+ /** The `opencode.goal` RPC contract as JSON Schema, so the package needs no runtime import of the host's schema library. */
10
+ const goalDefinition = Object.freeze({
11
+ id: "opencode.goal",
12
+ methods: Object.freeze({
13
+ get: Object.freeze({
14
+ input: Object.freeze({ type: "object", properties: Object.freeze({ sessionID: Object.freeze({ type: "string" }) }), required: Object.freeze(["sessionID"]) }),
15
+ output: Object.freeze({})
16
+ })
17
+ }),
18
+ events: Object.freeze({
19
+ changed: Object.freeze({ schema: Object.freeze({ type: "object", properties: Object.freeze({ sessionID: Object.freeze({ type: "string" }) }), required: Object.freeze(["sessionID"]) }) })
20
+ })
21
+ });
22
+ function isRecord(value) {
23
+ return typeof value === "object" && value !== null && !Array.isArray(value);
24
+ }
25
+ function parseGoal(value) {
26
+ if (value === null || value === undefined)
27
+ return null;
28
+ if (!isRecord(value) || typeof value.id !== "string" || typeof value.objective !== "string")
29
+ return "unreadable";
30
+ if (value.status !== "active" && value.status !== "paused" && value.status !== "complete")
31
+ return "unreadable";
32
+ return { id: value.id, objective: value.objective, status: value.status, ...(typeof value.reason === "string" ? { reason: value.reason } : {}) };
33
+ }
34
+ /** OpenCode 2.2.5 answers an RPC it does not register with type `rpc.unavailable`; any other failure is a failed read. */
35
+ function hostLacksGoalRpc(error) {
36
+ return isRecord(error) && error.type === "rpc.unavailable";
37
+ }
38
+ function failureDetail(error) {
39
+ const text = error instanceof Error ? error.message : isRecord(error) ? String(error.message ?? error.type ?? "failed") : String(error);
40
+ return text.replace(/\s+/gu, " ").slice(0, 160);
41
+ }
42
+ /** A prompt the host expanded from an installed command alias file arrives as the command body, unmarked. */
43
+ export function aliasGoalRoute(input) {
44
+ const text = input.trimStart();
45
+ for (const command of litOpenCodeCommands) {
46
+ const head = command.activationText.trim();
47
+ if (head === "" || !text.startsWith(head))
48
+ continue;
49
+ const route = nativeGoalRoute(command.id, "");
50
+ return route === undefined ? undefined : { route, work: text.slice(head.length) };
51
+ }
52
+ return undefined;
53
+ }
54
+ export function createNativeGoal(options) {
55
+ const { projectRoot, host } = options;
56
+ const client = host.rpc === undefined ? undefined : host.rpc(goalDefinition);
57
+ const said = new Set();
58
+ const running = new Set();
59
+ async function read(sessionID) {
60
+ if (client === undefined)
61
+ return { supported: false };
62
+ try {
63
+ return { supported: true, goal: parseGoal(await client.get({ sessionID })) };
64
+ }
65
+ catch (error) {
66
+ return hostLacksGoalRpc(error) ? { supported: false } : { supported: true, goal: "unreadable" };
67
+ }
68
+ }
69
+ async function notice(sessionID, line, key) {
70
+ if (line === "" || said.has(key))
71
+ return;
72
+ said.add(key);
73
+ try {
74
+ await host.session.synthetic({ sessionID, text: line, description: line, metadata: { source: "litopencode-goal" }, resume: false });
75
+ }
76
+ catch { // no-excuse-ok: catch -- a missing notice row must not stop the work; the ledger holds the record.
77
+ return;
78
+ }
79
+ }
80
+ async function fail(sessionID, route, step, error) {
81
+ await recordNativeGoal(projectRoot, sessionID, route, { type: "native-goal.failed", step, detail: failureDetail(error) });
82
+ }
83
+ async function perform(sessionID, route, action, goal, work) {
84
+ const record = goal === null || goal === "unreadable" ? undefined : goal;
85
+ const key = (name) => `${sessionID}:${name}:${record?.id ?? ""}:${action.kind === "report-conflict" ? action.reason : ""}`;
86
+ switch (action.kind) {
87
+ case "none":
88
+ case "keep":
89
+ return;
90
+ case "unavailable": {
91
+ if (route !== undefined)
92
+ await recordNativeGoalUnavailable(projectRoot, sessionID, route, "opencode-2-without-goal-rpc");
93
+ await notice(sessionID, nativeGoalLine(action, { route }), key("unavailable"));
94
+ return;
95
+ }
96
+ case "report-user-goal": {
97
+ if (!said.has(key("user-goal")) && record !== undefined) {
98
+ await recordNativeGoal(projectRoot, sessionID, route, { type: "native-goal.left-alone", goalID: record.id, status: record.status });
99
+ }
100
+ await notice(sessionID, nativeGoalLine(action, { route, ...(record === undefined ? {} : { goal: record }) }), key("user-goal"));
101
+ return;
102
+ }
103
+ case "report-conflict": {
104
+ if (!said.has(key("conflict"))) {
105
+ await recordNativeGoal(projectRoot, sessionID, route, { type: "native-goal.conflict", goalID: record?.id ?? "", reason: action.reason });
106
+ }
107
+ await notice(sessionID, nativeGoalLine(action, { route, ...(record === undefined ? {} : { goal: record }) }), key("conflict"));
108
+ return;
109
+ }
110
+ case "create": {
111
+ const objective = nativeGoalObjective(work, sessionID);
112
+ try {
113
+ await host.session.command({ sessionID, name: "goal", text: objective, delivery: "queue" });
114
+ }
115
+ catch (error) {
116
+ await fail(sessionID, route, "create", error);
117
+ return;
118
+ }
119
+ const after = await read(sessionID);
120
+ const created = after.supported && after.goal !== null && after.goal !== "unreadable" && after.goal.objective === objective ? after.goal : undefined;
121
+ if (created === undefined) {
122
+ await fail(sessionID, route, "create", "the goal was not readable after /goal; ownership not claimed");
123
+ return;
124
+ }
125
+ await recordNativeGoal(projectRoot, sessionID, route, { type: "native-goal.created", goalID: created.id });
126
+ await notice(sessionID, nativeGoalLine(action, { route, goal: created }), key("created"));
127
+ return;
128
+ }
129
+ case "resume":
130
+ case "clear": {
131
+ if (record === undefined)
132
+ return;
133
+ const control = action.kind;
134
+ try {
135
+ await host.session.command({ sessionID, name: "goal", text: control, delivery: "queue" });
136
+ }
137
+ catch (error) {
138
+ await fail(sessionID, route, control, error);
139
+ return;
140
+ }
141
+ const event = control === "resume" ? { type: "native-goal.resumed", goalID: record.id } : { type: "native-goal.cleared", goalID: record.id };
142
+ await recordNativeGoal(projectRoot, sessionID, route, event);
143
+ await notice(sessionID, nativeGoalLine(action, { route, goal: record }), key(control));
144
+ return;
145
+ }
146
+ }
147
+ }
148
+ /** One decision for one session: `route` set on an activation, absent on an idle check. */
149
+ async function step(sessionID, route, work) {
150
+ const owned = await nativeGoalOwnership(projectRoot, sessionID);
151
+ if (route === undefined && owned.goalID === undefined)
152
+ return;
153
+ const state = await read(sessionID);
154
+ const goal = state.supported ? state.goal : null;
155
+ const action = decideNativeGoal({
156
+ route,
157
+ supported: state.supported,
158
+ goal,
159
+ ownedGoalID: owned.goalID,
160
+ ledgerDone: owned.goalID === undefined ? false : await ledgerGoalDone(projectRoot, sessionID, owned.createdAt),
161
+ resumes: owned.resumes
162
+ });
163
+ await perform(sessionID, route, action, goal, work);
164
+ }
165
+ /** One step at a time per session, so a repeated activation sees the goal the first one made. */
166
+ const chains = new Map();
167
+ function track(sessionID, run) {
168
+ const previous = chains.get(sessionID) ?? Promise.resolve();
169
+ const safe = previous.then(run).catch(() => undefined).finally(() => {
170
+ running.delete(safe);
171
+ if (chains.get(sessionID) === safe)
172
+ chains.delete(sessionID);
173
+ });
174
+ chains.set(sessionID, safe);
175
+ running.add(safe);
176
+ return safe;
177
+ }
178
+ return {
179
+ /** Starts the goal step for a qualifying activation without holding up the user's prompt. */
180
+ activate(sessionID, route, work) {
181
+ return track(sessionID, () => step(sessionID, route, work));
182
+ },
183
+ /** The session finished its model work: clear a finished owned goal or report a conflict. */
184
+ idle(sessionID) {
185
+ return track(sessionID, () => step(sessionID, undefined, ""));
186
+ },
187
+ /** Resolves when every started goal step has finished. */
188
+ async settled() {
189
+ while (running.size > 0)
190
+ await Promise.all([...running]);
191
+ }
192
+ };
193
+ }
@@ -0,0 +1,49 @@
1
+ export type NativeGoalRoute = "lit-loop" | "start-work" | "litwork";
2
+ export type NativeGoalRecord = {
3
+ readonly id: string;
4
+ readonly objective: string;
5
+ readonly status: "active" | "paused" | "complete";
6
+ readonly reason?: string;
7
+ };
8
+ export type NativeGoalInput = {
9
+ /** The route that is activating now; absent on an idle check. */
10
+ readonly route: string | undefined;
11
+ /** Whether this host answers the `opencode.goal` RPC. */
12
+ readonly supported: boolean;
13
+ /** The host's goal for the session: null when none, "unreadable" when the read failed. */
14
+ readonly goal: NativeGoalRecord | null | "unreadable";
15
+ /** The goal id LitOpenCode created for this session, from its ledger. */
16
+ readonly ownedGoalID: string | undefined;
17
+ /** Whether LitOpenCode's evidence ledger marks the session's goal complete. */
18
+ readonly ledgerDone: boolean;
19
+ /** How many times LitOpenCode already resumed the owned goal. */
20
+ readonly resumes: number;
21
+ };
22
+ export type NativeGoalAction = {
23
+ readonly kind: "none";
24
+ } | {
25
+ readonly kind: "create";
26
+ } | {
27
+ readonly kind: "keep";
28
+ } | {
29
+ readonly kind: "resume";
30
+ } | {
31
+ readonly kind: "clear";
32
+ } | {
33
+ readonly kind: "report-user-goal";
34
+ } | {
35
+ readonly kind: "report-conflict";
36
+ readonly reason: string;
37
+ } | {
38
+ readonly kind: "unavailable";
39
+ };
40
+ /** The qualifying route for a chat mode or command id, or undefined for every other route. */
41
+ export declare function nativeGoalRoute(mode: string, text: string): NativeGoalRoute | undefined;
42
+ export declare function decideNativeGoal(input: NativeGoalInput): NativeGoalAction;
43
+ /** The text sent to `/goal`: the work, then the conditions OpenCode's reviewer judges it against. */
44
+ export declare function nativeGoalObjective(work: string, sessionID?: string): string;
45
+ /** The single line announced for an action; an empty string when nothing changed. */
46
+ export declare function nativeGoalLine(action: NativeGoalAction, detail: {
47
+ readonly route?: string;
48
+ readonly goal?: Pick<NativeGoalRecord, "id" | "status" | "objective">;
49
+ }): string;
@@ -0,0 +1,71 @@
1
+ // Policy for OpenCode 2's native `/goal`. It is pure: the route, the goal record the host reports and the
2
+ // state of LitOpenCode's own ledger go in; one action comes out. The host has no setter for a goal, only
3
+ // the `/goal` command, and that command replaces any goal that exists, so every create is decided here
4
+ // first. LitOpenCode manages only a goal it created (the ledger remembers the id); a goal the user made
5
+ // is reported and never touched. Done needs two things: OpenCode's verdict and the ledger.
6
+ import { containsSecret } from "../secret-shapes.js";
7
+ const goalRoutes = new Set(["lit-loop", "start-work", "litwork"]);
8
+ /** The host pauses a goal on a restart; it is the only pause that is safe to resume without asking. */
9
+ const restartPause = "The server restarted while the goal was active";
10
+ const maxResumes = 2;
11
+ const maxObjective = 4000;
12
+ const completionConditions = "Done only when all of these hold: the change works and the project's tests pass; a real-surface check of the path a user touches has run; temporary files are cleaned up with a cleanup receipt; and LitOpenCode's evidence ledger (.litopencode/litgoal) marks the goal complete.";
13
+ /** The qualifying route for a chat mode or command id, or undefined for every other route. */
14
+ export function nativeGoalRoute(mode, text) {
15
+ if (mode === "lit-loop" || mode === "start-work")
16
+ return mode;
17
+ if (mode === "litwork" || mode === "lit-work")
18
+ return "litwork";
19
+ if (mode !== "lit-task")
20
+ return undefined;
21
+ const first = text.trim().replace(/^\//u, "").split(/\s+/u)[0]?.toLowerCase();
22
+ return first === "litwork" || first === "lit-work" ? "litwork" : undefined;
23
+ }
24
+ export function decideNativeGoal(input) {
25
+ const routed = input.route !== undefined;
26
+ if (routed && !goalRoutes.has(input.route))
27
+ return { kind: "none" };
28
+ if (!input.supported)
29
+ return routed ? { kind: "unavailable" } : { kind: "none" };
30
+ const goal = input.goal;
31
+ if (goal === "unreadable")
32
+ return routed ? { kind: "report-conflict", reason: "the goal state could not be read, so none was set" } : { kind: "none" };
33
+ if (goal === null)
34
+ return routed ? { kind: "create" } : { kind: "none" };
35
+ if (input.ownedGoalID === undefined || goal.id !== input.ownedGoalID)
36
+ return routed ? { kind: "report-user-goal" } : { kind: "none" };
37
+ if (goal.status === "complete") {
38
+ return input.ledgerDone ? { kind: "clear" } : { kind: "report-conflict", reason: "OpenCode says the goal is met but the ledger is still open" };
39
+ }
40
+ if (goal.status === "paused") {
41
+ if (goal.reason === restartPause && input.resumes < maxResumes)
42
+ return routed ? { kind: "resume" } : { kind: "none" };
43
+ return routed ? { kind: "report-conflict", reason: `the goal is paused${goal.reason === undefined ? "" : ` (${oneLine(goal.reason, 60)})`} and needs a person to resume it` } : { kind: "none" };
44
+ }
45
+ return { kind: "keep" };
46
+ }
47
+ function oneLine(value, limit) {
48
+ const flat = value.replace(/\s+/gu, " ").trim();
49
+ return flat.length <= limit ? flat : `${flat.slice(0, limit - 1)}…`;
50
+ }
51
+ /** The text sent to `/goal`: the work, then the conditions OpenCode's reviewer judges it against. */
52
+ export function nativeGoalObjective(work, sessionID) {
53
+ const flat = work.replace(/\s+/gu, " ").trim();
54
+ const subject = flat === "" || containsSecret(flat) ? "the work requested in this session" : flat;
55
+ const conditions = sessionID === undefined || !/^[A-Za-z0-9_-]{1,80}$/u.test(sessionID) ? completionConditions : `${completionConditions} Open that ledger goal with --session-id ${sessionID}.`;
56
+ const room = maxObjective - conditions.length - 2;
57
+ return `${oneLine(subject, room)}\n\n${conditions}`;
58
+ }
59
+ /** The single line announced for an action; an empty string when nothing changed. */
60
+ export function nativeGoalLine(action, detail) {
61
+ const id = detail.goal === undefined ? "" : ` ${detail.goal.id.slice(0, 8)}`;
62
+ switch (action.kind) {
63
+ case "create": return `LitOpenCode goal: created for ${detail.route ?? "this work"}; OpenCode judges it, the ledger must agree`;
64
+ case "resume": return `LitOpenCode goal: resumed${id} after a server restart`;
65
+ case "clear": return `LitOpenCode goal: cleared${id}; OpenCode's verdict and the ledger both say done`;
66
+ case "report-user-goal": return `LitOpenCode goal: left yours alone${detail.goal === undefined ? "" : ` (${detail.goal.status}: ${oneLine(detail.goal.objective, 60)})`}`;
67
+ case "report-conflict": return `LitOpenCode goal: conflict, ${oneLine(action.reason, 120)}; not reporting done`;
68
+ case "unavailable": return "LitOpenCode goal: native-goal-unavailable (this OpenCode has no /goal); the ledger carries the work";
69
+ default: return "";
70
+ }
71
+ }
package/dist/v2/server.js CHANGED
@@ -11,6 +11,7 @@ import { applyLitAgents, createSessionModelRoute, enforcePlanningOnly, planLitAg
11
11
  import { createSessionLookupV2, projectRootFromLocation } from "./context.js";
12
12
  import { createV2Commands } from "./commands.js";
13
13
  import { createV2Events } from "./events.js";
14
+ import { createNativeGoal } from "./native-goal-runtime.js";
14
15
  import { createV2ToolHooks } from "./hooks.js";
15
16
  import { createV2SessionHooks } from "./session-hooks.js";
16
17
  import { addLitTools, litToolsForProject } from "./tools.js";
@@ -46,6 +47,7 @@ export async function setupLitOpenCodeServer(context) {
46
47
  model: context.model,
47
48
  onSessionDeleted: (sessionID) => sessionHooks.clear(sessionID)
48
49
  });
50
+ const nativeGoal = createNativeGoal({ projectRoot: root, host: { ...(context.rpc === undefined ? {} : { rpc: context.rpc }), session: context.session } });
49
51
  const sessionHooks = createV2SessionHooks({
50
52
  projectRoot: root,
51
53
  config: loaded.config,
@@ -54,14 +56,22 @@ export async function setupLitOpenCodeServer(context) {
54
56
  requestOptions: agentPlan.requestOptions,
55
57
  autoHandoffRoute: events.route,
56
58
  extraSystem: events.systemText,
57
- acceptCommandMark: commands.acceptMark
59
+ acceptCommandMark: commands.acceptMark,
60
+ nativeGoal
58
61
  });
59
62
  await context.session.hook("prompt", sessionHooks.prompt);
60
63
  await context.session.hook("context", sessionHooks.context);
61
64
  await context.session.hook("compaction", sessionHooks.compaction);
65
+ try {
66
+ await context.session.hook("experimental.idle", ({ sessionID }) => nativeGoal.idle(sessionID));
67
+ }
68
+ catch { // no-excuse-ok: catch -- an OpenCode 2 build without the idle hook has no native /goal either; the plugin still starts.
69
+ // Activation-time goal handling still runs; only the finished-goal check is missing.
70
+ }
62
71
  const stopEvents = events.start(context.event);
63
72
  return async () => {
64
73
  await stopEvents();
74
+ await nativeGoal.settled();
65
75
  deliverableHedgeGuard.dispose();
66
76
  await logger.dispose();
67
77
  };
@@ -2,6 +2,7 @@ import { type ChatActivationMode } from "../activation-routing.ts";
2
2
  import { type AutoHandoffRouteMode } from "../auto-handoff.ts";
3
3
  import type { LitOpenCodeConfig } from "../config.ts";
4
4
  import type { AgentRequestOptions } from "./agents.ts";
5
+ import { type NativeGoal } from "./native-goal-runtime.ts";
5
6
  export type SystemPartLike = {
6
7
  readonly type: "text";
7
8
  readonly text: string;
@@ -45,7 +46,10 @@ export type V2SessionHookOptions = {
45
46
  readonly acceptCommandMark?: (sessionID: string, marker: unknown) => {
46
47
  readonly command: string;
47
48
  readonly banner: string;
49
+ readonly work?: string;
48
50
  } | undefined;
51
+ /** Sets and manages OpenCode 2's native goal for lit-loop, start-work and litwork. */
52
+ readonly nativeGoal?: NativeGoal;
49
53
  };
50
54
  /**
51
55
  * `opencode2 run "lit fix it"` sends the message wrapped in double quotes (each multi-word argument is
@@ -17,6 +17,8 @@ import { startTypedWork, typedStartArgument } from "../start-work-typed.js";
17
17
  import { createLitGoalOperations } from "../ledger.js";
18
18
  import { applyCompactionRuleReset, applyStaticRuleInjection } from "../rules/hooks.js";
19
19
  import { isReadOnlyWorkflowFamilyPlan } from "../workflow-families.js";
20
+ import { aliasGoalRoute } from "./native-goal-runtime.js";
21
+ import { nativeGoalRoute } from "./native-goal.js";
20
22
  const bundledRulesDir = fileURLToPath(new URL("../../rules/bundled-rules", import.meta.url));
21
23
  /**
22
24
  * `opencode2 run "lit fix it"` sends the message wrapped in double quotes (each multi-word argument is
@@ -63,6 +65,12 @@ export function createV2SessionHooks(options) {
63
65
  }
64
66
  return state;
65
67
  }
68
+ /** Starts the native-goal step for a qualifying route; it never holds up or fails the prompt. */
69
+ function startNativeGoal(sessionID, route, work) {
70
+ if (route === undefined || options.nativeGoal === undefined)
71
+ return;
72
+ void options.nativeGoal.activate(sessionID, route, work);
73
+ }
66
74
  async function lookup(sessionID) {
67
75
  try {
68
76
  const session = await options.getSession(sessionID);
@@ -88,6 +96,7 @@ export function createV2SessionHooks(options) {
88
96
  }
89
97
  event.metadata = { ...rest, litopencode: { command: accepted.command, banner: accepted.banner, source: "command" } };
90
98
  options.onActivation?.(event.sessionID, accepted.command);
99
+ startNativeGoal(event.sessionID, nativeGoalRoute(accepted.command, ""), accepted.work ?? "");
91
100
  return true;
92
101
  }
93
102
  async function prompt(event) {
@@ -143,9 +152,13 @@ export function createV2SessionHooks(options) {
143
152
  options.onActivation?.(event.sessionID, "lit-adhd");
144
153
  return;
145
154
  }
155
+ const alias = aliasGoalRoute(input);
156
+ if (alias !== undefined)
157
+ startNativeGoal(event.sessionID, alias.route, alias.work);
146
158
  const mode = detectChatActivationMode(input, session.agent);
147
159
  if (mode === undefined)
148
160
  return;
161
+ startNativeGoal(event.sessionID, nativeGoalRoute(mode, input), input);
149
162
  const trigger = mode === "lit-handoff" ? "handoff" : mode === "lit-scientific-visualization" ? mode : "lit";
150
163
  let activation = promptForChatActivationMode(mode, input);
151
164
  if (mode === "lit-adhd")
@@ -1 +1 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" width="146" height="20" role="img" aria-label="release: 1.0.35"><title>release: 1.0.35</title><g shape-rendering="crispEdges"><rect width="49" height="20" fill="#080d14"/><rect x="49" width="97" height="20" fill="#ff6337"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text x="255" y="140" textLength="390" transform="scale(.1)">release</text><text x="965" y="140" textLength="870" transform="scale(.1)" fill="#080D14">1.0.35</text></g></svg>
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="146" height="20" role="img" aria-label="release: 1.0.36"><title>release: 1.0.36</title><g shape-rendering="crispEdges"><rect width="49" height="20" fill="#080d14"/><rect x="49" width="97" height="20" fill="#ff6337"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text x="255" y="140" textLength="390" transform="scale(.1)">release</text><text x="965" y="140" textLength="870" transform="scale(.1)" fill="#080D14">1.0.36</text></g></svg>
package/docs/migration.md CHANGED
@@ -29,7 +29,7 @@ This guide describes the supported migration shape for moving an OpenCode workfl
29
29
  - Planning routes now produce proportionate objective-achievable checklists rather than relying on a later review pass to discover missing execution detail. Each retained item carries action/output/verification, while risky or multi-stage work receives SDD-like gates only when justified. `/review-work` accepts both draft plans (read-only `PASS`/`ITERATE`/`NEEDS-CONTEXT`) and completed work (the existing five-lane review); it never implements a draft plan.
30
30
  - Keep `/start-work` as the native executable handoff to `lit-implement`. Chat-only start-work activation must begin with `start-work` or `lit start work`; diagnostic, copied, quoted, and blockquote mentions must not select start-work or append a false start-work event. An independent final `lit` remains an ordinary `lit-loop` activation.
31
31
  - Schema-3 bounded-authority state is package-owned under `.litopencode/litgoal/lit-loop`. Existing activity ledgers remain readable; strict `/start-work init|resume|cancel|complete|status` directives create and advance the separate work snapshot and bounded journal. Resume is trusted-user-only and the generic `start-work` tool does not gain a resume action.
32
- - Do not claim native goal binding unless the current OpenCode CLI or plugin API exposes a goal primitive; use LitOpenCode-owned `.litopencode/litgoal` state for durable goal behavior.
32
+ - Native goal binding exists only on OpenCode 2.2 and later (the `opencode.goal` RPC and the `/goal` command) and only for lit-loop, start-work and litwork; on OpenCode 1 and older OpenCode 2 hosts do not claim it, and keep LitOpenCode-owned `.litopencode/litgoal` state as the evidence record everywhere.
33
33
 
34
34
  ## Durable State
35
35
 
package/docs/reference.md CHANGED
@@ -255,8 +255,11 @@ folder would cover it. Paths in code fences and commands quoted in an inline cod
255
255
  When typed start-work begins, each valid `Scope roots:` line becomes a grant of the run, so it does
256
256
  not pause for a folder the approved plan named; the folder must exist then.
257
257
 
258
- Native goal verdict: the OpenCode host surface does not currently expose a native goal primitive;
259
- LitOpenCode uses `.litopencode/litgoal` instead. Static skills are installed under
258
+ Native goal verdict: OpenCode 2.2 and later has a native `/goal`, and LitOpenCode sets, resumes and
259
+ clears one only for lit-loop, start-work and litwork, only for goals it created; a goal you made is
260
+ reported and left alone. OpenCode 1 and older OpenCode 2 hosts record `native-goal-unavailable`.
261
+ The ledger under `.litopencode/litgoal` is the evidence record on every host, and done needs both
262
+ OpenCode's verdict and the ledger. Static skills are installed under
260
263
  `skills/*/SKILL.md` and remain documentation unless a documented OpenCode route selects them.
261
264
 
262
265
  ## Skill learning state
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@litfamily/litopencode",
3
- "version": "1.0.35",
3
+ "version": "1.0.36",
4
4
  "description": "Workflow plugin for OpenCode: add lit to a prompt and the work is planned, built, checked and written down so the next session can pick it up.",
5
5
  "keywords": [
6
6
  "opencode",
@@ -74,7 +74,7 @@
74
74
  },
75
75
  "devDependencies": {
76
76
  "@opencode-ai/plugin": "^1.17.6",
77
- "@opencode/plugin": "2.0.32",
77
+ "@opencode/plugin": "2.2.5",
78
78
  "@types/node": "^24.12.2",
79
79
  "picomatch": "^4.0.5",
80
80
  "typescript": "^5.9.3"
@@ -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`.
@@ -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`.