@litfamily/litmuse 0.1.6 → 0.1.8
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 +10 -0
- package/lib/install.mjs +25 -3
- package/package.json +1 -1
- package/plugin/.muse-plugin/plugin.json +11 -11
- package/plugin/hooks/lib/digest.mjs +9 -3
- package/plugin/hooks/session-start.mjs +2 -30
- package/plugin/hooks/user-prompt-submit.mjs +32 -4
- package/plugin/skills/lit-adhd/SKILL.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.8 — 2026-11-04
|
|
4
|
+
|
|
5
|
+
- `install --approve` no longer prints a stale waiting list computed before approving. It now says it is approving the capabilities, then reports what was approved and lists only what is really still waiting (or says none are). Without `--approve` the waiting list stays as it was.
|
|
6
|
+
|
|
7
|
+
## 0.1.7 — 2026-11-03
|
|
8
|
+
|
|
9
|
+
- Muse keeps the cached conversation across turns more often. The once-a-day suggestion to run `lit adhd` used to sit in the session-start text, which Muse rebuilds on every resume and places before the whole conversation; the line appeared on one resume and vanished on the next, so the cached history behind it was thrown away twice. The suggestion now rides on the first plain prompt instead, once per day as before and never while a route is live, and the session-start text stays the same for the whole session.
|
|
10
|
+
- Each hook now checks the code digest of only the files it runs. Before, every hook call also read the motion engine and the humanizer detector; now only Stop and PostToolUse do. A hook call is about a millisecond faster; most of its time is Muse starting Node, which LitMuse cannot change.
|
|
11
|
+
- The hook code changed, so Muse asks you to approve the LitMuse hooks again after the update.
|
|
12
|
+
|
|
3
13
|
## 0.1.6 — 2026-11-01
|
|
4
14
|
|
|
5
15
|
- `lit-typographic-motion` can lay real keyed footage over a stage film. A stage declares chroma-keyed clips in `stage/clips/clips.json` (an id, a folder, a frame count and a rate), the frames are 8-bit RGBA PNGs with real transparency, and the page registers each clip once with `LitStage.clip` and calls the returned function from `render(t)`; the frame shown is a plain function of film time, so a cue is `start = cue - (N - 1) / fps` and the flash audit and determinism replay see the final pixels. A declared clip is held to its own caps (12 clips, 360 frames in one and 1500 in all, 40 MB in one and 128 MB in all, 720 px on the long side, 30 fps) and no longer trips the flipbook rule; an undeclared frame folder is still refused with exit 17. Cover mode takes no clips. The manifest and the report list each clip.
|
package/lib/install.mjs
CHANGED
|
@@ -129,14 +129,21 @@ export async function install({ env = process.env, flags = {}, interactive = fal
|
|
|
129
129
|
let states = approvalStates(muse(env, ['plugins', 'inspect', 'litmuse', '--json']).json);
|
|
130
130
|
const pending = states.filter((s) => s.status !== 'trusted_enabled');
|
|
131
131
|
if (pending.length) {
|
|
132
|
-
print('');
|
|
133
|
-
print(`Muse runs plugin hooks and agents only after you approve them (an update re-arms the agent approvals, and the hook approvals whenever hook code changed). ${pending.length} are waiting:`);
|
|
134
|
-
for (const s of pending) print(` ${s.id} (${s.what})`);
|
|
135
132
|
let approve = Boolean(flags.approve);
|
|
133
|
+
const decided = approve;
|
|
134
|
+
if (!decided) {
|
|
135
|
+
print('');
|
|
136
|
+
print(`Muse runs plugin hooks and agents only after you approve them (an update re-arms the agent approvals, and the hook approvals whenever hook code changed). ${pending.length} are waiting:`);
|
|
137
|
+
for (const s of pending) print(` ${s.id} (${s.what})`);
|
|
138
|
+
}
|
|
136
139
|
if (!approve && interactive && ask) {
|
|
137
140
|
approve = await ask(`Approve these ${pending.length} LitMuse capabilities now? [y/N] `);
|
|
138
141
|
}
|
|
139
142
|
if (approve) {
|
|
143
|
+
if (decided) {
|
|
144
|
+
print('');
|
|
145
|
+
print(`Approving ${pending.length} LitMuse capabilities...`);
|
|
146
|
+
}
|
|
140
147
|
const failed = [];
|
|
141
148
|
for (const s of pending) {
|
|
142
149
|
const r = muse(env, ['plugins', 'approve', s.id, '--json']);
|
|
@@ -145,6 +152,21 @@ export async function install({ env = process.env, flags = {}, interactive = fal
|
|
|
145
152
|
written.push(`${p.museSettings} (runtime_capabilities for litmuse)`);
|
|
146
153
|
for (const f of failed) print(` FAIL approve ${f}`);
|
|
147
154
|
states = approvalStates(muse(env, ['plugins', 'inspect', 'litmuse', '--json']).json);
|
|
155
|
+
const byId = new Map(states.map((s) => [s.id, s]));
|
|
156
|
+
const approved = pending.filter((s) => byId.get(s.id)?.status === 'trusted_enabled');
|
|
157
|
+
const still = states.filter((s) => s.status !== 'trusted_enabled');
|
|
158
|
+
if (approved.length) {
|
|
159
|
+
print('');
|
|
160
|
+
print(`Approved ${approved.length} LitMuse capabilities:`);
|
|
161
|
+
for (const s of approved) print(` ${s.id} (${s.what})`);
|
|
162
|
+
}
|
|
163
|
+
if (still.length) {
|
|
164
|
+
print('');
|
|
165
|
+
print(`${still.length} are still waiting:`);
|
|
166
|
+
for (const s of still) print(` ${s.id} (${s.what})`);
|
|
167
|
+
} else {
|
|
168
|
+
print('No capabilities are still waiting for approval.');
|
|
169
|
+
}
|
|
148
170
|
} else {
|
|
149
171
|
print('');
|
|
150
172
|
print('Approval needed. Run these when you are ready (or rerun install with --approve):');
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"name": "litmuse",
|
|
4
4
|
"displayName": "LitMuse",
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.8",
|
|
6
6
|
"description": "LitFamily workflows for Muse Code: bare lit activation, planning, evidence-bound execution, review, research, handoff and Korean prose care.",
|
|
7
7
|
"compat": {
|
|
8
8
|
"source": "native",
|
|
@@ -251,7 +251,7 @@
|
|
|
251
251
|
"node",
|
|
252
252
|
"hooks/session-start.mjs",
|
|
253
253
|
"--digest",
|
|
254
|
-
"
|
|
254
|
+
"61918b7f2047a2b7"
|
|
255
255
|
],
|
|
256
256
|
"timeoutMs": 5000
|
|
257
257
|
},
|
|
@@ -262,7 +262,7 @@
|
|
|
262
262
|
"node",
|
|
263
263
|
"hooks/user-prompt-submit.mjs",
|
|
264
264
|
"--digest",
|
|
265
|
-
"
|
|
265
|
+
"ae51d52609149cdc"
|
|
266
266
|
],
|
|
267
267
|
"timeoutMs": 5000,
|
|
268
268
|
"statusMessage": "LitMuse route check"
|
|
@@ -274,7 +274,7 @@
|
|
|
274
274
|
"node",
|
|
275
275
|
"hooks/pre-tool-use.mjs",
|
|
276
276
|
"--digest",
|
|
277
|
-
"
|
|
277
|
+
"599863b80b61d799"
|
|
278
278
|
],
|
|
279
279
|
"timeoutMs": 5000
|
|
280
280
|
},
|
|
@@ -285,7 +285,7 @@
|
|
|
285
285
|
"node",
|
|
286
286
|
"hooks/post-tool-use.mjs",
|
|
287
287
|
"--digest",
|
|
288
|
-
"
|
|
288
|
+
"9fcb9beef235a153"
|
|
289
289
|
],
|
|
290
290
|
"timeoutMs": 5000
|
|
291
291
|
},
|
|
@@ -296,7 +296,7 @@
|
|
|
296
296
|
"node",
|
|
297
297
|
"hooks/post-tool-use-failure.mjs",
|
|
298
298
|
"--digest",
|
|
299
|
-
"
|
|
299
|
+
"01a173366e95cae1"
|
|
300
300
|
],
|
|
301
301
|
"timeoutMs": 5000
|
|
302
302
|
},
|
|
@@ -307,7 +307,7 @@
|
|
|
307
307
|
"node",
|
|
308
308
|
"hooks/stop.mjs",
|
|
309
309
|
"--digest",
|
|
310
|
-
"
|
|
310
|
+
"3b7902112879a54b"
|
|
311
311
|
],
|
|
312
312
|
"timeoutMs": 5000
|
|
313
313
|
},
|
|
@@ -318,7 +318,7 @@
|
|
|
318
318
|
"node",
|
|
319
319
|
"hooks/subagent-start.mjs",
|
|
320
320
|
"--digest",
|
|
321
|
-
"
|
|
321
|
+
"251d44534b9a1f81"
|
|
322
322
|
],
|
|
323
323
|
"timeoutMs": 5000
|
|
324
324
|
},
|
|
@@ -329,7 +329,7 @@
|
|
|
329
329
|
"node",
|
|
330
330
|
"hooks/subagent-stop.mjs",
|
|
331
331
|
"--digest",
|
|
332
|
-
"
|
|
332
|
+
"886752b6c1d4349e"
|
|
333
333
|
],
|
|
334
334
|
"timeoutMs": 5000
|
|
335
335
|
},
|
|
@@ -340,7 +340,7 @@
|
|
|
340
340
|
"node",
|
|
341
341
|
"hooks/session-end.mjs",
|
|
342
342
|
"--digest",
|
|
343
|
-
"
|
|
343
|
+
"b64cea281ecff20b"
|
|
344
344
|
],
|
|
345
345
|
"timeoutMs": 5000
|
|
346
346
|
},
|
|
@@ -351,7 +351,7 @@
|
|
|
351
351
|
"node",
|
|
352
352
|
"hooks/pre-compact.mjs",
|
|
353
353
|
"--digest",
|
|
354
|
-
"
|
|
354
|
+
"c14099b8ef862d39"
|
|
355
355
|
],
|
|
356
356
|
"timeoutMs": 5000
|
|
357
357
|
}
|
|
@@ -11,17 +11,23 @@ const PLUGIN = join(LIB, '..', '..');
|
|
|
11
11
|
// Files outside hooks/lib that a hook loads at run time: PostToolUse runs the lit-humanizer detector, and Stop runs the
|
|
12
12
|
// lit-typographic-motion done-check with every engine module it imports.
|
|
13
13
|
const MOTION_DONE = ['node/done.mjs', 'node/cover-assets.mjs', 'core/constants.mjs', 'node/chrome.mjs', 'node/cover-image.mjs', 'node/cover-treatment.mjs', 'node/treatment.mjs', 'node/sound.mjs', 'node/house.mjs', 'node/styles.mjs', 'node/score.mjs', 'core/presets.mjs', 'core/util.mjs', 'node/look.mjs', 'node/stills.mjs', 'node/analysis.mjs', 'core/flash.mjs', 'node/png.mjs'];
|
|
14
|
-
|
|
14
|
+
const HUMANIZER = ['skills/lit-humanizer/scripts/core.mjs', 'skills/lit-humanizer/rules.json'];
|
|
15
|
+
const MOTION = MOTION_DONE.map((f) => `skills/lit-typographic-motion/engine/${f}`);
|
|
16
|
+
export const DIGEST_EXTRAS = [...HUMANIZER, ...MOTION];
|
|
17
|
+
// The hook that loads each extra. Every hook runs on every matching host event, so hashing only its own extras keeps
|
|
18
|
+
// the per-call read small (the motion engine is most of the bytes) while still covering all the code that hook runs.
|
|
19
|
+
export const EXTRAS_BY_HOOK = { 'post-tool-use.mjs': HUMANIZER, 'stop.mjs': MOTION };
|
|
15
20
|
|
|
16
21
|
const listed = (dir, ext) => readdirSync(dir).filter((f) => f.endsWith(ext)).sort().map((f) => join(dir, f));
|
|
17
22
|
|
|
18
23
|
// Digest of everything a hook executes or injects: its own script, the shared hook library, the context text and
|
|
19
|
-
// the skill files
|
|
24
|
+
// the skill files that hook loads.
|
|
20
25
|
// Muse keys hook trust on the declared command, so carrying this digest in the command makes any code change a new
|
|
21
26
|
// declaration that the user approves again.
|
|
22
27
|
export function hookDigest(scriptPath) {
|
|
23
28
|
const h = createHash('sha256');
|
|
24
|
-
|
|
29
|
+
const extras = EXTRAS_BY_HOOK[basename(scriptPath)] ?? [];
|
|
30
|
+
for (const file of [scriptPath, ...listed(LIB, '.mjs'), ...listed(CONTEXT, '.md'), ...extras.map((rel) => join(PLUGIN, rel))]) {
|
|
25
31
|
h.update(`${basename(file)}\0`);
|
|
26
32
|
h.update(readFileSync(file));
|
|
27
33
|
h.update('\0');
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, mkdirSync, lstatSync
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, lstatSync } from 'node:fs';
|
|
2
2
|
import { join, dirname } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
|
-
import { run, emitContext, PLUGIN_ROOT, pluginVersion, cachedLatest, newerThan,
|
|
4
|
+
import { run, emitContext, PLUGIN_ROOT, pluginVersion, cachedLatest, newerThan, sessionPath, workspaceOf } from './lib/common.mjs';
|
|
5
5
|
import { SKILL_ROUTES } from './lib/routes.mjs';
|
|
6
6
|
import { readFreshSnapshot } from './lib/handoff-snapshot.mjs';
|
|
7
|
-
import { resolveProjectStateRoot } from './lib/workflow-stack.mjs';
|
|
8
7
|
import { recordLoadedVersion } from './lib/stage.mjs';
|
|
9
8
|
|
|
10
9
|
const STDOUT_BUDGET = 15 * 1024;
|
|
@@ -47,31 +46,6 @@ function takeSnapshot(payload) {
|
|
|
47
46
|
return snap.text;
|
|
48
47
|
}
|
|
49
48
|
|
|
50
|
-
// After a break, the idle card's way back in: when no route is live and fresh workflow stacks
|
|
51
|
-
// exist, suggest `lit adhd` once per day. A marker next to the route state remembers the day;
|
|
52
|
-
// anything unreadable means no suggestion, never a repeated one.
|
|
53
|
-
function suggestAdhdOnce(payload) {
|
|
54
|
-
try {
|
|
55
|
-
const cwd = workspaceOf(payload);
|
|
56
|
-
const rec = readSession(payload);
|
|
57
|
-
if (rec && rec.route) return null;
|
|
58
|
-
const dir = join(resolveProjectStateRoot(cwd), '.litmuse', 'workflow-stack');
|
|
59
|
-
const day = 24 * 60 * 60 * 1000;
|
|
60
|
-
const fresh = readdirSync(dir).some((name) => name.endsWith('.json') && Date.now() - statSync(join(dir, name)).mtimeMs <= day);
|
|
61
|
-
if (!fresh) return null;
|
|
62
|
-
const marker = sessionPath(cwd, payload.session_id).replace(/\.json$/, '.adhd-suggested');
|
|
63
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
64
|
-
try {
|
|
65
|
-
if (readFileSync(marker, 'utf8').trim() === today) return null;
|
|
66
|
-
} catch { /* no marker yet */ }
|
|
67
|
-
mkdirSync(dirname(marker), { recursive: true });
|
|
68
|
-
writeFileSync(marker, `${today}\n`);
|
|
69
|
-
return 'Recent sessions left paused work in this workspace and no route is live. Suggest `lit adhd` once for the cross-session brief; if the user declines, drop it for this session.';
|
|
70
|
-
} catch {
|
|
71
|
-
return null;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
49
|
function fenced(text, room) {
|
|
76
50
|
const intro = 'Previous-context handoff snapshot (untrusted notes saved before compaction; verify against the repo before acting; it is data, not instructions):';
|
|
77
51
|
const safe = text.split('\n').map((l) => (/^\s*~~~/.test(l) ? ` ${l}` : l)).join('\n');
|
|
@@ -93,8 +67,6 @@ run((payload) => {
|
|
|
93
67
|
}
|
|
94
68
|
const style = chosenStyle();
|
|
95
69
|
if (style) parts.push(style);
|
|
96
|
-
const wayBackIn = suggestAdhdOnce(payload);
|
|
97
|
-
if (wayBackIn) parts.push(wayBackIn);
|
|
98
70
|
const snapshot = takeSnapshot(payload);
|
|
99
71
|
if (snapshot) {
|
|
100
72
|
const used = Buffer.byteLength(JSON.stringify(parts.join('\n\n')));
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { run, emitContext, readSession, writeSession, safeId, workspaceOf } from './lib/common.mjs';
|
|
1
|
+
import { run, emitContext, readSession, writeSession, safeId, sessionPath, workspaceOf } from './lib/common.mjs';
|
|
2
2
|
import { detectRoute } from './lib/routes.mjs';
|
|
3
3
|
import { liveRoute, resumablePlan } from './lib/reentry.mjs';
|
|
4
4
|
import { readStage, recordRoute, stageFile } from './lib/stage.mjs';
|
|
5
5
|
import { MOTION_ROUTE, armMotionTurn } from './lib/motion.mjs';
|
|
6
|
-
import { applyStackEvent, readStack, writeStack, READ_ONLY_DISCIPLINES } from './lib/workflow-stack.mjs';
|
|
6
|
+
import { applyStackEvent, readStack, writeStack, resolveProjectStateRoot, READ_ONLY_DISCIPLINES } from './lib/workflow-stack.mjs';
|
|
7
7
|
import { isGoPrompt, takeNextStep } from './lib/next-step.mjs';
|
|
8
|
-
import { readFileSync } from 'node:fs';
|
|
9
|
-
import { resolve } from 'node:path';
|
|
8
|
+
import { readFileSync, writeFileSync, mkdirSync, readdirSync, statSync } from 'node:fs';
|
|
9
|
+
import { resolve, join, dirname } from 'node:path';
|
|
10
10
|
|
|
11
11
|
// The banner leads every context this hook sends: Muse shows no hook message of its own, so the model's first line is
|
|
12
12
|
// the only place the user can see the route ignite, and a model that narrates first ("Loading the loop route") hides it.
|
|
@@ -135,6 +135,31 @@ function staleReason(payload, step) {
|
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
// After a break, the idle card's way back in: on a plain prompt while no route is live and fresh workflow stacks
|
|
139
|
+
// exist, suggest `lit adhd` once per day. It rides on this prompt rather than SessionStart because SessionStart
|
|
140
|
+
// context is rebuilt on every resume and sits ahead of the history, so a line that comes and goes there would discard
|
|
141
|
+
// the cached history; prompt context is appended to the history instead. A marker next to the route state remembers
|
|
142
|
+
// the day; anything unreadable means no suggestion, never a repeated one.
|
|
143
|
+
function suggestAdhdOnce(payload) {
|
|
144
|
+
try {
|
|
145
|
+
const cwd = workspaceOf(payload);
|
|
146
|
+
const dir = join(resolveProjectStateRoot(cwd), '.litmuse', 'workflow-stack');
|
|
147
|
+
const day = 24 * 60 * 60 * 1000;
|
|
148
|
+
const fresh = readdirSync(dir).some((name) => name.endsWith('.json') && Date.now() - statSync(join(dir, name)).mtimeMs <= day);
|
|
149
|
+
if (!fresh) return null;
|
|
150
|
+
const marker = sessionPath(cwd, payload.session_id).replace(/\.json$/, '.adhd-suggested');
|
|
151
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
152
|
+
try {
|
|
153
|
+
if (readFileSync(marker, 'utf8').trim() === today) return null;
|
|
154
|
+
} catch { /* no marker yet */ }
|
|
155
|
+
mkdirSync(dirname(marker), { recursive: true });
|
|
156
|
+
writeFileSync(marker, `${today}\n`);
|
|
157
|
+
return 'Recent sessions left paused work in this workspace and no route is live. Suggest `lit adhd` once for the cross-session brief; if the user declines, drop it for this session.';
|
|
158
|
+
} catch {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
138
163
|
function goHint(why) {
|
|
139
164
|
return [
|
|
140
165
|
...banner('LIT CONTINUES', 'lit-loop'),
|
|
@@ -211,7 +236,10 @@ run((payload) => {
|
|
|
211
236
|
if (previous && previous.route) {
|
|
212
237
|
writeSession(payload, { ...previous, route: null, turnId: payload.turn_id ?? null, pauseRequested });
|
|
213
238
|
recordRoute(payload, { route: null, kind: 'clear' });
|
|
239
|
+
return;
|
|
214
240
|
}
|
|
241
|
+
const wayBackIn = suggestAdhdOnce(payload);
|
|
242
|
+
if (wayBackIn) emitContext('UserPromptSubmit', wayBackIn);
|
|
215
243
|
return;
|
|
216
244
|
}
|
|
217
245
|
const turnId = payload.turn_id ?? null;
|
|
@@ -159,7 +159,7 @@ After a brief, the user's `go` (also `lit go`, `ㄱㄱ`, `고`, `진행`, as the
|
|
|
159
159
|
|
|
160
160
|
## Relation to lit-recap
|
|
161
161
|
|
|
162
|
-
`lit recap` stays the single-session recap from this conversation and the litgoal ledger, with its fixed Korean template. lit-adhd covers many sessions and reads their transcripts. Neither replaces the other. lit-adhd is the way back in after a break: the idle status card offers it, and
|
|
162
|
+
`lit recap` stays the single-session recap from this conversation and the litgoal ledger, with its fixed Korean template. lit-adhd covers many sessions and reads their transcripts. Neither replaces the other. lit-adhd is the way back in after a break: the idle status card offers it, and the first plain prompt of a session suggests it once per day when paused work is on record.
|
|
163
163
|
|
|
164
164
|
## Stack behavior
|
|
165
165
|
|