@lawaty/lacode 1.3.0-beta.9 → 2.0.0-beta.0

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/dist/index.js CHANGED
@@ -951,8 +951,6 @@ You own exactly these files and nothing else:
951
951
  150 lines.**
952
952
  - \`.opencode/context/decisions.md\` \u2014 architectural constraints that bind future
953
953
  work. **At most 120 lines.**
954
- - \`.opencode/context/active.md\` \u2014 work in flight, grouped by the ISO week it was
955
- opened. **At most 60 lines.**
956
954
 
957
955
  The unit is non-blank lines. It is what a reader actually pays for, it bounds an
958
956
  entry that wraps to three lines, and the validator can count it exactly. An
@@ -968,8 +966,6 @@ failed run, not a small one.
968
966
 
969
967
  The plugin applies this on every run. Write entries in the shape that lets it.
970
968
 
971
- - \`active.md\`: a finished task (\`[x]\` plus \`finished YYYY-MM-DD\`) is dropped
972
- after 7 days. An open task never expires, however old it is.
973
969
  - \`architecture.md\`, \`contexts.md\`, \`conventions.md\`, \`workflows.md\`,
974
970
  \`decisions.md\`: a \`[provisional]\` entry is dropped 30 days after its
975
971
  \`verified\` date unless it is re-verified. \`[binding]\` and \`[disputed]\` are
@@ -1063,8 +1059,7 @@ You are mapping a city, not auditing every house. Do not attempt to understand
1063
1059
  every file.
1064
1060
 
1065
1061
  Then write the five navigation files within their line budgets, using the entry
1066
- schema above. Also create \`.opencode/context/active.md\` with its heading and no
1067
- tasks. For each significant subsystem, give a short "Start here" list \u2014 at most
1062
+ schema above. For each significant subsystem, give a short "Start here" list \u2014 at most
1068
1063
  about five canonical files, each with a one-phrase reason. This is the single
1069
1064
  most valuable thing you produce: where an agent should open its editor next.
1070
1065
 
@@ -1098,37 +1093,6 @@ update needed".
1098
1093
  Every new or changed entry uses the schema above and stays under the file's
1099
1094
  budget. If a file is already over budget, consolidate before adding anything.
1100
1095
 
1101
- Also keep \`.opencode/context/active.md\` current: record work that is in flight,
1102
- and mark a task finished \u2014 with the date it finished \u2014 when it is.
1103
-
1104
- # Task backlog
1105
-
1106
- \`.opencode/context/active.md\` holds the work in flight. It is a rolling window,
1107
- not a history: it answers "what is open, and what just finished".
1108
-
1109
- Format \u2014 one line per task, under the ISO week the task was opened in:
1110
-
1111
- ## 2026-W43
1112
-
1113
- - [ ] Ship the release notes \u2014 opened 2026-W43
1114
- - [x] Fix the flaky test \u2014 opened 2026-W43, finished 2026-10-19
1115
-
1116
- The rules:
1117
-
1118
- - A task that is not marked \`[x]\` is OPEN. An open task is never removed,
1119
- however old it is.
1120
- - A task is FINISHED when it is marked \`[x]\` AND carries a \`finished\` date.
1121
- Record the date it actually finished, as \`YYYY-MM-DD\`.
1122
- - The week is \`YYYY-Www\`, Monday to Sunday. \`date +%G-W%V\` prints it.
1123
- - A finished task is dropped once it is more than a week old. The plugin
1124
- enforces that: record the finished task with its date and let the window close
1125
- on its own. Do not delete old finished tasks by hand, and do not keep a
1126
- finished task back to preserve it \u2014 the file is meant to stay small.
1127
- - Never invent a task. Record work that actually happened, and mark a task
1128
- finished only once it is finished.
1129
- - Keep the file inside the window. If the backlog has grown past a handful of
1130
- weeks, that is a signal to close finished tasks, not to add sections.
1131
-
1132
1096
  # Minimal exploration
1133
1097
 
1134
1098
  Default shape:
@@ -1216,7 +1180,7 @@ Never drop a binding constraint to make room.
1216
1180
 
1217
1181
  Trigger: an explicit review (see the \`context-review\` command).
1218
1182
 
1219
- Audit the map files and the backlog for:
1183
+ Audit the map files for:
1220
1184
 
1221
1185
  - schema \u2014 every entry has one marker, at most three lines, a pointer, and a
1222
1186
  \`verified\` date
@@ -1231,8 +1195,6 @@ Audit the map files and the backlog for:
1231
1195
  - missing canonical entry points for significant subsystems
1232
1196
  - unsupported assumptions \u2014 anything that is not \`[binding]\` or \`[disputed]\`
1233
1197
  should be \`[provisional]\`
1234
- - backlog tasks that are finished but carry no date \u2014 the retention window
1235
- cannot age those out, so they stay forever
1236
1198
 
1237
1199
  Fix problems only in the files you own. Never modify source code or a project
1238
1200
  page during a review. Report what you found, what you fixed, what you left as
@@ -1704,7 +1666,7 @@ if the blocker is understanding rather than execution, report that instead.`,
1704
1666
  }
1705
1667
  },
