litclaude-ai 0.3.32 → 0.3.33
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 +21 -0
- package/README.md +27 -5
- package/README_ko-KR.md +27 -5
- package/RELEASE_CHECKLIST.md +34 -10
- package/bin/litclaude-ai.js +52 -24
- package/bin/update-notifier-refresh.mjs +7 -0
- package/bin/update-notifier.mjs +504 -0
- package/docs/native-goal-surface.md +41 -0
- package/package.json +2 -1
- package/plugins/litclaude/.claude-plugin/plugin.json +1 -1
- package/plugins/litclaude/bin/litclaude-mcp.js +1 -1
- package/plugins/litclaude/lib/canonical-skill-catalog.mjs +31 -0
- package/scripts/doctor.mjs +11 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.33 - 2026-07-24 — update notice and installed catalog verification
|
|
4
|
+
|
|
5
|
+
- Add a nonblocking npm update notice for interactive `install`, `update`, and
|
|
6
|
+
`doctor`, backed by a strict product cache, atomic concurrent reservation,
|
|
7
|
+
and bounded official registry refresh. Attempt timestamps throttle successful,
|
|
8
|
+
offline, failed, and interrupted checks for 24 hours without discarding the
|
|
9
|
+
last trusted result; stale trusted results may still notify, while future or
|
|
10
|
+
noncanonical timestamps fail safe. Transition/recovery mutex ownership and
|
|
11
|
+
per-attempt generation fencing prevent stale takeover or late-worker cache
|
|
12
|
+
overwrite. CI/JSON/dry-run/non-TTY and environment opt-outs remain authoritative.
|
|
13
|
+
- Preserve the installed permission mode and HUD accent during an optionless
|
|
14
|
+
reinstall while retaining explicit overrides.
|
|
15
|
+
- Make source doctor, install verification, installed doctor, and Claude plugin
|
|
16
|
+
details share the exact canonical skill catalog. Missing installed skill
|
|
17
|
+
payloads now fail with `DOCTOR_FAIL`, and packed-install corruption coverage
|
|
18
|
+
proves the user-facing doctor cannot report success for an incomplete catalog.
|
|
19
|
+
- Add a narrow native `/goal` capability matrix for user/Remote Control,
|
|
20
|
+
SDK-owned, separate print, and already-open TUI session boundaries without a
|
|
21
|
+
plugin runtime bridge, including minimum client, workspace trust, policy/hook,
|
|
22
|
+
and SDK-advertised command prerequisites.
|
|
23
|
+
|
|
3
24
|
## 0.3.32 - 2026-07-23 — bounded-authority start-work lifecycle
|
|
4
25
|
|
|
5
26
|
- Added a code-owned schema-3 start-work runtime with canonical authority roots,
|
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
</p>
|
|
10
10
|
<p align="center">
|
|
11
11
|
<img src="https://img.shields.io/badge/npm-litclaude--ai-cb3837" />
|
|
12
|
-
<img src="https://img.shields.io/badge/version-0.3.
|
|
12
|
+
<img src="https://img.shields.io/badge/version-0.3.33-2ea44f" />
|
|
13
13
|
<img src="https://img.shields.io/badge/Claude%20Code-plugin-blueviolet" />
|
|
14
14
|
<img src="https://img.shields.io/badge/license-MIT-blue" />
|
|
15
15
|
</p>
|
|
@@ -22,10 +22,10 @@
|
|
|
22
22
|
> `litclaude@litclaude-ai`, so normal `claude` launches can load the
|
|
23
23
|
> LitClaude skills and hooks without a long `--plugin-dir` command.
|
|
24
24
|
|
|
25
|
-
This checkout is prepared as `litclaude-ai@0.3.
|
|
25
|
+
This checkout is prepared as `litclaude-ai@0.3.33` for personal install
|
|
26
26
|
convenience. The repo can remain quiet; preparing npm package metadata here does
|
|
27
27
|
not imply public repo promotion, marketplace publication, or advertisement.
|
|
28
|
-
Future package releases still require explicit user approval. The v0.3.32 release material adds the code-owned schema-3 start-work lifecycle with canonical bounded authority, explicit single-use resume grants, PreToolUse enforcement, root/subagent lane identity, progress-bound Stop continuation receipts, and fail-closed release boundaries. The v0.3.31 release material hardens litgoal creation, replacement, completion, and autoloop receipts while making visual QA browser ownership, bounded waits, blocked states, and cleanup evidence explicit. The v0.3.30 release material strengthens LitResearch with root-owned scientific evidence records, DOI/PDF lifecycle receipts, bounded route coverage, and a connection-pinned public-source reader with inert-content and secret-redaction guarantees. The v0.3.29 release material bundles exact-source `lit-handoff` and `lit-scientific-visualization` skills with Claude-native commands, exact-bare hook invocation, visible LITBURN banners, installed-payload integrity checks, and read-only scientific dependency diagnostics. The v0.3.28 release material requires explicit leading start-work invocations so diagnostic or copied mentions remain inert. The v0.3.27 release material adds a structured, animated five-stage installer with a success or failure receipt while preserving deterministic automation output. The v0.3.26 release material adds adaptive objective-achievable planning, draft-plan review, and orchestration readiness hardening. The v0.3.25 release material aligns portable details, public-read, litgoal status JSON, and evidence wording. The v0.3.24 release material aligns auxiliary skill inventories and advisory probes. The v0.3.23
|
|
28
|
+
Future package releases still require explicit user approval. The v0.3.33 release material adds a quiet nonblocking npm update notice with strict cache, concurrency, timestamp, opt-out, and official-registry boundaries; preserves the installed permission mode and HUD accent on optionless updates; and makes source, install, installed-doctor, and Claude-details checks share the exact canonical skill catalog so missing packaged skills fail with `DOCTOR_FAIL`. The v0.3.32 release material adds the code-owned schema-3 start-work lifecycle with canonical bounded authority, explicit single-use resume grants, PreToolUse enforcement, root/subagent lane identity, progress-bound Stop continuation receipts, and fail-closed release boundaries. The v0.3.31 release material hardens litgoal creation, replacement, completion, and autoloop receipts while making visual QA browser ownership, bounded waits, blocked states, and cleanup evidence explicit. The v0.3.30 release material strengthens LitResearch with root-owned scientific evidence records, DOI/PDF lifecycle receipts, bounded route coverage, and a connection-pinned public-source reader with inert-content and secret-redaction guarantees. The v0.3.29 release material bundles exact-source `lit-handoff` and `lit-scientific-visualization` skills with Claude-native commands, exact-bare hook invocation, visible LITBURN banners, installed-payload integrity checks, and read-only scientific dependency diagnostics. The v0.3.28 release material requires explicit leading start-work invocations so diagnostic or copied mentions remain inert. The v0.3.27 release material adds a structured, animated five-stage installer with a success or failure receipt while preserving deterministic automation output. The v0.3.26 release material adds adaptive objective-achievable planning, draft-plan review, and orchestration readiness hardening. The v0.3.25 release material aligns portable details, public-read, litgoal status JSON, and evidence wording. The v0.3.24 release material aligns auxiliary skill inventories and advisory probes. The v0.3.23
|
|
29
29
|
release materials inject the bundled skill bodies for bare `hyperplan`,
|
|
30
30
|
`litresearch`, `lit research`, `init-deep`, and explicit `$start-work` prompt-hook routes while
|
|
31
31
|
preserving the v0.3.19 adversarial planning skill, the v0.3.18 read-only `lit-recap` session recap surface, the v0.3.17 separate-worker native `/goal` launcher, the v0.3.16
|
|
@@ -131,7 +131,7 @@ directory, the normal install command works:
|
|
|
131
131
|
|
|
132
132
|
```bash
|
|
133
133
|
cd /tmp
|
|
134
|
-
npx --yes litclaude-ai@0.3.
|
|
134
|
+
npx --yes litclaude-ai@0.3.33 install
|
|
135
135
|
```
|
|
136
136
|
|
|
137
137
|
Validate the installed plugin:
|
|
@@ -140,11 +140,26 @@ Validate the installed plugin:
|
|
|
140
140
|
npx --yes litclaude-ai doctor
|
|
141
141
|
```
|
|
142
142
|
|
|
143
|
+
Interactive `install`, `update`, and `doctor` runs may show a quiet update notice
|
|
144
|
+
on stderr from LitClaude's own cache. A strictly validated last successful
|
|
145
|
+
version may still be shown after it becomes stale. At most one new attempt is
|
|
146
|
+
reserved per 24 hours across concurrent parents; that reservation remains after
|
|
147
|
+
offline, failed, or interrupted refreshes while preserving the last successful
|
|
148
|
+
version and check time. A transition/recovery mutex and per-attempt ownership
|
|
149
|
+
generation serialize stale takeover and completion, so an old worker cannot
|
|
150
|
+
remove a newer reservation or overwrite its result. The refresh is a detached, three-second request to the
|
|
151
|
+
official npm registry, so the management command does not wait. Noncanonical or
|
|
152
|
+
future cache timestamps are not trusted. The notice is disabled for CI, JSON,
|
|
153
|
+
dry-run, or non-TTY use and by either `NO_UPDATE_NOTIFIER` or
|
|
154
|
+
`LITCLAUDE_NO_UPDATE_CHECK`. It never reads npm credentials or installs an
|
|
155
|
+
update. Follow the version-pinned `npx --yes litclaude-ai@<version> install`
|
|
156
|
+
command shown in the notice, then restart Claude Code.
|
|
157
|
+
|
|
143
158
|
The installer also sets Claude Code's `statusLine` command to the packaged
|
|
144
159
|
LitClaude HUD. A typical no-color render starts like:
|
|
145
160
|
|
|
146
161
|
```text
|
|
147
|
-
[🔥LITCLAUDE v0.3.
|
|
162
|
+
[🔥LITCLAUDE v0.3.33] | O4.8 │ ctx [▎░░] 9%/1000k │ 5h [▏░] 4% ↻2h15m │ 1w [▊░] 35% ↻3d6h │ git main +3 ✓
|
|
148
163
|
```
|
|
149
164
|
|
|
150
165
|
The `↻` suffix is a compact rate-limit reset countdown. It is separated from
|
|
@@ -159,6 +174,10 @@ To override the accent without the picker, set `LITCLAUDE_HUD_ACCENT` to one
|
|
|
159
174
|
of `cyan`, `blue`, `teal`, `green`, `lavender`, `rose`, `gold`, `orange`,
|
|
160
175
|
`slate`, or `gray` before installing.
|
|
161
176
|
|
|
177
|
+
An optionless `litclaude update` preserves the installed permission mode and
|
|
178
|
+
HUD accent. An explicit `--permission-mode` / `--yolo` choice or
|
|
179
|
+
`LITCLAUDE_HUD_ACCENT` value still overrides the saved choice.
|
|
180
|
+
|
|
162
181
|
Launch Claude Code normally:
|
|
163
182
|
|
|
164
183
|
```bash
|
|
@@ -294,6 +313,9 @@ litclaude litgoal native-worker --condition "<completion condition>" --mode bg -
|
|
|
294
313
|
`native-worker` starts a separate print/background Claude Code worker whose first
|
|
295
314
|
prompt begins with `/goal`; it does not arm the already-open TUI, does not use
|
|
296
315
|
tmux keystroke injection, and does not mutate private transcript metadata.
|
|
316
|
+
See the clean-room [native `/goal` surface matrix](./docs/native-goal-surface.md)
|
|
317
|
+
for the separate User / Remote Control, SDK-owned, `claude -p`, and already-open
|
|
318
|
+
TUI capability boundaries plus a ready-to-paste ledger-backed condition.
|
|
297
319
|
For large independent work, LitClaude applies the same principle to Claude
|
|
298
320
|
Code's available orchestration surfaces: `lit workflow`, `lit dynamic workflow`,
|
|
299
321
|
and `lit ultracode` prefer current `ultracode` or explicit “run a workflow” /
|
package/README_ko-KR.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
</p>
|
|
10
10
|
<p align="center">
|
|
11
11
|
<img src="https://img.shields.io/badge/npm-litclaude--ai-cb3837" />
|
|
12
|
-
<img src="https://img.shields.io/badge/version-0.3.
|
|
12
|
+
<img src="https://img.shields.io/badge/version-0.3.33-2ea44f" />
|
|
13
13
|
<img src="https://img.shields.io/badge/Claude%20Code-plugin-blueviolet" />
|
|
14
14
|
<img src="https://img.shields.io/badge/license-MIT-blue" />
|
|
15
15
|
</p>
|
|
@@ -26,11 +26,11 @@
|
|
|
26
26
|
> 설치되므로, 매번 긴 `--plugin-dir` 없이 일반 `claude` 실행에서
|
|
27
27
|
> LitClaude skill과 hook을 불러올 수 있습니다.
|
|
28
28
|
|
|
29
|
-
현재 checkout은 `litclaude-ai@0.3.
|
|
29
|
+
현재 checkout은 `litclaude-ai@0.3.33` 배포 준비용으로 정리되어 있습니다. 목적은
|
|
30
30
|
다른 PC에서도 빠르게 설치하기 위한 개인용 package metadata를 갖추는 것입니다.
|
|
31
31
|
npm package metadata를 준비했다고 해서 홍보, 공개 저장소 운영, Claude
|
|
32
32
|
marketplace 등록을 의미하지는 않습니다. 새 버전 배포는 항상 별도의 명시적
|
|
33
|
-
승인 후에 진행합니다. v0.3.32 release material은 canonical bounded authority, single-use resume grant, PreToolUse enforcement, root/subagent lane identity, progress-bound Stop continuation receipt, fail-closed release boundary를 갖춘 code-owned schema-3 start-work lifecycle을 추가합니다. v0.3.31 release material은 litgoal 생성·교체·완료와 autoloop receipt를 강화하고 visual QA의 browser ownership, bounded wait, blocked state, cleanup evidence를 명확히 합니다. v0.3.30 release material은 root-owned scientific evidence record, DOI/PDF lifecycle receipt, bounded route coverage, connection-pinned public-source reader, inert-content 및 secret-redaction 보장을 포함하는 LitResearch 강화판입니다. v0.3.29 release material은 exact-source `lit-handoff`와 `lit-scientific-visualization` skill, Claude-native command, exact-bare hook invocation, visible LITBURN banner, installed-payload integrity check, read-only scientific dependency diagnostic을 함께 포함합니다. v0.3.28 release material은 start-work 호출을 prompt 선두의 명시적 invocation으로 제한해 진단문이나 복사된 언급을 비활성 상태로 유지합니다. v0.3.27 release material은 success/failure receipt와 deterministic automation output을 갖춘 5-stage animated installer를 추가합니다. v0.3.26 release material은 adaptive objective-achievable planning, draft-plan review, orchestration readiness hardening을 추가합니다. v0.3.25 release material은 portable details, public-read, litgoal status JSON, evidence wording을 정렬합니다. v0.3.24 release material은 auxiliary skill inventory와 advisory probe를 정렬합니다. v0.3.23 release material은 bare `hyperplan`,
|
|
33
|
+
승인 후에 진행합니다. v0.3.33 release material은 strict cache, concurrency, timestamp, opt-out, official-registry boundary를 갖춘 조용한 nonblocking npm update notice를 추가하고, optionless update에서 기존 permission mode와 HUD accent를 보존하며, source/install/installed doctor/Claude details가 정확한 canonical skill catalog를 공유해 packaged skill 누락 시 `DOCTOR_FAIL`로 실패하도록 합니다. v0.3.32 release material은 canonical bounded authority, single-use resume grant, PreToolUse enforcement, root/subagent lane identity, progress-bound Stop continuation receipt, fail-closed release boundary를 갖춘 code-owned schema-3 start-work lifecycle을 추가합니다. v0.3.31 release material은 litgoal 생성·교체·완료와 autoloop receipt를 강화하고 visual QA의 browser ownership, bounded wait, blocked state, cleanup evidence를 명확히 합니다. v0.3.30 release material은 root-owned scientific evidence record, DOI/PDF lifecycle receipt, bounded route coverage, connection-pinned public-source reader, inert-content 및 secret-redaction 보장을 포함하는 LitResearch 강화판입니다. v0.3.29 release material은 exact-source `lit-handoff`와 `lit-scientific-visualization` skill, Claude-native command, exact-bare hook invocation, visible LITBURN banner, installed-payload integrity check, read-only scientific dependency diagnostic을 함께 포함합니다. v0.3.28 release material은 start-work 호출을 prompt 선두의 명시적 invocation으로 제한해 진단문이나 복사된 언급을 비활성 상태로 유지합니다. v0.3.27 release material은 success/failure receipt와 deterministic automation output을 갖춘 5-stage animated installer를 추가합니다. v0.3.26 release material은 adaptive objective-achievable planning, draft-plan review, orchestration readiness hardening을 추가합니다. v0.3.25 release material은 portable details, public-read, litgoal status JSON, evidence wording을 정렬합니다. v0.3.24 release material은 auxiliary skill inventory와 advisory probe를 정렬합니다. v0.3.23 release material은 bare `hyperplan`,
|
|
34
34
|
`litresearch`, `lit research`, `init-deep`, explicit `$start-work` prompt-hook route에
|
|
35
35
|
bundled skill body를 주입하면서, v0.3.19 adversarial planning
|
|
36
36
|
skill, v0.3.18 read-only `lit-recap` session recap surface, v0.3.17 별도 worker 기반 native `/goal`
|
|
@@ -131,7 +131,7 @@ checkout을 먼저 해석해서 `sh: litclaude-ai: command not found`로 실패
|
|
|
131
131
|
|
|
132
132
|
```bash
|
|
133
133
|
cd /tmp
|
|
134
|
-
npx --yes litclaude-ai@0.3.
|
|
134
|
+
npx --yes litclaude-ai@0.3.33 install
|
|
135
135
|
```
|
|
136
136
|
|
|
137
137
|
설치 상태를 확인합니다.
|
|
@@ -140,11 +140,26 @@ npx --yes litclaude-ai@0.3.32 install
|
|
|
140
140
|
npx --yes litclaude-ai doctor
|
|
141
141
|
```
|
|
142
142
|
|
|
143
|
+
interactive `install`, `update`, `doctor`에서는 LitClaude가 소유하는 cache의
|
|
144
|
+
마지막으로 검증된 성공 버전이 오래되었더라도 조용한 update notice를 stderr에
|
|
145
|
+
표시할 수 있습니다. 여러 parent가 동시에 실행되어도 atomic reservation으로
|
|
146
|
+
24시간에 한 번만 새 조회를 예약합니다. offline, 실패, 중단된 조회도 attempted
|
|
147
|
+
time을 남겨 24시간 동안 재시도를 제한하되 마지막 성공 version/check time은
|
|
148
|
+
보존합니다. transition/recovery mutex와 attempt별 ownership generation이 stale
|
|
149
|
+
takeover와 completion을 직렬화하므로 이전 worker가 새 reservation을 지우거나 새
|
|
150
|
+
결과를 덮어쓸 수 없습니다. refresh는 official npm registry를 최대 3초 동안 조회하는 detached
|
|
151
|
+
process이므로 management command는 기다리지 않습니다. canonical하지 않거나
|
|
152
|
+
미래인 cache timestamp는 신뢰하지 않습니다. CI, JSON, dry-run, non-TTY에서는
|
|
153
|
+
동작하지 않으며 `NO_UPDATE_NOTIFIER` 또는 `LITCLAUDE_NO_UPDATE_CHECK`로 끌 수
|
|
154
|
+
있습니다. npm credential을 읽거나 update를 자동 설치하지 않습니다. notice가 제시한
|
|
155
|
+
`npx --yes litclaude-ai@<version> install`을 실행한 뒤 Claude Code를 다시
|
|
156
|
+
시작하세요.
|
|
157
|
+
|
|
143
158
|
installer는 Claude Code의 `statusLine` command도 packaged LitClaude HUD로
|
|
144
159
|
설정합니다. 색상을 제거한 예시는 다음처럼 시작합니다.
|
|
145
160
|
|
|
146
161
|
```text
|
|
147
|
-
[🔥LITCLAUDE v0.3.
|
|
162
|
+
[🔥LITCLAUDE v0.3.33] | O4.8 │ ctx [▎░░] 9%/1000k │ 5h [▏░] 4% ↻2h15m │ 1w [▊░] 35% ↻3d6h │ git main +3 ✓
|
|
148
163
|
```
|
|
149
164
|
|
|
150
165
|
`↻` 표시는 rate-limit reset까지 남은 시간을 짧게 보여주는 countdown입니다.
|
|
@@ -158,6 +173,10 @@ picker 없이 색상을 지정하려면 설치 전에 `LITCLAUDE_HUD_ACCENT`를
|
|
|
158
173
|
`cyan`, `blue`, `teal`, `green`, `lavender`, `rose`, `gold`, `orange`,
|
|
159
174
|
`slate`, `gray` 중 하나로 설정하세요.
|
|
160
175
|
|
|
176
|
+
option을 생략한 `litclaude update`는 기존 permission mode와 HUD accent를
|
|
177
|
+
보존합니다. 명시적인 `--permission-mode` / `--yolo` 선택 또는
|
|
178
|
+
`LITCLAUDE_HUD_ACCENT` 값은 저장된 선택을 계속 override합니다.
|
|
179
|
+
|
|
161
180
|
설치 후에는 일반적인 `claude` 명령으로 Claude Code를 실행합니다.
|
|
162
181
|
|
|
163
182
|
```bash
|
|
@@ -284,6 +303,9 @@ litclaude litgoal native-worker --condition "<completion condition>" --mode bg -
|
|
|
284
303
|
`native-worker`는 첫 prompt가 `/goal`로 시작하는 별도 print/background Claude
|
|
285
304
|
Code worker를 시작합니다. 이미 열린 TUI를 arm하지 않고, tmux keystroke
|
|
286
305
|
injection이나 private transcript metadata 변경도 사용하지 않습니다.
|
|
306
|
+
User / Remote Control, SDK-owned session, 별도 `claude -p`, 이미 열린 TUI의
|
|
307
|
+
capability boundary와 ledger 기반 ready-to-paste condition은 clean-room
|
|
308
|
+
[native `/goal` surface matrix](./docs/native-goal-surface.md)에 정리되어 있습니다.
|
|
287
309
|
`Workflow`가 노출되면 먼저 제안하고 사용자 opt-in 또는 이미 허용된 orchestration
|
|
288
310
|
상태에서만 `Workflow` tool을 호출합니다. isolated edit lane이 model-facing
|
|
289
311
|
worktree를 필요로 하면 `EnterWorktree`를 사용합니다. CLI surface만 가능한 새 격리
|
package/RELEASE_CHECKLIST.md
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
# LitClaude Release Checklist
|
|
2
2
|
|
|
3
|
-
Status: `litclaude-ai@0.3.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
Status: `litclaude-ai@0.3.33` is the current release candidate — a quiet
|
|
4
|
+
nonblocking npm update notice with strict cache, concurrency, timestamp,
|
|
5
|
+
opt-out, and official-registry boundaries; optionless update preservation for
|
|
6
|
+
the installed permission mode and HUD accent; and a shared canonical skill
|
|
7
|
+
catalog enforced by source, install, installed-doctor, and Claude-details
|
|
8
|
+
checks, with `DOCTOR_FAIL` on missing installed skills. It also carries the
|
|
9
|
+
code-owned schema-3 start-work lifecycle with canonical bounded authority,
|
|
10
|
+
single-use resume grants, PreToolUse enforcement, root/subagent lane identity,
|
|
11
|
+
bounded Stop continuation receipts, and fail-closed release boundaries, plus guarded litgoal
|
|
7
12
|
creation and replacement, durable idempotent autoloop completion, and explicit
|
|
8
13
|
visual QA backend ownership, bounded waits, blocked states, and cleanup receipts,
|
|
9
14
|
plus root-owned scientific evidence records, DOI/PDF lifecycle receipts, bounded route coverage,
|
|
@@ -28,9 +33,9 @@ side-effect-free, the launcher starts only a separate Claude Code
|
|
|
28
33
|
print/background worker, and the release preserves the Korean polishing
|
|
29
34
|
command, strict multi-agent review pipeline, fidelity guardrails, package
|
|
30
35
|
hygiene checks, native route gates, and safe start-work handoff behavior.
|
|
31
|
-
`package.json` is aligned to `0.3.
|
|
32
|
-
`plugins/litclaude/.claude-plugin/plugin.json` is aligned to `0.3.
|
|
33
|
-
plugin-local MCP server reports `0.3.
|
|
36
|
+
`package.json` is aligned to `0.3.33`,
|
|
37
|
+
`plugins/litclaude/.claude-plugin/plugin.json` is aligned to `0.3.33`, and the
|
|
38
|
+
plugin-local MCP server reports `0.3.33`.
|
|
34
39
|
|
|
35
40
|
This release carries the v0.2.2 Dynamic workflow hardening surfaces:
|
|
36
41
|
`/dynamic-workflow`, `workflow-check --json`, native `/goal` fallback guidance,
|
|
@@ -57,6 +62,10 @@ behavior.
|
|
|
57
62
|
|
|
58
63
|
- `npm test`
|
|
59
64
|
- `npm run validate:plugin`
|
|
65
|
+
- `npm run doctor`
|
|
66
|
+
- `npm run check:version`
|
|
67
|
+
- `npm run scan:legacy-tokens`
|
|
68
|
+
- `npm run pack:payload-guard`
|
|
60
69
|
- `npm run qa:tmux`
|
|
61
70
|
- `npm run qa:portable`
|
|
62
71
|
- `npm run pack:dry-run`
|
|
@@ -74,6 +83,21 @@ Use this track when testing from the current checkout:
|
|
|
74
83
|
|
|
75
84
|
No npm publication is required for this track.
|
|
76
85
|
|
|
86
|
+
## v0.3.33 Update Notice and Canonical Catalog Gates
|
|
87
|
+
|
|
88
|
+
Before requesting publication approval, confirm interactive `install`, `update`,
|
|
89
|
+
and `doctor` can show only the quiet stderr update notice while CI, JSON,
|
|
90
|
+
dry-run, non-TTY, `NO_UPDATE_NOTIFIER`, and `LITCLAUDE_NO_UPDATE_CHECK` remain
|
|
91
|
+
authoritative opt-outs. Confirm the cache accepts only stable official-package
|
|
92
|
+
metadata, preserves the last trusted result across failed refreshes, throttles
|
|
93
|
+
attempts for 24 hours, and fences stale owners from newer reservations.
|
|
94
|
+
|
|
95
|
+
Confirm source doctor, installation verification, installed doctor, and
|
|
96
|
+
`claude plugin details` consume the same exact canonical skill catalog. A
|
|
97
|
+
packed isolated install must emit `SKILL_CATALOG_PASS` and `DOCTOR_PASS`; after
|
|
98
|
+
one canonical `SKILL.md` is moved aside, doctor must exit nonzero with
|
|
99
|
+
`DOCTOR_FAIL` and must not print `DOCTOR_PASS`.
|
|
100
|
+
|
|
77
101
|
## v0.3.32 Bounded-Authority Start-Work Gates
|
|
78
102
|
|
|
79
103
|
Before requesting publication approval, confirm schema-3 state remains
|
|
@@ -138,9 +162,9 @@ checkout and from an isolated install of the packed tarball:
|
|
|
138
162
|
Before requesting publication approval, confirm these artifacts from the current
|
|
139
163
|
checkout:
|
|
140
164
|
|
|
141
|
-
- `package.json` version is `0.3.
|
|
142
|
-
- `plugins/litclaude/.claude-plugin/plugin.json` version is `0.3.
|
|
143
|
-
- `plugins/litclaude/bin/litclaude-mcp.js` reports server version `0.3.
|
|
165
|
+
- `package.json` version is `0.3.33`.
|
|
166
|
+
- `plugins/litclaude/.claude-plugin/plugin.json` version is `0.3.33`.
|
|
167
|
+
- `plugins/litclaude/bin/litclaude-mcp.js` reports server version `0.3.33`.
|
|
144
168
|
- Prompt-hook tests cover bundled `SKILL.md` body injection for bare `hyperplan`, `litresearch`, `lit research`, `init-deep`, and explicit leading `$start-work`; diagnostic/copy mentions stay inert while leading natural-language `lit start work` stays BLOCKED.
|
|
145
169
|
- `lit search` and `lit query` route to `/litclaude:litresearch` without activating on slash mentions, code spans, or non-lit prompts.
|
|
146
170
|
- Litresearch web lanes require public API/feed preference, validator-first checks, route traces, prompt-injection quarantine, and honest auth/paywall/private-data stop reasons.
|
package/bin/litclaude-ai.js
CHANGED
|
@@ -15,6 +15,10 @@ import { homedir } from "node:os";
|
|
|
15
15
|
import { dirname, join, resolve } from "node:path";
|
|
16
16
|
import { createInterface } from "node:readline/promises";
|
|
17
17
|
import { fileURLToPath } from "node:url";
|
|
18
|
+
import {
|
|
19
|
+
canonicalSkillFiles,
|
|
20
|
+
canonicalSkillIds,
|
|
21
|
+
} from "../plugins/litclaude/lib/canonical-skill-catalog.mjs";
|
|
18
22
|
import { HUD_ACCENT_THEMES, normalizeHudAccent, themeForAccent } from "../plugins/litclaude/lib/hud-accent-palette.mjs";
|
|
19
23
|
import { readPublicSource } from "../plugins/litclaude/lib/public-source-reader/reader.mjs";
|
|
20
24
|
import { verifyBundledSkillsIntegrity } from "../plugins/litclaude/lib/bundled-skills-integrity.mjs";
|
|
@@ -22,6 +26,7 @@ import { runStartWorkContinuationCli } from "../plugins/litclaude/lib/start-work
|
|
|
22
26
|
import { runStartWorkCli } from "../plugins/litclaude/lib/start-work-cli.mjs";
|
|
23
27
|
import { runLitgoalCli } from "../plugins/litclaude/lib/litgoal/cli.mjs";
|
|
24
28
|
import { runWorkflowCheckCli } from "../plugins/litclaude/lib/workflow-check.mjs";
|
|
29
|
+
import { runUpdateNotifier } from "./update-notifier.mjs";
|
|
25
30
|
|
|
26
31
|
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
27
32
|
const packageJson = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
@@ -67,6 +72,7 @@ const parseArgs = (argv) => {
|
|
|
67
72
|
|
|
68
73
|
const litHome = () => resolve(process.env.LITCLAUDE_HOME ?? join(homedir(), ".litclaude"));
|
|
69
74
|
const claudeHome = () => resolve(process.env.CLAUDE_CONFIG_DIR ?? process.env.CLAUDE_HOME ?? join(homedir(), ".claude"));
|
|
75
|
+
const updateNotifierCachePath = (home = litHome()) => join(home, "update-notifier", "latest.json");
|
|
70
76
|
const versionRoot = (home = litHome()) => join(home, "litclaude-ai", version);
|
|
71
77
|
const currentRoot = (home = litHome()) => join(home, "current");
|
|
72
78
|
const pluginPathForRoot = (installRoot) => join(installRoot, "plugins", "litclaude");
|
|
@@ -96,8 +102,9 @@ const parsePermissionMode = (value) => {
|
|
|
96
102
|
return value;
|
|
97
103
|
};
|
|
98
104
|
|
|
99
|
-
const parseInstallOptions = (rest) => {
|
|
100
|
-
|
|
105
|
+
const parseInstallOptions = (rest, existingPermissionMode = "safe") => {
|
|
106
|
+
const preservedMode = permissionModes.has(existingPermissionMode) ? existingPermissionMode : "safe";
|
|
107
|
+
let permissionMode = parsePermissionMode(process.env.LITCLAUDE_PERMISSION_MODE || preservedMode);
|
|
101
108
|
let permissionExplicit = Boolean(process.env.LITCLAUDE_PERMISSION_MODE);
|
|
102
109
|
|
|
103
110
|
for (let index = 0; index < rest.length; index += 1) {
|
|
@@ -308,12 +315,13 @@ const shouldPromptHudAccent = () => {
|
|
|
308
315
|
return Boolean(process.stdin.isTTY && process.stdout.isTTY && !process.env.CI);
|
|
309
316
|
};
|
|
310
317
|
|
|
311
|
-
const chooseHudAccent = async () => {
|
|
318
|
+
const chooseHudAccent = async (existingAccent = "cyan") => {
|
|
319
|
+
const preservedAccent = normalizeHudAccent(existingAccent);
|
|
312
320
|
const envAccent = process.env.LITCLAUDE_HUD_ACCENT;
|
|
313
321
|
if (envAccent && process.env.LITCLAUDE_HUD_ACCENT_PROMPT !== "1") {
|
|
314
322
|
return normalizeHudAccent(envAccent);
|
|
315
323
|
}
|
|
316
|
-
if (!shouldPromptHudAccent()) return normalizeHudAccent(envAccent);
|
|
324
|
+
if (!shouldPromptHudAccent()) return envAccent ? normalizeHudAccent(envAccent) : preservedAccent;
|
|
317
325
|
|
|
318
326
|
process.stdout.write("Choose LitClaude HUD brand color:\n");
|
|
319
327
|
for (const [index, theme] of hudAccentThemes.entries()) {
|
|
@@ -322,10 +330,10 @@ const chooseHudAccent = async () => {
|
|
|
322
330
|
|
|
323
331
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
324
332
|
try {
|
|
325
|
-
const answer = await rl.question(
|
|
333
|
+
const answer = await rl.question(`Color 1-10 or name [${preservedAccent}]: `);
|
|
326
334
|
const trimmed = answer.trim().toLowerCase();
|
|
327
335
|
const byIndex = hudAccentThemes[Number.parseInt(trimmed, 10) - 1]?.name;
|
|
328
|
-
const selected = byIndex ?? normalizeHudAccent(trimmed);
|
|
336
|
+
const selected = byIndex ?? (trimmed ? normalizeHudAccent(trimmed) : preservedAccent);
|
|
329
337
|
if (trimmed && selected === "cyan" && trimmed !== "cyan" && byIndex === undefined) {
|
|
330
338
|
process.stdout.write("HUD_COLOR_WARNING: unknown color; using cyan\n");
|
|
331
339
|
}
|
|
@@ -563,6 +571,8 @@ const commandExists = (command) => {
|
|
|
563
571
|
};
|
|
564
572
|
|
|
565
573
|
const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
574
|
+
const exactInventoryToken = (value) =>
|
|
575
|
+
new RegExp(`(?<![A-Za-z0-9_-])${escapeRegex(value)}(?![A-Za-z0-9_-])`, "u");
|
|
566
576
|
|
|
567
577
|
const assertCurrentPluginDetails = (detailsOutput) => {
|
|
568
578
|
const missing = [];
|
|
@@ -586,11 +596,11 @@ const assertCurrentPluginDetails = (detailsOutput) => {
|
|
|
586
596
|
missing.push("expected nonzero Agents inventory with prometheus-planner");
|
|
587
597
|
}
|
|
588
598
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
if (
|
|
593
|
-
missing.push(
|
|
599
|
+
const missingSkills = canonicalSkillIds.filter(
|
|
600
|
+
(skillId) => !exactInventoryToken(skillId).test(skillsInventory),
|
|
601
|
+
);
|
|
602
|
+
if (missingSkills.length > 0) {
|
|
603
|
+
missing.push(`expected canonical skills: ${missingSkills.join(", ")}`);
|
|
594
604
|
}
|
|
595
605
|
|
|
596
606
|
if (missing.length > 0) {
|
|
@@ -642,13 +652,11 @@ const verifyInstalledPayload = ({ home, litClaudeHome, targetPlugin }) => {
|
|
|
642
652
|
"bin/litclaude-hook.js",
|
|
643
653
|
"lib/start-work-lifecycle.mjs",
|
|
644
654
|
"lib/start-work-cli.mjs",
|
|
645
|
-
|
|
655
|
+
...canonicalSkillFiles,
|
|
646
656
|
"commands/start-work.md",
|
|
647
657
|
"bin/litclaude-scientific-visualization-doctor.js",
|
|
648
|
-
"skills/lit-handoff/SKILL.md",
|
|
649
658
|
"commands/lit-handoff.md",
|
|
650
659
|
"vendor/022_handoff/SKILL.md",
|
|
651
|
-
"skills/lit-scientific-visualization/SKILL.md",
|
|
652
660
|
"commands/lit-scientific-visualization.md",
|
|
653
661
|
"vendor/045_scientific-visualization/SKILL.md",
|
|
654
662
|
"vendor/045_scientific-visualization/scripts/style_presets.py",
|
|
@@ -670,9 +678,14 @@ const verifyInstalledPayload = ({ home, litClaudeHome, targetPlugin }) => {
|
|
|
670
678
|
};
|
|
671
679
|
|
|
672
680
|
const install = async ({ dryRun, rest }) => {
|
|
673
|
-
const { permissionMode, permissionExplicit } = parseInstallOptions(rest);
|
|
674
681
|
const home = claudeHome();
|
|
675
682
|
const litClaudeHome = litHome();
|
|
683
|
+
const existingLitClaude = readClaudeSettings(home)[litClaudeSettingsKey] ?? {};
|
|
684
|
+
const { permissionMode, permissionExplicit } = parseInstallOptions(rest, existingLitClaude.permissionMode);
|
|
685
|
+
const existingHudAccent = normalizeHudAccent(existingLitClaude.hudAccent);
|
|
686
|
+
const requestedHudAccent = process.env.LITCLAUDE_HUD_ACCENT
|
|
687
|
+
? normalizeHudAccent(process.env.LITCLAUDE_HUD_ACCENT)
|
|
688
|
+
: existingHudAccent;
|
|
676
689
|
const targetPlugin = claudePluginRoot(home);
|
|
677
690
|
const sourcePlugin = sourcePluginPath();
|
|
678
691
|
const targetMarketplace = marketplaceRoot(litClaudeHome);
|
|
@@ -683,8 +696,11 @@ const install = async ({ dryRun, rest }) => {
|
|
|
683
696
|
process.stdout.write(`Would copy: ${sourcePlugin} -> ${targetPlugin}\n`);
|
|
684
697
|
process.stdout.write(`Would create local marketplace: ${targetMarketplace}\n`);
|
|
685
698
|
process.stdout.write(`Would register Claude plugin: ${pluginKey}\n`);
|
|
686
|
-
process.stdout.write(`Would set Claude statusLine HUD: ${hudCommandForPlugin(targetPlugin)}\n`);
|
|
687
|
-
|
|
699
|
+
process.stdout.write(`Would set Claude statusLine HUD: ${hudCommandForPlugin(targetPlugin, requestedHudAccent)}\n`);
|
|
700
|
+
const permissionSource = permissionExplicit
|
|
701
|
+
? "explicit"
|
|
702
|
+
: permissionModes.has(existingLitClaude.permissionMode) ? "preserved" : "default";
|
|
703
|
+
process.stdout.write(`Would record permission preference: ${permissionMode} (${permissionSource})\n`);
|
|
688
704
|
process.stdout.write(`Permission profile: ${permissionProfile(permissionMode).description}\n`);
|
|
689
705
|
process.stdout.write("Launch with: claude\n");
|
|
690
706
|
return;
|
|
@@ -694,7 +710,7 @@ const install = async ({ dryRun, rest }) => {
|
|
|
694
710
|
fail(`Packaged LitClaude plugin payload is missing: ${sourcePlugin}`);
|
|
695
711
|
}
|
|
696
712
|
|
|
697
|
-
const hudAccent = await chooseHudAccent();
|
|
713
|
+
const hudAccent = await chooseHudAccent(existingHudAccent);
|
|
698
714
|
progress.prepare({ claudeTarget: home, litTarget: litClaudeHome });
|
|
699
715
|
await progress.run(
|
|
700
716
|
{
|
|
@@ -801,17 +817,12 @@ const doctor = ({ dryRun }) => {
|
|
|
801
817
|
"bin/litclaude-mcp.js",
|
|
802
818
|
"lib/start-work-lifecycle.mjs",
|
|
803
819
|
"lib/start-work-cli.mjs",
|
|
804
|
-
"skills/start-work/SKILL.md",
|
|
805
820
|
"commands/start-work.md",
|
|
806
|
-
"skills/lit-loop/SKILL.md",
|
|
807
|
-
"skills/lit-plan/SKILL.md",
|
|
808
|
-
"skills/lit-handoff/SKILL.md",
|
|
809
821
|
"commands/lit-handoff.md",
|
|
810
822
|
"vendor/022_handoff/SKILL.md",
|
|
811
823
|
"vendor/022_handoff/evals/evals.json",
|
|
812
824
|
"vendor/022_handoff/examples/HANDOFF-example-generic-auth-refactor.md",
|
|
813
825
|
"vendor/022_handoff/templates/HANDOFF.md",
|
|
814
|
-
"skills/lit-scientific-visualization/SKILL.md",
|
|
815
826
|
"commands/lit-scientific-visualization.md",
|
|
816
827
|
"bin/litclaude-scientific-visualization-doctor.js",
|
|
817
828
|
"vendor/045_scientific-visualization/SKILL.md",
|
|
@@ -828,6 +839,13 @@ const doctor = ({ dryRun }) => {
|
|
|
828
839
|
const path = join(pluginPath, file);
|
|
829
840
|
if (!existsSync(path)) fail(`LitClaude install is missing ${file}. Run \`litclaude install\` again.`);
|
|
830
841
|
}
|
|
842
|
+
for (const file of canonicalSkillFiles) {
|
|
843
|
+
const path = join(pluginPath, file);
|
|
844
|
+
if (!existsSync(path)) {
|
|
845
|
+
fail(`DOCTOR_FAIL: LitClaude install is missing ${file}. Run \`litclaude install\` again.`);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
process.stdout.write("SKILL_CATALOG_PASS\n");
|
|
831
849
|
|
|
832
850
|
printLspDiagnostics(pluginPath, registry);
|
|
833
851
|
|
|
@@ -931,13 +949,15 @@ const runClaude = ({ dryRun, rest }) => {
|
|
|
931
949
|
const uninstall = ({ dryRun }) => {
|
|
932
950
|
const home = litHome();
|
|
933
951
|
const pluginCacheRoot = join(claudeHome(), "plugins", "cache", "litclaude-ai");
|
|
952
|
+
const updateNotifierRoot = dirname(updateNotifierCachePath(home));
|
|
934
953
|
if (dryRun) {
|
|
935
|
-
process.stdout.write(`DRY_RUN: remove ${join(home, "litclaude-ai")}, ${currentRoot(home)}, ${marketplaceRoot(home)}, ${pluginCacheRoot}, ${pluginKey}, and its marketplace/settings entries\n`);
|
|
954
|
+
process.stdout.write(`DRY_RUN: remove ${join(home, "litclaude-ai")}, ${currentRoot(home)}, ${marketplaceRoot(home)}, ${updateNotifierRoot}, ${pluginCacheRoot}, ${pluginKey}, and its marketplace/settings entries\n`);
|
|
936
955
|
return;
|
|
937
956
|
}
|
|
938
957
|
rmSync(currentRoot(home), { recursive: true, force: true });
|
|
939
958
|
rmSync(join(home, "litclaude-ai"), { recursive: true, force: true });
|
|
940
959
|
rmSync(marketplaceRoot(home), { recursive: true, force: true });
|
|
960
|
+
rmSync(updateNotifierRoot, { recursive: true, force: true });
|
|
941
961
|
unregisterClaudePlugin();
|
|
942
962
|
unregisterMarketplace();
|
|
943
963
|
uninstallHudStatusLine();
|
|
@@ -993,6 +1013,14 @@ const main = async () => {
|
|
|
993
1013
|
printUsage();
|
|
994
1014
|
fail(`Unknown command: ${command}`, 64);
|
|
995
1015
|
}
|
|
1016
|
+
|
|
1017
|
+
if (command === "install" || command === "update" || command === "doctor") {
|
|
1018
|
+
runUpdateNotifier({
|
|
1019
|
+
...parsed,
|
|
1020
|
+
currentVersion: version,
|
|
1021
|
+
cachePath: updateNotifierCachePath(),
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
996
1024
|
};
|
|
997
1025
|
|
|
998
1026
|
main().catch((error) => fail(error.message));
|
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { get as httpsGet } from "node:https";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
export const CACHE_TTL_MS = 24 * 60 * 60 * 1_000;
|
|
9
|
+
export const MAX_RESPONSE_BYTES = 64 * 1_024;
|
|
10
|
+
export const REGISTRY_URL = "https://registry.npmjs.org/litclaude-ai/latest";
|
|
11
|
+
const PACKAGE_NAME = "litclaude-ai";
|
|
12
|
+
const REQUEST_TIMEOUT_MS = 3_000;
|
|
13
|
+
const RESERVATION_STALE_MS = 30_000;
|
|
14
|
+
const RESERVATION_CLOCK_SKEW_MS = 5_000;
|
|
15
|
+
const stableSemverPattern = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/u;
|
|
16
|
+
const generationPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
|
|
17
|
+
|
|
18
|
+
const parseStableSemver = (value) => {
|
|
19
|
+
const match = typeof value === "string" ? value.match(stableSemverPattern) : null;
|
|
20
|
+
if (!match) throw new Error("version is not strict stable semver");
|
|
21
|
+
return match.slice(1).map((part) => BigInt(part));
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const compareStableSemver = (left, right) => {
|
|
25
|
+
const leftParts = parseStableSemver(left);
|
|
26
|
+
const rightParts = parseStableSemver(right);
|
|
27
|
+
for (let index = 0; index < leftParts.length; index += 1) {
|
|
28
|
+
if (leftParts[index] > rightParts[index]) return 1;
|
|
29
|
+
if (leftParts[index] < rightParts[index]) return -1;
|
|
30
|
+
}
|
|
31
|
+
return 0;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const contentTypeIsJson = (value) =>
|
|
35
|
+
typeof value === "string" && /^application\/json(?:\s*;\s*charset=(?:utf-8|utf8))?$/iu.test(value.trim());
|
|
36
|
+
|
|
37
|
+
export const parseRegistryResponse = ({ statusCode, headers = {}, body }) => {
|
|
38
|
+
if (statusCode !== 200) throw new Error("registry response status is not 200");
|
|
39
|
+
if (!contentTypeIsJson(headers["content-type"])) throw new Error("registry response is not application/json");
|
|
40
|
+
|
|
41
|
+
const contentLength = headers["content-length"];
|
|
42
|
+
if (contentLength !== undefined) {
|
|
43
|
+
if (!/^(0|[1-9]\d*)$/u.test(String(contentLength))) throw new Error("registry content-length is invalid");
|
|
44
|
+
if (BigInt(contentLength) > BigInt(MAX_RESPONSE_BYTES)) throw new Error("registry response is too large");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const bytes = Buffer.isBuffer(body) ? body : Buffer.from(body ?? "");
|
|
48
|
+
if (bytes.length > MAX_RESPONSE_BYTES) throw new Error("registry response is too large");
|
|
49
|
+
|
|
50
|
+
let parsed;
|
|
51
|
+
try {
|
|
52
|
+
parsed = JSON.parse(bytes.toString("utf8"));
|
|
53
|
+
} catch {
|
|
54
|
+
throw new Error("registry response JSON is invalid");
|
|
55
|
+
}
|
|
56
|
+
if (!parsed || Array.isArray(parsed) || typeof parsed !== "object") throw new Error("registry response is not an object");
|
|
57
|
+
if (parsed.name !== PACKAGE_NAME) throw new Error("registry package name does not match");
|
|
58
|
+
parseStableSemver(parsed.version);
|
|
59
|
+
return { packageName: PACKAGE_NAME, latestVersion: parsed.version };
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const canonicalTimestamp = (value, now) => {
|
|
63
|
+
if (typeof value !== "string") return false;
|
|
64
|
+
const timestamp = Date.parse(value);
|
|
65
|
+
return Number.isFinite(timestamp) && timestamp <= now && new Date(timestamp).toISOString() === value;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const validCacheRecord = (value, now) => {
|
|
69
|
+
if (!value || Array.isArray(value) || typeof value !== "object") return false;
|
|
70
|
+
if (value.schema !== 3 || value.packageName !== PACKAGE_NAME) return false;
|
|
71
|
+
if (Object.keys(value).sort().join(",") !== "attemptedAt,checkedAt,generation,latestVersion,packageName,schema") return false;
|
|
72
|
+
if (typeof value.generation !== "string" || !generationPattern.test(value.generation)) return false;
|
|
73
|
+
if (!canonicalTimestamp(value.attemptedAt, now)) return false;
|
|
74
|
+
if (value.latestVersion === null || value.checkedAt === null) {
|
|
75
|
+
return value.latestVersion === null && value.checkedAt === null;
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
parseStableSemver(value.latestVersion);
|
|
79
|
+
} catch {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
return canonicalTimestamp(value.checkedAt, now);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const readUpdateCache = (cachePath, { now = Date.now() } = {}) => {
|
|
86
|
+
try {
|
|
87
|
+
const parsed = JSON.parse(readFileSync(cachePath, "utf8"));
|
|
88
|
+
return validCacheRecord(parsed, now) ? parsed : null;
|
|
89
|
+
} catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export const writeUpdateCache = (
|
|
95
|
+
cachePath,
|
|
96
|
+
{ packageName, latestVersion, checkedAt, attemptedAt, generation },
|
|
97
|
+
{ now = Date.now() } = {},
|
|
98
|
+
) => {
|
|
99
|
+
const record = { schema: 3, packageName, latestVersion, checkedAt, attemptedAt, generation };
|
|
100
|
+
if (!validCacheRecord(record, now)) throw new Error("refusing to write invalid update cache");
|
|
101
|
+
const directory = dirname(cachePath);
|
|
102
|
+
mkdirSync(directory, { recursive: true });
|
|
103
|
+
const temporaryPath = `${cachePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
104
|
+
try {
|
|
105
|
+
writeFileSync(temporaryPath, `${JSON.stringify(record)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
106
|
+
renameSync(temporaryPath, cachePath);
|
|
107
|
+
} finally {
|
|
108
|
+
rmSync(temporaryPath, { force: true });
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export const fetchLatestVersion = ({ request = httpsGet } = {}) =>
|
|
113
|
+
new Promise((resolve, reject) => {
|
|
114
|
+
let settled = false;
|
|
115
|
+
let deadline;
|
|
116
|
+
const settle = (operation, value) => {
|
|
117
|
+
if (settled) return;
|
|
118
|
+
settled = true;
|
|
119
|
+
if (deadline) clearTimeout(deadline);
|
|
120
|
+
operation(value);
|
|
121
|
+
};
|
|
122
|
+
const req = request(
|
|
123
|
+
REGISTRY_URL,
|
|
124
|
+
{
|
|
125
|
+
method: "GET",
|
|
126
|
+
headers: {
|
|
127
|
+
accept: "application/json",
|
|
128
|
+
"user-agent": "litclaude-ai-update-notifier",
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
(response) => {
|
|
132
|
+
const chunks = [];
|
|
133
|
+
let size = 0;
|
|
134
|
+
response.on("data", (chunk) => {
|
|
135
|
+
const bytes = Buffer.from(chunk);
|
|
136
|
+
size += bytes.length;
|
|
137
|
+
if (size > MAX_RESPONSE_BYTES) {
|
|
138
|
+
response.destroy(new Error("registry response is too large"));
|
|
139
|
+
settle(reject, new Error("registry response is too large"));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
chunks.push(bytes);
|
|
143
|
+
});
|
|
144
|
+
response.once("error", (error) => settle(reject, error));
|
|
145
|
+
response.once("end", () => {
|
|
146
|
+
try {
|
|
147
|
+
settle(resolve, parseRegistryResponse({
|
|
148
|
+
statusCode: response.statusCode,
|
|
149
|
+
headers: response.headers,
|
|
150
|
+
body: Buffer.concat(chunks),
|
|
151
|
+
}));
|
|
152
|
+
} catch (error) {
|
|
153
|
+
settle(reject, error);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
},
|
|
157
|
+
);
|
|
158
|
+
req.once("error", (error) => settle(reject, error));
|
|
159
|
+
req.setTimeout(REQUEST_TIMEOUT_MS, () => req.destroy(new Error("registry request timed out")));
|
|
160
|
+
deadline = setTimeout(() => req.destroy(new Error("registry request deadline exceeded")), REQUEST_TIMEOUT_MS);
|
|
161
|
+
deadline.unref();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
export const refreshUpdateCache = async (
|
|
165
|
+
cachePath,
|
|
166
|
+
{ fetch = fetchLatestVersion, now = () => Date.now(), generation } = {},
|
|
167
|
+
) => {
|
|
168
|
+
const result = await fetch();
|
|
169
|
+
let transition;
|
|
170
|
+
for (let attempt = 0; attempt < 50 && !transition; attempt += 1) {
|
|
171
|
+
const transitionAt = now();
|
|
172
|
+
transition = acquireTransition(cachePath, transitionAt);
|
|
173
|
+
if (!transition) await new Promise((resolve) => setTimeout(resolve, 10));
|
|
174
|
+
}
|
|
175
|
+
if (!transition) return false;
|
|
176
|
+
try {
|
|
177
|
+
const completedAt = now();
|
|
178
|
+
const previous = readUpdateCache(cachePath, { now: completedAt });
|
|
179
|
+
const expectedGeneration = generation ?? previous?.generation;
|
|
180
|
+
if (!previous || previous.generation !== expectedGeneration) return false;
|
|
181
|
+
if (!continuesToOwnLock(transition)) return false;
|
|
182
|
+
writeUpdateCache(
|
|
183
|
+
cachePath,
|
|
184
|
+
{
|
|
185
|
+
...result,
|
|
186
|
+
checkedAt: new Date(completedAt).toISOString(),
|
|
187
|
+
attemptedAt: previous.attemptedAt,
|
|
188
|
+
generation: previous.generation,
|
|
189
|
+
},
|
|
190
|
+
{ now: completedAt },
|
|
191
|
+
);
|
|
192
|
+
return true;
|
|
193
|
+
} finally {
|
|
194
|
+
releaseOwnedLock(transition);
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const hasOptOut = (env, name) => Object.hasOwn(env, name);
|
|
199
|
+
|
|
200
|
+
export const shouldRunUpdateNotifier = ({ command, rest, dryRun, env, stdin, stdout, stderr }) => {
|
|
201
|
+
if (!new Set(["install", "update", "doctor"]).has(command)) return false;
|
|
202
|
+
if (dryRun || rest.some((arg) => arg === "--json" || arg.startsWith("--json="))) return false;
|
|
203
|
+
if (!stdin.isTTY || !stdout.isTTY || !stderr.isTTY) return false;
|
|
204
|
+
if (hasOptOut(env, "CI") || hasOptOut(env, "NO_UPDATE_NOTIFIER") || hasOptOut(env, "LITCLAUDE_NO_UPDATE_CHECK")) return false;
|
|
205
|
+
return true;
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
export const formatUpdateNotice = (currentVersion, latestVersion) =>
|
|
209
|
+
`\nLitClaude update available: ${currentVersion} → ${latestVersion}\n` +
|
|
210
|
+
`Run: npx --yes litclaude-ai@${latestVersion} install\n` +
|
|
211
|
+
"Then restart Claude Code.\n\n";
|
|
212
|
+
|
|
213
|
+
const detachedRefreshEnvKeys = [
|
|
214
|
+
"PATH",
|
|
215
|
+
"HOME",
|
|
216
|
+
"USERPROFILE",
|
|
217
|
+
"HOMEDRIVE",
|
|
218
|
+
"HOMEPATH",
|
|
219
|
+
"TMPDIR",
|
|
220
|
+
"TMP",
|
|
221
|
+
"TEMP",
|
|
222
|
+
"SystemRoot",
|
|
223
|
+
"WINDIR",
|
|
224
|
+
"ComSpec",
|
|
225
|
+
"PATHEXT",
|
|
226
|
+
"SSL_CERT_FILE",
|
|
227
|
+
"SSL_CERT_DIR",
|
|
228
|
+
"NODE_EXTRA_CA_CERTS",
|
|
229
|
+
"HTTP_PROXY",
|
|
230
|
+
"HTTPS_PROXY",
|
|
231
|
+
"ALL_PROXY",
|
|
232
|
+
"NO_PROXY",
|
|
233
|
+
"http_proxy",
|
|
234
|
+
"https_proxy",
|
|
235
|
+
"all_proxy",
|
|
236
|
+
"no_proxy",
|
|
237
|
+
"LANG",
|
|
238
|
+
"LC_ALL",
|
|
239
|
+
"LC_CTYPE",
|
|
240
|
+
"TZ",
|
|
241
|
+
];
|
|
242
|
+
|
|
243
|
+
const proxyUrlEnvKeys = new Set([
|
|
244
|
+
"HTTP_PROXY",
|
|
245
|
+
"HTTPS_PROXY",
|
|
246
|
+
"ALL_PROXY",
|
|
247
|
+
"http_proxy",
|
|
248
|
+
"https_proxy",
|
|
249
|
+
"all_proxy",
|
|
250
|
+
]);
|
|
251
|
+
|
|
252
|
+
const detachedRefreshEnvValueIsSafe = (name, value) => {
|
|
253
|
+
if (!proxyUrlEnvKeys.has(name)) return true;
|
|
254
|
+
try {
|
|
255
|
+
const proxy = new URL(value.includes("://") ? value : `http://${value}`);
|
|
256
|
+
return proxy.username === "" && proxy.password === "";
|
|
257
|
+
} catch {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const detachedRefreshEnv = (env) => Object.fromEntries(
|
|
263
|
+
detachedRefreshEnvKeys
|
|
264
|
+
.filter((name) => typeof env[name] === "string" && detachedRefreshEnvValueIsSafe(name, env[name]))
|
|
265
|
+
.map((name) => [name, env[name]]),
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
export const spawnDetachedRefresh = (
|
|
269
|
+
cachePath,
|
|
270
|
+
generation,
|
|
271
|
+
continuesToOwnTransition = () => true,
|
|
272
|
+
spawnProcess = spawn,
|
|
273
|
+
env = process.env,
|
|
274
|
+
) => {
|
|
275
|
+
if (!continuesToOwnTransition()) return false;
|
|
276
|
+
const child = spawnProcess(
|
|
277
|
+
process.execPath,
|
|
278
|
+
[fileURLToPath(new URL("./update-notifier-refresh.mjs", import.meta.url)), cachePath, generation],
|
|
279
|
+
{ detached: true, stdio: "ignore", windowsHide: true, env: detachedRefreshEnv(env) },
|
|
280
|
+
);
|
|
281
|
+
child.once("error", () => {});
|
|
282
|
+
child.unref();
|
|
283
|
+
return true;
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const reservationLockPath = (cachePath) => `${cachePath}.reservation.lock`;
|
|
287
|
+
const transitionLockPath = (cachePath) => `${cachePath}.transition.lock`;
|
|
288
|
+
const recoveryLockPath = (cachePath) => `${cachePath}.transition-recovery.lock`;
|
|
289
|
+
const ownerPath = (lockPath) => join(lockPath, "owner.json");
|
|
290
|
+
|
|
291
|
+
const readLockOwner = (lockPath) => {
|
|
292
|
+
try {
|
|
293
|
+
const owner = JSON.parse(readFileSync(ownerPath(lockPath), "utf8"));
|
|
294
|
+
if (!owner || Object.keys(owner).sort().join(",") !== "acquiredAt,generation,pid,schema") return null;
|
|
295
|
+
if (owner.schema !== 2 || !generationPattern.test(owner.generation)) return null;
|
|
296
|
+
if (!Number.isSafeInteger(owner.pid) || owner.pid <= 0) return null;
|
|
297
|
+
const acquiredAt = Date.parse(owner.acquiredAt);
|
|
298
|
+
if (!Number.isFinite(acquiredAt) || new Date(acquiredAt).toISOString() !== owner.acquiredAt) return null;
|
|
299
|
+
return owner;
|
|
300
|
+
} catch {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const processIsLive = (pid) => {
|
|
306
|
+
try {
|
|
307
|
+
process.kill(pid, 0);
|
|
308
|
+
return true;
|
|
309
|
+
} catch (error) {
|
|
310
|
+
return error?.code !== "ESRCH";
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
const createOwnedLock = (lockPath, now, generation = randomUUID()) => {
|
|
315
|
+
mkdirSync(dirname(lockPath), { recursive: true });
|
|
316
|
+
try {
|
|
317
|
+
mkdirSync(lockPath);
|
|
318
|
+
} catch (error) {
|
|
319
|
+
if (error?.code === "EEXIST") return null;
|
|
320
|
+
throw error;
|
|
321
|
+
}
|
|
322
|
+
const lease = { path: lockPath, generation };
|
|
323
|
+
try {
|
|
324
|
+
writeFileSync(ownerPath(lockPath), `${JSON.stringify({
|
|
325
|
+
schema: 2,
|
|
326
|
+
generation,
|
|
327
|
+
pid: process.pid,
|
|
328
|
+
acquiredAt: new Date(now).toISOString(),
|
|
329
|
+
})}\n`, { encoding: "utf8", mode: 0o600 });
|
|
330
|
+
return lease;
|
|
331
|
+
} catch (error) {
|
|
332
|
+
releaseOwnedLock(lease);
|
|
333
|
+
throw error;
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
const inspectOwnedLock = (lockPath, now) => {
|
|
338
|
+
try {
|
|
339
|
+
const stats = statSync(lockPath);
|
|
340
|
+
const owner = readLockOwner(lockPath);
|
|
341
|
+
const age = now - stats.mtimeMs;
|
|
342
|
+
const ownerLive = owner ? processIsLive(owner.pid) : null;
|
|
343
|
+
const stale = owner
|
|
344
|
+
? ownerLive === false && (age < -RESERVATION_CLOCK_SKEW_MS || age >= RESERVATION_STALE_MS)
|
|
345
|
+
: age >= RESERVATION_STALE_MS;
|
|
346
|
+
return {
|
|
347
|
+
exists: true,
|
|
348
|
+
generation: owner?.generation ?? null,
|
|
349
|
+
inode: String(stats.ino),
|
|
350
|
+
mtimeMs: stats.mtimeMs,
|
|
351
|
+
stale,
|
|
352
|
+
};
|
|
353
|
+
} catch {
|
|
354
|
+
return { exists: false, generation: null, inode: null, mtimeMs: null, stale: false };
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
const sameLockGeneration = (left, right) =>
|
|
359
|
+
left?.exists === true && right?.exists === true &&
|
|
360
|
+
left.generation === right.generation && left.inode === right.inode && left.mtimeMs === right.mtimeMs;
|
|
361
|
+
|
|
362
|
+
const continuesToOwnLock = (lease) => {
|
|
363
|
+
if (!lease) return false;
|
|
364
|
+
const owner = readLockOwner(lease.path);
|
|
365
|
+
return owner?.generation === lease.generation && owner.pid === process.pid;
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
const recoverObservedLock = (lockPath, observed, now) => {
|
|
369
|
+
if (!observed?.stale) return false;
|
|
370
|
+
const current = inspectOwnedLock(lockPath, now);
|
|
371
|
+
if (!current.stale || !sameLockGeneration(observed, current)) return false;
|
|
372
|
+
rmSync(lockPath, { recursive: true, force: true });
|
|
373
|
+
return true;
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
const releaseOwnedLock = (lease) => {
|
|
377
|
+
if (!lease) return false;
|
|
378
|
+
if (!continuesToOwnLock(lease)) return false;
|
|
379
|
+
rmSync(lease.path, { recursive: true, force: true });
|
|
380
|
+
return true;
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
export const inspectUpdateReservation = (cachePath, now = Date.now()) =>
|
|
384
|
+
inspectOwnedLock(reservationLockPath(cachePath), now);
|
|
385
|
+
|
|
386
|
+
export const recoverObservedReservation = (cachePath, observed, now = Date.now()) =>
|
|
387
|
+
recoverObservedLock(reservationLockPath(cachePath), observed, now);
|
|
388
|
+
|
|
389
|
+
const acquireRecovery = (cachePath, now) => {
|
|
390
|
+
const path = recoveryLockPath(cachePath);
|
|
391
|
+
let lease = createOwnedLock(path, now);
|
|
392
|
+
if (lease) return lease;
|
|
393
|
+
const observed = inspectOwnedLock(path, now);
|
|
394
|
+
if (!recoverObservedLock(path, observed, now)) return null;
|
|
395
|
+
lease = createOwnedLock(path, now);
|
|
396
|
+
return lease;
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
const acquireTransition = (cachePath, now) => {
|
|
400
|
+
const path = transitionLockPath(cachePath);
|
|
401
|
+
const observedRecovery = inspectOwnedLock(recoveryLockPath(cachePath), now);
|
|
402
|
+
if (observedRecovery.exists) {
|
|
403
|
+
if (!observedRecovery.stale) return null;
|
|
404
|
+
if (!recoverObservedLock(recoveryLockPath(cachePath), observedRecovery, now)) return null;
|
|
405
|
+
}
|
|
406
|
+
let lease = createOwnedLock(path, now);
|
|
407
|
+
if (lease) return lease;
|
|
408
|
+
|
|
409
|
+
const observed = inspectOwnedLock(path, now);
|
|
410
|
+
if (!observed.stale) return null;
|
|
411
|
+
const recovery = acquireRecovery(cachePath, now);
|
|
412
|
+
if (!recovery) return null;
|
|
413
|
+
try {
|
|
414
|
+
if (!recoverObservedLock(path, observed, now)) return null;
|
|
415
|
+
lease = createOwnedLock(path, now);
|
|
416
|
+
return lease;
|
|
417
|
+
} finally {
|
|
418
|
+
releaseOwnedLock(recovery);
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
const scheduleRefresh = ({ cachePath, now, spawnRefresh }) => {
|
|
423
|
+
const transition = acquireTransition(cachePath, now);
|
|
424
|
+
if (!transition) return "coalesced";
|
|
425
|
+
try {
|
|
426
|
+
const current = readUpdateCache(cachePath, { now });
|
|
427
|
+
if (current) {
|
|
428
|
+
const attemptAge = now - Date.parse(current.attemptedAt);
|
|
429
|
+
if (attemptAge >= 0 && attemptAge < CACHE_TTL_MS) return "throttled";
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const observedReservation = inspectUpdateReservation(cachePath, now);
|
|
433
|
+
if (observedReservation.exists) {
|
|
434
|
+
if (!observedReservation.stale) return "coalesced";
|
|
435
|
+
if (!recoverObservedReservation(cachePath, observedReservation, now)) return "coalesced";
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const generation = randomUUID();
|
|
439
|
+
const reservation = createOwnedLock(reservationLockPath(cachePath), now, generation);
|
|
440
|
+
if (!reservation) return "coalesced";
|
|
441
|
+
try {
|
|
442
|
+
if (!continuesToOwnLock(transition)) return "coalesced";
|
|
443
|
+
writeUpdateCache(
|
|
444
|
+
cachePath,
|
|
445
|
+
{
|
|
446
|
+
packageName: PACKAGE_NAME,
|
|
447
|
+
latestVersion: current?.latestVersion ?? null,
|
|
448
|
+
checkedAt: current?.checkedAt ?? null,
|
|
449
|
+
attemptedAt: new Date(now).toISOString(),
|
|
450
|
+
generation,
|
|
451
|
+
},
|
|
452
|
+
{ now },
|
|
453
|
+
);
|
|
454
|
+
if (!continuesToOwnLock(transition)) return "coalesced";
|
|
455
|
+
const spawned = spawnRefresh(cachePath, generation, () => continuesToOwnLock(transition));
|
|
456
|
+
if (spawned === false) return "coalesced";
|
|
457
|
+
return "scheduled";
|
|
458
|
+
} catch {
|
|
459
|
+
return "spawn-error";
|
|
460
|
+
} finally {
|
|
461
|
+
releaseOwnedLock(reservation);
|
|
462
|
+
}
|
|
463
|
+
} finally {
|
|
464
|
+
releaseOwnedLock(transition);
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
export const runUpdateNotifier = ({
|
|
469
|
+
command,
|
|
470
|
+
rest,
|
|
471
|
+
dryRun,
|
|
472
|
+
currentVersion,
|
|
473
|
+
cachePath,
|
|
474
|
+
env = process.env,
|
|
475
|
+
stdin = process.stdin,
|
|
476
|
+
stdout = process.stdout,
|
|
477
|
+
stderr = process.stderr,
|
|
478
|
+
now = Date.now(),
|
|
479
|
+
spawnRefresh = spawnDetachedRefresh,
|
|
480
|
+
}) => {
|
|
481
|
+
try {
|
|
482
|
+
if (!shouldRunUpdateNotifier({ command, rest, dryRun, env, stdin, stdout, stderr })) return "gated";
|
|
483
|
+
const cached = readUpdateCache(cachePath, { now });
|
|
484
|
+
const noticeAvailable = cached?.latestVersion
|
|
485
|
+
? compareStableSemver(cached.latestVersion, currentVersion) > 0
|
|
486
|
+
: false;
|
|
487
|
+
const attemptAge = cached ? now - Date.parse(cached.attemptedAt) : Number.POSITIVE_INFINITY;
|
|
488
|
+
const refreshDue = !cached || attemptAge < 0 || attemptAge >= CACHE_TTL_MS;
|
|
489
|
+
const refreshStatus = refreshDue
|
|
490
|
+
? scheduleRefresh({ cachePath, now, spawnRefresh })
|
|
491
|
+
: "throttled";
|
|
492
|
+
|
|
493
|
+
if (noticeAvailable) {
|
|
494
|
+
stderr.write(formatUpdateNotice(currentVersion, cached.latestVersion));
|
|
495
|
+
}
|
|
496
|
+
if (noticeAvailable && refreshStatus === "scheduled") return "noticed-refreshing";
|
|
497
|
+
if (noticeAvailable) return "noticed";
|
|
498
|
+
if (refreshStatus === "scheduled") return "refreshing";
|
|
499
|
+
if (refreshStatus === "spawn-error") return "silent-error";
|
|
500
|
+
return refreshStatus === "coalesced" ? "coalesced" : "current";
|
|
501
|
+
} catch {
|
|
502
|
+
return "silent-error";
|
|
503
|
+
}
|
|
504
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Native `/goal` Surface Matrix
|
|
2
|
+
|
|
3
|
+
This matrix separates who owns a Claude Code session from what the LitClaude
|
|
4
|
+
plugin can do. It is capability guidance only: there is no runtime bridge from
|
|
5
|
+
LitClaude into another session.
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
- Use Claude Code >=2.1.139; older clients do not provide the native `/goal`
|
|
10
|
+
surface described here.
|
|
11
|
+
- Complete workspace trust before relying on project-local plugin behavior.
|
|
12
|
+
- Confirm hooks are enabled and that user, project, organization, and managed policy
|
|
13
|
+
do not block the session or command surface. Hook context is still not
|
|
14
|
+
slash-command dispatch.
|
|
15
|
+
- For an SDK-owned session, inspect the system/init `slash_commands` list and
|
|
16
|
+
submit `/goal` only when that initialized session advertises the command. The
|
|
17
|
+
SDK client must own the target session and its input channel.
|
|
18
|
+
|
|
19
|
+
| Surface owner | Can submit `/goal`? | Which session changes? | LitClaude boundary |
|
|
20
|
+
| --- | --- | --- | --- |
|
|
21
|
+
| User / Remote Control | Yes, by submitting `/goal <condition>` through that user-controlled input surface. | The session receiving that input. | LitClaude may provide a condition, but does not submit it for the user. |
|
|
22
|
+
| SDK-owned session | Yes, when the SDK client submits `/goal <condition>` to the session it created and owns. | That SDK-owned session only. | The SDK client is the submitter; the installed plugin is not a cross-session controller. |
|
|
23
|
+
| Separate `claude -p` session | Yes, by starting a separate print session whose prompt begins with `/goal`. | The new print session, not an arbitrary already-open TUI. | `litclaude litgoal native-worker` can build or launch this separate process explicitly. |
|
|
24
|
+
| LitClaude plugin in an arbitrary already-open TUI | No supported binding path. | None. | A skill, command body, MCP server, or hook cannot claim that it armed the open TUI. |
|
|
25
|
+
|
|
26
|
+
## Ready to paste
|
|
27
|
+
|
|
28
|
+
After creating and reviewing the durable criteria, the user or owner of the
|
|
29
|
+
target session can paste this condition there:
|
|
30
|
+
|
|
31
|
+
```text
|
|
32
|
+
/goal Finish only when every criterion in .litclaude/litgoal/goals.json is pass, required evidence is recorded in .litclaude/litgoal/ledger.jsonl, targeted verification passes, and .litclaude/litgoal contains no unresolved review blocker.
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Adapt filenames only when the local ledger reports a different authoritative
|
|
36
|
+
path. The condition does not make the ledger self-validating; recorded evidence
|
|
37
|
+
still needs the normal LitClaude review gates.
|
|
38
|
+
|
|
39
|
+
This documentation does not add a fake command, MCP method, or hook dispatch.
|
|
40
|
+
It also does not auto-install anything, mutate a transcript, inject keystrokes,
|
|
41
|
+
or claim success based on prompt context alone.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "litclaude-ai",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.33",
|
|
4
4
|
"description": "Claude Code-native workflow distribution.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"docs/hooks.md",
|
|
14
14
|
"docs/lsp.md",
|
|
15
15
|
"docs/migration.md",
|
|
16
|
+
"docs/native-goal-surface.md",
|
|
16
17
|
"docs/workflow-compatibility-audit.md",
|
|
17
18
|
"plugins",
|
|
18
19
|
"scripts",
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export const canonicalSkillIds = Object.freeze([
|
|
2
|
+
"ai-slop-remover",
|
|
3
|
+
"comment-checker",
|
|
4
|
+
"debugging",
|
|
5
|
+
"deep-interview",
|
|
6
|
+
"frontend-ui-ux",
|
|
7
|
+
"git-master",
|
|
8
|
+
"hyperplan",
|
|
9
|
+
"init-deep",
|
|
10
|
+
"korean-ai-slop-remover",
|
|
11
|
+
"lit-handoff",
|
|
12
|
+
"lit-loop",
|
|
13
|
+
"lit-plan",
|
|
14
|
+
"lit-recap",
|
|
15
|
+
"lit-scientific-visualization",
|
|
16
|
+
"litgoal",
|
|
17
|
+
"litresearch",
|
|
18
|
+
"lsp",
|
|
19
|
+
"lsp-setup",
|
|
20
|
+
"programming",
|
|
21
|
+
"refactor",
|
|
22
|
+
"remove-ai-slops",
|
|
23
|
+
"review-work",
|
|
24
|
+
"rules",
|
|
25
|
+
"start-work",
|
|
26
|
+
"visual-qa",
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
export const canonicalSkillFiles = Object.freeze(
|
|
30
|
+
canonicalSkillIds.map((skillId) => `skills/${skillId}/SKILL.md`),
|
|
31
|
+
);
|
package/scripts/doctor.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { spawnSync } from "node:child_process";
|
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
4
|
import { join, resolve } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { canonicalSkillFiles } from "../plugins/litclaude/lib/canonical-skill-catalog.mjs";
|
|
6
7
|
|
|
7
8
|
const root = resolve(fileURLToPath(new URL("..", import.meta.url)));
|
|
8
9
|
|
|
@@ -35,15 +36,12 @@ for (const file of [
|
|
|
35
36
|
"plugins/litclaude/lib/litgoal/cli.mjs",
|
|
36
37
|
"plugins/litclaude/lib/start-work-lifecycle.mjs",
|
|
37
38
|
"plugins/litclaude/lib/start-work-cli.mjs",
|
|
38
|
-
"plugins/litclaude/skills/start-work/SKILL.md",
|
|
39
39
|
"plugins/litclaude/commands/start-work.md",
|
|
40
|
-
"plugins/litclaude/skills/lit-handoff/SKILL.md",
|
|
41
40
|
"plugins/litclaude/commands/lit-handoff.md",
|
|
42
41
|
"plugins/litclaude/vendor/022_handoff/SKILL.md",
|
|
43
42
|
"plugins/litclaude/vendor/022_handoff/evals/evals.json",
|
|
44
43
|
"plugins/litclaude/vendor/022_handoff/examples/HANDOFF-example-generic-auth-refactor.md",
|
|
45
44
|
"plugins/litclaude/vendor/022_handoff/templates/HANDOFF.md",
|
|
46
|
-
"plugins/litclaude/skills/lit-scientific-visualization/SKILL.md",
|
|
47
45
|
"plugins/litclaude/commands/lit-scientific-visualization.md",
|
|
48
46
|
"plugins/litclaude/vendor/045_scientific-visualization/SKILL.md",
|
|
49
47
|
"plugins/litclaude/vendor/045_scientific-visualization/scripts/style_presets.py",
|
|
@@ -59,6 +57,16 @@ for (const file of [
|
|
|
59
57
|
process.exit(1);
|
|
60
58
|
}
|
|
61
59
|
}
|
|
60
|
+
|
|
61
|
+
for (const file of canonicalSkillFiles) {
|
|
62
|
+
const relativePath = `plugins/litclaude/${file}`;
|
|
63
|
+
if (!existsSync(join(root, relativePath))) {
|
|
64
|
+
process.stderr.write(`Runtime payload missing: ${relativePath}\n`);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
process.stdout.write("\nSKILL_CATALOG_PASS\n");
|
|
69
|
+
|
|
62
70
|
process.stdout.write("\nLITGOAL_RUNTIME_PASS\n");
|
|
63
71
|
process.stdout.write("START_WORK_LIFECYCLE_PASS: schema-3 bounded-authority runtime enrolled\n");
|
|
64
72
|
process.stdout.write("START_WORK_HOST_HOOKS_PASS: PreToolUse, SubagentStart, SubagentStop, SessionEnd, and Stop enrolled\n");
|