@litfamily/litopencode 1.0.32 → 1.0.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 +14 -0
- package/README-Ko-KR.md +6 -6
- package/README.md +6 -6
- package/TRADEMARK.md +12 -0
- package/dist/activation-primary-prompts.js +5 -1
- package/dist/agents/defaults.js +1 -1
- package/dist/commands.js +10 -4
- package/dist/durable-plan.d.ts +5 -1
- package/dist/durable-plan.js +14 -2
- package/dist/hooks.js +3 -0
- package/dist/plan-scope-roots.d.ts +23 -0
- package/dist/plan-scope-roots.js +116 -0
- package/dist/restart-notice.d.ts +7 -0
- package/dist/restart-notice.js +40 -0
- package/dist/server.js +4 -0
- package/dist/session-liveness.d.ts +20 -0
- package/dist/session-liveness.js +99 -0
- package/dist/start-work-typed.js +12 -1
- package/dist/state.d.ts +1 -0
- package/dist/state.js +1 -0
- package/dist/tui/mission-band.d.ts +2 -0
- package/dist/tui/mission-band.js +2 -0
- package/dist/tui/mission-source.d.ts +3 -2
- package/dist/tui/mission-source.js +40 -7
- package/dist/tui/mission-threads.d.ts +10 -2
- package/dist/tui/mission-threads.js +15 -6
- package/dist/tui.js +28 -1
- package/dist/v2/events.js +3 -0
- package/docs/assets/readme/badge-version.svg +1 -1
- package/docs/reference-Ko-KR.md +20 -3
- package/docs/reference.md +22 -3
- package/package.json +1 -1
- package/skills/lit-adhd/SKILL.md +8 -7
- package/skills/lit-adhd/references/brief-format.md +5 -2
- package/skills/lit-adhd/references/card-examples.md +23 -2
- package/skills/lit-adhd/references/status-taxonomy.md +6 -1
- package/skills/lit-adhd/scripts/rewind.mjs +122 -24
- package/skills/lit-plan/SKILL.md +6 -0
- package/skills/managed-skill-manifest.json +4 -4
- package/tools/check-pack-payload.mjs +1 -0
- package/tools/version-manifests.json +2 -2
|
@@ -159,7 +159,8 @@ export function guardSql(sql) {
|
|
|
159
159
|
return sql;
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
|
|
162
|
+
// `skipped` counts regular files left unread because the budget ran out, so the brief can say so.
|
|
163
|
+
const budget = { left: TOTAL_BYTE_BUDGET, skipped: 0 };
|
|
163
164
|
|
|
164
165
|
function readSmall(file, max = FILE_CAP) {
|
|
165
166
|
let stat;
|
|
@@ -168,7 +169,11 @@ function readSmall(file, max = FILE_CAP) {
|
|
|
168
169
|
} catch {
|
|
169
170
|
return undefined;
|
|
170
171
|
}
|
|
171
|
-
if (!stat.isFile() || stat.size > max
|
|
172
|
+
if (!stat.isFile() || stat.size > max) return undefined;
|
|
173
|
+
if (stat.size > budget.left) {
|
|
174
|
+
budget.skipped += 1;
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
172
177
|
let fd;
|
|
173
178
|
try {
|
|
174
179
|
fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
@@ -260,9 +265,23 @@ function goalOf(raw) {
|
|
|
260
265
|
};
|
|
261
266
|
}
|
|
262
267
|
|
|
268
|
+
// The first list item or line of text under a `Next` or `Next Steps` heading, without its marker.
|
|
269
|
+
function handoffNextOf(text) {
|
|
270
|
+
const lines = text.split(/\r?\n/u);
|
|
271
|
+
const start = lines.findIndex((line) => /^#{1,6}\s+Next(?:\s+Steps)?\s*$/iu.test(line.trim()));
|
|
272
|
+
if (start < 0) return undefined;
|
|
273
|
+
for (const line of lines.slice(start + 1)) {
|
|
274
|
+
if (/^#{1,6}\s/u.test(line.trim())) return undefined;
|
|
275
|
+
const item = line.trim().replace(/^(?:[-*+]|\d+[.)])\s+/u, "");
|
|
276
|
+
if (item !== "") return item;
|
|
277
|
+
}
|
|
278
|
+
return undefined;
|
|
279
|
+
}
|
|
280
|
+
|
|
263
281
|
export function readLocalState(dir) {
|
|
264
|
-
const state = { plans: [], goal: undefined, work: undefined, stacks: new Map(), handoff: [], commits: [] };
|
|
282
|
+
const state = { plans: [], plansSkipped: 0, goal: undefined, work: undefined, stacks: new Map(), liveness: new Map(), handoff: [], handoffNext: undefined, commits: [] };
|
|
265
283
|
const litLoop = join(dir, ".litopencode", "litgoal", "lit-loop");
|
|
284
|
+
const skippedBefore = budget.skipped;
|
|
266
285
|
for (const planDir of [join(dir, "plans"), join(dir, ".litopencode", "plans")]) {
|
|
267
286
|
let names = [];
|
|
268
287
|
try {
|
|
@@ -281,19 +300,36 @@ export function readLocalState(dir) {
|
|
|
281
300
|
} catch {
|
|
282
301
|
// gone since the listing
|
|
283
302
|
}
|
|
284
|
-
state.plans.push({ path: relative(dir, join(planDir, name)), title: heading ? heading[1].replace(/\s*—\s*Work Plan$/u, "").trim() : name, changedMs, ...checklist });
|
|
303
|
+
state.plans.push({ path: relative(dir, join(planDir, name)), title: heading ? heading[1].replace(/\s*—\s*Work Plan$/u, "").trim() : name, changedMs, complete: checklist.done === checklist.total, ...checklist });
|
|
285
304
|
}
|
|
286
305
|
}
|
|
306
|
+
state.plansSkipped = budget.skipped - skippedBefore;
|
|
287
307
|
state.goal = goalOf(readJson(join(litLoop, "goals.json")));
|
|
288
308
|
const work = readJson(join(litLoop, "work-schema-3.json"));
|
|
289
309
|
if (isRecord(work) && work.schemaVersion === 3) {
|
|
310
|
+
const planPath = isRecord(work.plan) ? str(work.plan.path) : "";
|
|
290
311
|
state.work = {
|
|
291
312
|
sessionID: str(work.sessionID),
|
|
292
313
|
status: str(work.status),
|
|
293
|
-
planPath
|
|
314
|
+
planPath,
|
|
315
|
+
plan: planPath === "" ? "" : !isAbsolute(planPath) ? planPath : relative(dir, planPath).startsWith("..") ? basename(planPath) : relative(dir, planPath),
|
|
316
|
+
updatedMs: Date.parse(str(work.updatedAt)),
|
|
294
317
|
boundary: isRecord(work.pendingBoundary) ? `${str(work.pendingBoundary.action)} on ${basename(str(work.pendingBoundary.root))}` : undefined,
|
|
295
318
|
};
|
|
296
319
|
}
|
|
320
|
+
// Each session's last lifecycle signal (alive, idle, ended), written by the plugin's hooks.
|
|
321
|
+
const livenessDir = join(dir, ".litopencode", "liveness");
|
|
322
|
+
try {
|
|
323
|
+
for (const name of readdirSync(livenessDir).filter((entry) => entry.endsWith(".json")).slice(0, 40)) {
|
|
324
|
+
const record = readJson(join(livenessDir, name), 1024);
|
|
325
|
+
const atMs = isRecord(record) ? Date.parse(str(record.at)) : Number.NaN;
|
|
326
|
+
if (isRecord(record) && SAFE_SESSION.test(str(record.sessionId)) && ["alive", "idle", "ended"].includes(record.state) && Number.isFinite(atMs)) {
|
|
327
|
+
state.liveness.set(record.sessionId, { state: record.state, atMs });
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
} catch {
|
|
331
|
+
// no liveness records for this worktree
|
|
332
|
+
}
|
|
297
333
|
const stackDir = join(dir, ".litopencode", "workflow-stack");
|
|
298
334
|
try {
|
|
299
335
|
for (const name of readdirSync(stackDir).filter((entry) => entry.endsWith(".json")).slice(0, 40)) {
|
|
@@ -306,7 +342,10 @@ export function readLocalState(dir) {
|
|
|
306
342
|
try {
|
|
307
343
|
for (const name of readdirSync(dir).filter((entry) => /^HANDOFF.*\.md$/u.test(entry)).slice(0, 3)) {
|
|
308
344
|
const text = readSmall(join(dir, name), FILE_CAP);
|
|
309
|
-
if (text
|
|
345
|
+
if (text === undefined) continue;
|
|
346
|
+
state.handoff.push(...[...text.matchAll(/^#{1,3}\s+(.+)$/gmu)].slice(0, 4).map((match) => clean(match[1], 80)));
|
|
347
|
+
const next = handoffNextOf(text);
|
|
348
|
+
if (state.handoffNext === undefined && next !== undefined) state.handoffNext = clean(next, 120);
|
|
310
349
|
}
|
|
311
350
|
} catch {
|
|
312
351
|
// no handoff file
|
|
@@ -480,20 +519,36 @@ export function countChain({ sessionID, directory, local, detail }) {
|
|
|
480
519
|
return undefined;
|
|
481
520
|
}
|
|
482
521
|
|
|
483
|
-
|
|
522
|
+
// A liveness record written this long before the session's last stored move still describes that move.
|
|
523
|
+
const LIVENESS_SLACK_MS = 5000;
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* The session's status from its records. A start-work run it completed (and did not move past) makes it
|
|
527
|
+
* done; an idle or ended liveness record at least as new as its last move makes it idle, before the
|
|
528
|
+
* two-minute guess. The session asking for the brief (`self`) is working whatever it last wrote.
|
|
529
|
+
*/
|
|
530
|
+
export function statusOf({ detail, local, sessionID, now, self = false }) {
|
|
484
531
|
const pending = detail.tools.find((entry) => entry.tool === "question" && (entry.status === "running" || entry.status === "pending"));
|
|
485
532
|
if (pending || (local.work?.sessionID === sessionID && local.work.status === "paused" && local.work.boundary)) return "waits-on-you";
|
|
486
533
|
const latest = detail.tools[0];
|
|
487
534
|
if (latest && latest.status === "error" && now - detail.updated > ACTIVE_WINDOW_MS) return "blocked";
|
|
488
535
|
const stack = local.stacks.get(sessionID);
|
|
489
536
|
if (stack?.frames?.some((entry) => isRecord(entry) && entry.status === "paused")) return "paused";
|
|
537
|
+
if (self) return "working";
|
|
538
|
+
const run = local.work?.sessionID === sessionID && local.work.status === "completed" ? local.work : undefined;
|
|
539
|
+
if (run && Number.isFinite(run.updatedMs) && detail.updated <= run.updatedMs + LIVENESS_SLACK_MS) return "done";
|
|
540
|
+
// An ended session (deleted in OpenCode) reads idle: nothing proves its work finished, only that it stopped.
|
|
541
|
+
const live = local.liveness?.get(sessionID);
|
|
542
|
+
if (live && live.state !== "alive" && live.atMs >= detail.updated - LIVENESS_SLACK_MS) return "idle";
|
|
490
543
|
if (now - detail.updated <= ACTIVE_WINDOW_MS) return "working";
|
|
491
544
|
const count = countChain({ sessionID, directory: detail.directory, local, detail });
|
|
492
545
|
if (count && count.done === count.total) return "done";
|
|
493
546
|
return "idle";
|
|
494
547
|
}
|
|
495
548
|
|
|
496
|
-
function nextOf(detail, local,
|
|
549
|
+
function nextOf(detail, local, status) {
|
|
550
|
+
// A finished session's leftovers are history, never its next step.
|
|
551
|
+
if (status === "done") return undefined;
|
|
497
552
|
const todo = detail.todos.find((entry) => entry.status !== "completed" && entry.status !== "cancelled");
|
|
498
553
|
if (todo) return clean(todo.content, 100);
|
|
499
554
|
const open = local.plans.filter((entry) => entry.done < entry.total);
|
|
@@ -529,9 +584,9 @@ function touchedOf(detail) {
|
|
|
529
584
|
return { edited: edited.slice(0, 20).map((name) => clean(name, 60)), tools };
|
|
530
585
|
}
|
|
531
586
|
|
|
532
|
-
export function buildCard({ row, detail, local, lanes, now }) {
|
|
587
|
+
export function buildCard({ row, detail, local, lanes, now, self = false }) {
|
|
533
588
|
const count = countChain({ sessionID: row.id, directory: row.directory, local, detail });
|
|
534
|
-
const status = statusOf({ detail: { ...detail, updated: row.updated, directory: row.directory }, local, sessionID: row.id, now });
|
|
589
|
+
const status = statusOf({ detail: { ...detail, updated: row.updated, directory: row.directory }, local, sessionID: row.id, now, self });
|
|
535
590
|
const latest = detail.tools[0];
|
|
536
591
|
const defaultTitle = /^(?:new session|child session|untitled)/iu.test(row.title) || row.title === "";
|
|
537
592
|
const touched = touchedOf(detail);
|
|
@@ -540,7 +595,7 @@ export function buildCard({ row, detail, local, lanes, now }) {
|
|
|
540
595
|
status,
|
|
541
596
|
goal: clean(defaultTitle ? detail.firstPrompt || "(no title)" : row.title, 160),
|
|
542
597
|
done: doneOf(detail, local),
|
|
543
|
-
next: nextOf(detail, local,
|
|
598
|
+
next: nextOf(detail, local, status) ?? null,
|
|
544
599
|
edited: touched.edited,
|
|
545
600
|
tools: touched.tools,
|
|
546
601
|
...(count ? { counts: count } : {}),
|
|
@@ -592,6 +647,8 @@ export function recommend({ groups, sessionID }) {
|
|
|
592
647
|
const here = groups.find((group) => group.current);
|
|
593
648
|
const open = here?.plans.filter((plan) => plan.done < plan.total) ?? [];
|
|
594
649
|
if (open.length === 1) return { kind: "plan", sessionID, target: open[0].path, text: `Start work on ${open[0].path}: ${open[0].total - open[0].done} step${open[0].total - open[0].done === 1 ? "" : "s"} left.` };
|
|
650
|
+
// With nothing for go to run here, this worktree's own HANDOFF.md Next item comes before older sessions.
|
|
651
|
+
if (here?.handoffNext) return { kind: "handoff", sessionID, text: `From HANDOFF.md: ${clip(here.handoffNext, 100)}`, short: clip(here.handoffNext, 60) };
|
|
595
652
|
const unfinished = cards.find(({ card }) => card.status === "idle" && card.next);
|
|
596
653
|
if (unfinished) return { kind: "session", sessionID: unfinished.card.id, text: `Pick up "${clip(unfinished.card.goal, 50)}": next is ${clip(unfinished.card.next, 60)}.` };
|
|
597
654
|
// Nothing can run on go, so the box still names a real step from the records: an open plan in another
|
|
@@ -624,10 +681,19 @@ export function writeNext(root, record) {
|
|
|
624
681
|
export async function collect(options = {}) {
|
|
625
682
|
const root = resolve(options.root ?? process.cwd());
|
|
626
683
|
const now = options.now ?? Date.now();
|
|
627
|
-
budget.left = TOTAL_BYTE_BUDGET;
|
|
684
|
+
budget.left = Math.min(TOTAL_BYTE_BUDGET, Math.max(1, Number.isInteger(options.budgetBytes) ? options.budgetBytes : TOTAL_BYTE_BUDGET));
|
|
685
|
+
budget.skipped = 0;
|
|
628
686
|
const family = options.allDirs ? [] : worktreeFamily(root);
|
|
629
687
|
const dirs = family.map((entry) => resolve(entry.dir));
|
|
630
688
|
const limit = Math.min(Math.max(options.maxSessions ?? 10, 1), 30);
|
|
689
|
+
// The small local records first (plans, goal, run, stacks, liveness, handoff): the session store read
|
|
690
|
+
// after them may use up the read budget. A plan outside the window is left out, here or elsewhere.
|
|
691
|
+
const locals = family.map((entry) => {
|
|
692
|
+
const dir = resolve(entry.dir);
|
|
693
|
+
const local = readLocalState(dir);
|
|
694
|
+
local.plans = local.plans.filter((plan) => now - plan.changedMs <= PLAN_WINDOW_MS);
|
|
695
|
+
return { entry, dir, local };
|
|
696
|
+
});
|
|
631
697
|
const dataHome = process.env.XDG_DATA_HOME && isAbsolute(process.env.XDG_DATA_HOME) ? process.env.XDG_DATA_HOME : join(homedir(), ".local", "share");
|
|
632
698
|
const dbFile = options.db ?? join(dataHome, "opencode", "opencode.db");
|
|
633
699
|
let store = lstatOk(dbFile) ? await openSqlite(dbFile) : { error: "no session database was found" };
|
|
@@ -638,23 +704,30 @@ export async function collect(options = {}) {
|
|
|
638
704
|
const report = { schema: REWIND_SCHEMA, generatedAt: new Date(now).toISOString(), root: clean(root, 200), source: { store: store.kind ?? "none", ...(store.error ? { note: store.error } : {}) }, groups: [] };
|
|
639
705
|
const rows = store.kind ? store.sessions(dirs, limit) : [];
|
|
640
706
|
const lanes = store.kind ? store.lanes(rows.map((row) => row.id)) : new Map();
|
|
641
|
-
for (const entry of
|
|
642
|
-
const
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
const sessions = rows.filter((row) => resolve(row.directory) === dir).map((row) => buildCard({ row, detail: store.detail(row), local, lanes: lanes.get(row.id) ?? 0, now }));
|
|
646
|
-
if (sessions.length === 0 && local.plans.length === 0 && local.work === undefined) continue;
|
|
707
|
+
for (const { entry, dir, local } of locals) {
|
|
708
|
+
const sessions = rows.filter((row) => resolve(row.directory) === dir).map((row) => buildCard({ row, detail: store.detail(row), local, lanes: lanes.get(row.id) ?? 0, now, self: row.id === options.session }));
|
|
709
|
+
if (sessions.length === 0 && local.plans.length === 0 && local.work === undefined && local.handoffNext === undefined) continue;
|
|
710
|
+
const run = local.work?.status === "completed" && local.work.plan !== "" && now - local.work.updatedMs <= PLAN_WINDOW_MS ? local.work.plan : undefined;
|
|
647
711
|
report.groups.push({
|
|
648
712
|
worktree: clean(basename(dir), 60),
|
|
649
713
|
branch: clean(entry.branch, 60),
|
|
650
714
|
current: dir === root,
|
|
651
715
|
sessions,
|
|
652
|
-
plans: local.plans.map(({ path, title, done, total, rows }) => ({ path, title: clean(title, 80), done, total, next: clean(rows.find((entry) => !entry.done)?.title ?? "", 90) })),
|
|
653
|
-
work: local.work ? { sessionID: local.work.sessionID, status: local.work.status } : undefined,
|
|
716
|
+
plans: local.plans.map(({ path, title, done, total, rows, complete }) => ({ path, title: clean(title, 80), done, total, complete, next: clean(rows.find((entry) => !entry.done)?.title ?? "", 90) })),
|
|
717
|
+
work: local.work ? { sessionID: local.work.sessionID, status: local.work.status, plan: clean(local.work.plan, 90) } : undefined,
|
|
654
718
|
handoff: local.handoff,
|
|
719
|
+
...(local.handoffNext === undefined ? {} : { handoffNext: local.handoffNext }),
|
|
655
720
|
roadmap: roadmapOf(local),
|
|
721
|
+
// Finished inside the window, for the brief's opener: commits, a completed start-work run, ticked plans and a met goal.
|
|
722
|
+
finished: {
|
|
723
|
+
commits: local.commits.filter((commit) => now - Date.parse(commit.at) <= PLAN_WINDOW_MS).slice(0, 2).map((commit) => commit.subject),
|
|
724
|
+
runs: run === undefined ? [] : [clean(run, 90)],
|
|
725
|
+
plans: local.plans.filter((plan) => plan.complete).map(({ path, done, total }) => ({ path, done, total })),
|
|
726
|
+
...(local.goal && local.goal.total > 0 && local.goal.done === local.goal.total && local.goal.title ? { goal: clean(local.goal.title, 100) } : {}),
|
|
727
|
+
},
|
|
656
728
|
});
|
|
657
729
|
}
|
|
730
|
+
report.plansSkipped = locals.reduce((total, { local }) => total + local.plansSkipped, 0);
|
|
658
731
|
report.recommended = recommend({ groups: report.groups, sessionID: options.session });
|
|
659
732
|
const all = report.groups.flatMap((group) => group.sessions);
|
|
660
733
|
const waits = all.filter((card) => card.status === "waits-on-you").length;
|
|
@@ -667,7 +740,7 @@ export async function collect(options = {}) {
|
|
|
667
740
|
if (options.writeNext && forThisSession && (kind === "continue" || kind === "work" || kind === "plan")) {
|
|
668
741
|
const record = { kind, target: report.recommended.target ?? kind, at: now, sessionID: options.session };
|
|
669
742
|
if (writeNext(root, record)) report.nextRecord = { kind, target: record.target };
|
|
670
|
-
} else if (options.writeNext && options.session !== undefined && kind !== "none") {
|
|
743
|
+
} else if (options.writeNext && options.session !== undefined && kind !== "none" && kind !== "handoff") {
|
|
671
744
|
// The step belongs to another session or worktree: go here cannot run it, so it answers with where to go.
|
|
672
745
|
const record = { kind: "elsewhere", target: clip(report.recommended.text, 300), at: now, sessionID: options.session };
|
|
673
746
|
if (writeNext(root, record)) report.nextRecord = { kind: "elsewhere", target: record.target };
|
|
@@ -755,10 +828,34 @@ function endedTable(rows, now) {
|
|
|
755
828
|
return ["| id | where | goal | ago | done |", "|---|---|---|---|---|", ...body, ...more];
|
|
756
829
|
}
|
|
757
830
|
|
|
831
|
+
// The three lines that open the brief, from records only. Done: this worktree's newest commits in the
|
|
832
|
+
// window (two at most), its completed start-work run and its fully ticked plans (three items in all),
|
|
833
|
+
// else its met goal. Doing: this session's own frame or goal while it works, else the running start-work
|
|
834
|
+
// run, else the newest working session. Next: the recommended step, with the word to type when go runs it.
|
|
835
|
+
function openerLines(report, sessionID, runsHere) {
|
|
836
|
+
const here = report.groups.find((group) => group.current);
|
|
837
|
+
const finished = here?.finished;
|
|
838
|
+
const runs = new Set(finished?.runs ?? []);
|
|
839
|
+
const items = [
|
|
840
|
+
...(finished?.commits ?? []).map(md),
|
|
841
|
+
...[...runs].map((plan) => `${code(plan)} (start-work complete)`),
|
|
842
|
+
...(finished?.plans ?? []).filter((plan) => !runs.has(plan.path)).map((plan) => `${code(plan.path)} ${code(gauge(plan.done, plan.total) ?? `${plan.done}/${plan.total}`)}`),
|
|
843
|
+
].slice(0, 3);
|
|
844
|
+
const done = items.length > 0 ? items.join("; ") : finished?.goal ? md(finished.goal) : "nothing finished in this window";
|
|
845
|
+
const cards = report.groups.flatMap((group) => group.sessions);
|
|
846
|
+
const self = cards.find((card) => card.id === sessionID && card.status === "working");
|
|
847
|
+
const running = here?.work?.status === "active" ? here.work.plan : undefined;
|
|
848
|
+
const newest = cards.filter((card) => card.status === "working").sort((a, b) => Date.parse(b.updated) - Date.parse(a.updated))[0];
|
|
849
|
+
const doing = self ? md(cutCells(self.goal, 80)) : running !== undefined ? ["start-work", running].filter(Boolean).map(md).join(" · ") : newest ? md(cutCells(newest.goal, 80)) : "nothing running";
|
|
850
|
+
const step = md(cutCells(report.recommended.short ?? report.recommended.text, 90));
|
|
851
|
+
return [`**Done so far:** ${cutCells(done, 100)} `, `**Doing now:** ${doing} `, `**Next:** ${step}${runsHere ? " (type `go`)" : ""}`];
|
|
852
|
+
}
|
|
853
|
+
|
|
758
854
|
export function renderBrief(report, { ascii = false, width = 72, sessionID } = {}) {
|
|
759
855
|
const glyph = (fancy, plain) => (ascii ? plain : fancy);
|
|
760
856
|
const now = Date.parse(report.generatedAt) || Date.now();
|
|
761
|
-
const
|
|
857
|
+
const runsHere = Boolean(report.nextRecord && report.nextRecord.kind !== "elsewhere");
|
|
858
|
+
const lines = [...openerLines(report, sessionID, runsHere), "", `## ${glyph("🧭 ", "")}${md(cutCells(report.headline, 110))}`];
|
|
762
859
|
const worktrees = report.groups.length;
|
|
763
860
|
lines.push(`*this worktree family · ${worktrees} worktree${worktrees === 1 ? "" : "s"} with records${report.source.note ? ` · ${md(report.source.note)}` : ""}*`);
|
|
764
861
|
const ended = [];
|
|
@@ -786,13 +883,14 @@ export function renderBrief(report, { ascii = false, width = 72, sessionID } = {
|
|
|
786
883
|
}
|
|
787
884
|
const inner = width - 4;
|
|
788
885
|
const title = " Recommended next step ";
|
|
789
|
-
|
|
886
|
+
// An untagged fence: a language tag would make the chat colour the box as code.
|
|
887
|
+
lines.push("", "```");
|
|
790
888
|
lines.push(`${glyph("╭─", "+-")}${title}${glyph("─", "-").repeat(Math.max(1, inner + 1 - cellWidth(title)))}${glyph("╮", "+")}`);
|
|
791
889
|
for (const text of wrapCells(report.recommended.text, inner)) lines.push(`${glyph("│", "|")} ${padCells(text, inner)} ${glyph("│", "|")}`);
|
|
792
|
-
const runsHere = report.nextRecord && report.nextRecord.kind !== "elsewhere";
|
|
793
890
|
if (runsHere) lines.push(`${glyph("│", "|")} ${padCells("type: go", inner)} ${glyph("│", "|")}`);
|
|
794
891
|
lines.push(`${glyph("╰", "+")}${glyph("─", "-").repeat(inner + 2)}${glyph("╯", "+")}`, "```", "");
|
|
795
892
|
lines.push(runsHere ? "Type **`go`** to do it." : report.nextRecord ? "Nothing runs on its own here: open that session, or say what to do (`go` says where)." : "Nothing runs on its own here: open that session, or say what to do.");
|
|
893
|
+
if (report.plansSkipped > 0) lines.push("", `*${report.plansSkipped} ${report.plansSkipped === 1 ? "plan was" : "plans were"} not read (read budget used up).*`);
|
|
796
894
|
return lines.join("\n");
|
|
797
895
|
}
|
|
798
896
|
|
package/skills/lit-plan/SKILL.md
CHANGED
|
@@ -351,3 +351,9 @@ Do not call OpenCode's task tool from `lit-plan`, even for a nominally read-only
|
|
|
351
351
|
## Approval Language
|
|
352
352
|
|
|
353
353
|
End planning with a clear boundary. Example: “If you approve this plan, run `/start-work`; implementation should stay within this product checkout, change docs/tests only, run targeted tests, then run `npm test`, and stop before any version bump, commit, tag, push, publish, or release.” This sentence prevents accidental mutation from planning mode and gives the next agent a precise starting point.
|
|
354
|
+
|
|
355
|
+
When `/start-work save-plan` has saved the approved plan, the saved-plan notice arms a one-word start: the user can type `go` within 30 minutes in that same session, and LitOpenCode starts the typed start-work run on that plan. A `go` from another session, or after 30 minutes, is an ordinary word. Mention `go` in one line next to `/start-work`; do not replace the approval boundary with it.
|
|
356
|
+
|
|
357
|
+
## Scope Roots For Outside Folders
|
|
358
|
+
|
|
359
|
+
When a checklist item names an absolute path outside the project, the plan must carry a line of the form `Scope roots: <absolute folder> (<actions>)` near the top, with actions drawn from `read`, `write` and `execute`, for example `Scope roots: <absolute folder> (read, write)`. `/start-work save-plan` refuses a plan whose checklist section reaches an uncovered outside folder and replies with the exact line to add; a folder too broad to grant (the filesystem root, a top-level folder or the home folder itself) gets a request for a narrower existing folder instead. Paths inside code fences, and commands quoted in an inline code span, are data and do not count. When typed start-work begins (`start-work`, `/start-work` or `go` after a save), every valid `Scope roots:` line becomes a grant of the run, so the executor does not pause to ask for a folder the approved plan already named. The folder must exist when the run starts. Never use a Scope roots line to widen into release, publish, version, host-config or destructive actions; those stay refused.
|
|
@@ -84,19 +84,19 @@
|
|
|
84
84
|
"canonicalFiles": [
|
|
85
85
|
{
|
|
86
86
|
"path": "references/brief-format.md",
|
|
87
|
-
"sha256": "
|
|
87
|
+
"sha256": "01e84c87740a7069daabb331b915c3dc9888f2c4052efc0c870d2450740e353d"
|
|
88
88
|
},
|
|
89
89
|
{
|
|
90
90
|
"path": "references/card-examples.md",
|
|
91
|
-
"sha256": "
|
|
91
|
+
"sha256": "3946212954ee5f3c26e98032ad5199f4d20826dc112e15b66da67d98c489f698"
|
|
92
92
|
},
|
|
93
93
|
{
|
|
94
94
|
"path": "references/status-taxonomy.md",
|
|
95
|
-
"sha256": "
|
|
95
|
+
"sha256": "b4c16cbbefdf72b8e04b7aa67dcd45f7232dfc30b6f926cae3ce0f22724f2463"
|
|
96
96
|
},
|
|
97
97
|
{
|
|
98
98
|
"path": "scripts/rewind.mjs",
|
|
99
|
-
"sha256": "
|
|
99
|
+
"sha256": "a00d565d382c60bb179db25b7cae4ce8d8221c9dcc360f913ce0b0bb366f734a"
|
|
100
100
|
}
|
|
101
101
|
]
|
|
102
102
|
},
|
|
@@ -17,13 +17,13 @@
|
|
|
17
17
|
{
|
|
18
18
|
"path": "README-npm-Ko-KR.md",
|
|
19
19
|
"kind": "pinned",
|
|
20
|
-
"occurrences":
|
|
20
|
+
"occurrences": 14,
|
|
21
21
|
"why": "The npm package pages pin artwork and package links to the current package version; tools/readme-for-npm.mjs copies them over README.md and README-Ko-KR.md at pack time."
|
|
22
22
|
},
|
|
23
23
|
{
|
|
24
24
|
"path": "README-npm.md",
|
|
25
25
|
"kind": "pinned",
|
|
26
|
-
"occurrences":
|
|
26
|
+
"occurrences": 13,
|
|
27
27
|
"why": "The npm package pages pin artwork and package links to the current package version; tools/readme-for-npm.mjs copies them over README.md and README-Ko-KR.md at pack time."
|
|
28
28
|
},
|
|
29
29
|
{
|