1706
1668
  "context": {
1707
- "description": "Repository cartographer and memory maintainer. Bootstraps and incrementally maintains .opencode/context/ (architecture, contexts, conventions, workflows, decisions) as a small navigation map for future agents, and keeps .opencode/context/active.md \u2014 the active backlog of work in flight, grouped by week. Runs automatically after sessions that changed files; can also be invoked with /context-init, /context-update, /context-review. Never use it for general coding tasks.",
1669
+ "description": "Repository cartographer and memory maintainer. Bootstraps and incrementally maintains .opencode/context/ (architecture, contexts, conventions, workflows, decisions) as a small navigation map for future agents. Runs automatically after sessions that changed files; can also be invoked with /context-init, /context-update, /context-review. Never use it for general coding tasks.",
1708
1670
  "mode": "subagent",
1709
1671
  "temperature": 0.1,
1710
1672
  prompt: CARTOGRAPHER_PROMPT,
@@ -1930,8 +1892,7 @@ exploration (README \u2192 existing docs \u2192 manifests \u2192 top-level struc
1930
1892
  and framework config \u2192 entry points \u2192 selective source), then write the five
1931
1893
  context files inside their line budgets, using the entry schema (provenance
1932
1894
  marker, rule of at most three lines, \`\u2014 See <path>[ \xA7Section] (verified
1933
- YYYY-MM-DD)\`). Also create \`.opencode/context/active.md\` with its heading and no
1934
- tasks.
1895
+ YYYY-MM-DD)\`).
1935
1896
 
1936
1897
  Remember: write conclusions, not inventories. Point at the project's existing
1937
1898
  documentation instead of restating it. Run the drift pass and report a
@@ -1977,7 +1938,7 @@ Follow the review procedure and entry schema in your system prompt. Everything
1977
1938
  outside \`.opencode/context/\` is read-only for this run. Do not modify source
1978
1939
  code under any circumstances.
1979
1940
 
1980
- Audit the context files and the backlog for:
1941
+ Audit the context files for:
1981
1942
 
1982
1943
  - schema \u2014 every entry has one provenance marker, at most three lines, a
1983
1944
  pointer, and a \`(verified YYYY-MM-DD)\` date
@@ -1996,8 +1957,6 @@ Audit the context files and the backlog for:
1996
1957
  - missing canonical entry points for significant subsystems
1997
1958
  - unsupported assumptions \u2014 anything that is not \`[binding]\` or \`[disputed]\`
1998
1959
  should be \`[provisional]\`
1999
- - backlog tasks that are finished but carry no date \u2014 the retention window
2000
- cannot age those out, so they stay forever
2001
1960
 
2002
1961
  Fix what you find in \`.opencode/context/\` only, then report: findings, fixes,
2003
1962
  and anything you could not verify.`
@@ -2085,9 +2044,13 @@ $ARGUMENTS
2085
2044
 
2086
2045
  Report the tool's output as it came back, then add one short line naming what changed: re-admitted, still limited, or benched because the provider does not serve the model. If a model was benched as \`model-missing\`, say so and name the replacement in \`~/.config/lacode/pool.txt\` \u2014 but change nothing yourself.`
2087
2046
  },
2047
+ // `agent: "implement"` is a subagent, so without `subtask: false` this command
2048
+ // would run in a child session and `timeline_reprocess` would rebuild the
2049
+ // child's timeline instead of the caller's.
2088
2050
  "reprocess-timeline": {
2089
2051
  description: "Rebuild this session's timeline from its full transcript",
2090
2052
  agent: "implement",
2053
+ subtask: false,
2091
2054
  template: "Call the `timeline_reprocess` tool exactly once with no arguments. Then reply with only its output."
2092
2055
  },
2093
2056
  "image-in-cb": {
@@ -4121,110 +4084,10 @@ one ordered pass in slot order: each slot fills to its weight, then overflow goe
4121
4084
  });
4122
4085
 
4123
4086
  // src/plugins/context-autoupdate.ts
4124
- import { existsSync as existsSync4, mkdirSync as mkdirSync5, readdirSync as readdirSync2, readFileSync as readFileSync7, statSync as statSync6, unlinkSync as unlinkSync5, writeFileSync as writeFileSync6 } from "node:fs";
4087
+ import { existsSync as existsSync4, mkdirSync as mkdirSync5, readdirSync as readdirSync2, statSync as statSync6, unlinkSync as unlinkSync5, writeFileSync as writeFileSync6 } from "node:fs";
4125
4088
  import { homedir as homedir4 } from "node:os";
4126
4089
  import * as path3 from "node:path";
4127
4090
 
