@litfamily/litmuse 0.1.2 → 0.1.3
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/README.md +3 -1
- package/README_ko-KR.md +3 -1
- package/TRADEMARK.md +12 -0
- package/lib/stage-card.mjs +97 -29
- package/package.json +3 -2
- package/plugin/.muse-plugin/plugin.json +11 -11
- package/plugin/commands/lit-status.md +2 -1
- package/plugin/hooks/lib/next-step.mjs +48 -4
- package/plugin/hooks/lib/stage.mjs +53 -6
- package/plugin/hooks/lib/workflow-stack.mjs +21 -3
- package/plugin/hooks/post-tool-use-failure.mjs +2 -0
- package/plugin/hooks/post-tool-use.mjs +2 -0
- package/plugin/hooks/session-end.mjs +2 -1
- package/plugin/hooks/session-start.mjs +4 -1
- package/plugin/hooks/stop.mjs +23 -3
- package/plugin/hooks/user-prompt-submit.mjs +1 -1
- package/plugin/lib/rewind/collect.mjs +191 -62
- package/plugin/lib/rewind/transcripts.mjs +2 -0
- package/plugin/skills/lit-adhd/SKILL.md +19 -12
- package/plugin/skills/lit-adhd/references/status-taxonomy.md +6 -4
- package/plugin/skills/lit-plan/SKILL.md +12 -1
- package/plugin/skills/lit-plan/scripts/check-plan.mjs +71 -4
- package/plugin/skills/lit-team/scripts/state-root.mjs +5 -18
- package/plugin/skills/start-work/SKILL.md +4 -1
|
@@ -4,6 +4,8 @@ import { homedir } from 'node:os';
|
|
|
4
4
|
import { run, emitContext, PLUGIN_ROOT, pluginVersion, cachedLatest, newerThan, readSession, 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
|
+
import { recordLoadedVersion } from './lib/stage.mjs';
|
|
7
9
|
|
|
8
10
|
const STDOUT_BUDGET = 15 * 1024;
|
|
9
11
|
const STYLE_FILES = { eli5: 'style-eli5.md', 'asd-ste100': 'style-asd-ste100.md' };
|
|
@@ -53,7 +55,7 @@ function suggestAdhdOnce(payload) {
|
|
|
53
55
|
const cwd = workspaceOf(payload);
|
|
54
56
|
const rec = readSession(payload);
|
|
55
57
|
if (rec && rec.route) return null;
|
|
56
|
-
const dir = join(cwd, '.litmuse', 'workflow-stack');
|
|
58
|
+
const dir = join(resolveProjectStateRoot(cwd), '.litmuse', 'workflow-stack');
|
|
57
59
|
const day = 24 * 60 * 60 * 1000;
|
|
58
60
|
const fresh = readdirSync(dir).some((name) => name.endsWith('.json') && Date.now() - statSync(join(dir, name)).mtimeMs <= day);
|
|
59
61
|
if (!fresh) return null;
|
|
@@ -81,6 +83,7 @@ function fenced(text, room) {
|
|
|
81
83
|
|
|
82
84
|
run((payload) => {
|
|
83
85
|
const version = pluginVersion();
|
|
86
|
+
recordLoadedVersion(payload, version);
|
|
84
87
|
const base = readFileSync(join(PLUGIN_ROOT, 'context', 'session.md'), 'utf8').replaceAll('{{version}}', version);
|
|
85
88
|
const routes = SKILL_ROUTES.map((r) => `- ${r.id}: ${r.when}`).join('\n');
|
|
86
89
|
const parts = [base.trim(), `LitMuse routes (skill id: use when)\n${routes}`];
|
package/plugin/hooks/stop.mjs
CHANGED
|
@@ -3,16 +3,31 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { run, block, activeRoute, writeSession, workspaceOf } from './lib/common.mjs';
|
|
4
4
|
import { continuationHold } from './lib/continuation.mjs';
|
|
5
5
|
import { MOTION_ROUTE, evaluateMotionStop } from './lib/motion.mjs';
|
|
6
|
+
import { putNextStep } from './lib/next-step.mjs';
|
|
7
|
+
import { markLiveness } from './lib/stage.mjs';
|
|
6
8
|
|
|
9
|
+
// The newest plan written since the route started, as `plans/<name>.md`, or null.
|
|
7
10
|
function planWrittenSince(cwd, since) {
|
|
8
11
|
const dir = join(cwd, 'plans');
|
|
9
|
-
if (!existsSync(dir)) return
|
|
10
|
-
|
|
12
|
+
if (!existsSync(dir)) return null;
|
|
13
|
+
let best = null;
|
|
14
|
+
for (const f of readdirSync(dir)) {
|
|
15
|
+
if (!f.endsWith('.md')) continue;
|
|
16
|
+
let mtime;
|
|
17
|
+
try {
|
|
18
|
+
mtime = statSync(join(dir, f)).mtimeMs;
|
|
19
|
+
} catch {
|
|
20
|
+
continue; // removed since the listing
|
|
21
|
+
}
|
|
22
|
+
if (mtime >= since && (!best || mtime > best.mtime)) best = { name: `plans/${f}`, mtime };
|
|
23
|
+
}
|
|
24
|
+
return best?.name ?? null;
|
|
11
25
|
}
|
|
12
26
|
|
|
13
27
|
// The lit-plan gate holds a turn at most once, so a model that cannot write the plan still ends. The motion gate holds
|
|
14
28
|
// at most twice, then asks the reply to say the render is not done.
|
|
15
29
|
run((payload) => {
|
|
30
|
+
markLiveness(payload, 'idle');
|
|
16
31
|
const state = activeRoute(payload);
|
|
17
32
|
if (!state) return;
|
|
18
33
|
if (state.route === MOTION_ROUTE) {
|
|
@@ -24,8 +39,13 @@ run((payload) => {
|
|
|
24
39
|
return;
|
|
25
40
|
}
|
|
26
41
|
const turn = payload.turn_id ?? state.turnId ?? 'turn';
|
|
42
|
+
const saved = planWrittenSince(workspaceOf(payload), state.activatedAt ?? 0);
|
|
43
|
+
// A saved plan arms a one-word `go` for this session; the skill's closing line says so.
|
|
44
|
+
if (saved) {
|
|
45
|
+
putNextStep(payload, saved);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
27
48
|
if (state.stopHeldFor === turn) return;
|
|
28
|
-
if (planWrittenSince(workspaceOf(payload), state.activatedAt ?? 0)) return;
|
|
29
49
|
writeSession(payload, { ...state, stopHeldFor: turn });
|
|
30
50
|
block('LitMuse lit-plan gate: this planning turn has no plan file yet. Write the plan to plans/<slug>.md (task rows as "- [ ] N. <title>"), then end the turn and point the user to /start-work.');
|
|
31
51
|
});
|
|
@@ -164,7 +164,7 @@ run((payload) => {
|
|
|
164
164
|
stackStart(payload, 'start-work', { plan }, false);
|
|
165
165
|
recordRoute(payload, { route: 'start-work', keyword: 'go', kind: 'ignite', plan, activatedAt: Date.now() });
|
|
166
166
|
emitContext('UserPromptSubmit', [
|
|
167
|
-
|
|
167
|
+
activation({ route: 'start-work', keyword: 'go', bare: false, resume: false, restart: false, trailing: false }, payload),
|
|
168
168
|
`Plan: ${plan}. The typed go is the approval: the work starts without a separate approval turn; work through its open rows in order.`,
|
|
169
169
|
].join('\n'));
|
|
170
170
|
return;
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { execFileSync } from 'node:child_process';
|
|
7
7
|
import { closeSync, constants, fstatSync, lstatSync, openSync, readSync, readdirSync, existsSync } from 'node:fs';
|
|
8
8
|
import { homedir } from 'node:os';
|
|
9
|
-
import { basename,
|
|
9
|
+
import { basename, join, resolve } from 'node:path';
|
|
10
10
|
|
|
11
11
|
import { parsePlanTaskRows } from '../plan-task-rows.mjs';
|
|
12
12
|
import { PULSE_UNKNOWN, aggregateProgress, clipToWidth, displayWidth, progressOf } from './gauge.mjs';
|
|
@@ -14,6 +14,8 @@ import { chooseNextStep, writeNextStep } from './next-step.mjs';
|
|
|
14
14
|
import { makeRedactor } from './redact.mjs';
|
|
15
15
|
import { findTranscripts, makeBudget, readTranscriptLines, sessionRecord } from './transcripts.mjs';
|
|
16
16
|
import { rainbowLine } from '../../../lib/stage-card.mjs';
|
|
17
|
+
import { projectKey, resolveProjectStateRoot } from '../../hooks/lib/workflow-stack.mjs';
|
|
18
|
+
import { readLiveness } from '../../hooks/lib/stage.mjs';
|
|
17
19
|
|
|
18
20
|
const SMALL_FILE_BYTES = 1024 * 1024;
|
|
19
21
|
const HERDR_TIMEOUT_MS = 2000;
|
|
@@ -67,18 +69,6 @@ const entries = (directory) => {
|
|
|
67
69
|
|
|
68
70
|
const defaultExec = (file, args, options) => execFileSync(file, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], ...options });
|
|
69
71
|
|
|
70
|
-
const outermostRoot = (startDir) => {
|
|
71
|
-
const start = resolve(startDir);
|
|
72
|
-
let root = null;
|
|
73
|
-
let current = start;
|
|
74
|
-
for (;;) {
|
|
75
|
-
if (existsSync(join(current, '.git'))) root = current;
|
|
76
|
-
const parent = dirname(current);
|
|
77
|
-
if (parent === current) return root ?? start;
|
|
78
|
-
current = parent;
|
|
79
|
-
}
|
|
80
|
-
};
|
|
81
|
-
|
|
82
72
|
const worktreesOf = (cwd, exec) => {
|
|
83
73
|
try {
|
|
84
74
|
const out = exec('git', ['worktree', 'list', '--porcelain'], { cwd, timeout: GIT_TIMEOUT_MS });
|
|
@@ -99,24 +89,37 @@ const mtimeOf = (path) => {
|
|
|
99
89
|
}
|
|
100
90
|
};
|
|
101
91
|
|
|
102
|
-
|
|
92
|
+
// Every plan with real rows, finished ones included (a finished plan is done work, not a missing
|
|
93
|
+
// one). A plan the read budget can no longer cover whole is counted in `skipped`, never half-read.
|
|
94
|
+
const plansIn = (root, budget, redact, local, skipped) => {
|
|
103
95
|
const list = entries(join(root, 'plans'));
|
|
104
96
|
if (!list) return null;
|
|
105
97
|
return list.filter((entry) => entry.isFile() && entry.name.endsWith('.md')).sort((a, b) => a.name.localeCompare(b.name)).flatMap((entry) => {
|
|
106
|
-
const
|
|
98
|
+
const path = join(root, 'plans', entry.name);
|
|
99
|
+
let size = 0;
|
|
100
|
+
try {
|
|
101
|
+
size = lstatSync(path).size;
|
|
102
|
+
} catch {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
if (budget.limit - budget.used < Math.min(size, SMALL_FILE_BYTES)) {
|
|
106
|
+
budget.exhausted = true;
|
|
107
|
+
skipped.count += 1;
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
const text = readSmallFile(path, budget);
|
|
107
111
|
if (text === null) return [];
|
|
108
112
|
const rows = parsePlanTaskRows(text);
|
|
109
113
|
if (!rows.length || isTemplate(rows)) return [];
|
|
110
114
|
const open = rows.filter((row) => !row.checked);
|
|
111
|
-
if (!open.length) return [];
|
|
112
115
|
const done = rows.length - open.length;
|
|
113
116
|
return [{
|
|
114
117
|
worktree: redact(root),
|
|
115
118
|
plan: `plans/${entry.name}`,
|
|
116
119
|
local,
|
|
117
|
-
changedMs: mtimeOf(
|
|
120
|
+
changedMs: mtimeOf(path),
|
|
118
121
|
rows: rows.map((row) => ({ checked: row.checked, title: clipToWidth(redact(row.text), NAME_MAX) })),
|
|
119
|
-
current: clipToWidth(redact(open[0].text), NAME_MAX),
|
|
122
|
+
current: open.length ? clipToWidth(redact(open[0].text), NAME_MAX) : null,
|
|
120
123
|
progress: progressOf(done, rows.length, open.map((row) => clipToWidth(redact(row.text), NAME_MAX))),
|
|
121
124
|
}];
|
|
122
125
|
});
|
|
@@ -141,7 +144,21 @@ const goalsOf = (root, budget, redact) => {
|
|
|
141
144
|
});
|
|
142
145
|
};
|
|
143
146
|
|
|
144
|
-
|
|
147
|
+
// The first item under a `## Next` or `## Next Steps` heading, list marker removed, or null.
|
|
148
|
+
const nextItemOf = (text) => {
|
|
149
|
+
const lines = text.split('\n');
|
|
150
|
+
const at = lines.findIndex((line) => /^#{1,3} Next(?: Steps)?\s*$/iu.test(line.trim()));
|
|
151
|
+
if (at < 0) return null;
|
|
152
|
+
for (const line of lines.slice(at + 1)) {
|
|
153
|
+
if (/^#{1,6} /u.test(line)) return null;
|
|
154
|
+
const item = line.replace(/^\s*(?:[-*+]|\d+[.)])\s+/u, '').trim();
|
|
155
|
+
if (item) return item;
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// Handoff headings from the first 40 lines; for this worktree's own files only, the Next item too.
|
|
161
|
+
const handoffsOf = (root, budget, redact, local) => {
|
|
145
162
|
const files = (entries(root) ?? [])
|
|
146
163
|
.filter((entry) => entry.isFile() && /^HANDOFF.*\.md$/u.test(entry.name))
|
|
147
164
|
.map((entry) => join(root, entry.name));
|
|
@@ -150,7 +167,8 @@ const handoffsOf = (root, budget, redact) => {
|
|
|
150
167
|
return files.map((path) => {
|
|
151
168
|
const text = readSmallFile(path, budget) ?? '';
|
|
152
169
|
const headings = text.split('\n').slice(0, 40).filter((line) => /^#{1,3} /u.test(line)).map((line) => clipToWidth(redact(line), 100));
|
|
153
|
-
|
|
170
|
+
const next = local ? nextItemOf(text) : null;
|
|
171
|
+
return { worktree: redact(root), file: basename(path), headings, local, next: next ? clipToWidth(redact(next), 100) : null };
|
|
154
172
|
});
|
|
155
173
|
};
|
|
156
174
|
|
|
@@ -189,29 +207,49 @@ const stacksOf = (root, budget, redact) => {
|
|
|
189
207
|
});
|
|
190
208
|
};
|
|
191
209
|
|
|
192
|
-
// This host has no start-work lifecycle state: the
|
|
193
|
-
// start-work run is the live route and which plan it carries
|
|
194
|
-
//
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
210
|
+
// This host has no start-work lifecycle state: the stage records say it. This session's record
|
|
211
|
+
// says whether a start-work run is the live route and which plan it carries; any session's record
|
|
212
|
+
// whose plan is fully ticked is a completed run (`complete`, with the plan's change time). Only a
|
|
213
|
+
// start-work route stores a plan on the record, so a plan there means a run. Boundaries are not
|
|
214
|
+
// tracked here, so a paused boundary never comes from this source.
|
|
215
|
+
const PLAN_NAME = /^plans\/[A-Za-z0-9._-]+\.md$/u;
|
|
216
|
+
const stageDirOf = (env) => join(env.XDG_STATE_HOME || join(env.HOME || homedir(), '.local', 'state'), 'litmuse', 'stage');
|
|
217
|
+
const STAGE_RECORDS_MAX = 200;
|
|
218
|
+
|
|
219
|
+
const runOf = (root, record, sessionId, budget, redact) => {
|
|
220
|
+
const plan = typeof record.plan === 'string' && PLAN_NAME.test(record.plan) ? record.plan : null;
|
|
201
221
|
const planText = plan ? readSmallFile(join(root, plan), budget) : null;
|
|
202
222
|
const rows = planText === null ? [] : parsePlanTaskRows(planText);
|
|
203
223
|
const open = rows.filter((row) => !row.checked);
|
|
204
|
-
|
|
224
|
+
const id = typeof record.session === 'string' ? record.session : sessionId ?? null;
|
|
225
|
+
return {
|
|
205
226
|
worktree: redact(root),
|
|
206
|
-
workId: `stage-${
|
|
227
|
+
workId: `stage-${id ?? 'unknown'}`,
|
|
207
228
|
plan: plan ?? 'unknown',
|
|
208
|
-
status: 'active',
|
|
209
|
-
sessionId:
|
|
210
|
-
id8: typeof
|
|
229
|
+
status: rows.length && !open.length ? 'complete' : 'active',
|
|
230
|
+
sessionId: id,
|
|
231
|
+
id8: typeof id === 'string' ? id.slice(0, 8) : 'unknown',
|
|
232
|
+
completedMs: rows.length && !open.length ? mtimeOf(join(root, plan)) : null,
|
|
211
233
|
progress: rows.length
|
|
212
234
|
? progressOf(rows.length - open.length, rows.length, open.map((row) => clipToWidth(redact(row.text), NAME_MAX)))
|
|
213
235
|
: progressOf(null, null),
|
|
214
|
-
}
|
|
236
|
+
};
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const startWorkOf = (root, budget, redact, env, sessionId) => {
|
|
240
|
+
const dir = stageDirOf(env);
|
|
241
|
+
const own = sessionId ? readJson(join(dir, `${String(sessionId).replace(/[^A-Za-z0-9_-]/gu, '_').slice(0, 128)}.json`), budget) : null;
|
|
242
|
+
const runs = own && own.route === 'start-work' ? [runOf(root, own, sessionId, budget, redact)] : [];
|
|
243
|
+
const list = entries(dir);
|
|
244
|
+
const here = projectKey(root);
|
|
245
|
+
for (const entry of (list ?? []).filter((item) => item.isFile() && item.name.endsWith('.json') && item.name !== 'latest.json').slice(0, STAGE_RECORDS_MAX)) {
|
|
246
|
+
const record = readJson(join(dir, entry.name), budget);
|
|
247
|
+
if (!record || typeof record.session !== 'string' || typeof record.cwd !== 'string' || !PLAN_NAME.test(String(record.plan))) continue;
|
|
248
|
+
if (projectKey(record.cwd) !== here) continue;
|
|
249
|
+
const run = runOf(root, record, null, budget, redact);
|
|
250
|
+
if (run.status === 'complete' && !runs.some((item) => item.workId === run.workId)) runs.push(run);
|
|
251
|
+
}
|
|
252
|
+
return runs.length || own ? runs : null;
|
|
215
253
|
};
|
|
216
254
|
|
|
217
255
|
const herdrOf = (env, exec, redact) => {
|
|
@@ -231,7 +269,9 @@ const sessionStoreDir = (env) => {
|
|
|
231
269
|
};
|
|
232
270
|
|
|
233
271
|
const known = (list) => (list === null ? 'unknown' : 'ok');
|
|
234
|
-
const statusRank = { 'waiting-on-user': 0, active: 1,
|
|
272
|
+
const statusRank = { 'waiting-on-user': 0, active: 1, idle: 2, done: 3, ended: 4, unknown: 5 };
|
|
273
|
+
// A host may log a line or two after the Stop hook or the last plan tick; this much lag still counts.
|
|
274
|
+
const SETTLE_SLACK_MS = 30_000;
|
|
235
275
|
|
|
236
276
|
/**
|
|
237
277
|
* Collects the brief. Options: `cwd`, `here` (cwd only), `since` (`7d`), `max` (12), `sessionId`
|
|
@@ -254,10 +294,30 @@ export const collectRewind = ({
|
|
|
254
294
|
const sinceMs = parseSince(since) ?? parseSince(DEFAULT_SINCE);
|
|
255
295
|
const limit = Number.isInteger(max) && max > 0 ? Math.min(max, 50) : DEFAULT_MAX;
|
|
256
296
|
const start = resolve(cwd);
|
|
257
|
-
const stateRoot =
|
|
297
|
+
const stateRoot = resolveProjectStateRoot(start);
|
|
258
298
|
const listed = here ? null : worktreesOf(start, exec);
|
|
259
299
|
const roots = [...new Set([start, ...(here ? [] : listed ?? [stateRoot])])];
|
|
260
300
|
|
|
301
|
+
// The small records come first, then transcripts: a busy session store must never crowd the
|
|
302
|
+
// plans out of the shared read budget.
|
|
303
|
+
const isLocal = (root) => resolve(root) === stateRoot || resolve(root) === start;
|
|
304
|
+
const collectEach = (read) => {
|
|
305
|
+
const results = roots.map(read);
|
|
306
|
+
return results.every((value) => value === null) ? null : results.flatMap((value) => value ?? []);
|
|
307
|
+
};
|
|
308
|
+
const skippedPlans = { count: 0 };
|
|
309
|
+
const plans = collectEach((root) => plansIn(root, budget, redact, isLocal(root), skippedPlans));
|
|
310
|
+
const litgoal = collectEach((root) => {
|
|
311
|
+
const goals = goalsOf(root, budget, redact);
|
|
312
|
+
return goals ? goals.map((goal) => ({ ...goal, local: isLocal(root) })) : null;
|
|
313
|
+
});
|
|
314
|
+
const handoffs = roots.flatMap((root) => handoffsOf(root, budget, redact, isLocal(root)));
|
|
315
|
+
const workflows = collectEach((root) => stacksOf(root, budget, redact));
|
|
316
|
+
const startWorkRuns = collectEach((root) => startWorkOf(root, budget, redact, env, sessionId));
|
|
317
|
+
const startWork = startWorkRuns && startWorkRuns.filter((run, index) => startWorkRuns.findIndex((item) => item.workId === run.workId) === index);
|
|
318
|
+
const commits = collectEach((root) => commitsOf(root, sinceMs, now, exec, redact));
|
|
319
|
+
const herdr = herdrOf(env, exec, redact);
|
|
320
|
+
|
|
261
321
|
const transcripts = findTranscripts(sessionStoreDir(env), roots);
|
|
262
322
|
const sessions = [];
|
|
263
323
|
for (const file of transcripts.files) {
|
|
@@ -269,22 +329,26 @@ export const collectRewind = ({
|
|
|
269
329
|
sessions.push(session);
|
|
270
330
|
}
|
|
271
331
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
return results.every((value) => value === null) ? null : results.flatMap((value) => value ?? []);
|
|
275
|
-
};
|
|
276
|
-
const plans = collectEach((root) => plansIn(root, budget, redact, resolve(root) === stateRoot || resolve(root) === start));
|
|
277
|
-
const litgoal = collectEach((root) => {
|
|
278
|
-
const goals = goalsOf(root, budget, redact);
|
|
279
|
-
return goals ? goals.map((goal) => ({ ...goal, local: resolve(root) === stateRoot || resolve(root) === start })) : null;
|
|
280
|
-
});
|
|
281
|
-
const handoffs = roots.flatMap((root) => handoffsOf(root, budget, redact));
|
|
282
|
-
const commits = collectEach((root) => commitsOf(root, sinceMs, now, exec, redact));
|
|
283
|
-
const workflows = collectEach((root) => stacksOf(root, budget, redact));
|
|
284
|
-
const startWork = collectEach((root) => startWorkOf(root, budget, redact, env, sessionId));
|
|
285
|
-
const herdr = herdrOf(env, exec, redact);
|
|
332
|
+
// The hooks' liveness records: Stop marks a session idle, SessionEnd marks it ended.
|
|
333
|
+
const liveness = new Map(readLiveness(env).map((item) => [item.session, item]));
|
|
286
334
|
|
|
287
335
|
// A session waits on the user when it asked something, or holds a paused frame or a boundary.
|
|
336
|
+
// Another session that is otherwise active reads `done` when its start-work run completed after its
|
|
337
|
+
// last log line, and `idle` when its last hook event is a Stop no older than that line.
|
|
338
|
+
for (const session of sessions) {
|
|
339
|
+
const live = liveness.get(session.sessionId);
|
|
340
|
+
const finished = (startWork ?? []).find((item) => item.status === 'complete' && item.sessionId === session.sessionId);
|
|
341
|
+
const self = Boolean(sessionId) && session.sessionId === sessionId;
|
|
342
|
+
if (!self && session.status === 'active' && session.lastMs !== null) {
|
|
343
|
+
if (finished && finished.completedMs !== null && finished.completedMs >= session.lastMs - SETTLE_SLACK_MS) {
|
|
344
|
+
session.status = 'done';
|
|
345
|
+
session.finishedPlan = finished.plan;
|
|
346
|
+
} else if (live?.state === 'idle' && Date.parse(live.at) >= session.lastMs - SETTLE_SLACK_MS) {
|
|
347
|
+
session.status = 'idle';
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
if (!self && live?.state === 'ended' && session.status !== 'waiting-on-user') session.status = 'ended';
|
|
351
|
+
}
|
|
288
352
|
for (const session of sessions) {
|
|
289
353
|
const frames = (workflows ?? []).filter((frame) => frame.sessionId === session.sessionId);
|
|
290
354
|
const top = frames.at(-1);
|
|
@@ -308,11 +372,12 @@ export const collectRewind = ({
|
|
|
308
372
|
}
|
|
309
373
|
sessions.sort((a, b) => statusRank[a.status] - statusRank[b.status] || (b.lastMs ?? 0) - (a.lastMs ?? 0));
|
|
310
374
|
|
|
311
|
-
//
|
|
312
|
-
// this
|
|
313
|
-
// left out
|
|
314
|
-
// counts this worktree only, since a sum over unrelated
|
|
315
|
-
|
|
375
|
+
// Plans changed inside the window are listed, this worktree's included, finished ones too (they
|
|
376
|
+
// count as done): this worktree's first, then the most recently changed, up to a short cap; a plan
|
|
377
|
+
// last changed before the window is left out, here or elsewhere. Unfilled scaffolds never reach the
|
|
378
|
+
// list (plansIn drops them). The roadmap gauge counts this worktree only, since a sum over unrelated
|
|
379
|
+
// branches measures nothing.
|
|
380
|
+
const recent = (plan) => plan.changedMs !== null && now - plan.changedMs <= sinceMs;
|
|
316
381
|
const orderedPlans = (plans ?? []).filter(recent).sort((a, b) => Number(b.local) - Number(a.local) || (b.changedMs ?? 0) - (a.changedMs ?? 0));
|
|
317
382
|
const shownPlans = orderedPlans.slice(0, PLAN_LIMIT);
|
|
318
383
|
const localGoals = (litgoal ?? []).filter((goal) => goal.local);
|
|
@@ -341,6 +406,7 @@ export const collectRewind = ({
|
|
|
341
406
|
roadmap: {
|
|
342
407
|
plans: shownPlans,
|
|
343
408
|
plansOmitted: orderedPlans.length - shownPlans.length,
|
|
409
|
+
plansSkipped: skippedPlans.count,
|
|
344
410
|
// Another worktree's finished objective is history, not roadmap.
|
|
345
411
|
litgoal: (litgoal ?? []).filter((goal) => goal.local || goal.status !== 'complete'),
|
|
346
412
|
progress: roadmapItems.length ? aggregateProgress(roadmapItems) : progressOf(null, null),
|
|
@@ -353,6 +419,7 @@ export const collectRewind = ({
|
|
|
353
419
|
};
|
|
354
420
|
const next = chooseNextStep(data, sessionId);
|
|
355
421
|
data.next = next;
|
|
422
|
+
data.opener = openerOf(data, sessionId, redact(start));
|
|
356
423
|
data.nextRecorded = false;
|
|
357
424
|
if (record && sessionId) {
|
|
358
425
|
try {
|
|
@@ -382,10 +449,12 @@ const remainingLine = (names, width) => {
|
|
|
382
449
|
// ANSI pieces never reach the width math: every row is clipped raw, then painted.
|
|
383
450
|
const ESC = '\u001b[';
|
|
384
451
|
const paint = (code, text) => `${ESC}${code}m${text}${ESC}0m`;
|
|
385
|
-
const STATUS_BADGE = { 'waiting-on-user': '● waiting on you', active: '● ACTIVE', ended: '○ Ended', unknown: '○ unknown' };
|
|
452
|
+
const STATUS_BADGE = { 'waiting-on-user': '● waiting on you', active: '● ACTIVE', idle: '◌ idle', done: '✓ done', ended: '○ Ended', unknown: '○ unknown' };
|
|
386
453
|
const BADGE_PAINT = {
|
|
387
454
|
'waiting-on-user': (text) => paint('33;1', text),
|
|
388
455
|
active: (text) => paint('32;1', text),
|
|
456
|
+
idle: (text) => paint('2', text),
|
|
457
|
+
done: (text) => paint('32', text),
|
|
389
458
|
ended: (text) => paint('2', text),
|
|
390
459
|
unknown: (text) => paint('2', text),
|
|
391
460
|
};
|
|
@@ -412,6 +481,7 @@ const sessionNext = (session, data) => {
|
|
|
412
481
|
const frame = data.workflows.find((item) => item.id8 === session.id8 && item.status === 'paused' && data.next.say);
|
|
413
482
|
if (frame) return `say "${data.next.say}" to resume the paused ${frame.skill}`;
|
|
414
483
|
if (session.status === 'waiting-on-user') return session.waitingReason === 'start-work boundary' ? 'answer the boundary: `lit approve` or `lit deny`' : 'answer its question';
|
|
484
|
+
if (session.status === 'done' && session.finishedPlan) return `start-work finished ${session.finishedPlan}`;
|
|
415
485
|
return null;
|
|
416
486
|
};
|
|
417
487
|
|
|
@@ -489,8 +559,10 @@ const sessionCard = (session, data, emitLine, now) => {
|
|
|
489
559
|
const commits = (session.done?.commits ?? []).map((commit) => `${commit.hash} ${commit.subject}`);
|
|
490
560
|
if (tools || commits.length) emitLine([['✅ Done: ', (text) => paint('32', text)], [[tools, ...commits].filter(Boolean).join(' · '), null]]);
|
|
491
561
|
// A card with no recorded edit yet names what it is doing now: its last tool call, name only.
|
|
492
|
-
// (This host's log keeps no file list, so commits are the only edits it can show.)
|
|
493
|
-
|
|
562
|
+
// (This host's log keeps no file list, so commits are the only edits it can show.) An idle or done
|
|
563
|
+
// session is doing nothing now, so its last call is history and never shown as current.
|
|
564
|
+
const settled = session.status === 'idle' || session.status === 'done';
|
|
565
|
+
if (!settled && !commits.length && !(session.done?.files ?? []).length && session.lastAction?.tool) emitLine([['▶ Now: ', (text) => paint('33', text)], [session.lastAction.tool, null]]);
|
|
494
566
|
const next = sessionNext(session, data);
|
|
495
567
|
if (next) emitLine([['➤ Next: ', (text) => paint('36;1', text)], [next, null]]);
|
|
496
568
|
};
|
|
@@ -566,11 +638,60 @@ const suggestion = (data) => {
|
|
|
566
638
|
if (plan) return { text: `Next in ${plan.local ? plan.plan : `${basename(String(plan.worktree))}/${plan.plan}`}: ${clipToWidth(String(plan.current), 60)}`, say: 'go' };
|
|
567
639
|
const active = sessions.find((session) => session.status === 'active' && !session.self);
|
|
568
640
|
if (active) return { text: `Check on ${active.id8} in ${place(active)} (${goal(active)})`, say: null };
|
|
569
|
-
|
|
570
|
-
|
|
641
|
+
// Then the newest session that is not running: review what a done one finished, or pick up an
|
|
642
|
+
// idle or ended one. A finished step is never offered again as the thing to do.
|
|
643
|
+
const settled = sessions.find((session) => ['done', 'idle', 'ended'].includes(session.status) && !session.self);
|
|
644
|
+
if (settled?.status === 'done') return { text: `Review what ${settled.id8} finished in ${place(settled)}: ${settled.finishedPlan ?? goal(settled)}`, say: null };
|
|
645
|
+
if (settled) return { text: `Pick up ${settled.id8} in ${place(settled)}: ${goal(settled)}`, say: null };
|
|
571
646
|
return { text: 'No session or plan in this window: say what to start', say: null };
|
|
572
647
|
};
|
|
573
648
|
|
|
649
|
+
// With no step for `go`, this worktree's HANDOFF Next item comes before the generic suggestion; it
|
|
650
|
+
// is shown only, never something `go` runs.
|
|
651
|
+
const handoffNextOf = (data) => (data.handoffs ?? []).find((handoff) => handoff.local && handoff.next)?.next ?? null;
|
|
652
|
+
const stepOf = (data) => {
|
|
653
|
+
if (data.next?.kind && data.next.kind !== 'none') return null;
|
|
654
|
+
const handoff = handoffNextOf(data);
|
|
655
|
+
return handoff ? { text: handoff, say: null } : suggestion(data);
|
|
656
|
+
};
|
|
657
|
+
const goStepText = (next) => {
|
|
658
|
+
const target = next.target?.plan ?? next.target?.skill ?? '';
|
|
659
|
+
return `${next.kind.replace(/-/g, ' ')}${target ? `: ${target}` : ''}${next.say ? ` (say "${next.say}")` : ''}`;
|
|
660
|
+
};
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* The three lines that open the brief, from records only. Done: this worktree's newest commits in the
|
|
664
|
+
* window (two at most), then its completed start-work runs and fully ticked plans (three items in
|
|
665
|
+
* all), else its finished goal. Doing: this session's top frame, else its running start-work, else
|
|
666
|
+
* the newest active session's goal. Next: the recommended step and the word to say.
|
|
667
|
+
*/
|
|
668
|
+
const openerOf = (data, sessionId, here) => {
|
|
669
|
+
const subjects = data.commits.filter((commit) => commit.worktree === here).slice(0, 2).map((commit) => commit.subject);
|
|
670
|
+
const runs = data.startWork.filter((work) => work.status === 'complete' && work.worktree === here);
|
|
671
|
+
const runPlans = new Set(runs.map((work) => work.plan));
|
|
672
|
+
const finished = [
|
|
673
|
+
...runs.map((work) => `${work.plan} (start-work complete)`),
|
|
674
|
+
...data.roadmap.plans.filter((plan) => plan.local && plan.current === null && !runPlans.has(plan.plan)).map((plan) => `${plan.plan} ${plan.progress.gauge}`),
|
|
675
|
+
];
|
|
676
|
+
const items = [...subjects, ...new Set(finished)].slice(0, 3);
|
|
677
|
+
const goal = data.roadmap.litgoal.find((item) => item.local && item.status === 'complete');
|
|
678
|
+
const frame = data.workflows.filter((item) => Boolean(sessionId) && item.sessionId === sessionId).at(-1);
|
|
679
|
+
const work = data.startWork.find((item) => item.status === 'active' && Boolean(sessionId) && item.sessionId === sessionId);
|
|
680
|
+
const active = data.sessions.find((session) => session.status === 'active');
|
|
681
|
+
const known = (part) => part && part !== 'unknown';
|
|
682
|
+
const doing = frame
|
|
683
|
+
? [frame.skill, frame.stage, frame.progress?.gauge].filter(known).join(' · ')
|
|
684
|
+
: work
|
|
685
|
+
? ['start-work', work.plan, work.progress?.gauge].filter(known).join(' · ')
|
|
686
|
+
: active ? clipToWidth(String(known(active.title) ? active.title : active.asked?.[0] ?? 'unknown'), 80) : 'nothing running';
|
|
687
|
+
const step = stepOf(data);
|
|
688
|
+
return {
|
|
689
|
+
done: items.length ? items.join('; ') : goal ? goal.objective : 'nothing finished in this window',
|
|
690
|
+
doing,
|
|
691
|
+
next: step ? `${step.text}${step.say ? ` (say "${step.say}")` : ''}` : goStepText(data.next),
|
|
692
|
+
};
|
|
693
|
+
};
|
|
694
|
+
|
|
574
695
|
// Wraps a step to the box at word breaks, three lines at most, the last one clipped.
|
|
575
696
|
const wrap = (text, width, rows = 3) => {
|
|
576
697
|
const out = [''];
|
|
@@ -590,9 +711,15 @@ export const renderRewind = (data, width = 100, { color = false } = {}) => {
|
|
|
590
711
|
const emitLine = (segments) => emit(out, color, width, segments);
|
|
591
712
|
const put = (raw, fn = null) => emitLine([[raw, fn]]);
|
|
592
713
|
const frame = Math.max(width, 20);
|
|
593
|
-
const step =
|
|
714
|
+
const step = stepOf(data);
|
|
594
715
|
const nextKind = data.next.kind.replace(/-/g, ' ');
|
|
595
716
|
const nextSay = data.next.say ? ` (say "${data.next.say}")` : '';
|
|
717
|
+
// Three plain lines open the brief: done so far, doing now, next.
|
|
718
|
+
if (data.opener) {
|
|
719
|
+
put(`Done so far: ${data.opener.done}`);
|
|
720
|
+
put(`Doing now: ${data.opener.doing}`);
|
|
721
|
+
put(`Next: ${data.opener.next}`);
|
|
722
|
+
}
|
|
596
723
|
// The one static rainbow line of this surface marks the adhd turn (no animation on this host).
|
|
597
724
|
const headline = `lit-adhd: ${data.summary.sessions} session(s) · ${data.summary.waitingOnUser} waiting on you · next: ${step ? step.text : `${nextKind}${nextSay}`}`;
|
|
598
725
|
put(headline, color ? (text) => rainbowLine(text) : null);
|
|
@@ -646,6 +773,8 @@ export const renderRewind = (data, width = 100, { color = false } = {}) => {
|
|
|
646
773
|
put('Nothing runs on its own here: open that session, or say what to do.');
|
|
647
774
|
const unread = Object.entries(data.sources).filter(([, status]) => status !== 'ok').map(([name]) => SOURCE_LABELS[name] ?? name);
|
|
648
775
|
if (unread.length) put(`Not read: ${unread.join(', ')}.`);
|
|
776
|
+
const skipped = data.roadmap.plansSkipped ?? 0;
|
|
777
|
+
if (skipped > 0) put(`${skipped} ${skipped === 1 ? 'plan was' : 'plans were'} not read (read budget used up).`);
|
|
649
778
|
return `${out.join('\n')}\n`;
|
|
650
779
|
};
|
|
651
780
|
|
|
@@ -17,6 +17,8 @@ const ACTIVE_MS = 15 * 60 * 1000;
|
|
|
17
17
|
export const makeBudget = (limit = TOTAL_BUDGET_BYTES) => ({ limit, used: 0, exhausted: false });
|
|
18
18
|
|
|
19
19
|
const readSlice = (fd, position, length, budget) => {
|
|
20
|
+
// An empty slice (a head-only peek) reads nothing; it must not mark the budget used up.
|
|
21
|
+
if (length <= 0) return '';
|
|
20
22
|
const allowed = Math.min(length, budget.limit - budget.used);
|
|
21
23
|
if (allowed <= 0) {
|
|
22
24
|
budget.exhausted = true;
|
|
@@ -47,14 +47,15 @@ verdicts: [PASS, FAIL, BLOCKED]
|
|
|
47
47
|
|
|
48
48
|
## #contract.outputs
|
|
49
49
|
|
|
50
|
-
The brief has six parts, in this order:
|
|
50
|
+
The brief opens with three lines, then has six parts, in this order:
|
|
51
51
|
|
|
52
|
+
0. **Three opening lines**, copied from `opener` in the user's language: `Done so far:` (`opener.done`: this worktree's newest commits in the window, two at most, then its completed start-work runs as `<plan> (start-work complete)` and its fully ticked plans with their gauge, three items in all, else its finished goal, else "nothing finished in this window"); `Doing now:` (`opener.doing`: this session's top frame with stage and gauge, else its running start-work plan, else the newest active session's goal, else "nothing running"); `Next:` (`opener.next`: the one next step, with the word to say in brackets). In Korean: `지금까지:`, `지금:`, `다음:`.
|
|
52
53
|
1. **Headline.** Sessions found, how many wait on the user, the one next step: `7 sessions · 1 waiting on you · next: resume frame (say "lit continue")` / `세션 7개 · 나를 기다리는 것 1 · 다음: resume frame ("lit continue")`.
|
|
53
|
-
2. **One card per live session.** Sessions that wait on the user
|
|
54
|
+
2. **One card per live session.** Sessions that wait on the user come first, then active, idle and done ones, grouped under their worktree. A status badge (`● waiting on you`, `● ACTIVE`, `◌ idle`, `✓ done`, in the user's language) with the id, the route when known, and the age; the goal in plain words; the known facts (`⏳ 3m of work · last ask: "…"`, in the user's language); a real gauge with what is left; what was done (`✅ Done:`, tools named never counted, and commits); what it is doing now when no edit is on record (`▶ Now:`, the last tool call by name, for a session that is still running); what is next (`➤ Next:`). A done session's next line names the plan its run finished (`start-work finished plans/a.md`); an idle or done session's last tool call is history, never offered as its next step.
|
|
54
55
|
3. **Ended sessions, one bordered table.** Ended sessions collapse into a ruled table (`id | tree | goal | ago | done`), at most 8 rows with a count of the rest. The age reads the largest unit (`6h`, not `6h 40m`); the done cell reads commits or the file count, never both.
|
|
55
|
-
4. **Roadmap timeline.** Done `✓` (finished plans and landed commits with dates), now `▶` (the current item of each open plan), later `○` (remaining names), then goal objectives with their gauges.
|
|
56
|
+
4. **Roadmap timeline.** Done `✓` (finished plans, which count toward the roadmap gauge, and landed commits with dates), now `▶` (the current item of each open plan), later `○` (remaining names), then goal objectives with their gauges. Every plan is held to the window: only plans changed inside it are listed, this worktree's included, and plans from other worktrees carry their worktree name; unfilled plan scaffolds never appear. When `plansOmitted` is above zero, say how many more open plans exist; when `roadmap.plansSkipped` is above zero, say on the last line how many plans were not read because the read budget ran out.
|
|
56
57
|
5. **Paused work.** Each paused frame from `workflows` with its gauge and a resume offer: `lit-loop [▉▉▉░░░░] 3/7 paused, resume?` / `lit-loop [▉▉▉░░░░] 3/7 일시정지, 이어갈까요?`.
|
|
57
|
-
6. **One next step in a box.** Exactly one, from `next`: what it does, then "say go" (or the `say` word). When nothing can run on `go`, the box still recommends a real step from the records (a session waiting on the user, a paused workflow, an open plan item, the latest active session, the
|
|
58
|
+
6. **One next step in a box.** Exactly one, from `next`: what it does, then "say go" (or the `say` word). When nothing can run on `go`, the box still recommends a real step from the records, in this order: the first item under `## Next` or `## Next Steps` in this worktree's own `HANDOFF*.md` (`handoffs[].next`), a session waiting on the user, a paused workflow, an open plan item, the latest active session, then the newest session that is not running (review what a done one finished, or pick up an idle or ended one). It says that nothing runs on its own, and a HANDOFF step never carries a word to type. A step that is already done is never offered again.
|
|
58
59
|
|
|
59
60
|
**Progress gauges (D9), required.** One real gauge per session and per plan, each copied verbatim from the collector, in the form `[▉▉▉░░] 3/5` (filled `▉` done, light `░` remaining, then done/total). The remaining items are listed by name under each gauge, from its `remaining` array, clipped by the collector to fit. A gauge renders only when both numbers are real: an unknown total is the known-facts line (`… 3 steps`, or `… running` when nothing is counted), never `?`, never a guess, never an estimate, never a gauge of your own.
|
|
60
61
|
|
|
@@ -116,24 +117,27 @@ The collector (`plugin/lib/rewind/collect.mjs`, served as `litmuse rewind`) read
|
|
|
116
117
|
| Source | What it gives | Missing reads |
|
|
117
118
|
| --- | --- | --- |
|
|
118
119
|
| Muse transcripts, `<data>/muse/sessions/*/*/*/session.jsonl` | per session: title, first and last ask, tools used, status | `transcripts: unknown` |
|
|
119
|
-
| `plans/*.md` checkbox rows | open
|
|
120
|
+
| `plans/*.md` checkbox rows | plans changed inside the window (open and finished), current item, done/total, remaining names | `plans: unknown` |
|
|
120
121
|
| `.litmuse/goals/*.json` | objective and passed/total criteria per goal | `litgoal: unknown` |
|
|
121
|
-
| `HANDOFF*.md` and `.litmuse/handoff/latest.md` | headings from the first 40 lines
|
|
122
|
+
| `HANDOFF*.md` and `.litmuse/handoff/latest.md` | headings from the first 40 lines; for this worktree's own files, the first item under `## Next` / `## Next Steps` | `handoffs: unknown` |
|
|
122
123
|
| `git log --since` | commits in the window, matched to sessions by time and branch | `git: unknown` |
|
|
123
124
|
| `.litmuse/workflow-stack/*.json` | running and paused disciplines with stage and k/n | `workflowStack: unknown` |
|
|
124
|
-
| the
|
|
125
|
+
| the stage records (`$XDG_STATE_HOME/litmuse/stage/`) | this session's live start-work route and plan; any session's completed run (its plan fully ticked) | `startWork: unknown` |
|
|
126
|
+
| the liveness records (`stage/liveness/`) | each session's last hook event: `alive` (a tool call), `idle` (Stop), `ended` (SessionEnd) | the transcript time alone decides |
|
|
125
127
|
| `herdr agent list` (only when `HERDR_ENV=1`) | agent panes | `herdr: unknown` |
|
|
126
128
|
|
|
127
|
-
Transcripts are read head (64 KiB) and tail (256 KiB) only, under one 8 MiB budget for the whole run, through descriptors opened without following links; subagent transcripts are skipped. A session belongs to a worktree when the `cwd` its log records sits inside it. Every string is redacted before it leaves the collector: credential shapes, `NAME=value` with a secret-like name, URI userinfo, any current environment value of eight or more characters, and the home directory (shown as `~`). Assistant text and changed files are not in this host's log records, so the brief shows commits and tool names instead.
|
|
129
|
+
Plans, goals, handoffs, workflow stacks and stage records are read first, then transcripts, so a busy session store never crowds out the plans; a plan the budget can no longer cover is counted in `roadmap.plansSkipped`, never half-read. Transcripts are read head (64 KiB) and tail (256 KiB) only, under one 8 MiB budget for the whole run, through descriptors opened without following links; subagent transcripts are skipped. A session belongs to a worktree when the `cwd` its log records sits inside it. Every string is redacted before it leaves the collector: credential shapes, `NAME=value` with a secret-like name, URI userinfo, any current environment value of eight or more characters, and the home directory (shown as `~`). Assistant text and changed files are not in this host's log records, so the brief shows commits and tool names instead.
|
|
128
130
|
|
|
129
131
|
## Status taxonomy
|
|
130
132
|
|
|
131
133
|
- `waiting-on-user`: the session's last word was the assistant asking something, or it holds a paused workflow frame or a start-work boundary awaiting `lit approve`.
|
|
132
134
|
- `active`: written to within the last 15 minutes and not waiting.
|
|
133
|
-
- `
|
|
135
|
+
- `idle`: another session whose last hook event is a Stop no older than its last log line.
|
|
136
|
+
- `done`: another session whose start-work run completed (plan fully ticked) after its last log line.
|
|
137
|
+
- `ended`: quiet for longer than 15 minutes, or its last hook event was SessionEnd.
|
|
134
138
|
- `unknown`: the transcript held no timestamp.
|
|
135
139
|
|
|
136
|
-
Sessions waiting on the user come first, then active ones, newest first.
|
|
140
|
+
Sessions waiting on the user come first, then active, idle and done ones, newest first. This session always reads active while it runs the brief.
|
|
137
141
|
|
|
138
142
|
## The next step
|
|
139
143
|
|
|
@@ -172,7 +176,7 @@ lit-adhd is a read-only discipline: it never pushes, pauses or finishes a workfl
|
|
|
172
176
|
## Stop and report
|
|
173
177
|
|
|
174
178
|
- Stop with `BLOCKED: lit-adhd collector unavailable (<reason>)` when the collector exits non-zero or is not found; never rebuild the brief from memory.
|
|
175
|
-
- The reply is the six-part brief (summary, live cards, ended rows, roadmap, paused work, one next step) and nothing more. Name every `unknown` source once on the last line.
|
|
179
|
+
- The reply is the three opening lines and the six-part brief (summary, live cards, ended rows, roadmap, paused work, one next step) and nothing more. Name every `unknown` source once on the last line.
|
|
176
180
|
- Start nothing: the user's `go` starts the next step, not this turn.
|
|
177
181
|
|
|
178
182
|
## Pitfalls
|
|
@@ -183,6 +187,9 @@ lit-adhd is a read-only discipline: it never pushes, pauses or finishes a workfl
|
|
|
183
187
|
| `sources.transcripts: unknown` | No `muse/sessions/` folder under the data dir | Say no session records were found; still show plans and goals |
|
|
184
188
|
| `budget.exhausted: true` | The 8 MiB budget ran out | Say older sessions were not read |
|
|
185
189
|
| no gauge line on a card | No stack or start-work records progress for that session | Say the elapsed time and last action instead of a count; never print a placeholder pulse |
|
|
186
|
-
| `next.kind: none` | No single next step runs on `go` | The box recommends a real step from the records; say it, do not run it or invent another |
|
|
190
|
+
| `next.kind: none` | No single next step runs on `go` | The box recommends a real step from the records (this worktree's HANDOFF Next item first); say it, do not run it or invent another |
|
|
191
|
+
| `roadmap.plansSkipped` above zero | The read budget ran out before those plan files | Say how many plans were not read |
|
|
192
|
+
| a plan you expected is missing | It was last changed before the window (`--since`, 7 days by default) | Say a wider `--since` lists it |
|
|
193
|
+
| `opener.doing: nothing running` | No frame, start-work or active session is on record | Print it as given |
|
|
187
194
|
|
|
188
195
|
See `references/card-examples.md` for complete briefs in English and Korean and `references/status-taxonomy.md` for the status wording in both languages.
|