@litfamily/litopencode 1.0.35 → 1.0.37

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 (47) hide show
  1. package/CHANGELOG.md +10 -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/commands.js +17 -4
  7. package/dist/native-goal-ledger.d.ts +43 -0
  8. package/dist/native-goal-ledger.js +74 -0
  9. package/dist/tui/mission-skills.js +3 -1
  10. package/dist/tui/mission-source-v2.d.ts +23 -0
  11. package/dist/tui/mission-source-v2.js +29 -4
  12. package/dist/tui/mission-source.d.ts +1 -0
  13. package/dist/tui/mission-source.js +3 -2
  14. package/dist/v2/commands.d.ts +1 -0
  15. package/dist/v2/commands.js +2 -2
  16. package/dist/v2/context.d.ts +6 -1
  17. package/dist/v2/native-goal-runtime.d.ts +74 -0
  18. package/dist/v2/native-goal-runtime.js +193 -0
  19. package/dist/v2/native-goal.d.ts +49 -0
  20. package/dist/v2/native-goal.js +71 -0
  21. package/dist/v2/server.js +11 -1
  22. package/dist/v2/session-hooks.d.ts +4 -0
  23. package/dist/v2/session-hooks.js +13 -0
  24. package/dist/v2/tui-notices.d.ts +7 -0
  25. package/dist/v2/tui-notices.js +45 -0
  26. package/dist/v2/tui.d.ts +37 -1
  27. package/dist/v2/tui.js +309 -31
  28. package/docs/assets/readme/badge-version.svg +1 -1
  29. package/docs/migration.md +1 -1
  30. package/docs/reference-Ko-KR.md +13 -5
  31. package/docs/reference.md +22 -9
  32. package/package.json +2 -2
  33. package/skills/lit-typographic-motion/engine/cover/render.mjs +1 -1
  34. package/skills/lit-typographic-motion/engine/stage/capture.mjs +3 -2
  35. package/skills/lit-typographic-motion/engine/stage/gate.mjs +1 -0
  36. package/skills/lit-typographic-motion/engine/stage/render.mjs +1 -0
  37. package/skills/lit-typographic-motion/engine/stage/scan.mjs +109 -5
  38. package/skills/lit-typographic-motion/engine/stage/stage-kit.js +39 -1
  39. package/skills/lit-typographic-motion/references/cover.md +2 -1
  40. package/skills/lit-typographic-motion/references/runtime.md +1 -1
  41. package/skills/lit-typographic-motion/references/stage.md +48 -2
  42. package/skills/litgoal/SKILL.md +2 -2
  43. package/skills/litwork/SKILL.md +4 -0
  44. package/skills/managed-skill-manifest.json +9 -9
  45. package/skills/native-goal-verdict/SKILL.md +34 -60
  46. package/skills/start-work/SKILL.md +4 -0
  47. package/skills/workflow-loop/SKILL.md +4 -0
package/CHANGELOG.md CHANGED
@@ -5,6 +5,16 @@ are tracked separately from this product history.
5
5
 
6
6
  ## Unreleased
7
7
 
8
+ ## 1.0.37 - 2026-11-01
9
+
10
+ - `lit-typographic-motion` films can now carry real keyed footage, such as a reaction clip or a spinning-cat loop shot on a green screen. Key it outside the renderer into 8-bit RGBA PNG frames, put them in `stage/clips/<folder>/` and list them in `stage/clips/clips.json` (`id`, `dir`, `count`, `fps`). The page loads `/lit/clips.js`, registers a clip once with `LitStage.clip({ parent, id, box })` and shows it from `render(t)`; the frame is picked from film time, so a cue is plain arithmetic and the determinism replay holds. A declared clip is checked against its bytes (12 clips, 360 frames each and 1500 in all, 40 MB each and 128 MB in all, 720 px on the long side, up to 30 fps, real transparency) and is exempt from the flipbook rule; any frame sequence you did not declare is still refused with exit 17. A cover takes no clips. The flash audit and the text checks run on the final pixels, and the report lists each clip. `LitStage.clip.circle`, `wipe` and `iris` keep working.
11
+ - On OpenCode 2 the lit HUD is as complete as on OpenCode 1. The status card (discipline, workflow map, bar, live step, skill chips, lanes in parallel, context use and branch) now sits under every session, the stage chip appears beside the ignition line under the prompt, and the home screen shows the last goal. Until now only the ignition line and the sidebar panel were drawn there. The card reads OpenCode 2's own tool names, so a `skill` call (`id`) lights its chip and `subagent` calls show as lanes.
12
+ - OpenCode 2 also gets pop-ups for the workflow stage moving, a discipline switching and a lane starting or finishing, next to the plan, goal, blocker and completion pop-ups it already had. The restart notice, the `lit adhd` suggestion and the opt-in finish alert (`notify` in the plugin options) work there too.
13
+
14
+ ## 1.0.36 - 2026-10-31
15
+
16
+ - 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.
17
+
8
18
  ## 1.0.35 - 2026-10-31