4128
- // src/lib/active-backlog.ts
4129
- var ACTIVE_BACKLOG_FILE = "active.md";
4130
- var RETENTION_DAYS = 7;
4131
- var DAY_MS = 24 * 60 * 60 * 1e3;
4132
- var HEADER = `# Active backlog
4133
-
4134
- Tasks in flight, grouped by the week they were opened. A finished task stays
4135
- here for a week after it was finished and is then dropped; an open task stays
4136
- however long it takes. Add a line under the right week, and give a finished
4137
- task its date: \`- [x] Title \u2014 opened 2026-W43, finished 2026-10-19\`.`;
4138
- var TASK_LINE = /^\s*(?:[-*+]|\d+[.)])\s*\[([ xX])\]\s*(.*)$/;
4139
- var META = /(?:—|–|-)\s*opened\s+(\d{4}-W(?:0[1-9]|[1-4]\d|5[0-3]))|[,—–-]\s*finished\s+(\d{4}-\d{2}-\d{2})/gi;
4140
- var isCalendarDate = (s) => {
4141
- const at = Date.parse(`${s}T00:00:00Z`);
4142
- return Number.isFinite(at) && new Date(at).toISOString().slice(0, 10) === s;
4143
- };
4144
- var parseBacklog = (text) => {
4145
- const entries = [];
4146
- for (const raw of text.split(/\r?\n/)) {
4147
- const line = TASK_LINE.exec(raw);
4148
- if (!line) continue;
4149
- const finished = line[1] !== " ";
4150
- let week;
4151
- let finishedAt;
4152
- const stripped = line[2].replace(META, (fragment, openedWeek, finishedDate) => {
4153
- if (openedWeek !== void 0) {
4154
- week ??= openedWeek.toUpperCase();
4155
- return "";
4156
- }
4157
- if (finishedDate !== void 0 && isCalendarDate(finishedDate)) {
4158
- finishedAt ??= finishedDate;
4159
- return "";
4160
- }
4161
- return fragment;
4162
- });
4163
- const title = stripped.replace(/^[\s—–-]+|[\s—–-]+$/g, "");
4164
- if (!title) continue;
4165
- entries.push({ title, ...week ? { week } : {}, finished, ...finishedAt ? { finishedAt } : {} });
4166
- }
4167
- return entries;
4168
- };
4169
- var ISO_WEEK = /^(\d{4})-W(\d{2})$/i;
4170
- var newestFirst = (a, b) => {
4171
- const ma = ISO_WEEK.exec(a);
4172
- const mb = ISO_WEEK.exec(b);
4173
- if (ma && mb) return Number(mb[1]) - Number(ma[1]) || Number(mb[2]) - Number(ma[2]);
4174
- if (ma) return -1;
4175
- if (mb) return 1;
4176
- return a < b ? 1 : a > b ? -1 : 0;
4177
- };
4178
- var renderLine = (entry) => {
4179
- const box = entry.finished ? "[x]" : "[ ]";
4180
- const meta = [];
4181
- if (entry.week) meta.push(`opened ${entry.week}`);
4182
- if (entry.finished && entry.finishedAt) meta.push(`finished ${entry.finishedAt}`);
4183
- const head = `- ${box} ${entry.title}`;
4184
- if (meta.length === 0) return head;
4185
- return entry.week ? `${head} \u2014 ${meta.join(", ")}` : `${head}, ${meta.join(", ")}`;
4186
- };
4187
- var renderBacklog = (entries) => {
4188
- const buckets = /* @__PURE__ */ new Map();
4189
- for (const entry of entries) {
4190
- const key = entry.week ? entry.week : null;
4191
- const bucket = buckets.get(key);
4192
- if (bucket) bucket.push(entry);
4193
- else buckets.set(key, [entry]);
4194
- }
4195
- const weeks = [...buckets.keys()].filter((week) => week !== null).sort(newestFirst);
4196
- let out = HEADER;
4197
- for (const week of weeks) out += `
4198
-
4199
- ## ${week}
4200
-
4201
- ${buckets.get(week).map(renderLine).join("\n")}`;
4202
- const unscheduled = buckets.get(null);
4203
- if (unscheduled) out += `
4204
-
4205
- ## Unscheduled
4206
-
4207
- ${unscheduled.map(renderLine).join("\n")}`;
4208
- return `${out}
4209
- `;
4210
- };
4211
- var normalizeBacklog = (text, now = Date.now()) => {
4212
- const entries = parseBacklog(text);
4213
- const kept = [];
4214
- const removed = [];
4215
- for (const entry of entries) {
4216
- if (entry.finished && entry.finishedAt) {
4217
- const at = Date.parse(`${entry.finishedAt}T00:00:00Z`);
4218
- if (Number.isFinite(at) && now - at > RETENTION_DAYS * DAY_MS) {
4219
- removed.push(entry.title);
4220
- continue;
4221
- }
4222
- }
4223
- kept.push(entry);
4224
- }
4225
- return { text: renderBacklog(kept), removed };
4226
- };
4227
-
4228
4091
  // src/lib/map-schema.ts
4229
4092
  import { existsSync as existsSync3, readFileSync as readFileSync5, statSync as statSync5, writeFileSync as writeFileSync5 } from "node:fs";
4230
4093
  import * as path from "node:path";
@@ -4274,21 +4137,11 @@ var MAP_SPECS = [
4274
4137
  kind: "entries",
4275
4138
  provisionalDays: PROVISIONAL_DAYS,
4276
4139
  sectionRequired: true
4277
- },
4278
- {
4279
- file: ACTIVE_BACKLOG_FILE,
4280
- charter: "work in flight, grouped by the ISO week it was opened",
4281
- budget: 60,
4282
- minEntries: 0,
4283
- kind: "backlog",
4284
- provisionalDays: PROVISIONAL_DAYS,
4285
- sectionRequired: false
4286
4140
  }
4287
4141
  ];
4142
+ var NAVIGATION_FILES = MAP_SPECS.map((spec) => spec.file);
4288
4143
  var MAP_SPEC_BY_FILE = new Map(MAP_SPECS.map((spec) => [spec.file, spec]));
4289
- var MAP_FILES = MAP_SPECS.map((spec) => spec.file);
4290
- var NAVIGATION_FILES = MAP_SPECS.filter((spec) => spec.kind === "entries").map((spec) => spec.file);
4291
- var DAY_MS2 = 24 * 60 * 60 * 1e3;
4144
+ var DAY_MS = 24 * 60 * 60 * 1e3;
4292
4145
  var ENTRY_START = /^([-*+])\s+\[(binding|provisional|disputed)\]\s+(.*)$/i;
4293
4146
  var CONTINUATION = /^\s{2,}\S/;
4294
4147
  var LIST_ITEM = /^\s*(?:[-*+]|\d+[.)])\s+/;
