@lawaty/lacode 1.3.1 → 2.1.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/CHANGELOG.md +40 -1
- package/README.md +3 -6
- package/dist/agents.js +3 -41
- package/dist/commands.js +2 -5
- package/dist/index.js +445 -249
- package/docs/CONTEXT.md +17 -33
- package/docs/USER-STORIES.md +27 -2
- package/package.json +1 -1
- package/scripts/refresh-pool.mjs +74 -63
- package/scripts/score-models.mjs +111 -37
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.
|
|
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
|
|
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
|
|
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)\`).
|
|
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
|
|
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.`
|
|
@@ -4125,110 +4084,10 @@ one ordered pass in slot order: each slot fills to its weight, then overflow goe
|
|
|
4125
4084
|
});
|
|
4126
4085
|
|
|
4127
4086
|
// src/plugins/context-autoupdate.ts
|
|
4128
|
-
import { existsSync as existsSync4, mkdirSync as mkdirSync5, readdirSync as readdirSync2,
|
|
4087
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync5, readdirSync as readdirSync2, statSync as statSync6, unlinkSync as unlinkSync5, writeFileSync as writeFileSync6 } from "node:fs";
|
|
4129
4088
|
import { homedir as homedir4 } from "node:os";
|
|
4130
4089
|
import * as path3 from "node:path";
|
|
4131
4090
|
|
|
4132
|
-
// src/lib/active-backlog.ts
|
|
4133
|
-
var ACTIVE_BACKLOG_FILE = "active.md";
|
|
4134
|
-
var RETENTION_DAYS = 7;
|
|
4135
|
-
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
4136
|
-
var HEADER = `# Active backlog
|
|
4137
|
-
|
|
4138
|
-
Tasks in flight, grouped by the week they were opened. A finished task stays
|
|
4139
|
-
here for a week after it was finished and is then dropped; an open task stays
|
|
4140
|
-
however long it takes. Add a line under the right week, and give a finished
|
|
4141
|
-
task its date: \`- [x] Title \u2014 opened 2026-W43, finished 2026-10-19\`.`;
|
|
4142
|
-
var TASK_LINE = /^\s*(?:[-*+]|\d+[.)])\s*\[([ xX])\]\s*(.*)$/;
|
|
4143
|
-
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;
|
|
4144
|
-
var isCalendarDate = (s) => {
|
|
4145
|
-
const at = Date.parse(`${s}T00:00:00Z`);
|
|
4146
|
-
return Number.isFinite(at) && new Date(at).toISOString().slice(0, 10) === s;
|
|
4147
|
-
};
|
|
4148
|
-
var parseBacklog = (text) => {
|
|
4149
|
-
const entries = [];
|
|
4150
|
-
for (const raw of text.split(/\r?\n/)) {
|
|
4151
|
-
const line = TASK_LINE.exec(raw);
|
|
4152
|
-
if (!line) continue;
|
|
4153
|
-
const finished = line[1] !== " ";
|
|
4154
|
-
let week;
|
|
4155
|
-
let finishedAt;
|
|
4156
|
-
const stripped = line[2].replace(META, (fragment, openedWeek, finishedDate) => {
|
|
4157
|
-
if (openedWeek !== void 0) {
|
|
4158
|
-
week ??= openedWeek.toUpperCase();
|
|
4159
|
-
return "";
|
|
4160
|
-
}
|
|
4161
|
-
if (finishedDate !== void 0 && isCalendarDate(finishedDate)) {
|
|
4162
|
-
finishedAt ??= finishedDate;
|
|
4163
|
-
return "";
|
|
4164
|
-
}
|
|
4165
|
-
return fragment;
|
|
4166
|
-
});
|
|
4167
|
-
const title = stripped.replace(/^[\s—–-]+|[\s—–-]+$/g, "");
|
|
4168
|
-
if (!title) continue;
|
|
4169
|
-
entries.push({ title, ...week ? { week } : {}, finished, ...finishedAt ? { finishedAt } : {} });
|
|
4170
|
-
}
|
|
4171
|
-
return entries;
|
|
4172
|
-
};
|
|
4173
|
-
var ISO_WEEK = /^(\d{4})-W(\d{2})$/i;
|
|
4174
|
-
var newestFirst = (a, b) => {
|
|
4175
|
-
const ma = ISO_WEEK.exec(a);
|
|
4176
|
-
const mb = ISO_WEEK.exec(b);
|
|
4177
|
-
if (ma && mb) return Number(mb[1]) - Number(ma[1]) || Number(mb[2]) - Number(ma[2]);
|
|
4178
|
-
if (ma) return -1;
|
|
4179
|
-
if (mb) return 1;
|
|
4180
|
-
return a < b ? 1 : a > b ? -1 : 0;
|
|
4181
|
-
};
|
|
4182
|
-
var renderLine = (entry) => {
|
|
4183
|
-
const box = entry.finished ? "[x]" : "[ ]";
|
|
4184
|
-
const meta = [];
|
|
4185
|
-
if (entry.week) meta.push(`opened ${entry.week}`);
|
|
4186
|
-
if (entry.finished && entry.finishedAt) meta.push(`finished ${entry.finishedAt}`);
|
|
4187
|
-
const head = `- ${box} ${entry.title}`;
|
|
4188
|
-
if (meta.length === 0) return head;
|
|
4189
|
-
return entry.week ? `${head} \u2014 ${meta.join(", ")}` : `${head}, ${meta.join(", ")}`;
|
|
4190
|
-
};
|
|
4191
|
-
var renderBacklog = (entries) => {
|
|
4192
|
-
const buckets = /* @__PURE__ */ new Map();
|
|
4193
|
-
for (const entry of entries) {
|
|
4194
|
-
const key = entry.week ? entry.week : null;
|
|
4195
|
-
const bucket = buckets.get(key);
|
|
4196
|
-
if (bucket) bucket.push(entry);
|
|
4197
|
-
else buckets.set(key, [entry]);
|
|
4198
|
-
}
|
|
4199
|
-
const weeks = [...buckets.keys()].filter((week) => week !== null).sort(newestFirst);
|
|
4200
|
-
let out = HEADER;
|
|
4201
|
-
for (const week of weeks) out += `
|
|
4202
|
-
|
|
4203
|
-
## ${week}
|
|
4204
|
-
|
|
4205
|
-
${buckets.get(week).map(renderLine).join("\n")}`;
|
|
4206
|
-
const unscheduled = buckets.get(null);
|
|
4207
|
-
if (unscheduled) out += `
|
|
4208
|
-
|
|
4209
|
-
## Unscheduled
|
|
4210
|
-
|
|
4211
|
-
${unscheduled.map(renderLine).join("\n")}`;
|
|
4212
|
-
return `${out}
|
|
4213
|
-
`;
|
|
4214
|
-
};
|
|
4215
|
-
var normalizeBacklog = (text, now = Date.now()) => {
|
|
4216
|
-
const entries = parseBacklog(text);
|
|
4217
|
-
const kept = [];
|
|
4218
|
-
const removed = [];
|
|
4219
|
-
for (const entry of entries) {
|
|
4220
|
-
if (entry.finished && entry.finishedAt) {
|
|
4221
|
-
const at = Date.parse(`${entry.finishedAt}T00:00:00Z`);
|
|
4222
|
-
if (Number.isFinite(at) && now - at > RETENTION_DAYS * DAY_MS) {
|
|
4223
|
-
removed.push(entry.title);
|
|
4224
|
-
continue;
|
|
4225
|
-
}
|
|
4226
|
-
}
|
|
4227
|
-
kept.push(entry);
|
|
4228
|
-
}
|
|
4229
|
-
return { text: renderBacklog(kept), removed };
|
|
4230
|
-
};
|
|
4231
|
-
|
|
4232
4091
|
// src/lib/map-schema.ts
|
|
4233
4092
|
import { existsSync as existsSync3, readFileSync as readFileSync5, statSync as statSync5, writeFileSync as writeFileSync5 } from "node:fs";
|
|
4234
4093
|
import * as path from "node:path";
|
|
@@ -4278,21 +4137,11 @@ var MAP_SPECS = [
|
|
|
4278
4137
|
kind: "entries",
|
|
4279
4138
|
provisionalDays: PROVISIONAL_DAYS,
|
|
4280
4139
|
sectionRequired: true
|
|
4281
|
-
},
|
|
4282
|
-
{
|
|
4283
|
-
file: ACTIVE_BACKLOG_FILE,
|
|
4284
|
-
charter: "work in flight, grouped by the ISO week it was opened",
|
|
4285
|
-
budget: 60,
|
|
4286
|
-
minEntries: 0,
|
|
4287
|
-
kind: "backlog",
|
|
4288
|
-
provisionalDays: PROVISIONAL_DAYS,
|
|
4289
|
-
sectionRequired: false
|
|
4290
4140
|
}
|
|
4291
4141
|
];
|
|
4142
|
+
var NAVIGATION_FILES = MAP_SPECS.map((spec) => spec.file);
|
|
4292
4143
|
var MAP_SPEC_BY_FILE = new Map(MAP_SPECS.map((spec) => [spec.file, spec]));
|
|
4293
|
-
var
|
|
4294
|
-
var NAVIGATION_FILES = MAP_SPECS.filter((spec) => spec.kind === "entries").map((spec) => spec.file);
|
|
4295
|
-
var DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
4144
|
+
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
4296
4145
|
var ENTRY_START = /^([-*+])\s+\[(binding|provisional|disputed)\]\s+(.*)$/i;
|
|
4297
4146
|
var CONTINUATION = /^\s{2,}\S/;
|
|
4298
4147
|
var LIST_ITEM = /^\s*(?:[-*+]|\d+[.)])\s+/;
|
|
@@ -4303,7 +4152,7 @@ var VERIFIED = /\(verified\s+(\d{4}-\d{2}-\d{2})\)\s*$/i;
|
|
|
4303
4152
|
var POINTER = /(?:—|–|→|->)\s*See\s+`([^`\n]+)`(?:\s*§\s*([^()\n]+?))?(?:\s*\(verified\s+(\d{4}-\d{2}-\d{2})\))?\s*$/i;
|
|
4304
4153
|
var DECISION_RECORD = /^\s*(?:#{1,6}\s*)?(?:\*\*)?(Context|Decision|Consequences|Alternatives|Status|Trade-?offs|Rationale)(?:\*\*)?\s*(?::|$)/im;
|
|
4305
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;
|
|
4306
|
-
var
|
|
4155
|
+
var isCalendarDate = (value) => {
|
|
4307
4156
|
const at = Date.parse(`${value}T00:00:00Z`);
|
|
4308
4157
|
return Number.isFinite(at) && new Date(at).toISOString().slice(0, 10) === value;
|
|
4309
4158
|
};
|
|
@@ -4521,10 +4370,6 @@ var validateMapText = (spec, text, options) => {
|
|
|
4521
4370
|
if (nonBlank > spec.budget) {
|
|
4522
4371
|
push("budget", `${nonBlank} non-blank lines exceeds the ${spec.budget}-line budget; consolidate, do not pad`);
|
|
4523
4372
|
}
|
|
4524
|
-
if (spec.kind === "backlog") {
|
|
4525
|
-
if (!text.startsWith("# ")) push("charter", "active backlog must start with a top-level heading");
|
|
4526
|
-
return { spec, lines: nonBlank, entries: [], violations };
|
|
4527
|
-
}
|
|
4528
4373
|
const entries = parseMapEntries(text);
|
|
4529
4374
|
let inFence = false;
|
|
4530
4375
|
let tableReported = false;
|
|
@@ -4566,9 +4411,9 @@ var validateMapText = (spec, text, options) => {
|
|
|
4566
4411
|
}
|
|
4567
4412
|
if (!entry.verified) {
|
|
4568
4413
|
push("schema", "entry has no (verified YYYY-MM-DD); the lifecycle and drift checks need it", line);
|
|
4569
|
-
} else if (!
|
|
4414
|
+
} else if (!isCalendarDate(entry.verified)) {
|
|
4570
4415
|
push("schema", `verified date ${entry.verified} is not a real calendar date`, line);
|
|
4571
|
-
} else if (Date.parse(`${entry.verified}T00:00:00Z`) > (options.now ?? Date.now()) +
|
|
4416
|
+
} else if (Date.parse(`${entry.verified}T00:00:00Z`) > (options.now ?? Date.now()) + DAY_MS) {
|
|
4572
4417
|
push("schema", `verified date ${entry.verified} is more than a day in the future`, line);
|
|
4573
4418
|
}
|
|
4574
4419
|
if (!entry.pointer) {
|
|
@@ -4618,14 +4463,6 @@ var validateMapText = (spec, text, options) => {
|
|
|
4618
4463
|
};
|
|
4619
4464
|
var normalizeMapText = (spec, text, options = {}) => {
|
|
4620
4465
|
const now = options.now ?? Date.now();
|
|
4621
|
-
if (spec.kind === "backlog") {
|
|
4622
|
-
const result = normalizeBacklog(text, now);
|
|
4623
|
-
return {
|
|
4624
|
-
text: result.text,
|
|
4625
|
-
removed: result.removed.map((title) => ({ line: 0, text: title })),
|
|
4626
|
-
kept: 0
|
|
4627
|
-
};
|
|
4628
|
-
}
|
|
4629
4466
|
const entries = parseMapEntries(text);
|
|
4630
4467
|
const drop = /* @__PURE__ */ new Set();
|
|
4631
4468
|
const removed = [];
|
|
@@ -4635,7 +4472,7 @@ var normalizeMapText = (spec, text, options = {}) => {
|
|
|
4635
4472
|
if (options.canResolve && !options.canResolve(entry.pointer, entry.section)) continue;
|
|
4636
4473
|
const at = Date.parse(`${entry.verified}T00:00:00Z`);
|
|
4637
4474
|
if (!Number.isFinite(at)) continue;
|
|
4638
|
-
if (now - at <= spec.provisionalDays *
|
|
4475
|
+
if (now - at <= spec.provisionalDays * DAY_MS) continue;
|
|
4639
4476
|
for (let i = entry.startLine; i < entry.endLine; i++) drop.add(i);
|
|
4640
4477
|
removed.push({ line: entry.startLine + 1, text: truncate(entry.rule) });
|
|
4641
4478
|
}
|
|
@@ -4665,8 +4502,6 @@ var maintainMap = (root, mapDir, now = Date.now()) => {
|
|
|
4665
4502
|
if (result.removed.length > 0) {
|
|
4666
4503
|
removedTotal += result.removed.length;
|
|
4667
4504
|
files.push({ file: spec.file, changed, removed: result.removed });
|
|
4668
|
-
} else if (changed) {
|
|
4669
|
-
files.push({ file: spec.file, changed, removed: [] });
|
|
4670
4505
|
}
|
|
4671
4506
|
}
|
|
4672
4507
|
return { files, removedTotal };
|
|
@@ -4692,21 +4527,19 @@ var auditMap = (root, mapDir, now = Date.now()) => {
|
|
|
4692
4527
|
const result = validateMapText(spec, text, { root, now });
|
|
4693
4528
|
files.push(result);
|
|
4694
4529
|
violations.push(...result.violations);
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
rulesSeen.set(key, spec.file);
|
|
4709
|
-
}
|
|
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);
|
|
4710
4543
|
}
|
|
4711
4544
|
}
|
|
4712
4545
|
}
|
|
@@ -4974,7 +4807,6 @@ var contextAutoUpdateHooks = (async ({ client, directory }, options) => {
|
|
|
4974
4807
|
});
|
|
4975
4808
|
const root = path3.resolve(directory);
|
|
4976
4809
|
const contextAbs = path3.join(root, CONTEXT_DIR2);
|
|
4977
|
-
const activeBacklogPath = path3.join(contextAbs, ACTIVE_BACKLOG_FILE);
|
|
4978
4810
|
const config = findProjectConfig();
|
|
4979
4811
|
const writerProblem = config ? verifyWriterRule(config, contextAbs, root) : void 0;
|
|
4980
4812
|
if (writerProblem) void log("error", writerProblem);
|
|
@@ -4984,22 +4816,6 @@ var contextAutoUpdateHooks = (async ({ client, directory }, options) => {
|
|
|
4984
4816
|
if (absolute === contextAbs || absolute.startsWith(contextAbs + path3.sep)) return false;
|
|
4985
4817
|
return !toPosix2(absolute).split("/").some((segment) => IGNORED_SEGMENTS.has(segment));
|
|
4986
4818
|
};
|
|
4987
|
-
const normalizeActiveBacklog = async () => {
|
|
4988
|
-
try {
|
|
4989
|
-
if (!existsSync4(activeBacklogPath)) return { removed: 0 };
|
|
4990
|
-
const before = readFileSync7(activeBacklogPath, "utf8");
|
|
4991
|
-
const { text, removed } = normalizeBacklog(before);
|
|
4992
|
-
if (text !== before) writeFileSync6(activeBacklogPath, text);
|
|
4993
|
-
if (removed.length > 0) {
|
|
4994
|
-
await log("info", `active backlog: dropped ${removed.length} task(s) finished more than a week ago`, {
|
|
4995
|
-
removed: removed.length
|
|
4996
|
-
});
|
|
4997
|
-
}
|
|
4998
|
-
return { removed: removed.length };
|
|
4999
|
-
} catch {
|
|
5000
|
-
return { removed: 0 };
|
|
5001
|
-
}
|
|
5002
|
-
};
|
|
5003
4819
|
const applyLifecycle = async () => {
|
|
5004
4820
|
try {
|
|
5005
4821
|
const result = maintainMap(root, contextAbs);
|
|
@@ -5059,7 +4875,7 @@ var contextAutoUpdateHooks = (async ({ client, directory }, options) => {
|
|
|
5059
4875
|
"",
|
|
5060
4876
|
ENTRY_CONTRACT,
|
|
5061
4877
|
"",
|
|
5062
|
-
"Follow the bootstrap procedure in your system prompt: progressive exploration, then write the five context files within their line budgets.
|
|
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."
|
|
5063
4879
|
].join("\n") : [
|
|
5064
4880
|
"Incremental context update. Project root: " + directory,
|
|
5065
4881
|
"",
|
|
@@ -5070,17 +4886,13 @@ var contextAutoUpdateHooks = (async ({ client, directory }, options) => {
|
|
|
5070
4886
|
"",
|
|
5071
4887
|
ENTRY_CONTRACT,
|
|
5072
4888
|
"",
|
|
5073
|
-
'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
|
|
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".'
|
|
5074
4890
|
].join("\n");
|
|
5075
4891
|
return {
|
|
5076
4892
|
event: async ({ event }) => {
|
|
5077
4893
|
try {
|
|
5078
4894
|
if (event.type === "file.edited") {
|
|
5079
4895
|
const file = event.properties.file;
|
|
5080
|
-
if (path3.resolve(root, file) === activeBacklogPath) {
|
|
5081
|
-
await normalizeActiveBacklog();
|
|
5082
|
-
return;
|
|
5083
|
-
}
|
|
5084
4896
|
if (isTracked(file)) pending.add(file);
|
|
5085
4897
|
return;
|
|
5086
4898
|
}
|
|
@@ -5192,7 +5004,7 @@ var contextAutoUpdateHooks = (async ({ client, directory }, options) => {
|
|
|
5192
5004
|
});
|
|
5193
5005
|
|
|
5194
5006
|
// src/lib/feedback.ts
|
|
5195
|
-
import { mkdirSync as mkdirSync6, readFileSync as
|
|
5007
|
+
import { mkdirSync as mkdirSync6, readFileSync as readFileSync7, renameSync as renameSync4, unlinkSync as unlinkSync6, writeFileSync as writeFileSync7 } from "node:fs";
|
|
5196
5008
|
import { homedir as homedir5 } from "node:os";
|
|
5197
5009
|
import { dirname as dirname4, join as join9 } from "node:path";
|
|
5198
5010
|
var FEEDBACK_HINT_ENV = "LACODE_NO_FEEDBACK_HINT";
|
|
@@ -5200,7 +5012,7 @@ var FEEDBACK_HINT = "Enjoying lacode? Bugs, ideas, or a quick note on how you us
|
|
|
5200
5012
|
var feedbackStatePath = () => join9(homedir5(), ".local", "share", "opencode", "lacode", "feedback.json");
|
|
5201
5013
|
function readFeedbackState(path4 = feedbackStatePath()) {
|
|
5202
5014
|
try {
|
|
5203
|
-
const parsed = JSON.parse(
|
|
5015
|
+
const parsed = JSON.parse(readFileSync7(path4, "utf8"));
|
|
5204
5016
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return void 0;
|
|
5205
5017
|
return parsed;
|
|
5206
5018
|
} catch (error) {
|
|
@@ -5274,7 +5086,7 @@ async function feedbackHooks(input, options, deps = {}) {
|
|
|
5274
5086
|
|
|
5275
5087
|
// src/plugins/image-paste.ts
|
|
5276
5088
|
import { createHash, randomUUID } from "node:crypto";
|
|
5277
|
-
import { globSync, mkdirSync as mkdirSync7, readFileSync as
|
|
5089
|
+
import { globSync, mkdirSync as mkdirSync7, readFileSync as readFileSync8, renameSync as renameSync5, statSync as statSync7, writeFileSync as writeFileSync8 } from "node:fs";
|
|
5278
5090
|
import { homedir as homedir6 } from "node:os";
|
|
5279
5091
|
import { basename as basename3, join as join10 } from "node:path";
|
|
5280
5092
|
|
|
@@ -5531,7 +5343,7 @@ async function imagePasteHooks(input, options, deps) {
|
|
|
5531
5343
|
}
|
|
5532
5344
|
let bytes;
|
|
5533
5345
|
try {
|
|
5534
|
-
bytes =
|
|
5346
|
+
bytes = readFileSync8(filePath);
|
|
5535
5347
|
} catch {
|
|
5536
5348
|
skipped.push(name);
|
|
5537
5349
|
continue;
|
|
@@ -6677,27 +6489,36 @@ async function selfUpdateHooks(input, options) {
|
|
|
6677
6489
|
}
|
|
6678
6490
|
|
|
6679
6491
|
// src/plugins/timeline.ts
|
|
6680
|
-
import { writeFileSync as
|
|
6492
|
+
import { existsSync as existsSync5, writeFileSync as writeFileSync10, renameSync as renameSync7, rmSync as rmSync3 } from "node:fs";
|
|
6493
|
+
import { join as join12 } from "node:path";
|
|
6681
6494
|
import { tool as tool2 } from "@opencode-ai/plugin";
|
|
6682
6495
|
|
|
6683
6496
|
// src/lib/timeline.ts
|
|
6684
|
-
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync8, readFileSync as
|
|
6497
|
+
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";
|
|
6685
6498
|
import { homedir as homedir7 } from "node:os";
|
|
6686
6499
|
import { dirname as dirname5, join as join11, resolve as resolve4 } from "node:path";
|
|
6500
|
+
var TIMELINE_STATE_VERSION = 1;
|
|
6687
6501
|
function timelineDir() {
|
|
6688
6502
|
const env = process.env.LACODE_TIMELINE_DIR;
|
|
6689
6503
|
if (env) return resolve4(env);
|
|
6690
6504
|
return join11(homedir7(), ".local/share/opencode/lacode/timeline");
|
|
6691
6505
|
}
|
|
6692
|
-
function
|
|
6506
|
+
function assertValidSessionID(sessionID) {
|
|
6693
6507
|
if (!/^[A-Za-z0-9_.-]+$/.test(sessionID)) throw new Error("invalid sessionID");
|
|
6508
|
+
}
|
|
6509
|
+
function timelineFile(sessionID) {
|
|
6510
|
+
assertValidSessionID(sessionID);
|
|
6694
6511
|
return join11(timelineDir(), sessionID + ".jsonl");
|
|
6695
6512
|
}
|
|
6513
|
+
function timelineStateFile(sessionID) {
|
|
6514
|
+
assertValidSessionID(sessionID);
|
|
6515
|
+
return join11(timelineDir(), sessionID + ".state.json");
|
|
6516
|
+
}
|
|
6696
6517
|
function normalizeThrough(through) {
|
|
6697
6518
|
if (!through || typeof through !== "object") return void 0;
|
|
6698
6519
|
const out = {};
|
|
6699
|
-
if (typeof through.messageID === "string") out.messageID = through.messageID;
|
|
6700
|
-
if (typeof through.time === "number") out.time = through.time;
|
|
6520
|
+
if (typeof through.messageID === "string" && through.messageID.length > 0) out.messageID = through.messageID;
|
|
6521
|
+
if (typeof through.time === "number" && Number.isFinite(through.time)) out.time = through.time;
|
|
6701
6522
|
if (out.messageID === void 0 && out.time === void 0) return void 0;
|
|
6702
6523
|
return out;
|
|
6703
6524
|
}
|
|
@@ -6738,7 +6559,7 @@ function appendEntriesAt(file, texts, now, through) {
|
|
|
6738
6559
|
function readEntriesAt(file, limit) {
|
|
6739
6560
|
let raw = "";
|
|
6740
6561
|
try {
|
|
6741
|
-
raw =
|
|
6562
|
+
raw = readFileSync9(file, "utf8");
|
|
6742
6563
|
} catch {
|
|
6743
6564
|
return [];
|
|
6744
6565
|
}
|
|
@@ -6772,6 +6593,70 @@ function removeEntries(sessionID) {
|
|
|
6772
6593
|
} catch {
|
|
6773
6594
|
}
|
|
6774
6595
|
}
|
|
6596
|
+
function normalizePersistedState(raw) {
|
|
6597
|
+
const obj = raw && typeof raw === "object" ? raw : {};
|
|
6598
|
+
const cursor = normalizeThrough(obj.cursor);
|
|
6599
|
+
const failures = typeof obj.failures === "number" && Number.isFinite(obj.failures) ? Math.max(0, Math.trunc(obj.failures)) : 0;
|
|
6600
|
+
const pending = obj.pending === true;
|
|
6601
|
+
const nextAttemptAt = typeof obj.nextAttemptAt === "number" && Number.isFinite(obj.nextAttemptAt) && obj.nextAttemptAt > 0 ? obj.nextAttemptAt : void 0;
|
|
6602
|
+
const updatedAt = typeof obj.updatedAt === "number" && Number.isFinite(obj.updatedAt) ? obj.updatedAt : Date.now();
|
|
6603
|
+
const state2 = {
|
|
6604
|
+
version: TIMELINE_STATE_VERSION,
|
|
6605
|
+
failures,
|
|
6606
|
+
pending,
|
|
6607
|
+
updatedAt
|
|
6608
|
+
};
|
|
6609
|
+
if (cursor) state2.cursor = cursor;
|
|
6610
|
+
if (nextAttemptAt !== void 0) state2.nextAttemptAt = nextAttemptAt;
|
|
6611
|
+
return state2;
|
|
6612
|
+
}
|
|
6613
|
+
function readTimelineState(sessionID) {
|
|
6614
|
+
try {
|
|
6615
|
+
const raw = readFileSync9(timelineStateFile(sessionID), "utf8");
|
|
6616
|
+
return normalizePersistedState(JSON.parse(raw));
|
|
6617
|
+
} catch {
|
|
6618
|
+
return void 0;
|
|
6619
|
+
}
|
|
6620
|
+
}
|
|
6621
|
+
function writeTimelineState(sessionID, state2) {
|
|
6622
|
+
const file = timelineStateFile(sessionID);
|
|
6623
|
+
mkdirSync8(dirname5(file), { recursive: true });
|
|
6624
|
+
const tmp = file + ".tmp." + process.pid + "." + Math.random().toString(36).slice(2);
|
|
6625
|
+
try {
|
|
6626
|
+
writeFileSync9(tmp, JSON.stringify(state2) + "\n");
|
|
6627
|
+
renameSync6(tmp, file);
|
|
6628
|
+
} catch (err) {
|
|
6629
|
+
try {
|
|
6630
|
+
rmSync2(tmp, { force: true });
|
|
6631
|
+
} catch {
|
|
6632
|
+
}
|
|
6633
|
+
throw err;
|
|
6634
|
+
}
|
|
6635
|
+
}
|
|
6636
|
+
function removeTimelineState(sessionID) {
|
|
6637
|
+
try {
|
|
6638
|
+
rmSync2(timelineStateFile(sessionID), { force: true });
|
|
6639
|
+
} catch {
|
|
6640
|
+
}
|
|
6641
|
+
}
|
|
6642
|
+
function listTimelineStates() {
|
|
6643
|
+
let names;
|
|
6644
|
+
try {
|
|
6645
|
+
names = readdirSync3(timelineDir());
|
|
6646
|
+
} catch {
|
|
6647
|
+
return [];
|
|
6648
|
+
}
|
|
6649
|
+
const out = [];
|
|
6650
|
+
for (const name of names) {
|
|
6651
|
+
if (!name.endsWith(".state.json")) continue;
|
|
6652
|
+
const sessionID = name.slice(0, -".state.json".length);
|
|
6653
|
+
if (!/^[A-Za-z0-9_.-]+$/.test(sessionID)) continue;
|
|
6654
|
+
const state2 = readTimelineState(sessionID);
|
|
6655
|
+
if (!state2) continue;
|
|
6656
|
+
out.push({ sessionID, state: state2 });
|
|
6657
|
+
}
|
|
6658
|
+
return out;
|
|
6659
|
+
}
|
|
6775
6660
|
function collapse(s) {
|
|
6776
6661
|
return s.replace(/\s+/g, " ").trim();
|
|
6777
6662
|
}
|
|
@@ -6877,6 +6762,49 @@ function summarizeToolResult(state2) {
|
|
|
6877
6762
|
}
|
|
6878
6763
|
return "";
|
|
6879
6764
|
}
|
|
6765
|
+
function isSideTool(tool3, input) {
|
|
6766
|
+
if (tool3 === "timeline_reprocess" || tool3 === "pool_status" || tool3 === "pool_recheck") return true;
|
|
6767
|
+
if (tool3 === "task" && input !== null && input !== void 0 && typeof input === "object") {
|
|
6768
|
+
return input.subagent_type === "context";
|
|
6769
|
+
}
|
|
6770
|
+
return false;
|
|
6771
|
+
}
|
|
6772
|
+
function mentionsSideMaintenance(text) {
|
|
6773
|
+
return /\b(?:timeline_reprocess|pool_status|pool_recheck|reprocess-timeline|cartographer)\b/i.test(text);
|
|
6774
|
+
}
|
|
6775
|
+
function isSideMaintenanceTask(input, rootDirectory, projectOwnsTooling) {
|
|
6776
|
+
if (projectOwnsTooling) return false;
|
|
6777
|
+
let description = "";
|
|
6778
|
+
let prompt = "";
|
|
6779
|
+
if (input !== null && input !== void 0 && typeof input === "object") {
|
|
6780
|
+
const rec = input;
|
|
6781
|
+
if (typeof rec.description === "string") description = rec.description;
|
|
6782
|
+
if (typeof rec.prompt === "string") prompt = rec.prompt;
|
|
6783
|
+
}
|
|
6784
|
+
const text = [description, prompt].filter((s) => typeof s === "string" && s.length > 0).join("\n");
|
|
6785
|
+
if (text === "") return false;
|
|
6786
|
+
if (/\.config\/(?:opencode|lacode)|\.local\/share\/opencode/i.test(text)) return true;
|
|
6787
|
+
if (mentionsSideMaintenance(text)) return true;
|
|
6788
|
+
const root = rootDirectory.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
6789
|
+
for (const match of text.matchAll(/\/(?:home|Users|root|opt|srv|mnt|workspace|workspaces)\/[^\s"'`),;]+/gi)) {
|
|
6790
|
+
const cleaned = match[0].replace(/[.,:;!?]+$/, "");
|
|
6791
|
+
if (!(cleaned.includes("/Projects/") || cleaned.includes("/projects/"))) continue;
|
|
6792
|
+
if (cleaned === root || cleaned.startsWith(root + "/")) continue;
|
|
6793
|
+
return true;
|
|
6794
|
+
}
|
|
6795
|
+
return false;
|
|
6796
|
+
}
|
|
6797
|
+
function taskChildSessionId(part) {
|
|
6798
|
+
const sessionId = part.state?.metadata?.sessionId;
|
|
6799
|
+
if (typeof sessionId === "string" && sessionId.length > 0) return sessionId;
|
|
6800
|
+
const output = part.state?.output;
|
|
6801
|
+
if (typeof output !== "string") return void 0;
|
|
6802
|
+
const taskMatch = output.match(/task id="([A-Za-z0-9_.-]+)"/);
|
|
6803
|
+
if (taskMatch) return taskMatch[1];
|
|
6804
|
+
const sesMatch = output.match(/ses_[A-Za-z0-9]+/);
|
|
6805
|
+
if (sesMatch) return sesMatch[0];
|
|
6806
|
+
return void 0;
|
|
6807
|
+
}
|
|
6880
6808
|
function renderMessage(m) {
|
|
6881
6809
|
const role = m.info.role;
|
|
6882
6810
|
const lines = [];
|
|
@@ -6890,18 +6818,29 @@ function renderMessage(m) {
|
|
|
6890
6818
|
}
|
|
6891
6819
|
} else if (role === "assistant") {
|
|
6892
6820
|
for (const p of m.parts) {
|
|
6821
|
+
if (p.side === true) continue;
|
|
6893
6822
|
if (p.type === "text") {
|
|
6894
6823
|
const text = collapse(p.text ?? "");
|
|
6895
6824
|
lines.push(`[assistant] ${trunc(text, 1500)}`);
|
|
6896
6825
|
} else if (p.type === "tool") {
|
|
6826
|
+
if (p.side === true) continue;
|
|
6897
6827
|
const tool3 = p.tool ?? "";
|
|
6898
|
-
if (tool3
|
|
6828
|
+
if (isSideTool(tool3, p.state?.input)) continue;
|
|
6899
6829
|
const status = p.state?.status;
|
|
6900
6830
|
const summary = inputSummary(p.state?.input);
|
|
6901
|
-
|
|
6831
|
+
let result = summarizeToolResult(p.state);
|
|
6832
|
+
let suppressInput = false;
|
|
6833
|
+
if (tool3 === "task" && typeof p.childTitle === "string" && collapse(p.childTitle) !== "") {
|
|
6834
|
+
const title = trunc(collapse(p.childTitle), 160);
|
|
6835
|
+
result = title;
|
|
6836
|
+
if (typeof p.childOutcome === "string" && collapse(p.childOutcome) !== "") {
|
|
6837
|
+
result = `${title}: ${trunc(collapse(p.childOutcome), 600)}`;
|
|
6838
|
+
}
|
|
6839
|
+
if (summary && title.startsWith(summary)) suppressInput = true;
|
|
6840
|
+
}
|
|
6902
6841
|
let line = `[tool] ${tool3}`;
|
|
6903
6842
|
if (status) line += ` (${status})`;
|
|
6904
|
-
const inputPart = summary ? trunc(summary, 220) : "";
|
|
6843
|
+
const inputPart = summary && !suppressInput ? trunc(summary, 220) : "";
|
|
6905
6844
|
let rest = "";
|
|
6906
6845
|
if (inputPart) rest += ` ${inputPart}`;
|
|
6907
6846
|
if (result) rest += ` -> ${result}`;
|
|
@@ -7026,12 +6965,17 @@ var DEFAULT_MAX_ENTRIES = 8;
|
|
|
7026
6965
|
var MAX_ENTRIES = 10;
|
|
7027
6966
|
var PROMPT_TIMEOUT_MS = 9e4;
|
|
7028
6967
|
var SYSTEM = `You are the session timeline summarizer. You receive a slice of a coding session transcript.
|
|
7029
|
-
|
|
6968
|
+
Maintain a timeline of the project's own work: features, fixes, refactors, behavior changes, user-visible capabilities, architectural decisions, data/model changes, and documentation that defines project behavior.
|
|
6969
|
+
Write 1-8 timeline entries describing that project work.
|
|
7030
6970
|
|
|
7031
6971
|
Rules:
|
|
7032
6972
|
- One entry per line, each starting with "- ".
|
|
7033
6973
|
- Each entry is 6-16 words, past tense, plain text.
|
|
7034
|
-
- Cover each distinct task in the slice. Do not drop a
|
|
6974
|
+
- Cover each distinct project task in the slice. Do not drop a feature, fix, refactor, behavior change, user-visible capability, architectural decision, data/model change, or documentation that defines project behavior; merge only exact duplicates.
|
|
6975
|
+
- Record project test changes at the capability level, not as housekeeping; do not report counts.
|
|
6976
|
+
- Ignore self-maintenance and infrastructure: the context map/cartographer, timeline rebuilding/summarization, model-pool status/rechecks, plugin install/update, agent/command configuration, formatting/lint/dependency churn, and build/dist artifacts.
|
|
6977
|
+
- Name modules, components, services, or capabilities when they are the subject. Do not list file paths, line numbers, counts, commit hashes, or build artifacts.
|
|
6978
|
+
- Record decisions or findings that changed the plan, not routine searches or reads.
|
|
7035
6979
|
- Describe the TASK or OUTCOME, not the implementation. "Pool: added bucket-based routing" is right; "added b:<name> to pool.ts and agent-pool.ts" is too detailed.
|
|
7036
6980
|
- Never describe the mechanism: no flags, aliases, overrides, endpoints, probes, config keys, declarations, tags, or file changes. State what changed for the system: which capability now works, which bug is fixed, which behavior changed.
|
|
7037
6981
|
- Never use these words in an entry: tag, alias, override, flag, probe, endpoint, config key, catalog, file, line, count.
|
|
@@ -7039,13 +6983,12 @@ Rules:
|
|
|
7039
6983
|
- Use a short area prefix when it clarifies: "Pool:", "Providers:", "Config:", "Docs:", "Tests:", "Fix:", "Feature:".
|
|
7040
6984
|
- Name the capability, behavior, feature, bug, or decision. Do not name files, paths, functions, commands, environment variables, API keys, raw model IDs, test file names, or build artifacts. Provider and product names are allowed when they identify the task.
|
|
7041
6985
|
- Do not include counts of slots, providers, tests, lines, or files.
|
|
7042
|
-
- Omit internal housekeeping: builds, dist rebuilds, test-file additions, formatting, and dependency churn. If the slice only contains housekeeping, output nothing.
|
|
7043
6986
|
- Describe what was DONE or DECIDED. Never write "User asked...", "Assistant said...", "Discussed...", "Reviewed...", "Explained...", or "Considered...".
|
|
7044
6987
|
- Merge related changes into one stage; do not repeat the same task in different words.
|
|
7045
6988
|
- No markdown: no bold, italics, backticks, or code blocks.
|
|
7046
6989
|
- If a technical detail is the essence of the task, name it at the capability level: "Added Cloudflare Workers AI as a provider", not its account-id variables.
|
|
7047
|
-
- If the slice contains no
|
|
7048
|
-
- Output only the entries. No headings, no preamble, no closing sentence.
|
|
6990
|
+
- If the slice contains no project work, output exactly NO_PROJECT_CHANGES and nothing else. Otherwise output only the bullets.
|
|
6991
|
+
- Output only the entries or NO_PROJECT_CHANGES. No headings, no preamble, no closing sentence.
|
|
7049
6992
|
|
|
7050
6993
|
Example output:
|
|
7051
6994
|
- Pool: added bucket-based routing for model groups.
|
|
@@ -7073,7 +7016,7 @@ This:
|
|
|
7073
7016
|
- Pool: moved shared-window models to the end.
|
|
7074
7017
|
- Providers: replaced the retired Gemini 2.5 Flash with Gemini 3.5 Flash.
|
|
7075
7018
|
|
|
7076
|
-
Your entire response must consist only of lines beginning with "- ".`;
|
|
7019
|
+
Your entire response must consist only of lines beginning with "- ", or exactly NO_PROJECT_CHANGES.`;
|
|
7077
7020
|
function errorText(value) {
|
|
7078
7021
|
if (value instanceof Error) return value.message;
|
|
7079
7022
|
if (typeof value === "string") return value;
|
|
@@ -7088,6 +7031,85 @@ function errorText(value) {
|
|
|
7088
7031
|
}
|
|
7089
7032
|
return String(value);
|
|
7090
7033
|
}
|
|
7034
|
+
var MAX_TASK_CHILDREN_EXPANDED = 40;
|
|
7035
|
+
async function expandTaskSessions(client, messages, directory, owned, onError, cursor) {
|
|
7036
|
+
const projectOwnsTooling = existsSync5(join12(directory, "src/plugins/timeline.ts"));
|
|
7037
|
+
let attempts = 0;
|
|
7038
|
+
const expandedMessages = [];
|
|
7039
|
+
let startIndex = 0;
|
|
7040
|
+
if (cursor) {
|
|
7041
|
+
const cursorIndex = messages.findIndex((m) => m.info.id === cursor);
|
|
7042
|
+
if (cursorIndex >= 0) startIndex = cursorIndex + 1;
|
|
7043
|
+
}
|
|
7044
|
+
for (let messageIndex = 0; messageIndex < messages.length; messageIndex++) {
|
|
7045
|
+
const message = messages[messageIndex];
|
|
7046
|
+
if (messageIndex < startIndex) {
|
|
7047
|
+
expandedMessages.push(message);
|
|
7048
|
+
continue;
|
|
7049
|
+
}
|
|
7050
|
+
const parts = message.parts ?? [];
|
|
7051
|
+
let changed = false;
|
|
7052
|
+
const nextParts = [];
|
|
7053
|
+
for (const part of parts) {
|
|
7054
|
+
if (message.info.role === "assistant" && part.type === "text" && !projectOwnsTooling && mentionsSideMaintenance(part.text ?? "")) {
|
|
7055
|
+
nextParts.push({ ...part, side: true });
|
|
7056
|
+
changed = true;
|
|
7057
|
+
continue;
|
|
7058
|
+
}
|
|
7059
|
+
if (part.type !== "tool" || part.tool !== "task" || attempts >= MAX_TASK_CHILDREN_EXPANDED) {
|
|
7060
|
+
nextParts.push(part);
|
|
7061
|
+
continue;
|
|
7062
|
+
}
|
|
7063
|
+
if (isSideMaintenanceTask(part.state?.input, directory, projectOwnsTooling)) {
|
|
7064
|
+
nextParts.push({ ...part, side: true });
|
|
7065
|
+
continue;
|
|
7066
|
+
}
|
|
7067
|
+
if (isSideTool("task", part.state?.input)) {
|
|
7068
|
+
nextParts.push({ ...part, side: true });
|
|
7069
|
+
continue;
|
|
7070
|
+
}
|
|
7071
|
+
if (typeof part.childTitle === "string" && part.childTitle.trim() !== "") {
|
|
7072
|
+
nextParts.push(part);
|
|
7073
|
+
continue;
|
|
7074
|
+
}
|
|
7075
|
+
const childId = taskChildSessionId(part);
|
|
7076
|
+
if (!childId || owned.has(childId)) {
|
|
7077
|
+
nextParts.push(part);
|
|
7078
|
+
continue;
|
|
7079
|
+
}
|
|
7080
|
+
attempts++;
|
|
7081
|
+
try {
|
|
7082
|
+
const fetched = await client.session.get({ path: { id: childId }, query: { directory } });
|
|
7083
|
+
const info = fetched?.data;
|
|
7084
|
+
const rawTitle = typeof info?.title === "string" ? info.title.trim() : "";
|
|
7085
|
+
if (!rawTitle || isInternalTitle(info?.title)) {
|
|
7086
|
+
nextParts.push(part);
|
|
7087
|
+
continue;
|
|
7088
|
+
}
|
|
7089
|
+
const childRes = await client.session.messages({ path: { id: childId }, query: { directory } });
|
|
7090
|
+
const childMessages = childRes?.data ?? [];
|
|
7091
|
+
let childOutcome;
|
|
7092
|
+
for (let i = childMessages.length - 1; i >= 0; i--) {
|
|
7093
|
+
const childMessage = childMessages[i];
|
|
7094
|
+
if (childMessage?.info?.role !== "assistant") continue;
|
|
7095
|
+
const textParts = (childMessage.parts ?? []).filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text ?? "");
|
|
7096
|
+
const joined = textParts.join("\n").replace(/\s+/g, " ").trim();
|
|
7097
|
+
if (!joined) continue;
|
|
7098
|
+
childOutcome = joined.length > 800 ? joined.slice(0, 800) : joined;
|
|
7099
|
+
break;
|
|
7100
|
+
}
|
|
7101
|
+
const cloned = childOutcome ? { ...part, childTitle: rawTitle, childOutcome } : { ...part, childTitle: rawTitle };
|
|
7102
|
+
nextParts.push(cloned);
|
|
7103
|
+
changed = true;
|
|
7104
|
+
} catch (error) {
|
|
7105
|
+
if (onError) onError("expand-task-session failed", { childId, error });
|
|
7106
|
+
nextParts.push(part);
|
|
7107
|
+
}
|
|
7108
|
+
}
|
|
7109
|
+
expandedMessages.push(changed ? { ...message, parts: nextParts } : message);
|
|
7110
|
+
}
|
|
7111
|
+
return expandedMessages;
|
|
7112
|
+
}
|
|
7091
7113
|
function timelineHooks(input, options) {
|
|
7092
7114
|
const { client, directory } = input;
|
|
7093
7115
|
const opts = options ?? {};
|
|
@@ -7104,6 +7126,11 @@ function timelineHooks(input, options) {
|
|
|
7104
7126
|
const intervalMs = Number.isFinite(rawInterval) ? Math.max(MIN_INTERVAL_MS, rawInterval) : DEFAULT_INTERVAL_MS;
|
|
7105
7127
|
const rawMaxEntries = Number(config?.maxEntries ?? DEFAULT_MAX_ENTRIES);
|
|
7106
7128
|
const maxEntries = Number.isFinite(rawMaxEntries) ? Math.min(MAX_ENTRIES, Math.max(1, Math.trunc(rawMaxEntries))) : DEFAULT_MAX_ENTRIES;
|
|
7129
|
+
const rawRetryMs = Number(config?.retryMs ?? 3e4);
|
|
7130
|
+
const retryMs = Number.isFinite(rawRetryMs) ? Math.max(1, rawRetryMs) : 3e4;
|
|
7131
|
+
const rawMaxBackoffMs = Number(config?.maxBackoffMs ?? 30 * 6e4);
|
|
7132
|
+
const maxBackoffMs = Math.max(retryMs, Number.isFinite(rawMaxBackoffMs) ? Math.max(1, rawMaxBackoffMs) : 30 * 6e4);
|
|
7133
|
+
const retryDelay = (failures) => Math.min(retryMs * 2 ** Math.min(Math.max(failures - 1, 0), 20), maxBackoffMs);
|
|
7107
7134
|
const shared = globalThis[/* @__PURE__ */ Symbol.for("lacode:timeline:state")] ??= {
|
|
7108
7135
|
sessions: /* @__PURE__ */ new Map(),
|
|
7109
7136
|
owned: /* @__PURE__ */ new Set(),
|
|
@@ -7120,21 +7147,37 @@ function timelineHooks(input, options) {
|
|
|
7120
7147
|
if (!id) return void 0;
|
|
7121
7148
|
return { providerID: provider, modelID: id };
|
|
7122
7149
|
};
|
|
7150
|
+
const persist = (id, state2) => {
|
|
7151
|
+
try {
|
|
7152
|
+
writeTimelineState(id, {
|
|
7153
|
+
version: TIMELINE_STATE_VERSION,
|
|
7154
|
+
cursor: state2.cursor,
|
|
7155
|
+
failures: state2.failures,
|
|
7156
|
+
pending: state2.pending,
|
|
7157
|
+
nextAttemptAt: state2.nextAttemptAt,
|
|
7158
|
+
updatedAt: Date.now()
|
|
7159
|
+
});
|
|
7160
|
+
} catch (error) {
|
|
7161
|
+
debug("persist failed", { sessionID: id, error });
|
|
7162
|
+
}
|
|
7163
|
+
};
|
|
7123
7164
|
const stateFor = (id) => {
|
|
7124
7165
|
let state2 = sessions.get(id);
|
|
7125
7166
|
if (!state2) {
|
|
7167
|
+
const persisted = readTimelineState(id);
|
|
7126
7168
|
state2 = {
|
|
7127
7169
|
running: false,
|
|
7128
|
-
pending: false,
|
|
7129
|
-
failures: 0,
|
|
7130
|
-
cursor: readEntries(id, 1)[0]?.through
|
|
7170
|
+
pending: persisted?.pending ?? false,
|
|
7171
|
+
failures: persisted?.failures ?? 0,
|
|
7172
|
+
cursor: persisted?.cursor ?? readEntries(id, 1)[0]?.through,
|
|
7173
|
+
nextAttemptAt: persisted?.nextAttemptAt
|
|
7131
7174
|
};
|
|
7132
7175
|
sessions.set(id, state2);
|
|
7133
7176
|
while (sessions.size > 500) {
|
|
7134
7177
|
let evicted = false;
|
|
7135
7178
|
for (const [key, st] of sessions) {
|
|
7136
7179
|
if (key === id) continue;
|
|
7137
|
-
if (!st.timer && !st.running) {
|
|
7180
|
+
if (!st.timer && !st.running && !st.pending && st.nextAttemptAt === void 0) {
|
|
7138
7181
|
sessions.delete(key);
|
|
7139
7182
|
evicted = true;
|
|
7140
7183
|
break;
|
|
@@ -7149,16 +7192,29 @@ function timelineHooks(input, options) {
|
|
|
7149
7192
|
if (disabled || disposed) return;
|
|
7150
7193
|
const state2 = stateFor(id);
|
|
7151
7194
|
if (state2.root === false) return;
|
|
7195
|
+
const wasPending = state2.pending;
|
|
7152
7196
|
state2.pending = true;
|
|
7197
|
+
if (!wasPending) persist(id, state2);
|
|
7153
7198
|
if (state2.timer || state2.running) return;
|
|
7154
7199
|
schedule(id, state2);
|
|
7155
7200
|
};
|
|
7156
7201
|
function flush(id) {
|
|
7157
7202
|
if (disabled || disposed) return;
|
|
7158
|
-
|
|
7159
|
-
if (!state2)
|
|
7203
|
+
let state2 = sessions.get(id);
|
|
7204
|
+
if (!state2) {
|
|
7205
|
+
if (readTimelineState(id)?.pending === true) state2 = stateFor(id);
|
|
7206
|
+
else return;
|
|
7207
|
+
}
|
|
7160
7208
|
if (state2.root === false) return;
|
|
7161
7209
|
if (!state2.pending || state2.running) return;
|
|
7210
|
+
if (state2.nextAttemptAt && state2.nextAttemptAt > Date.now()) {
|
|
7211
|
+
if (state2.timer) {
|
|
7212
|
+
clearTimeout(state2.timer);
|
|
7213
|
+
state2.timer = void 0;
|
|
7214
|
+
}
|
|
7215
|
+
schedule(id, state2);
|
|
7216
|
+
return;
|
|
7217
|
+
}
|
|
7162
7218
|
if (state2.timer) {
|
|
7163
7219
|
clearTimeout(state2.timer);
|
|
7164
7220
|
state2.timer = void 0;
|
|
@@ -7167,10 +7223,11 @@ function timelineHooks(input, options) {
|
|
|
7167
7223
|
}
|
|
7168
7224
|
function schedule(id, state2) {
|
|
7169
7225
|
if (state2.timer) clearTimeout(state2.timer);
|
|
7226
|
+
const delay = typeof state2.nextAttemptAt === "number" ? Math.max(0, state2.nextAttemptAt - Date.now()) : state2.failures > 0 ? retryDelay(state2.failures) : intervalMs;
|
|
7170
7227
|
state2.timer = setTimeout(() => {
|
|
7171
7228
|
state2.timer = void 0;
|
|
7172
7229
|
void tick(id, state2);
|
|
7173
|
-
},
|
|
7230
|
+
}, delay);
|
|
7174
7231
|
state2.timer.unref?.();
|
|
7175
7232
|
}
|
|
7176
7233
|
async function summarize(id, title, excerptText, prior, abort) {
|
|
@@ -7260,13 +7317,17 @@ function timelineHooks(input, options) {
|
|
|
7260
7317
|
const textParts = parts.filter((part) => part.type === "text").map((part) => part.text ?? "").join("\n").trim();
|
|
7261
7318
|
const reasoningParts = parts.filter((part) => part.type === "reasoning").map((part) => part.text ?? "").join("\n").trim();
|
|
7262
7319
|
const text = textParts || reasoningParts;
|
|
7320
|
+
const trimmed = text.trim();
|
|
7321
|
+
if (trimmed === "NO_PROJECT_CHANGES") {
|
|
7322
|
+
return { ok: true, bullets: [] };
|
|
7323
|
+
}
|
|
7324
|
+
if (trimmed === "") {
|
|
7325
|
+
return { ok: false, bullets: [], error: "empty summarizer output" };
|
|
7326
|
+
}
|
|
7263
7327
|
const bullets = parseBullets(text, maxEntries);
|
|
7264
7328
|
if (bullets.length > 0) {
|
|
7265
7329
|
return { ok: true, bullets };
|
|
7266
7330
|
}
|
|
7267
|
-
if (text.trim() === "") {
|
|
7268
|
-
return { ok: true, bullets: [] };
|
|
7269
|
-
}
|
|
7270
7331
|
const salvaged = salvageBullets(text, maxEntries);
|
|
7271
7332
|
if (salvaged.length > 0) {
|
|
7272
7333
|
log("warn", "timeline: salvaged non-bulleted output", { sessionID: id, text: text.slice(0, 300) });
|
|
@@ -7289,13 +7350,13 @@ function timelineHooks(input, options) {
|
|
|
7289
7350
|
if (reprocessing.has(id)) return;
|
|
7290
7351
|
try {
|
|
7291
7352
|
state2.running = true;
|
|
7292
|
-
state2.pending = false;
|
|
7293
7353
|
let title;
|
|
7294
7354
|
let info = {};
|
|
7295
7355
|
if (state2.root === void 0) {
|
|
7296
7356
|
info = await client.session.get({ path: { id }, query: { directory } }).then((result2) => result2?.data).catch(() => void 0);
|
|
7297
7357
|
if (!info) {
|
|
7298
7358
|
sessions.delete(id);
|
|
7359
|
+
removeTimelineState(id);
|
|
7299
7360
|
return;
|
|
7300
7361
|
}
|
|
7301
7362
|
title = info.title;
|
|
@@ -7303,13 +7364,19 @@ function timelineHooks(input, options) {
|
|
|
7303
7364
|
state2.root = !info.parentID && !isInternalTitle(info.title);
|
|
7304
7365
|
if (!state2.root) {
|
|
7305
7366
|
sessions.delete(id);
|
|
7367
|
+
removeTimelineState(id);
|
|
7306
7368
|
return;
|
|
7307
7369
|
}
|
|
7308
7370
|
}
|
|
7309
7371
|
const response = await client.session.messages({ path: { id }, query: { directory } });
|
|
7310
7372
|
const messages = response?.data ?? [];
|
|
7311
|
-
const
|
|
7373
|
+
const expanded = await expandTaskSessions(client, messages, directory, owned, debug, state2.cursor?.messageID);
|
|
7374
|
+
const excerpt = buildExcerpt(expanded, state2.cursor);
|
|
7312
7375
|
if (!excerpt.text) {
|
|
7376
|
+
state2.pending = false;
|
|
7377
|
+
state2.failures = 0;
|
|
7378
|
+
state2.nextAttemptAt = void 0;
|
|
7379
|
+
persist(id, state2);
|
|
7313
7380
|
return;
|
|
7314
7381
|
}
|
|
7315
7382
|
const result = await summarize(id, state2.title ?? info?.title ?? "untitled", excerpt.text, readEntries(id, 4));
|
|
@@ -7317,8 +7384,14 @@ function timelineHooks(input, options) {
|
|
|
7317
7384
|
if (result.bullets.length) appendEntries(id, result.bullets, Date.now(), excerpt.next);
|
|
7318
7385
|
state2.cursor = excerpt.next;
|
|
7319
7386
|
state2.failures = 0;
|
|
7387
|
+
state2.pending = false;
|
|
7388
|
+
state2.nextAttemptAt = void 0;
|
|
7389
|
+
persist(id, state2);
|
|
7320
7390
|
} else {
|
|
7321
7391
|
state2.failures++;
|
|
7392
|
+
state2.pending = true;
|
|
7393
|
+
state2.nextAttemptAt = Date.now() + retryDelay(state2.failures);
|
|
7394
|
+
persist(id, state2);
|
|
7322
7395
|
log("warn", "timeline: summarize failed", { sessionID: id, error: result.error });
|
|
7323
7396
|
}
|
|
7324
7397
|
} catch (error) {
|
|
@@ -7356,8 +7429,8 @@ function timelineHooks(input, options) {
|
|
|
7356
7429
|
const info = infoRes?.data;
|
|
7357
7430
|
if (!info) return "Session not found.";
|
|
7358
7431
|
const msgRes = await client.session.messages({ path: { id: rootId }, query: { directory } });
|
|
7359
|
-
|
|
7360
|
-
|
|
7432
|
+
let messages = msgRes?.data ?? [];
|
|
7433
|
+
writeFileSync10(temp, "");
|
|
7361
7434
|
for (let i = 0; i < MAX_REPROCESS_SLICES; i++) {
|
|
7362
7435
|
if (abort?.aborted) {
|
|
7363
7436
|
try {
|
|
@@ -7366,6 +7439,7 @@ function timelineHooks(input, options) {
|
|
|
7366
7439
|
}
|
|
7367
7440
|
return `Aborted after ${calls} summarizer calls; previous timeline kept.`;
|
|
7368
7441
|
}
|
|
7442
|
+
messages = await expandTaskSessions(client, messages, directory, owned, debug, cursor?.messageID);
|
|
7369
7443
|
const excerpt = buildExcerpt(messages, cursor);
|
|
7370
7444
|
if (!excerpt.text) break;
|
|
7371
7445
|
const prior = readEntriesAt(temp, 4);
|
|
@@ -7397,13 +7471,21 @@ function timelineHooks(input, options) {
|
|
|
7397
7471
|
}
|
|
7398
7472
|
return "No entries generated; previous timeline kept.";
|
|
7399
7473
|
}
|
|
7400
|
-
|
|
7474
|
+
renameSync7(temp, targetFile);
|
|
7401
7475
|
const state2 = sessions.get(rootId);
|
|
7402
7476
|
if (state2) {
|
|
7403
7477
|
state2.cursor = cursor;
|
|
7404
7478
|
state2.failures = 0;
|
|
7405
7479
|
state2.pending = false;
|
|
7480
|
+
state2.nextAttemptAt = void 0;
|
|
7406
7481
|
}
|
|
7482
|
+
persist(rootId, {
|
|
7483
|
+
running: state2?.running ?? false,
|
|
7484
|
+
pending: false,
|
|
7485
|
+
failures: 0,
|
|
7486
|
+
cursor,
|
|
7487
|
+
nextAttemptAt: void 0
|
|
7488
|
+
});
|
|
7407
7489
|
return `Rebuilt ${messages.length} messages into ${entries} entries with ${calls} summarizer calls.`;
|
|
7408
7490
|
} catch (error) {
|
|
7409
7491
|
try {
|
|
@@ -7415,6 +7497,28 @@ function timelineHooks(input, options) {
|
|
|
7415
7497
|
reprocessing.delete(rootId);
|
|
7416
7498
|
}
|
|
7417
7499
|
}
|
|
7500
|
+
function resume() {
|
|
7501
|
+
if (disabled || disposed) return;
|
|
7502
|
+
let listed;
|
|
7503
|
+
try {
|
|
7504
|
+
listed = listTimelineStates();
|
|
7505
|
+
} catch (error) {
|
|
7506
|
+
debug("resume failed", error);
|
|
7507
|
+
return;
|
|
7508
|
+
}
|
|
7509
|
+
listed.sort((a, b) => b.state.updatedAt - a.state.updatedAt);
|
|
7510
|
+
let scheduled = 0;
|
|
7511
|
+
for (const { sessionID, state: state2 } of listed) {
|
|
7512
|
+
if (state2.pending !== true) continue;
|
|
7513
|
+
if (sessions.has(sessionID)) continue;
|
|
7514
|
+
if (scheduled >= 50) continue;
|
|
7515
|
+
const loaded = stateFor(sessionID);
|
|
7516
|
+
if (loaded.root === false) continue;
|
|
7517
|
+
schedule(sessionID, loaded);
|
|
7518
|
+
scheduled++;
|
|
7519
|
+
}
|
|
7520
|
+
}
|
|
7521
|
+
resume();
|
|
7418
7522
|
const timelineReprocess = tool2({
|
|
7419
7523
|
description: "Rebuild the root session's entire timeline from its transcript, replacing the existing entries. Used by /reprocess-timeline.",
|
|
7420
7524
|
args: {},
|
|
@@ -7464,6 +7568,7 @@ function timelineHooks(input, options) {
|
|
|
7464
7568
|
}
|
|
7465
7569
|
sessions.delete(id);
|
|
7466
7570
|
removeEntries(id);
|
|
7571
|
+
removeTimelineState(id);
|
|
7467
7572
|
owned.delete(id);
|
|
7468
7573
|
return;
|
|
7469
7574
|
}
|
|
@@ -7488,6 +7593,96 @@ function timelineHooks(input, options) {
|
|
|
7488
7593
|
};
|
|
7489
7594
|
}
|
|
7490
7595
|
|
|
7596
|
+
// src/lib/timer-migration.ts
|
|
7597
|
+
import { execFile } from "node:child_process";
|
|
7598
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync9, readFileSync as readFileSync10, renameSync as renameSync8, writeFileSync as writeFileSync11 } from "node:fs";
|
|
7599
|
+
import { homedir as homedir8 } from "node:os";
|
|
7600
|
+
import { dirname as dirname6, join as join13 } from "node:path";
|
|
7601
|
+
var TIMER_MIGRATION_ENV = "LACODE_NO_TIMER_MIGRATION";
|
|
7602
|
+
var TIMER_MIGRATION_STATE_VERSION = 1;
|
|
7603
|
+
var defaultRun2 = (file, args, options) => new Promise((resolve5, reject) => {
|
|
7604
|
+
execFile(file, args, { stdio: options?.stdio ?? "ignore" }, (error) => {
|
|
7605
|
+
if (error) reject(error);
|
|
7606
|
+
else resolve5();
|
|
7607
|
+
});
|
|
7608
|
+
});
|
|
7609
|
+
function timerMigrationStatePath(home = homedir8()) {
|
|
7610
|
+
return join13(home, ".local", "share", "opencode", "lacode", "migrations.json");
|
|
7611
|
+
}
|
|
7612
|
+
function timerMigrationUnitPath(home = homedir8()) {
|
|
7613
|
+
return join13(home, ".config", "systemd", "user", "lacode-refresh-pool.timer");
|
|
7614
|
+
}
|
|
7615
|
+
function readState2(path4) {
|
|
7616
|
+
try {
|
|
7617
|
+
return JSON.parse(readFileSync10(path4, "utf8"));
|
|
7618
|
+
} catch {
|
|
7619
|
+
return {};
|
|
7620
|
+
}
|
|
7621
|
+
}
|
|
7622
|
+
function writeState2(path4, state2) {
|
|
7623
|
+
mkdirSync9(dirname6(path4), { recursive: true });
|
|
7624
|
+
const tmp = `${path4}.${process.pid}.tmp`;
|
|
7625
|
+
writeFileSync11(tmp, JSON.stringify(state2) + "\n");
|
|
7626
|
+
renameSync8(tmp, path4);
|
|
7627
|
+
}
|
|
7628
|
+
async function runTimerMigration(options = {}) {
|
|
7629
|
+
const env = options.env ?? process.env;
|
|
7630
|
+
if (env[TIMER_MIGRATION_ENV] === "1" || env.NODE_TEST_CONTEXT !== void 0) return { action: "disabled" };
|
|
7631
|
+
const platform = options.platform ?? process.platform;
|
|
7632
|
+
if (platform !== "linux") return { action: "skipped", reason: "not-linux" };
|
|
7633
|
+
const packageRoot = options.packageRoot ?? PACKAGE_ROOT;
|
|
7634
|
+
const version = options.version ?? readInstalledVersion(packageRoot);
|
|
7635
|
+
if (!version) return { action: "skipped", reason: "no-version" };
|
|
7636
|
+
const home = options.home ?? homedir8();
|
|
7637
|
+
const statePath = options.statePath ?? timerMigrationStatePath(home);
|
|
7638
|
+
const state2 = readState2(statePath);
|
|
7639
|
+
if (state2.timersVersion === version && existsSync6(timerMigrationUnitPath(home))) {
|
|
7640
|
+
return { action: "up-to-date", version };
|
|
7641
|
+
}
|
|
7642
|
+
const run = options.run ?? defaultRun2;
|
|
7643
|
+
try {
|
|
7644
|
+
await run("systemctl", ["--user", "show-environment"], { stdio: "ignore" });
|
|
7645
|
+
} catch {
|
|
7646
|
+
return { action: "skipped", reason: "no-systemd" };
|
|
7647
|
+
}
|
|
7648
|
+
const script = join13(packageRoot, "scripts", "refresh-pool.mjs");
|
|
7649
|
+
if (!existsSync6(script)) return { action: "failed", version, error: `timer script not found: ${script}` };
|
|
7650
|
+
try {
|
|
7651
|
+
await run(process.execPath, [script, "--install-timer", "--stable"], { stdio: "ignore" });
|
|
7652
|
+
} catch (error) {
|
|
7653
|
+
return { action: "failed", version, error: String(error?.message ?? error) };
|
|
7654
|
+
}
|
|
7655
|
+
try {
|
|
7656
|
+
writeState2(statePath, {
|
|
7657
|
+
version: TIMER_MIGRATION_STATE_VERSION,
|
|
7658
|
+
timersVersion: version,
|
|
7659
|
+
installedAt: (options.now?.() ?? /* @__PURE__ */ new Date()).toISOString()
|
|
7660
|
+
});
|
|
7661
|
+
} catch (error) {
|
|
7662
|
+
options.log?.("warn", `timer migration state not written: ${String(error?.message ?? error)}`);
|
|
7663
|
+
}
|
|
7664
|
+
return { action: "installed", version };
|
|
7665
|
+
}
|
|
7666
|
+
|
|
7667
|
+
// src/plugins/timer-migration.ts
|
|
7668
|
+
function timerMigrationHooks(input, options) {
|
|
7669
|
+
if (options?.enabled === false) return {};
|
|
7670
|
+
const log = (level, message, extra) => {
|
|
7671
|
+
void input.client.app.log({ body: { service: "lacode", level, message, extra } }).catch(() => {
|
|
7672
|
+
});
|
|
7673
|
+
};
|
|
7674
|
+
void runTimerMigration({
|
|
7675
|
+
packageRoot: PACKAGE_ROOT,
|
|
7676
|
+
version: readInstalledVersion(PACKAGE_ROOT),
|
|
7677
|
+
log
|
|
7678
|
+
}).then((result) => {
|
|
7679
|
+
if (result.action === "installed") log("info", `timer migration installed for ${result.version}`);
|
|
7680
|
+
else if (result.action === "failed") log("warn", `timer migration failed: ${result.error}`);
|
|
7681
|
+
else log("debug", `timer migration: ${result.action}`);
|
|
7682
|
+
}).catch((error) => log("error", `timer migration threw: ${String(error)}`));
|
|
7683
|
+
return {};
|
|
7684
|
+
}
|
|
7685
|
+
|
|
7491
7686
|
// src/index.ts
|
|
7492
7687
|
var inertHooks = {
|
|
7493
7688
|
config: async () => {
|
|
@@ -7528,6 +7723,7 @@ var LaCode = (async (input, options) => {
|
|
|
7528
7723
|
const autoupdate = await settled("context-autoupdate", () => contextAutoUpdateHooks(input, options)) ?? inertHooks;
|
|
7529
7724
|
const notifier = await settled("notify", () => notifyHooks(input, options)) ?? inertHooks;
|
|
7530
7725
|
const selfupdate = await settled("self-update", () => selfUpdateHooks(input, options)) ?? inertHooks;
|
|
7726
|
+
void timerMigrationHooks(input, options);
|
|
7531
7727
|
const feedback = await settled("feedback", () => feedbackHooks(input, options)) ?? inertHooks;
|
|
7532
7728
|
const omniroute = await settled("omniroute", () => omniRouteHooks(input, options)) ?? inertHooks;
|
|
7533
7729
|
const compat = await settled("provider-compat", () => providerCompatHooks(input)) ?? inertHooks;
|