9
19
 
10
20
  - `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.37/docs/assets/cover-motion-still.webp" /><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.37/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.37/docs/assets/readme/badge-version.svg" alt="1.0.37" /></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.37/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.37/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.37/docs/reference-Ko-KR.md) · [English reference](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.37/docs/reference.md)
61
+ - [마이그레이션 안내](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.37/docs/migration.md) · [Changelog](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.37/CHANGELOG.md) · [개인정보와 네트워크 동작](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.37/docs/privacy.md)
62
+ - [LitFamily 허브](https://github.com/wjgoarxiv/litfamily) · [MIT 라이선스](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.37/LICENSE) · [상표 정책](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.37/TRADEMARK.md) · [ASCII 글꼴 라이선스](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.37/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.37/docs/assets/cover-motion-still.webp" /><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.37/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.37/docs/assets/readme/badge-version.svg" alt="1.0.37" /></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.37/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.37/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.37/docs/reference.md)
61
+ - [Migration notes](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.37/docs/migration.md) · [Changelog](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.37/CHANGELOG.md) · [Privacy and network behavior](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.37/docs/privacy.md)
62
+ - [LitFamily hub](https://github.com/wjgoarxiv/litfamily) · [MIT license](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.37/LICENSE) · [Trademark policy](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.37/TRADEMARK.md) · [ASCII font license](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.37/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
+ }
@@ -25,7 +25,9 @@ export function readSkillUse(part, at) {
25
25
  const input = isRecord(state.input) ? state.input : {};
26
26
  const start = isRecord(state.time) && typeof state.time.start === "number" ? state.time.start : at;
27
27
  if (part.tool === "skill") {
28
- return typeof input.name === "string" && ownSkills.has(input.name) ? { name: input.name, at: start } : undefined;
28
+ // OpenCode 1's skill tool takes `name`; OpenCode 2's takes `id`.
29
+ const skill = typeof input.name === "string" ? input.name : typeof input.id === "string" ? input.id : undefined;
30
+ return skill !== undefined && ownSkills.has(skill) ? { name: skill, at: start } : undefined;
29
31
  }
30
32
  const name = ownTools[part.tool];
31
33
  if (name === undefined || input.action === "status")
@@ -15,16 +15,39 @@ type V2Message = {
15
15
  readonly content?: ReadonlyArray<{
16
16
  readonly type: string;
17
17
  readonly name?: string;
18
+ readonly text?: string;
18
19
  readonly state?: {
19
20
  readonly status?: string;
21
+ readonly input?: unknown;
22
+ readonly metadata?: unknown;
20
23
  };
21
24
  readonly time?: {
22
25
  readonly created?: number;
23
26
  readonly ran?: number;
27
+ readonly completed?: number;
24
28
  };
25
29
  }>;
26
30
  };
31
+ type V2Model = {
32
+ readonly providerID: string;
33
+ readonly modelID: string;
34
+ readonly limit?: {
35
+ readonly context?: number;
36
+ };
37
+ };
27
38
  export type MissionDataV2 = {
39
+ readonly location?: {
40
+ readonly vcs?: {
41
+ info(): {
42
+ readonly branch?: {
43
+ readonly current?: string;
44
+ };
45
+ } | undefined;
46
+ };
47
+ readonly model?: {
48
+ list(): ReadonlyArray<V2Model> | undefined;
49
+ };
50
+ };
28
51
  readonly session: {
29
52
  status(sessionID: string): "idle" | "running";
30
53
  readonly message: {
@@ -5,11 +5,23 @@ function isMessage(value) {
5
5
  export function sessionMessagesV2(data, sessionID) {
6
6
  return (data.session.message.list(sessionID) ?? []).filter(isMessage).filter((message) => message.type === "user" || message.type === "assistant");
7
7
  }
8
+ /** The provider table the context figure reads: each model's context window, from the host's model list. */
9
+ function providersV2(data) {
10
+ const byProvider = new Map();
11
+ for (const model of data.location?.model?.list() ?? []) {
12
+ const models = byProvider.get(model.providerID) ?? {};
13
+ models[model.modelID] = { limit: { context: model.limit?.context } };
14
+ byProvider.set(model.providerID, models);
15
+ }
16
+ return [...byProvider].map(([id, models]) => ({ id, models }));
17
+ }
8
18
  export function missionStateFromV2(data, directory) {
9
19
  const index = new Map();
20
+ const branch = data.location?.vcs?.info()?.branch?.current?.trim();
10
21
  return {
11
22
  path: directory === undefined ? {} : { directory },
12
- provider: [],
23
+ ...(branch === undefined || branch === "" ? {} : { vcs: { branch } }),
24
+ provider: providersV2(data),
13
25
  session: {
14
26
  messages(sessionID) {
15
27
  return sessionMessagesV2(data, sessionID).map((message) => {
@@ -35,9 +47,22 @@ export function missionStateFromV2(data, directory) {
35
47
  return [];
36
48
  if (message.type === "user")
37
49
  return message.metadata === undefined ? [] : [{ metadata: message.metadata }];
38
- return (message.content ?? [])
39
- .filter((item) => item.type === "tool" && typeof item.name === "string")
40
- .map((item) => ({ type: "tool", tool: item.name, state: { status: item.state?.status, time: { start: item.time?.ran ?? item.time?.created } } }));
50
+ return (message.content ?? []).flatMap((item) => {
51
+ if (item.type === "text" && typeof item.text === "string")
52
+ return [{ type: "text", text: item.text }];
53
+ if (item.type !== "tool" || typeof item.name !== "string")
54
+ return [];
55
+ return [{
56
+ type: "tool",
57
+ tool: item.name,
58
+ state: {
59
+ status: item.state?.status,
60
+ input: item.state?.input,
61
+ metadata: item.state?.metadata,
62
+ time: { start: item.time?.ran ?? item.time?.created, ...(item.time?.completed === undefined ? {} : { end: item.time.completed }) }
63
+ }
64
+ }];
65
+ });
41
66
  }
42
67
  };
43
68
  }
@@ -52,6 +52,7 @@ export type MissionStateSource = {
52
52
  readonly tool?: unknown;
53
53
  readonly state?: unknown;
54
54
  readonly metadata?: unknown;
55
+ readonly text?: unknown;
55
56
  }>;
56
57
  };
57
58
  /** The same root the server plugin uses: the worktree unless it is the filesystem root, else the directory. */
@@ -330,7 +330,8 @@ function taskSources(state, sessionID, now) {
330
330
  const assistants = state.session.messages(sessionID).filter((message) => message.role === "assistant").slice(-recentAssistantMessages);
331
331
  for (const message of assistants) {
332
332
  for (const part of state.part(message.id)) {
333
- if (part.type !== "tool" || part.tool !== "task" || !isRecord(part.state))
333
+ // OpenCode 1 delegates through `task`, OpenCode 2 through `subagent`.
334
+ if (part.type !== "tool" || (part.tool !== "task" && part.tool !== "subagent") || !isRecord(part.state))
334
335
  continue;
335
336
  const toolState = part.state;
336
337
  const status = toolState.status;
@@ -348,7 +349,7 @@ function taskSources(state, sessionID, now) {
348
349
  children.add(child);
349
350
  const todos = child === undefined ? [] : state.session.todo(child).filter((todo) => todo.status !== "cancelled");
350
351
  const done = todos.filter((todo) => todo.status === "completed").length;
351
- const name = typeof input.description === "string" && input.description.trim() !== "" ? input.description.trim() : typeof input.subagent_type === "string" ? input.subagent_type : "subagent";
352
+ const name = typeof input.description === "string" && input.description.trim() !== "" ? input.description.trim() : typeof input.subagent_type === "string" ? input.subagent_type : typeof input.agent === "string" ? input.agent : "subagent";
352
353
  // A running task last moved when its own session did; its start time alone would read a long, busy run as idle.
353
354
  const moved = child === undefined ? undefined : lastActivity(state, child);
354
355
  sources.push({
@@ -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 {};