@@ -4299,7 +4152,7 @@ var VERIFIED = /\(verified\s+(\d{4}-\d{2}-\d{2})\)\s*$/i;
4299
4152
  var POINTER = /(?:—|–|→|->)\s*See\s+`([^`\n]+)`(?:\s*§\s*([^()\n]+?))?(?:\s*\(verified\s+(\d{4}-\d{2}-\d{2})\))?\s*$/i;
4300
4153
  var DECISION_RECORD = /^\s*(?:#{1,6}\s*)?(?:\*\*)?(Context|Decision|Consequences|Alternatives|Status|Trade-?offs|Rationale)(?:\*\*)?\s*(?::|$)/im;
4301
4154
  var ARCHIVE_LANGUAGE = /\b(?:archive\s+of\s+record|this\s+(?:is|file\s+is|map\s+is)\s+the\s+only\s+(?:copy|record)|the\s+only\s+place\s+[^.\n]{0,80}?\b(?:remains|survives)\b|the\s+only\s+(?:copy|record)\s+[^.\n]{0,80}?\b(?:remains|survives|is\s+kept)\b|(?:survives|remains|is\s+kept)\s+only\s+here|no\s+longer\s+exists\s+(?:in|anywhere)|exists\s+only\s+(?:in|here))\b/i;
4302
- var isCalendarDate2 = (value) => {
4155
+ var isCalendarDate = (value) => {
4303
4156
  const at = Date.parse(`${value}T00:00:00Z`);
4304
4157
  return Number.isFinite(at) && new Date(at).toISOString().slice(0, 10) === value;
4305
4158
  };
@@ -4517,10 +4370,6 @@ var validateMapText = (spec, text, options) => {
4517
4370
  if (nonBlank > spec.budget) {
4518
4371
  push("budget", `${nonBlank} non-blank lines exceeds the ${spec.budget}-line budget; consolidate, do not pad`);
4519
4372
  }
4520
- if (spec.kind === "backlog") {
4521
- if (!text.startsWith("# ")) push("charter", "active backlog must start with a top-level heading");
4522
- return { spec, lines: nonBlank, entries: [], violations };
4523
- }
4524
4373
  const entries = parseMapEntries(text);
4525
4374
  let inFence = false;
4526
4375
  let tableReported = false;
@@ -4562,9 +4411,9 @@ var validateMapText = (spec, text, options) => {
4562
4411
  }
4563
4412
  if (!entry.verified) {
4564
4413
  push("schema", "entry has no (verified YYYY-MM-DD); the lifecycle and drift checks need it", line);
4565
- } else if (!isCalendarDate2(entry.verified)) {
4414
+ } else if (!isCalendarDate(entry.verified)) {
4566
4415
  push("schema", `verified date ${entry.verified} is not a real calendar date`, line);
4567
- } else if (Date.parse(`${entry.verified}T00:00:00Z`) > (options.now ?? Date.now()) + DAY_MS2) {
4416
+ } else if (Date.parse(`${entry.verified}T00:00:00Z`) > (options.now ?? Date.now()) + DAY_MS) {
4568
4417
  push("schema", `verified date ${entry.verified} is more than a day in the future`, line);
4569
4418
  }
4570
4419
  if (!entry.pointer) {
@@ -4614,14 +4463,6 @@ var validateMapText = (spec, text, options) => {
4614
4463
  };
4615
4464
  var normalizeMapText = (spec, text, options = {}) => {
4616
4465
  const now = options.now ?? Date.now();
4617
- if (spec.kind === "backlog") {
4618
- const result = normalizeBacklog(text, now);
4619
- return {
4620
- text: result.text,
4621
- removed: result.removed.map((title) => ({ line: 0, text: title })),
4622
- kept: 0
4623
- };
4624
- }
4625
4466
  const entries = parseMapEntries(text);
4626
4467
  const drop = /* @__PURE__ */ new Set();
4627
4468
  const removed = [];
@@ -4631,7 +4472,7 @@ var normalizeMapText = (spec, text, options = {}) => {
4631
4472
  if (options.canResolve && !options.canResolve(entry.pointer, entry.section)) continue;
4632
4473
  const at = Date.parse(`${entry.verified}T00:00:00Z`);
4633
4474
  if (!Number.isFinite(at)) continue;
4634
- if (now - at <= spec.provisionalDays * DAY_MS2) continue;
4475
+ if (now - at <= spec.provisionalDays * DAY_MS) continue;
4635
4476
  for (let i = entry.startLine; i < entry.endLine; i++) drop.add(i);
4636
4477
  removed.push({ line: entry.startLine + 1, text: truncate(entry.rule) });
4637
4478
  }
@@ -4661,8 +4502,6 @@ var maintainMap = (root, mapDir, now = Date.now()) => {
4661
4502
  if (result.removed.length > 0) {
4662
4503
  removedTotal += result.removed.length;
4663
4504
  files.push({ file: spec.file, changed, removed: result.removed });
4664
- } else if (changed) {
4665
- files.push({ file: spec.file, changed, removed: [] });
4666
4505
  }
4667
4506
  }
4668
4507
  return { files, removedTotal };
@@ -4688,21 +4527,19 @@ var auditMap = (root, mapDir, now = Date.now()) => {
4688
4527
  const result = validateMapText(spec, text, { root, now });
4689
4528
  files.push(result);
4690
4529
  violations.push(...result.violations);
4691
- if (spec.kind === "entries") {
4692
- for (const entry of result.entries) {
4693
- const key = normalizeRule(entry.rule);
4694
- if (key.length < 12) continue;
4695
- const home = rulesSeen.get(key);
4696
- if (home !== void 0 && home !== spec.file) {
4697
- violations.push({
4698
- file: spec.file,
4699
- line: entry.startLine + 1,
4700
- kind: "duplicate",
4701
- detail: `same rule is already stated in ${home}; one fact has one home`
4702
- });
4703
- } else if (home === void 0) {
4704
- rulesSeen.set(key, spec.file);
4705
- }
4530
+ for (const entry of result.entries) {
4531
+ const key = normalizeRule(entry.rule);
4532
+ if (key.length < 12) continue;
4533
+ const home = rulesSeen.get(key);
4534
+ if (home !== void 0 && home !== spec.file) {
4535
+ violations.push({
4536
+ file: spec.file,
4537
+ line: entry.startLine + 1,
4538
+ kind: "duplicate",
4539
+ detail: `same rule is already stated in ${home}; one fact has one home`
4540
+ });
4541
+ } else if (home === void 0) {
4542
+ rulesSeen.set(key, spec.file);
4706
4543
  }
4707
4544
  }
4708
4545
  }
@@ -4970,7 +4807,6 @@ var contextAutoUpdateHooks = (async ({ client, directory }, options) => {
4970
4807
  });
4971
4808
  const root = path3.resolve(directory);
4972
4809
  const contextAbs = path3.join(root, CONTEXT_DIR2);
4973
- const activeBacklogPath = path3.join(contextAbs, ACTIVE_BACKLOG_FILE);
4974
4810
  const config = findProjectConfig();
4975
4811
  const writerProblem = config ? verifyWriterRule(config, contextAbs, root) : void 0;
4976
4812
  if (writerProblem) void log("error", writerProblem);
@@ -4980,22 +4816,6 @@ var contextAutoUpdateHooks = (async ({ client, directory }, options) => {
4980
4816
  if (absolute === contextAbs || absolute.startsWith(contextAbs + path3.sep)) return false;
4981
4817
  return !toPosix2(absolute).split("/").some((segment) => IGNORED_SEGMENTS.has(segment));
4982
4818
  };
4983
- const normalizeActiveBacklog = async () => {
4984
- try {
4985
- if (!existsSync4(activeBacklogPath)) return { removed: 0 };
4986
- const before = readFileSync7(activeBacklogPath, "utf8");
4987
- const { text, removed } = normalizeBacklog(before);
4988
- if (text !== before) writeFileSync6(activeBacklogPath, text);
4989
- if (removed.length > 0) {
4990
- await log("info", `active backlog: dropped ${removed.length} task(s) finished more than a week ago`, {
4991
- removed: removed.length
4992
- });
4993
- }
4994
- return { removed: removed.length };
4995
- } catch {
4996
- return { removed: 0 };
4997
- }
4998
- };
4999
4819
  const applyLifecycle = async () => {
5000
4820
  try {
5001
4821
  const result = maintainMap(root, contextAbs);
@@ -5055,7 +4875,7 @@ var contextAutoUpdateHooks = (async ({ client, directory }, options) => {
5055
4875
  "",
5056
4876
  ENTRY_CONTRACT,
5057
4877
  "",
5058
- "Follow the bootstrap procedure in your system prompt: progressive exploration, then write the five context files within their line budgets. Also create `.opencode/context/active.md` with its heading and no tasks. When the project already has architecture or decision documentation, link to it instead of duplicating it. Write conclusions, not inventories."
4878
+ "Follow the bootstrap procedure in your system prompt: progressive exploration, then write the five context files within their line budgets. When the project already has architecture or decision documentation, link to it instead of duplicating it. Write conclusions, not inventories."
5059
4879
  ].join("\n") : [
5060
4880
  "Incremental context update. Project root: " + directory,
5061
4881
  "",
@@ -5066,17 +4886,13 @@ var contextAutoUpdateHooks = (async ({ client, directory }, options) => {
5066
4886
  "",
5067
4887
  ENTRY_CONTRACT,
5068
4888
  "",
5069
- 'Follow the incremental-update procedure in your system prompt. Update a context file only if this work introduced or revealed information that will materially help future agents: a new boundary or module, a new or moved canonical entry point, a changed workflow, a new convention, an architectural decision, a new dependency between subsystems, a renamed or moved subsystem, or an invariant not yet recorded. Do not edit documentation merely because source files changed. If nothing material changed, make no edits and report "no update needed". Also keep `.opencode/context/active.md` current: record work that is in flight, and mark a task finished \u2014 with the date it finished \u2014 when it is.'
4889
+ 'Follow the incremental-update procedure in your system prompt. Update a context file only if this work introduced or revealed information that will materially help future agents: a new boundary or module, a new or moved canonical entry point, a changed workflow, a new convention, an architectural decision, a new dependency between subsystems, a renamed or moved subsystem, or an invariant not yet recorded. Do not update documentation merely because source files changed. If nothing material changed, make no edits and report "no update needed".'
5070
4890
  ].join("\n");
5071
4891
  return {
5072
4892
  event: async ({ event }) => {
5073
4893
  try {
5074
4894
  if (event.type === "file.edited") {
5075
4895
  const file = event.properties.file;
5076
- if (path3.resolve(root, file) === activeBacklogPath) {
5077
- await normalizeActiveBacklog();
5078
- return;
5079
- }
5080
4896
  if (isTracked(file)) pending.add(file);
5081
4897
  return;
5082
4898
  }
@@ -5188,7 +5004,7 @@ var contextAutoUpdateHooks = (async ({ client, directory }, options) => {
5188
5004
  });
5189
5005
 
5190
5006
  // src/lib/feedback.ts
5191
- import { mkdirSync as mkdirSync6, readFileSync as readFileSync8, renameSync as renameSync4, unlinkSync as unlinkSync6, writeFileSync as writeFileSync7 } from "node:fs";
5007
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync7, renameSync as renameSync4, unlinkSync as unlinkSync6, writeFileSync as writeFileSync7 } from "node:fs";
5192
5008
  import { homedir as homedir5 } from "node:os";
5193
5009
  import { dirname as dirname4, join as join9 } from "node:path";
5194
5010
  var FEEDBACK_HINT_ENV = "LACODE_NO_FEEDBACK_HINT";
@@ -5196,7 +5012,7 @@ var FEEDBACK_HINT = "Enjoying lacode? Bugs, ideas, or a quick note on how you us
5196
5012
  var feedbackStatePath = () => join9(homedir5(), ".local", "share", "opencode", "lacode", "feedback.json");
5197
5013
  function readFeedbackState(path4 = feedbackStatePath()) {
5198
5014
  try {
5199
- const parsed = JSON.parse(readFileSync8(path4, "utf8"));
5015
+ const parsed = JSON.parse(readFileSync7(path4, "utf8"));
5200
5016
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return void 0;
5201
5017
  return parsed;
5202
5018
  } catch (error) {
@@ -5270,7 +5086,7 @@ async function feedbackHooks(input, options, deps = {}) {
5270
5086
 
5271
5087
  // src/plugins/image-paste.ts
5272
5088
  import { createHash, randomUUID } from "node:crypto";
5273
- import { globSync, mkdirSync as mkdirSync7, readFileSync as readFileSync9, renameSync as renameSync5, statSync as statSync7, writeFileSync as writeFileSync8 } from "node:fs";
5089
+ import { globSync, mkdirSync as mkdirSync7, readFileSync as readFileSync8, renameSync as renameSync5, statSync as statSync7, writeFileSync as writeFileSync8 } from "node:fs";
5274
5090
  import { homedir as homedir6 } from "node:os";
5275
5091
  import { basename as basename3, join as join10 } from "node:path";
5276
5092
 
@@ -5527,7 +5343,7 @@ async function imagePasteHooks(input, options, deps) {
5527
5343
  }
5528
5344
  let bytes;
5529
5345
  try {
5530
- bytes = readFileSync9(filePath);
5346
+ bytes = readFileSync8(filePath);
5531
5347
  } catch {
5532
5348
  skipped.push(name);
5533
5349
  continue;
@@ -6673,27 +6489,35 @@ async function selfUpdateHooks(input, options) {
6673
6489
  }
6674
6490
 
6675
6491
  // src/plugins/timeline.ts
6676
- import { writeFileSync as writeFileSync9, renameSync as renameSync6, rmSync as rmSync3 } from "node:fs";
6492
+ import { writeFileSync as writeFileSync10, renameSync as renameSync7, rmSync as rmSync3 } from "node:fs";
6677
6493
  import { tool as tool2 } from "@opencode-ai/plugin";
6678
6494
 
6679
6495
  // src/lib/timeline.ts
6680
- import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync8, readFileSync as readFileSync10, rmSync as rmSync2 } from "node:fs";
6496
+ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync8, readdirSync as readdirSync3, readFileSync as readFileSync9, renameSync as renameSync6, rmSync as rmSync2, writeFileSync as writeFileSync9 } from "node:fs";
6681
6497
  import { homedir as homedir7 } from "node:os";
6682
6498
  import { dirname as dirname5, join as join11, resolve as resolve4 } from "node:path";
6499
+ var TIMELINE_STATE_VERSION = 1;
6683
6500
  function timelineDir() {
6684
6501
  const env = process.env.LACODE_TIMELINE_DIR;
6685
6502
  if (env) return resolve4(env);
6686
6503
  return join11(homedir7(), ".local/share/opencode/lacode/timeline");
6687
6504
  }
6688
- function timelineFile(sessionID) {
6505
+ function assertValidSessionID(sessionID) {
6689
6506
  if (!/^[A-Za-z0-9_.-]+$/.test(sessionID)) throw new Error("invalid sessionID");
6507
+ }
6508
+ function timelineFile(sessionID) {
6509
+ assertValidSessionID(sessionID);
6690
6510
  return join11(timelineDir(), sessionID + ".jsonl");
6691
6511
  }
6512
+ function timelineStateFile(sessionID) {
6513
+ assertValidSessionID(sessionID);
6514
+ return join11(timelineDir(), sessionID + ".state.json");
6515
+ }
6692
6516
  function normalizeThrough(through) {
6693
6517
  if (!through || typeof through !== "object") return void 0;
6694
6518
  const out = {};
6695
- if (typeof through.messageID === "string") out.messageID = through.messageID;
6696
- if (typeof through.time === "number") out.time = through.time;
6519
+ if (typeof through.messageID === "string" && through.messageID.length > 0) out.messageID = through.messageID;
6520
+ if (typeof through.time === "number" && Number.isFinite(through.time)) out.time = through.time;
6697
6521
  if (out.messageID === void 0 && out.time === void 0) return void 0;
6698
6522
  return out;
6699
6523
  }
@@ -6734,7 +6558,7 @@ function appendEntriesAt(file, texts, now, through) {
6734
6558
  function readEntriesAt(file, limit) {
6735
6559
  let raw = "";
6736
6560
  try {
6737
- raw = readFileSync10(file, "utf8");
6561
+ raw = readFileSync9(file, "utf8");
6738
6562
  } catch {
6739
6563
  return [];
6740
6564
  }
@@ -6768,6 +6592,70 @@ function removeEntries(sessionID) {
6768
6592
  } catch {
6769
6593
  }
6770
6594
  }
6595
+ function normalizePersistedState(raw) {
6596
+ const obj = raw && typeof raw === "object" ? raw : {};
6597
+ const cursor = normalizeThrough(obj.cursor);
6598
+ const failures = typeof obj.failures === "number" && Number.isFinite(obj.failures) ? Math.max(0, Math.trunc(obj.failures)) : 0;
6599
+ const pending = obj.pending === true;
6600
+ const nextAttemptAt = typeof obj.nextAttemptAt === "number" && Number.isFinite(obj.nextAttemptAt) && obj.nextAttemptAt > 0 ? obj.nextAttemptAt : void 0;
6601
+ const updatedAt = typeof obj.updatedAt === "number" && Number.isFinite(obj.updatedAt) ? obj.updatedAt : Date.now();
6602
+ const state2 = {
6603
+ version: TIMELINE_STATE_VERSION,
6604
+ failures,
6605
+ pending,
6606
+ updatedAt
6607
+ };
6608
+ if (cursor) state2.cursor = cursor;
6609
+ if (nextAttemptAt !== void 0) state2.nextAttemptAt = nextAttemptAt;
6610
+ return state2;
6611
+ }
6612
+ function readTimelineState(sessionID) {
6613
+ try {
6614
+ const raw = readFileSync9(timelineStateFile(sessionID), "utf8");
6615
+ return normalizePersistedState(JSON.parse(raw));
6616
+ } catch {
6617
+ return void 0;
6618
+ }
6619
+ }
6620
+ function writeTimelineState(sessionID, state2) {
6621
+ const file = timelineStateFile(sessionID);
6622
+ mkdirSync8(dirname5(file), { recursive: true });
6623
+ const tmp = file + ".tmp." + process.pid + "." + Math.random().toString(36).slice(2);
6624
+ try {
6625
+ writeFileSync9(tmp, JSON.stringify(state2) + "\n");
6626
+ renameSync6(tmp, file);
6627
+ } catch (err) {
6628
+ try {
6629
+ rmSync2(tmp, { force: true });
6630
+ } catch {
6631
+ }
6632
+ throw err;
6633
+ }
6634
+ }
6635
+ function removeTimelineState(sessionID) {
6636
+ try {
6637
+ rmSync2(timelineStateFile(sessionID), { force: true });
6638
+ } catch {
6639
+ }
6640
+ }
6641
+ function listTimelineStates() {
6642
+ let names;
6643
+ try {
6644
+ names = readdirSync3(timelineDir());
6645
+ } catch {
6646
+ return [];
6647
+ }
6648
+ const out = [];
6649
+ for (const name of names) {
6650
+ if (!name.endsWith(".state.json")) continue;
6651
+ const sessionID = name.slice(0, -".state.json".length);
6652
+ if (!/^[A-Za-z0-9_.-]+$/.test(sessionID)) continue;
6653
+ const state2 = readTimelineState(sessionID);
6654
+ if (!state2) continue;
6655
+ out.push({ sessionID, state: state2 });
6656
+ }
6657
+ return out;
6658
+ }
6771
6659
  function collapse(s) {
6772
6660
  return s.replace(/\s+/g, " ").trim();
6773
6661
  }
@@ -7100,6 +6988,11 @@ function timelineHooks(input, options) {
7100
6988
  const intervalMs = Number.isFinite(rawInterval) ? Math.max(MIN_INTERVAL_MS, rawInterval) : DEFAULT_INTERVAL_MS;
7101
6989
  const rawMaxEntries = Number(config?.maxEntries ?? DEFAULT_MAX_ENTRIES);
7102
6990
  const maxEntries = Number.isFinite(rawMaxEntries) ? Math.min(MAX_ENTRIES, Math.max(1, Math.trunc(rawMaxEntries))) : DEFAULT_MAX_ENTRIES;
6991
+ const rawRetryMs = Number(config?.retryMs ?? 3e4);
6992
+ const retryMs = Number.isFinite(rawRetryMs) ? Math.max(1, rawRetryMs) : 3e4;
6993
+ const rawMaxBackoffMs = Number(config?.maxBackoffMs ?? 30 * 6e4);
6994
+ const maxBackoffMs = Math.max(retryMs, Number.isFinite(rawMaxBackoffMs) ? Math.max(1, rawMaxBackoffMs) : 30 * 6e4);
6995
+ const retryDelay = (failures) => Math.min(retryMs * 2 ** Math.min(Math.max(failures - 1, 0), 20), maxBackoffMs);
7103
6996
  const shared = globalThis[/* @__PURE__ */ Symbol.for("lacode:timeline:state")] ??= {
7104
6997
  sessions: /* @__PURE__ */ new Map(),
7105
6998
  owned: /* @__PURE__ */ new Set(),
@@ -7116,21 +7009,37 @@ function timelineHooks(input, options) {
7116
7009
  if (!id) return void 0;
7117
7010
  return { providerID: provider, modelID: id };
7118
7011
  };
7012
+ const persist = (id, state2) => {
7013
+ try {
7014
+ writeTimelineState(id, {
7015
+ version: TIMELINE_STATE_VERSION,
7016
+ cursor: state2.cursor,
7017
+ failures: state2.failures,
7018
+ pending: state2.pending,
7019
+ nextAttemptAt: state2.nextAttemptAt,
7020
+ updatedAt: Date.now()
7021
+ });
7022
+ } catch (error) {
7023
+ debug("persist failed", { sessionID: id, error });
7024
+ }
7025
+ };
7119
7026
  const stateFor = (id) => {
7120
7027
  let state2 = sessions.get(id);
7121
7028
  if (!state2) {
7029
+ const persisted = readTimelineState(id);
7122
7030
  state2 = {
7123
7031
  running: false,
7124
- pending: false,
7125
- failures: 0,
7126
- cursor: readEntries(id, 1)[0]?.through
7032
+ pending: persisted?.pending ?? false,
7033
+ failures: persisted?.failures ?? 0,
7034
+ cursor: persisted?.cursor ?? readEntries(id, 1)[0]?.through,
7035
+ nextAttemptAt: persisted?.nextAttemptAt
7127
7036
  };
7128
7037
  sessions.set(id, state2);
7129
7038
  while (sessions.size > 500) {
7130
7039
  let evicted = false;
7131
7040
  for (const [key, st] of sessions) {
7132
7041
  if (key === id) continue;
7133
- if (!st.timer && !st.running) {
7042
+ if (!st.timer && !st.running && !st.pending && st.nextAttemptAt === void 0) {
7134
7043
  sessions.delete(key);
7135
7044
  evicted = true;
7136
7045
  break;
@@ -7145,16 +7054,29 @@ function timelineHooks(input, options) {
7145
7054
  if (disabled || disposed) return;
7146
7055
  const state2 = stateFor(id);
7147
7056
  if (state2.root === false) return;
7057
+ const wasPending = state2.pending;
7148
7058
  state2.pending = true;
7059
+ if (!wasPending) persist(id, state2);
7149
7060
  if (state2.timer || state2.running) return;
7150
7061
  schedule(id, state2);
7151
7062
  };
7152
7063
  function flush(id) {
7153
7064
  if (disabled || disposed) return;
7154
- const state2 = sessions.get(id);
7155
- if (!state2) return;
7065
+ let state2 = sessions.get(id);
7066
+ if (!state2) {
7067
+ if (readTimelineState(id)?.pending === true) state2 = stateFor(id);
7068
+ else return;
7069
+ }
7156
7070
  if (state2.root === false) return;
7157
7071
  if (!state2.pending || state2.running) return;
7072
+ if (state2.nextAttemptAt && state2.nextAttemptAt > Date.now()) {
7073
+ if (state2.timer) {
7074
+ clearTimeout(state2.timer);
7075
+ state2.timer = void 0;
7076
+ }
7077
+ schedule(id, state2);
7078
+ return;
7079
+ }
7158
7080
  if (state2.timer) {
7159
7081
  clearTimeout(state2.timer);
7160
7082
  state2.timer = void 0;
@@ -7163,10 +7085,11 @@ function timelineHooks(input, options) {
7163
7085
  }
7164
7086
  function schedule(id, state2) {
7165
7087
  if (state2.timer) clearTimeout(state2.timer);
7088
+ const delay = typeof state2.nextAttemptAt === "number" ? Math.max(0, state2.nextAttemptAt - Date.now()) : state2.failures > 0 ? retryDelay(state2.failures) : intervalMs;
7166
7089
  state2.timer = setTimeout(() => {
7167
7090
  state2.timer = void 0;
7168
7091
  void tick(id, state2);
7169
- }, intervalMs);
7092
+ }, delay);
7170
7093
  state2.timer.unref?.();
7171
7094
  }
7172
7095
  async function summarize(id, title, excerptText, prior, abort) {
@@ -7285,13 +7208,13 @@ function timelineHooks(input, options) {
7285
7208
  if (reprocessing.has(id)) return;
7286
7209
  try {
7287
7210
  state2.running = true;
7288
- state2.pending = false;
7289
7211
  let title;
7290
7212
  let info = {};
7291
7213
  if (state2.root === void 0) {
7292
7214
  info = await client.session.get({ path: { id }, query: { directory } }).then((result2) => result2?.data).catch(() => void 0);
7293
7215
  if (!info) {
7294
7216
  sessions.delete(id);
7217
+ removeTimelineState(id);
7295
7218
  return;
7296
7219
  }
7297
7220
  title = info.title;
@@ -7299,6 +7222,7 @@ function timelineHooks(input, options) {
7299
7222
  state2.root = !info.parentID && !isInternalTitle(info.title);
7300
7223
  if (!state2.root) {
7301
7224
  sessions.delete(id);
7225
+ removeTimelineState(id);
7302
7226
  return;
7303
7227
  }
7304
7228
  }
@@ -7306,6 +7230,10 @@ function timelineHooks(input, options) {
7306
7230
  const messages = response?.data ?? [];
7307
7231
  const excerpt = buildExcerpt(messages, state2.cursor);
7308
7232
  if (!excerpt.text) {
7233
+ state2.pending = false;
7234
+ state2.failures = 0;
7235
+ state2.nextAttemptAt = void 0;
7236
+ persist(id, state2);
7309
7237
  return;
7310
7238
  }
7311
7239
  const result = await summarize(id, state2.title ?? info?.title ?? "untitled", excerpt.text, readEntries(id, 4));
@@ -7313,8 +7241,14 @@ function timelineHooks(input, options) {
7313
7241
  if (result.bullets.length) appendEntries(id, result.bullets, Date.now(), excerpt.next);
7314
7242
  state2.cursor = excerpt.next;
7315
7243
  state2.failures = 0;
7244
+ state2.pending = false;
7245
+ state2.nextAttemptAt = void 0;
7246
+ persist(id, state2);
7316
7247
  } else {
7317
7248
  state2.failures++;
7249
+ state2.pending = true;
7250
+ state2.nextAttemptAt = Date.now() + retryDelay(state2.failures);
7251
+ persist(id, state2);
7318
7252
  log("warn", "timeline: summarize failed", { sessionID: id, error: result.error });
7319
7253
  }
7320
7254
  } catch (error) {
@@ -7353,7 +7287,7 @@ function timelineHooks(input, options) {
7353
7287
  if (!info) return "Session not found.";
7354
7288
  const msgRes = await client.session.messages({ path: { id: rootId }, query: { directory } });
7355
7289
  const messages = msgRes?.data ?? [];
7356
- writeFileSync9(temp, "");
7290
+ writeFileSync10(temp, "");
7357
7291
  for (let i = 0; i < MAX_REPROCESS_SLICES; i++) {
7358
7292
  if (abort?.aborted) {
7359
7293
  try {
@@ -7393,13 +7327,21 @@ function timelineHooks(input, options) {
7393
7327
  }
7394
7328
  return "No entries generated; previous timeline kept.";
7395
7329
  }
7396
- renameSync6(temp, targetFile);
7330
+ renameSync7(temp, targetFile);
7397
7331
  const state2 = sessions.get(rootId);
7398
7332
  if (state2) {
7399
7333
  state2.cursor = cursor;
7400
7334
  state2.failures = 0;
7401
7335
  state2.pending = false;
7336
+ state2.nextAttemptAt = void 0;
7402
7337
  }
7338
+ persist(rootId, {
7339
+ running: state2?.running ?? false,
7340
+ pending: false,
7341
+ failures: 0,
7342
+ cursor,
7343
+ nextAttemptAt: void 0
7344
+ });
7403
7345
  return `Rebuilt ${messages.length} messages into ${entries} entries with ${calls} summarizer calls.`;
7404
7346
  } catch (error) {
7405
7347
  try {
@@ -7411,6 +7353,28 @@ function timelineHooks(input, options) {
7411
7353
  reprocessing.delete(rootId);
7412
7354
  }
7413
7355
  }
7356
+ function resume() {
7357
+ if (disabled || disposed) return;
7358
+ let listed;
7359
+ try {
7360
+ listed = listTimelineStates();
7361
+ } catch (error) {
7362
+ debug("resume failed", error);
7363
+ return;
7364
+ }
7365
+ listed.sort((a, b) => b.state.updatedAt - a.state.updatedAt);
7366
+ let scheduled = 0;
7367
+ for (const { sessionID, state: state2 } of listed) {
7368
+ if (state2.pending !== true) continue;
7369
+ if (sessions.has(sessionID)) continue;
7370
+ if (scheduled >= 50) continue;
7371
+ const loaded = stateFor(sessionID);
7372
+ if (loaded.root === false) continue;
7373
+ schedule(sessionID, loaded);
7374
+ scheduled++;
7375
+ }
7376
+ }
7377
+ resume();
7414
7378
  const timelineReprocess = tool2({
7415
7379
  description: "Rebuild the root session's entire timeline from its transcript, replacing the existing entries. Used by /reprocess-timeline.",
7416
7380
  args: {},
@@ -7460,6 +7424,7 @@ function timelineHooks(input, options) {
7460
7424
  }
7461
7425
  sessions.delete(id);
7462
7426
  removeEntries(id);
7427
+ removeTimelineState(id);
7463
7428
  owned.delete(id);
7464
7429
  return;
7465
7430
  }