@litfamily/litopencode 1.0.26 → 1.0.28

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,22 @@ are tracked separately from this product history.
5
5
 
6
6
  ## Unreleased
7
7
 
8
+ ## 1.0.28 - 2026-10-18
9
+
10
+ - The README gets you started sooner and says who LitOpenCode is for: people who give OpenCode work that spans more than one sitting, such as a feature over a few sessions, a planned refactor, research they come back to, or a report or deck built from their notes. A short evening-and-morning sketch shows a session picked up the next day, and the requirements now name OpenCode 1.18 or 1.19, because OpenCode 2 loads plugins differently and is not supported yet. The images are unchanged.
11
+
12
+ - Leftover notes about the retired skill-hint feature are gone from the release checklist and the privacy page.
13
+
14
+ ## 1.0.27 - 2026-10-18
15
+
16
+ - When a startup update fails and the rollback leaves the old setup working, OpenCode no longer reruns the snapshot and npm install on every start. Startup leaves that same version alone for a day; an explicit install still runs it, and a newer release is tried at once. The receipt names the failed step, a timeout or a missing command, without command output. `litopencode doctor` now reports the update state, its cause, what the next start will run ("installed X, running Y") and a remedy for each cause, and says it cannot see inside a running OpenCode.
17
+
18
+ - The optional Jev skill hint is gone. In testing it improved skill choice by no more than run-to-run noise, and in real use the model followed none of the 45 hints it received. LitOpenCode no longer contacts TypeSafe, adds no hint line or toast, and `litopencode doctor` no longer reports it. `LITOPENCODE_JEV`, the other `LITOPENCODE_JEV_*` variables and `TYPESAFE_API_KEY` are now ignored and can be deleted, and so can an old debug trace at `.litopencode/logs/jev-skill-hint.jsonl`.
19
+
20
+ - lit-loop now loads the skill that matches a request before it inspects the workspace; plain questions and requests no listed skill covers still load none. The review-work trigger now names reviewing a patch, diff, PR or open changes, with Korean cues, and the workflow-loop trigger names carrying one feature, fix or ticket through build, tests, real-surface checks and cleanup.
21
+
22
+ - A request whose deliverable is a Markdown or plain-text file, such as `report.md` or `slides.md`, now gets ordinary Markdown. lit-docx and lit-pptx no longer write their converter source (YAML front matter and `:::` blocks) into those files or start a DOCX or PPTX build; they keep that source format only for files they render.
23
+
8
24
  ## 1.0.26 - 2026-10-16
9
25
 
10
26
  - The write guard now refuses more of the lines that tell a reader what a document is not for, such as `This table is not a ranking.` or `이 문서는 제품을 비교하려는 것이 아닙니다.` These forms came from real sessions. Bold-label callouts such as `> **주의:** ...` are now read too, while ordinary quoted text is still skipped, and lines from manuals and lab reports that only look similar stay allowed. Files saved through `apply_patch` are now checked before they land, the same way the write and edit tools already were.
package/README-Ko-KR.md CHANGED
@@ -1,29 +1,29 @@
1
- <p align="center"><picture><source media="(prefers-reduced-motion: reduce)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.26/docs/assets/cover-motion-still.webp" /><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.26/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.28/docs/assets/cover-motion-still.webp" /><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.28/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.26/docs/assets/readme/badge-version.svg" alt="1.0.26" /></a>
8
+ <a href="https://www.npmjs.com/package/@litfamily/litopencode"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.28/docs/assets/readme/badge-version.svg" alt="1.0.28" /></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.26/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.26/docs/assets/readme/badge-license.svg" alt="MIT 라이선스" /></a>
10
+ <a href="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.28/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.28/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
 
14
- LitOpenCode는 목표와 계획, 확인한 내용, 다음 할 일을 프로젝트 안의 `.litopencode/litgoal/`에 남겨 두기 때문에, 긴 작업도 다음 날 멈춘 자리에서 이어 갈 수 있습니다.
14
+ LitOpenCode는 목표와 계획, 확인한 내용, 다음 할 일을 프로젝트 안의 `.litopencode/litgoal/`에 남겨 두기 때문에, 긴 작업도 다음 날 멈춘 자리에서 이어 갈 수 있습니다. 몇 세션에 걸쳐 만드는 기능, 계획을 세워 진행하는 리팩터링, 며칠씩 이어 가는 조사, 메모로 만드는 보고서나 발표자료처럼 한 번에 끝나지 않는 OpenCode 작업에 잘 맞습니다.
15
15
 
