@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.
Files changed (41) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README-Ko-KR.md +6 -6
  3. package/README.md +6 -6
  4. package/TRADEMARK.md +12 -0
  5. package/dist/activation-primary-prompts.js +5 -1
  6. package/dist/agents/defaults.js +1 -1
  7. package/dist/commands.js +10 -4
  8. package/dist/durable-plan.d.ts +5 -1
  9. package/dist/durable-plan.js +14 -2
  10. package/dist/hooks.js +3 -0
  11. package/dist/plan-scope-roots.d.ts +23 -0
  12. package/dist/plan-scope-roots.js +116 -0
  13. package/dist/restart-notice.d.ts +7 -0
  14. package/dist/restart-notice.js +40 -0
  15. package/dist/server.js +4 -0
  16. package/dist/session-liveness.d.ts +20 -0
  17. package/dist/session-liveness.js +99 -0
  18. package/dist/start-work-typed.js +12 -1
  19. package/dist/state.d.ts +1 -0
  20. package/dist/state.js +1 -0
  21. package/dist/tui/mission-band.d.ts +2 -0
  22. package/dist/tui/mission-band.js +2 -0
  23. package/dist/tui/mission-source.d.ts +3 -2
  24. package/dist/tui/mission-source.js +40 -7
  25. package/dist/tui/mission-threads.d.ts +10 -2
  26. package/dist/tui/mission-threads.js +15 -6
  27. package/dist/tui.js +28 -1
  28. package/dist/v2/events.js +3 -0
  29. package/docs/assets/readme/badge-version.svg +1 -1
  30. package/docs/reference-Ko-KR.md +20 -3
  31. package/docs/reference.md +22 -3
  32. package/package.json +1 -1
  33. package/skills/lit-adhd/SKILL.md +8 -7
  34. package/skills/lit-adhd/references/brief-format.md +5 -2
  35. package/skills/lit-adhd/references/card-examples.md +23 -2
  36. package/skills/lit-adhd/references/status-taxonomy.md +6 -1
  37. package/skills/lit-adhd/scripts/rewind.mjs +122 -24
  38. package/skills/lit-plan/SKILL.md +6 -0
  39. package/skills/managed-skill-manifest.json +4 -4
  40. package/tools/check-pack-payload.mjs +1 -0
  41. package/tools/version-manifests.json +2 -2
@@ -159,7 +159,8 @@ export function guardSql(sql) {
159
159
  return sql;
160
160
  }
161
161
 
162
- const budget = { left: TOTAL_BYTE_BUDGET };
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 || stat.size > budget.left) return undefined;
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: isRecord(work.plan) ? str(work.plan.path) : "",
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 !== undefined) state.handoff.push(...[...text.matchAll(/^#{1,3}\s+(.+)$/gmu)].slice(0, 4).map((match) => clean(match[1], 80)));
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
- export function statusOf({ detail, local, sessionID, now }) {
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, sessionID) {
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, row.id) ?? null,
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 family) {
642
- const dir = resolve(entry.dir);
643
- const local = readLocalState(dir);
644
- if (dir !== root) local.plans = local.plans.filter((plan) => now - plan.changedMs <= PLAN_WINDOW_MS);
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 lines = [`## ${glyph("🧭 ", "")}${md(cutCells(report.headline, 110))}`];
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
- lines.push("", "```text");
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
 
@@ -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": "0106bcd7d209a3c2a36791665744ba145b94f7f99c64c5a04e22ab78ed47c8c9"
87
+ "sha256": "01e84c87740a7069daabb331b915c3dc9888f2c4052efc0c870d2450740e353d"
88
88
  },
89
89
  {
90
90
  "path": "references/card-examples.md",
91
- "sha256": "4a31b6bacb675e243eefb0c60736fa7b753c86054dc9ee5776d39c5f79c6e667"
91
+ "sha256": "3946212954ee5f3c26e98032ad5199f4d20826dc112e15b66da67d98c489f698"
92
92
  },
93
93
  {
94
94
  "path": "references/status-taxonomy.md",
95
- "sha256": "8de250f50c84e4a2a8ac39ddb46e8fc90098d92406eae0f029389cb23f6b9df2"
95
+ "sha256": "b4c16cbbefdf72b8e04b7aa67dcd45f7232dfc30b6f926cae3ce0f22724f2463"
96
96
  },
97
97
  {
98
98
  "path": "scripts/rewind.mjs",
99
- "sha256": "649eee3424f78879591b2ba178a6fda860c7cb67f87c8daa1f618b899346b443"
99
+ "sha256": "a00d565d382c60bb179db25b7cae4ce8d8221c9dcc360f913ce0b0bb366f734a"
100
100
  }
101
101
  ]
102
102
  },
@@ -63,6 +63,7 @@ const REQUIRED_README_PATHS = [
63
63
  "CONTRIBUTING.md",
64
64
  "SECURITY.md",
65
65
  "SUPPORT.md",
66
+ "TRADEMARK.md",
66
67
  "docs/lit-mark.md",
67
68
  "docs/migration.md",
68
69
  "docs/privacy.md",
@@ -17,13 +17,13 @@
17
17
  {
18
18
  "path": "README-npm-Ko-KR.md",
19
19
  "kind": "pinned",
20
- "occurrences": 13,
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": 12,
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
  {