16
16
  **[전체 안내, 화면, 스킬 갤러리는 GitHub에서 →](https://github.com/wjgoarxiv/litopencode/blob/main/README-Ko-KR.md)**
17
17
 
18
18
  ## 빠르게 시작하기
19
19
 
20
- Node.js와 npm, 그리고 모델 제공자를 설정해 둔 OpenCode가 있으면 됩니다.
20
+ Node.js와 npm, 그리고 모델 제공자를 설정해 둔 OpenCode 1.18 또는 1.19가 있으면 됩니다. LitOpenCode는 OpenCode 1용입니다. 플러그인 로딩 방식이 달라진 OpenCode 2는 아직 지원하지 않습니다.
21
21
 
22
22
  ```sh
23
23
  npm exec --package @litfamily/litopencode@latest -- litopencode install
24
24
  ```
25
25
 
26
- 기본값으로 바로 설치하려면 `--yes`를, 아무것도 쓰지 않고 무엇이 바뀔지만 보려면 `--dry-run`을 붙이세요. OpenCode를 다시 시작하고 **Tab** 키로 **lit-loop**를 고른 뒤, 빈 폴더를 열고 이렇게 보냅니다.
26
+ 무엇이 바뀔지 먼저 보려면 `--dry-run`을, 기본값으로 질문 없이 설치하려면 `--yes`를 붙이세요. OpenCode를 다시 시작하고 **Tab** 키로 **lit-loop**를 고른 뒤, 빈 폴더를 열고 이렇게 보냅니다.
27
27
 
28
28
  ```text
29
29
  lit 외부 의존성 없이 index.html 하나로 할 일 목록을 만들고, 추가와 완료 표시를 확인한 뒤 다음 할 일을 남겨줘.
@@ -46,7 +46,7 @@ lit 외부 의존성 없이 index.html 하나로 할 일 목록을 만들고,
46
46
 
47
47
  ## 업데이트와 제거
48
48
 
49
- LitOpenCode는 대화형으로 시작할 때와 install·doctor가 성공한 뒤 새 버전을 확인합니다. 끄려면 `--no-auto-update`를 붙이거나 `LITOPENCODE_NO_AUTO_UPDATE=1`을 설정하세요. `npm exec --package @litfamily/litopencode@latest -- litopencode doctor`는 설치 상태를 확인할 뿐 아무것도 바꾸지 않습니다.
49
+ LitOpenCode는 대화형으로 시작할 때와 install·doctor가 성공한 뒤 새 버전을 확인합니다. 업데이트 시점을 직접 정하고 싶다면 `--no-auto-update`를 붙이거나 `LITOPENCODE_NO_AUTO_UPDATE=1`을 설정하세요. `npm exec --package @litfamily/litopencode@latest -- litopencode doctor`는 설치 상태를 확인할 뿐 아무것도 바꾸지 않습니다.
50
50
 
51
51
  제거하려면 OpenCode의 `opencode.jsonc`에서 `plugin` 배열의 `@litfamily/litopencode` 항목을 지우고 OpenCode를 다시 시작하세요. 전역으로 설치했다면 다음 명령도 실행합니다.
52
52
 
@@ -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.26/docs/reference-Ko-KR.md) · [English reference](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.26/docs/reference.md)
61
- - [마이그레이션 안내](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.26/docs/migration.md) · [Changelog](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.26/CHANGELOG.md) · [개인정보와 네트워크 동작](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.26/docs/privacy.md)
62
- - [LitFamily 허브](https://github.com/wjgoarxiv/litfamily) · [MIT 라이선스](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.26/LICENSE) · [ASCII 글꼴 라이선스](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.26/docs/assets/readme/JetBrainsMono-OFL.txt)
60
+ - [워크플로 상세 안내](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.28/docs/reference-Ko-KR.md) · [English reference](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.28/docs/reference.md)
61
+ - [마이그레이션 안내](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.28/docs/migration.md) · [Changelog](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.28/CHANGELOG.md) · [개인정보와 네트워크 동작](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.28/docs/privacy.md)
62
+ - [LitFamily 허브](https://github.com/wjgoarxiv/litfamily) · [MIT 라이선스](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.28/LICENSE) · [ASCII 글꼴 라이선스](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.28/docs/assets/readme/JetBrainsMono-OFL.txt)
63
63
 
64
64
  <details>
65
65
  <summary>ASCII 로고 복사</summary>
package/README.md CHANGED
@@ -1,29 +1,29 @@
1
- <p align="center"><picture><source media="(prefers-reduced-motion: reduce)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.26/docs/assets/cover-motion-still.webp" /><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.26/docs/assets/cover-motion.webp" width="100%" alt="LitFamily motion cover: five 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.28/docs/assets/cover-motion-still.webp" /><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.28/docs/assets/cover-motion.webp" width="100%" alt="LitFamily motion cover: five 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.26/docs/assets/readme/badge-version.svg" alt="1.0.26" /></a>
8
+ <a href="https://www.npmjs.com/package/@litfamily/litopencode"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.28/docs/assets/readme/badge-version.svg" alt="1.0.28" /></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.26/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.26/docs/assets/readme/badge-license.svg" alt="MIT license" /></a>
10
+ <a href="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.28/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.28/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
 
14
- LitOpenCode keeps the goal, the plan, what was checked and the next step in `.litopencode/litgoal/` inside your project, so a long task picks up the next day where it stopped.
14
+ LitOpenCode keeps the goal, the plan, what was checked and the next step in `.litopencode/litgoal/` inside your project, so a long task picks up the next day where it stopped. It is made for OpenCode work that spans more than one sitting: features over several sessions, planned refactors, research you return to, and reports or decks built from your notes.
15
15
 
16
16
  **[Full guide, screens and skills gallery on GitHub →](https://github.com/wjgoarxiv/litopencode#readme)**
17
17
 
18
18
  ## Quick start
19
19
 
20
- You need Node.js with npm, and OpenCode with a model provider set up.
20
+ You need Node.js with npm, and OpenCode 1.18 or 1.19 with a model provider set up. LitOpenCode is built for OpenCode 1. OpenCode 2 changed how plugins load, and LitOpenCode does not support it yet.
21
21
 
22
22
  ```sh
23
23
  npm exec --package @litfamily/litopencode@latest -- litopencode install
24
24
  ```
25
25
 
26
- Add `--yes` to take the defaults, or `--dry-run` to preview without writing anything. Restart OpenCode, press **Tab**, pick **lit-loop**, open an empty folder and send:
26
+ Add `--dry-run` to see what would change before anything is written, or `--yes` to take the defaults and skip the questions. Restart OpenCode, press **Tab**, pick **lit-loop**, open an empty folder and send:
27
27
 
28
28
  ```text
29
29
  lit Build a to-do list in one index.html with no dependencies; check adding and completing items, then leave results and next steps.
@@ -46,7 +46,7 @@ Each skill is pictured in the [skills gallery on GitHub](https://github.com/wjgo
46
46
 
47
47
  ## Updates and removal
48
48
 
49
- LitOpenCode checks for updates on interactive start and after a successful install or doctor run. To turn that off, pass `--no-auto-update` or set `LITOPENCODE_NO_AUTO_UPDATE=1`. `npm exec --package @litfamily/litopencode@latest -- litopencode doctor` checks the setup and changes nothing.
49
+ LitOpenCode checks for updates on interactive start and after a successful install or doctor run. If you'd rather choose when to update, pass `--no-auto-update` or set `LITOPENCODE_NO_AUTO_UPDATE=1`. `npm exec --package @litfamily/litopencode@latest -- litopencode doctor` checks the setup and changes nothing.
50
50
 
51
51
  To remove it, delete the `@litfamily/litopencode` entry from the `plugin` array in OpenCode's `opencode.jsonc` and restart OpenCode. If you installed it globally:
52
52
 
@@ -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.26/docs/reference.md)
61
- - [Migration notes](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.26/docs/migration.md) · [Changelog](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.26/CHANGELOG.md) · [Privacy and network behavior](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.26/docs/privacy.md)
62
- - [LitFamily hub](https://github.com/wjgoarxiv/litfamily) · [MIT license](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.26/LICENSE) · [ASCII font license](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.26/docs/assets/readme/JetBrainsMono-OFL.txt)
60
+ - [Workflow reference](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.28/docs/reference.md)
61
+ - [Migration notes](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.28/docs/migration.md) · [Changelog](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.28/CHANGELOG.md) · [Privacy and network behavior](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.28/docs/privacy.md)
62
+ - [LitFamily hub](https://github.com/wjgoarxiv/litfamily) · [MIT license](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.28/LICENSE) · [ASCII font license](https://cdn.jsdelivr.net/npm/@litfamily/litopencode@1.0.28/docs/assets/readme/JetBrainsMono-OFL.txt)
63
63
 
64
64
  <details>
65
65
  <summary>Copy ASCII logo</summary>
@@ -67,6 +67,11 @@ const diagramCreationIntent = /^\s*(?:lit[,:]?\s+)?(?:please\s+)?(?:create|draw|
67
67
  const officeAuthoringVerb = /(?<![a-z])(?:make|create|write|draft|prepare|produce|generate|convert|edit|build)(?![a-z])|(?<![a-z])turn(?=(?:[^.!?\n]|\.(?=\S))*?(?<![a-z])into(?![a-z])(?:[^.!?\n]|\.(?=\S))*?(?<![a-z])(?:slides?|decks?|presentations?|pptx?|reports?|documents?|docx|word)(?![a-z]))|만들|작성|써줘|제작|변환|편집/u;
68
68
  const reportNoun = /(?<![a-z])(?:reports?|documents?|docx|word)(?![a-z])|보고서|리포트|기획서|제안서|문서|워드/u;
69
69
  const slidesNoun = /(?<![a-z])(?:slides?|decks?|presentations?|pptx?)(?![a-z])|발표자료|발표|슬라이드|피피티|덱/u;
70
+ // A deliverable the user asked to receive as Markdown or plain text ("saved as report.md", "in Markdown",
71
+ // "as plain text"). An input file such as "turn notes.md into slides" is not one. A named Office or PDF
72
+ // output keeps the skill route, because the skill then renders that file from its own source.
73
+ const markdownDeliverable = /(?<![a-z])(?:save|saved|saving|write|written|output|deliver|export)\s+(?:(?:it|them|this|both|each)\s+)?(?:as|to|in|into)\s+`?[\w./-]*\.(?:md|markdown|txt)(?![a-z])|(?<![a-z])(?:as|in)\s+(?:plain\s+)?markdown(?![a-z])|(?<![a-z])plain[- ]text(?![a-z])|마크다운(?:으로|\s*파일로|\s*형식)|\.md\s*(?:로|으로|파일로)\s*(?:저장|작성)/u;
74
+ const officeFileFormat = /(?<![a-z])(?:docx|pptx?|pdf|word|powerpoint)(?![a-z])|워드|파워포인트|피피티/u;
70
75
  // lit-typographic-motion (MO-C-18..24). The trigger is a creation verb plus a compound motion-video
71
76
  // noun, or plus a bare video noun that none of the five exclusions claims. Bare 모션/motion and
72
77
  // 인트로/intro never trigger alone. The verbs are the office/UI making verbs plus a small motion set
@@ -378,7 +383,12 @@ export function promptForChatActivationMode(mode, triggerText = "") {
378
383
  After the workspace check, use OpenCode's native skill tool to load lit-diagram-drawer before authoring the requested conceptual diagram. This is the one narrowly relevant skill for this bounded task. Resolve resources from the skill location returned by OpenCode, follow its verification and export steps, and inspect the final PNG. If the skill is unavailable, report that limit instead of claiming it ran.
379
384
  </lit-task-mode>`);
380
385
  }
381
- if (mode === "lit-task" && officeAuthoringVerb.test(raw)) {
386
+ if (mode === "lit-task" && officeAuthoringVerb.test(raw) && markdownDeliverable.test(raw) && !officeFileFormat.test(raw) && (reportNoun.test(raw) || slidesNoun.test(raw))) {
387
+ targetedPrompt = targetedPrompt.replace("</lit-task-mode>", `
388
+ The requested deliverable is a Markdown or plain-text file, not an Office file. Write each named file as ordinary Markdown that a reader opens directly: headings, paragraphs, lists and tables, with no YAML front matter, no \`:::\` directive blocks and no tonality, layout or title-treatment keys. Do not build a DOCX or PPTX and do not load lit-docx or lit-pptx for it; their Markdown is converter source and belongs only beside a file that skill renders.
389
+ </lit-task-mode>`);
390
+ }
391
+ else if (mode === "lit-task" && officeAuthoringVerb.test(raw)) {
382
392
  const skills = [reportNoun.test(raw) ? "lit-docx" : undefined, slidesNoun.test(raw) ? "lit-pptx" : undefined].filter(Boolean);
383
393
  if (skills.length > 0)
384
394
  targetedPrompt = targetedPrompt.replace("</lit-task-mode>", `
@@ -2,7 +2,6 @@ import type { Hooks, PluginInput } from "@opencode-ai/plugin";
2
2
  import type { SessionLookup } from "./session-lineage.ts";
3
3
  import type { BoundedAuthorityOptions } from "./bounded-authority.ts";
4
4
  import { type AutoHandoffRouteMode } from "./auto-handoff.ts";
5
- import { type JevSkillHintPart } from "./jev-skill-hint.ts";
6
5
  export { litHandoffPromptInjection, scientificVisualizationPromptInjection } from "./activation-managed-prompts.ts";
7
6
  export { litActivationBanner } from "./activation-prompt-utils.ts";
8
7
  export { litLoopPromptInjection, litPlanPromptInjection } from "./activation-primary-prompts.ts";
@@ -11,12 +10,6 @@ export { comprehendPromptInjection, debuggingPromptInjection, deepInterviewPromp
11
10
  type ChatMessageHook = NonNullable<Hooks["chat.message"]>;
12
11
  type ActivationToastClient = Pick<PluginInput["client"], "tui">;
13
12
  export declare function showActivationToast(client: ActivationToastClient | undefined, skillId: string, env?: NodeJS.ProcessEnv): void;
14
- /**
15
- * One quiet `info` toast per hinted turn; notes, `none` and silent turns never reach it. On the first
16
- * eligible turn of a session (`announce`) the standout ON notice goes out instead, with that turn's hint
17
- * as its body: the TUI keeps a single toast, so a second one would replace the notice at once.
18
- */
19
- export declare function showJevSkillHintToast(client: ActivationToastClient | undefined, hint: JevSkillHintPart | undefined, announce?: boolean): void;
20
13
  export type ChatMessageActivationOptions = {
21
14
  readonly getSession?: SessionLookup;
22
15
  readonly hostArgv?: readonly string[];
@@ -30,8 +23,5 @@ export type ChatMessageActivationOptions = {
30
23
  readonly trustedInjectedSkill?: (sessionID: string) => string | undefined;
31
24
  /** Restores that skill after the root-turn reset; the command already showed its activation toast. */
32
25
  readonly onTrustedSkillResume?: (sessionID: string, skillId: string) => void | Promise<void>;
33
- /** Consumes the mark `command.execute.before` left for this session's next message. */
34
- readonly isCommandTurn?: (sessionID: string) => boolean;
35
- readonly skillHint?: (sessionID: string, promptText: string) => Promise<JevSkillHintPart | undefined>;
36
26
  };
37
27
  export declare function createChatMessageActivationHook(projectRoot: string, options?: ChatMessageActivationOptions): ChatMessageHook;
@@ -8,7 +8,6 @@ import { isReadOnlyWorkflowFamilyPlan } from "./workflow-families.js";
8
8
  import { queryKnowledge } from "./knowledge.js";
9
9
  import { activationDiscipline } from "./activation-probe.js";
10
10
  import { micro, supportsMarkGlyphs } from "./lit-mark.js";
11
- import { jevSkillHintAwarenessText, jevSkillHintToastText } from "./jev-skill-hint.js";
12
11
  // Prompt modules preserve minimum-first, single-task or few-task planning, avoidable custom code review,
13
12
  // and external-source safety; this module only coordinates their unchanged activation surfaces.
14
13
  export { litHandoffPromptInjection, scientificVisualizationPromptInjection } from "./activation-managed-prompts.js";
@@ -35,32 +34,6 @@ export function showActivationToast(client, skillId, env = process.env) {
35
34
  // The activation prompt is the primary surface; this toast is best-effort UI feedback.
36
35
  }
37
36
  }
38
- /**
39
- * One quiet `info` toast per hinted turn; notes, `none` and silent turns never reach it. On the first
40
- * eligible turn of a session (`announce`) the standout ON notice goes out instead, with that turn's hint
41
- * as its body: the TUI keeps a single toast, so a second one would replace the notice at once.
42
- */
43
- export function showJevSkillHintToast(client, hint, announce = false) {
44
- const hinted = hint?.kind === "hint" ? jevSkillHintToastText(hint.skillId, hint.latencyMs) : undefined;
45
- let body;
46
- if (announce) {
47
- body = hinted === undefined
48
- ? { message: jevSkillHintAwarenessText, variant: "warning" }
49
- : { title: jevSkillHintAwarenessText, message: hinted, variant: "warning" };
50
- }
51
- else if (hinted !== undefined) {
52
- body = { message: hinted, variant: "info" };
53
- }
54
- else {
55
- return;
56
- }
57
- try {
58
- const pending = client?.tui?.showToast({ body });
59
- void pending?.catch(() => undefined);
60
- }
61
- catch { // no-excuse-ok: catch -- a missing or failing TUI must never block the user message.
62
- }
63
- }
64
37
  const knownSubagentAgentNames = new Set([
65
38
  ...litOpenCodeAgents.filter((agent) => agent.mode !== "primary").map((agent) => agent.id),
66
39
  "build",
@@ -92,9 +65,6 @@ export function createChatMessageActivationHook(projectRoot, options = {}) {
92
65
  if (processedOutputs.has(output))
93
66
  return;
94
67
  processedOutputs.add(output);
95
- // Command marks are keyed by session and consumed here, on every turn: the host hands this hook a
96
- // new parts array, so the array `command.execute.before` saw can never be matched by identity.
97
- const commandTurn = options.isCommandTurn?.(input.sessionID) === true;
98
68
  const trustedInjectedSkill = options.trustedInjectedSkill?.(input.sessionID);
99
69
  const agent = input.agent ?? (typeof output.message.agent === "string" ? output.message.agent : undefined);
100
70
  const childSession = await isChildSessionMessage(input.sessionID, agent, options.getSession);
@@ -161,30 +131,6 @@ export function createChatMessageActivationHook(projectRoot, options = {}) {
161
131
  // A malformed or unreadable knowledge store stays silent instead of inventing context.
162
132
  }
163
133
  }
164
- // The optional skill hint only speaks on root turns that no command or deterministic route claimed.
165
- if (!childSession && mode === undefined && options.skillHint !== undefined && !commandTurn) {
166
- const promptText = nonEmptyTextParts
167
- .filter((part) => part.synthetic !== true)
168
- .map((part) => part.text)
169
- .join("\n");
170
- try {
171
- const hint = await options.skillHint(input.sessionID, promptText);
172
- if (hint !== undefined) {
173
- output.parts.push({
174
- id: `prt_litopencode_skill_hint_${idSuffix(input.messageID ?? output.message.id)}`,
175
- sessionID: input.sessionID,
176
- messageID: input.messageID ?? output.message.id,
177
- type: "text",
178
- text: hint.text,
179
- // Sent to the model like any text part, but not shown or copied as the user's own words.
180
- synthetic: true,
181
- metadata: { litopencodeSkillHint: { kind: hint.kind, source: "chat.message" } }
182
- });
183
- }
184
- }
185
- catch { // no-excuse-ok: catch -- the optional hint must never block the user message.
186
- }
187
- }
188
134
  if (mode === undefined || childSession)
189
135
  return;
190
136
  const trigger = mode === "lit-handoff" ? "handoff" : mode === "lit-scientific-visualization" ? mode : "lit";
@@ -64,7 +64,8 @@ const litLoopPrompt = [
64
64
  interfaceWorkHandoff,
65
65
  "Do not call the task, lit, litwork, start-work, or review-work tools, and do not delegate for that path.",
66
66
  "Do not create a TODO plan, durable goal or ledger, or evidence tree. Do not preload the workflow skills or agent roster. The activation hook has already handled bare lit.",
67
- "Do not load skill documentation before inspecting the task and repository. Use the supplied acceptance criteria directly; consult at most one narrowly relevant skill only if inspection reveals a specific gap it can resolve.",
67
+ "On that bounded path, use the supplied acceptance criteria directly; consult at most one narrowly relevant skill only if inspection reveals a specific gap it can resolve.",
68
+ "Otherwise, when the request matches a skill in the available skills list, load that skill with the skill tool first, then inspect the workspace under its guidance. Plain questions, explanations, and requests no listed skill covers need no skill.",
68
69
  "Use the durable loop only when the user explicitly asks for tracked multi-turn work, or the task genuinely needs resumption across turns or independent workstreams.",
69
70
  "For a long workflow, delegate only independent slices that materially reduce work. Treat at most 20 concurrent subagents as an advisory ceiling; OpenCode exposes no verified numeric hard limit, so never claim host enforcement.",
70
71
  "When delegation is used, require each worker DoneClaim to name changed files, exact tests, real-surface evidence, risks, and a cleanup receipt. An independent verifier must confirm it before the slice becomes FullyDone.",
@@ -6,6 +6,8 @@ export declare const autoUpdateLockTimeoutMs = 2000;
6
6
  /** Keep a live owner protected for the whole bounded transaction plus one command window. */
7
7
  export declare const autoUpdateInstallLockStaleMs: number;
8
8
  export declare const autoUpdateRecursionEnv = "LITOPENCODE_AUTO_UPDATE_IN_PROGRESS";
9
+ /** After a safe rollback, OpenCode starts leave the same target alone for this long. */
10
+ export declare const autoUpdateFailureBackoffMs: number;
9
11
  type CommandOutput = {
10
12
  readonly stdout?: string;
11
13
  readonly stderr?: string;
@@ -62,6 +64,11 @@ type DoctorReceipt = {
62
64
  };
63
65
  readonly error?: string;
64
66
  };
67
+ export type AutoUpdateFailure = {
68
+ readonly step: "prepare" | "npm-install" | "install" | "doctor";
69
+ readonly timedOut: boolean;
70
+ readonly missingCommand: boolean;
71
+ };
65
72
  type AutoUpdateReceipt = {
66
73
  readonly schemaVersion: 1;
67
74
  readonly status: AutoUpdateRunResult["status"];
@@ -82,6 +89,7 @@ type AutoUpdateReceipt = {
82
89
  readonly ok: boolean;
83
90
  readonly error?: string;
84
91
  };
92
+ readonly failure?: AutoUpdateFailure;
85
93
  };
86
94
  /**
87
95
  * The staged CLI needs Node. Inside OpenCode the plugin runs on a Bun-compiled
@@ -92,6 +100,7 @@ export declare function childNodeExecutable(execPath?: string, versions?: NodeJS
92
100
  export declare function autoUpdateInstallLockPath(homeDir?: string): string;
93
101
  export declare function autoUpdateJournalPath(homeDir?: string): string;
94
102
  export declare function autoUpdateReceiptPath(homeDir?: string): string;
103
+ export declare function autoUpdateCheckPath(homeDir?: string): string;
95
104
  export declare function isAutoUpdateDisabled(env?: NodeJS.ProcessEnv, argv?: readonly string[]): boolean;
96
105
  export declare function shouldRunInteractiveAutoUpdate(input: InteractiveAutoUpdateGate): boolean;
97
106
  /**
@@ -115,11 +124,28 @@ export type AutoUpdateReceiptSummary = {
115
124
  readonly stagedStateKnown: boolean;
116
125
  readonly rollbackOk: boolean;
117
126
  readonly finishedAt: number;
127
+ readonly currentVersion?: string;
128
+ readonly targetVersion?: string;
129
+ readonly failure?: AutoUpdateFailure;
118
130
  };
119
131
  /** Read only the bounded safety summary used by doctor; never echo command output. */
120
132
  export declare function readAutoUpdateReceipt(homeDir?: string): Promise<AutoUpdateReceiptSummary | {
121
133
  readonly status: "unreadable";
122
134
  } | undefined>;
123
- export declare function runPluginAutoUpdate(): Promise<AutoUpdateRunResult>;
135
+ export type AutoUpdateCheckRecord = {
136
+ readonly schemaVersion: 1;
137
+ readonly at: number;
138
+ /** The plugin version this OpenCode start loaded, which keeps running until the next start. */
139
+ readonly runningVersion: string;
140
+ readonly status: AutoUpdateRunResult["status"];
141
+ readonly reason?: string;
142
+ readonly targetVersion?: string;
143
+ };
144
+ /** Best effort: a state root that is not safe to write never stops the plugin. */
145
+ export declare function recordAutoUpdateCheck(homeDir: string, runningVersion: string, result: AutoUpdateRunResult): Promise<void>;
146
+ export declare function readAutoUpdateCheck(homeDir?: string): Promise<AutoUpdateCheckRecord | undefined>;
147
+ export declare function runPluginAutoUpdate(options?: {
148
+ readonly homeDir?: string;
149
+ }): Promise<AutoUpdateRunResult>;
124
150
  export declare function runInteractiveAutoUpdate(input: InteractiveAutoUpdateGate): Promise<AutoUpdateRunResult>;
125
151
  export {};
@@ -15,10 +15,13 @@ export const autoUpdateLockTimeoutMs = 2_000;
15
15
  /** Keep a live owner protected for the whole bounded transaction plus one command window. */
16
16
  export const autoUpdateInstallLockStaleMs = autoUpdateTransactionBoundMs + autoUpdateTimeoutMs;
17
17
  export const autoUpdateRecursionEnv = "LITOPENCODE_AUTO_UPDATE_IN_PROGRESS";
18
+ /** After a safe rollback, OpenCode starts leave the same target alone for this long. */
19
+ export const autoUpdateFailureBackoffMs = 24 * 60 * 60 * 1000;
18
20
  const stateRootName = ".litopencode";
19
21
  const receiptFileName = "auto-update-receipt.json";
20
22
  const journalFileName = "auto-update-journal.jsonl";
21
23
  const installLockFileName = "auto-update-install.lock";
24
+ const checkFileName = "auto-update-check.json";
22
25
  const maxCommandOutputBytes = 4 * 1024 * 1024;
23
26
  const execFileAsync = promisify(execFile);
24
27
  /**
@@ -43,6 +46,9 @@ export function autoUpdateJournalPath(homeDir = os.homedir()) {
43
46
  export function autoUpdateReceiptPath(homeDir = os.homedir()) {
44
47
  return path.join(stateRoot(homeDir), receiptFileName);
45
48
  }
49
+ export function autoUpdateCheckPath(homeDir = os.homedir()) {
50
+ return path.join(stateRoot(homeDir), checkFileName);
51
+ }
46
52
  function truthyEnvironmentValue(value) {
47
53
  return value !== undefined;
48
54
  }
@@ -110,6 +116,16 @@ function commandFailure(error) {
110
116
  .filter(Boolean)
111
117
  .join("; ");
112
118
  }
119
+ /** Classify a failure by kind only; command output never reaches the receipt. */
120
+ function failureKind(step, error) {
121
+ const candidate = (typeof error === "object" && error !== null ? error : {});
122
+ const message = typeof candidate.message === "string" ? candidate.message : "";
123
+ return {
124
+ step,
125
+ timedOut: candidate.killed === true || /automatic update command exceeded/u.test(message),
126
+ missingCommand: candidate.code === "ENOENT"
127
+ };
128
+ }
113
129
  async function defaultRunCommand(command, args, options) {
114
130
  return await execFileAsync(command, [...args], options);
115
131
  }
@@ -400,6 +416,8 @@ async function updateTransaction(options, homeDir, currentVersion, targetVersion
400
416
  // resulting host state unknown until the snapshot is restored and verified.
401
417
  let stagedStateKnown = true;
402
418
  let priorStateVerified = false;
419
+ let step = "prepare";
420
+ let failure;
403
421
  try {
404
422
  await fs.mkdir(stageRoot, { recursive: true, mode: 0o700 });
405
423
  await fs.writeFile(path.join(stageRoot, "package.json"), '{"private":true}\n', { encoding: "utf8", mode: 0o600 });
@@ -423,16 +441,19 @@ async function updateTransaction(options, homeDir, currentVersion, targetVersion
423
441
  "--no-save",
424
442
  spec
425
443
  ];
444
+ step = "npm-install";
426
445
  await bounded(() => runCommand("npm", npmArgs, { cwd: stageRoot, env: childEnv, timeout: autoUpdateTimeoutMs, maxBuffer: maxCommandOutputBytes }), autoUpdateTimeoutMs);
427
446
  await appendJournal(homeDir, "npm-install", { spec });
428
447
  const entrypoint = await packageEntrypoint(stageRoot, targetVersion);
429
448
  const nodeExecutable = options.nodeExecutable ?? childNodeExecutable();
430
449
  const installArgs = [entrypoint, "install", "--root", options.configRoot, "--no-model-prompt", "--no-permission-prompt", "--no-auto-update"];
450
+ step = "install";
431
451
  stagedStateKnown = false;
432
452
  await bounded(() => runCommand(nodeExecutable, installArgs, { cwd: options.configRoot, env: childEnv, timeout: autoUpdateTimeoutMs, maxBuffer: maxCommandOutputBytes }), autoUpdateTimeoutMs);
433
453
  stagedStateKnown = true;
434
454
  await appendJournal(homeDir, "install", { spec });
435
455
  const doctorArgs = [entrypoint, "doctor", "--root", options.configRoot, "--json", "--no-auto-update"];
456
+ step = "doctor";
436
457
  stagedStateKnown = false;
437
458
  const doctorOutput = await bounded(() => runCommand(nodeExecutable, doctorArgs, { cwd: options.configRoot, env: childEnv, timeout: autoUpdateTimeoutMs, maxBuffer: maxCommandOutputBytes }), autoUpdateTimeoutMs);
438
459
  doctor = parseDoctorOutput(doctorOutput, targetVersion);
@@ -444,8 +465,8 @@ async function updateTransaction(options, homeDir, currentVersion, targetVersion
444
465
  await appendJournal(homeDir, "updated", { spec });
445
466
  }
446
467
  catch (error) {
447
- const failure = commandFailure(error);
448
- await tryAppendJournal(homeDir, "failure", { error: failure });
468
+ failure = failureKind(step, error);
469
+ await tryAppendJournal(homeDir, "failure", { error: commandFailure(error) });
449
470
  try {
450
471
  await restoreSnapshot(options.configRoot, snapshot);
451
472
  priorStateVerified = await verifySnapshot(options.configRoot, snapshot);
@@ -482,7 +503,8 @@ async function updateTransaction(options, homeDir, currentVersion, targetVersion
482
503
  priorStateVerified: status === "updated" ? true : priorStateVerified,
483
504
  stagedStateKnown,
484
505
  doctor,
485
- rollback
506
+ rollback,
507
+ ...(failure === undefined ? {} : { failure })
486
508
  };
487
509
  await writeReceipt(homeDir, receipt);
488
510
  return {
@@ -497,6 +519,20 @@ async function updateTransaction(options, homeDir, currentVersion, targetVersion
497
519
  ...(status === "rolled-back" || status === "failed" ? { rollback } : {})
498
520
  };
499
521
  }
522
+ /**
523
+ * A rollback that left the old setup proven in place is safe to wait on. Without
524
+ * this, a failing update re-ran its snapshot and npm install on every OpenCode
525
+ * start. Anything less certain keeps retrying, because a retry is the repair path.
526
+ */
527
+ async function inFailureBackoff(homeDir, targetVersion) {
528
+ const last = await readAutoUpdateReceipt(homeDir);
529
+ if (last === undefined || last.status !== "rolled-back")
530
+ return false;
531
+ if (!last.rollbackOk || !last.priorStateVerified || !last.stagedStateKnown || last.targetVersion !== targetVersion)
532
+ return false;
533
+ const age = Date.now() - last.finishedAt;
534
+ return age >= 0 && age < autoUpdateFailureBackoffMs;
535
+ }
500
536
  export async function runAutoUpdate(options) {
501
537
  const env = options.env ?? process.env;
502
538
  if (isAutoUpdateDisabled(env, options.argv))
@@ -517,6 +553,9 @@ export async function runAutoUpdate(options) {
517
553
  const comparison = compareStableVersions(targetVersion, options.currentVersion);
518
554
  if (comparison !== 1)
519
555
  return { status: "skipped", currentVersion: options.currentVersion, targetVersion, reason: "up-to-date" };
556
+ if (options.reason === "plugin" && (await inFailureBackoff(homeDir, targetVersion))) {
557
+ return { status: "skipped", currentVersion: options.currentVersion, targetVersion, reason: "backoff" };
558
+ }
520
559
  let lock;
521
560
  try {
522
561
  lock = await acquireInstallLock(homeDir);
@@ -551,6 +590,14 @@ export function autoUpdateDiagnostic(result) {
551
590
  const backup = result.backupPath === undefined ? "" : ` Retained backup: ${result.backupPath}.`;
552
591
  return `Automatic update stopped without claiming success (${result.status}). Inspect the receipt at ${receipt}.${backup}`;
553
592
  }
593
+ function isFailureRecord(value) {
594
+ if (typeof value !== "object" || value === null)
595
+ return false;
596
+ const record = value;
597
+ return ((record.step === "prepare" || record.step === "npm-install" || record.step === "install" || record.step === "doctor") &&
598
+ typeof record.timedOut === "boolean" &&
599
+ typeof record.missingCommand === "boolean");
600
+ }
554
601
  /** Read only the bounded safety summary used by doctor; never echo command output. */
555
602
  export async function readAutoUpdateReceipt(homeDir = os.homedir()) {
556
603
  const receiptPath = autoUpdateReceiptPath(homeDir);
@@ -580,7 +627,10 @@ export async function readAutoUpdateReceipt(homeDir = os.homedir()) {
580
627
  priorStateVerified: parsed.priorStateVerified,
581
628
  stagedStateKnown: parsed.stagedStateKnown,
582
629
  rollbackOk: parsed.rollback.ok,
583
- finishedAt: parsed.finishedAt
630
+ finishedAt: parsed.finishedAt,
631
+ ...(typeof parsed.currentVersion === "string" ? { currentVersion: parsed.currentVersion } : {}),
632
+ ...(typeof parsed.targetVersion === "string" ? { targetVersion: parsed.targetVersion } : {}),
633
+ ...(isFailureRecord(parsed.failure) ? { failure: { step: parsed.failure.step, timedOut: parsed.failure.timedOut, missingCommand: parsed.failure.missingCommand } } : {})
584
634
  };
585
635
  }
586
636
  catch (error) {
@@ -598,16 +648,65 @@ async function isLocalCheckout(packageRoot) {
598
648
  return false;
599
649
  }
600
650
  }
601
- export async function runPluginAutoUpdate() {
651
+ /** Best effort: a state root that is not safe to write never stops the plugin. */
652
+ export async function recordAutoUpdateCheck(homeDir, runningVersion, result) {
653
+ try {
654
+ await ensurePrivateStateRoot(homeDir);
655
+ const record = {
656
+ schemaVersion: 1,
657
+ at: Date.now(),
658
+ runningVersion,
659
+ status: result.status,
660
+ ...(result.reason === undefined ? {} : { reason: result.reason }),
661
+ ...(result.targetVersion === undefined ? {} : { targetVersion: result.targetVersion })
662
+ };
663
+ const checkPath = autoUpdateCheckPath(homeDir);
664
+ const temporary = `${checkPath}.${randomUUID()}.tmp`;
665
+ await fs.writeFile(temporary, `${JSON.stringify(record)}\n`, { encoding: "utf8", mode: 0o600 });
666
+ await fs.rename(temporary, checkPath);
667
+ }
668
+ catch {
669
+ // The record only feeds doctor.
670
+ }
671
+ }
672
+ export async function readAutoUpdateCheck(homeDir = os.homedir()) {
673
+ try {
674
+ const checkPath = autoUpdateCheckPath(homeDir);
675
+ const stat = await fs.lstat(checkPath);
676
+ if (stat.isSymbolicLink() || !stat.isFile() || (stat.mode & 0o077) !== 0)
677
+ return undefined;
678
+ if (typeof process.getuid === "function" && stat.uid !== process.getuid())
679
+ return undefined;
680
+ const parsed = JSON.parse(await fs.readFile(checkPath, "utf8"));
681
+ if (parsed.schemaVersion !== 1 || typeof parsed.at !== "number" || typeof parsed.runningVersion !== "string" || typeof parsed.status !== "string")
682
+ return undefined;
683
+ return {
684
+ schemaVersion: 1,
685
+ at: parsed.at,
686
+ runningVersion: parsed.runningVersion,
687
+ status: parsed.status,
688
+ ...(typeof parsed.reason === "string" ? { reason: parsed.reason } : {}),
689
+ ...(typeof parsed.targetVersion === "string" ? { targetVersion: parsed.targetVersion } : {})
690
+ };
691
+ }
692
+ catch {
693
+ return undefined;
694
+ }
695
+ }
696
+ export async function runPluginAutoUpdate(options = {}) {
602
697
  const metadata = await readPackageMetadata();
603
- if (await isLocalCheckout(metadata.packageRoot))
604
- return { status: "skipped", reason: "local-checkout" };
605
- return await runAutoUpdate({
606
- reason: "plugin",
607
- configRoot: defaultOpenCodeConfigRoot(),
608
- currentVersion: metadata.version,
609
- env: process.env
610
- });
698
+ const homeDir = options.homeDir ?? os.homedir();
699
+ const result = (await isLocalCheckout(metadata.packageRoot))
700
+ ? { status: "skipped", reason: "local-checkout" }
701
+ : await runAutoUpdate({
702
+ reason: "plugin",
703
+ homeDir,
704
+ configRoot: defaultOpenCodeConfigRoot(),
705
+ currentVersion: metadata.version,
706
+ env: process.env
707
+ });
708
+ await recordAutoUpdateCheck(homeDir, metadata.version, result);
709
+ return result;
611
710
  }
612
711
  export async function runInteractiveAutoUpdate(input) {
613
712
  if (!shouldRunInteractiveAutoUpdate(input))
@@ -1,4 +1,13 @@
1
1
  import type { CliResult } from "./types.ts";
2
+ /**
3
+ * Doctor reads the receipt and the record each OpenCode start leaves behind. It
4
+ * cannot see inside a running OpenCode, so "running" means the last start seen.
5
+ */
6
+ export declare function inspectAutoUpdate(input: {
7
+ readonly homeDir?: string;
8
+ readonly env: NodeJS.ProcessEnv;
9
+ readonly pinnedVersion: string | null;
10
+ }): Promise<Record<string, unknown>>;
2
11
  /**
3
12
  * The project the plugin would use from here: OpenCode's worktree is the git root above the working
4
13
  * directory, and without one the directory itself.