p-backlog 0.5.0 → 0.6.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.
Files changed (34) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +9 -1
  3. package/README.ru.md +10 -1
  4. package/dist/cli.js +670 -322
  5. package/dist/web/assets/{CodeTab-BU7UU1i4.js → CodeTab-D3I5lIJJ.js} +1 -1
  6. package/dist/web/assets/CostTab-8f2AC5ER.js +1 -0
  7. package/dist/web/assets/EffectTab-1jObLK9a.js +1 -0
  8. package/dist/web/assets/Figure-BHBCd7Dt.js +1 -0
  9. package/dist/web/assets/OverviewTab-DjNyxzJq.js +1 -0
  10. package/dist/web/assets/QualityTab-W2NdYBhI.js +1 -0
  11. package/dist/web/assets/StatsPage-rJH3k8Ye.js +1 -0
  12. package/dist/web/assets/StatsTabState-BoimPoKm.js +1 -0
  13. package/dist/web/assets/{StatsTable-C0coWtNM.js → StatsTable-B0tbx68M.js} +1 -1
  14. package/dist/web/assets/{UnavailableRepos-DDHmBHg0.js → UnavailableRepos-SpC6Dzwu.js} +1 -1
  15. package/dist/web/assets/cx-DDl6tKXq.js +58 -0
  16. package/dist/web/assets/{index-DN4lSwOL.js → index-C8xqRmo-.js} +2 -2
  17. package/dist/web/assets/start-YlC0KTe5.js +38 -0
  18. package/dist/web/assets/use-grain-series-Bx70IZvb.js +55 -0
  19. package/dist/web/assets/value-dot-B7BsbWBD.js +1 -0
  20. package/dist/web/index.html +1 -1
  21. package/package.json +1 -1
  22. package/dist/web/assets/ChartTooltip-b-yZtIRd.js +0 -55
  23. package/dist/web/assets/CostTab-Be8xR9DU.js +0 -1
  24. package/dist/web/assets/EffectTab-D0kaZiSl.js +0 -1
  25. package/dist/web/assets/Figure-B6Ry1zXo.js +0 -1
  26. package/dist/web/assets/OverviewTab-DqU7o9Ug.js +0 -1
  27. package/dist/web/assets/QualityTab-DtnTgxXj.js +0 -1
  28. package/dist/web/assets/StatsPage-BLZKtSL6.js +0 -1
  29. package/dist/web/assets/StatsTabState-CYkDWYp7.js +0 -1
  30. package/dist/web/assets/cx-Cfec8QGz.js +0 -58
  31. package/dist/web/assets/rolldown-runtime-hePW80VL.js +0 -1
  32. package/dist/web/assets/start-sbZVqXmP.js +0 -38
  33. package/dist/web/assets/value-dot-CfFRNT_m.js +0 -1
  34. /package/dist/web/assets/{ChartTooltip-tQC8-pw2.css → use-grain-series-tQC8-pw2.css} +0 -0
package/dist/cli.js CHANGED
@@ -42,7 +42,7 @@ function formatLocalIso(date) {
42
42
  }
43
43
 
44
44
  // src/core/store/runs.ts
45
- import { appendFile, mkdir, open } from "node:fs/promises";
45
+ import { mkdir } from "node:fs/promises";
46
46
  import { join as join3 } from "node:path";
47
47
  import { z } from "zod";
48
48
 
@@ -127,6 +127,10 @@ function percent(part, total) {
127
127
  // src/core/model/lifecycle.ts
128
128
  var RETENTION_DAYS = 7;
129
129
  var DAY_MS = 24 * 60 * 60 * 1e3;
130
+ var DAYS_PER_WEEK = 7;
131
+ var WEEK_MS = DAYS_PER_WEEK * DAY_MS;
132
+ var STATS_WEEKS = 12;
133
+ var STATS_HISTORY_DAYS = STATS_WEEKS * DAYS_PER_WEEK;
130
134
  var RESOLUTION_STATUS = {
131
135
  fixed: "done",
132
136
  obsolete: "cancelled",
@@ -197,7 +201,7 @@ import { setTimeout as sleep2 } from "node:timers/promises";
197
201
 
198
202
  // src/core/store/fs-utils.ts
199
203
  import { createHash, randomUUID } from "node:crypto";
200
- import { chmod, link, readdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
204
+ import { chmod, link, open, readdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
201
205
  import { basename, dirname, join } from "node:path";
202
206
  import { setTimeout as sleep } from "node:timers/promises";
203
207
  function contentVersion(text) {
@@ -211,16 +215,56 @@ async function readTextOrNull(path) {
211
215
  throw error;
212
216
  }
213
217
  }
214
- async function readJsonLines(path, schema) {
215
- const text = await readTextOrNull(path) ?? "";
218
+ function parseJsonLines(text, schema) {
216
219
  const parsed = text.split("\n").filter((line) => line.trim() !== "").map((line) => parseJson(line, schema));
217
220
  const values = parsed.filter((value) => value !== null);
218
221
  return { values, invalidLines: parsed.length - values.length };
219
222
  }
223
+ async function readJsonLines(path, schema) {
224
+ const text = await readTextOrNull(path) ?? "";
225
+ return parseJsonLines(text, schema);
226
+ }
220
227
  function toJsonLines(values) {
221
228
  return values.map((value) => `${JSON.stringify(value)}
222
229
  `).join("");
223
230
  }
231
+ var NEWLINE = 10;
232
+ async function appendJsonLines(path, values) {
233
+ await withFile(
234
+ path,
235
+ async (handle) => {
236
+ const { size } = await handle.stat();
237
+ const endsMidLine = size > 0 && (await readAt(handle, size - 1, 1))[0] !== NEWLINE;
238
+ await handle.appendFile((endsMidLine ? "\n" : "") + toJsonLines(values), "utf8");
239
+ },
240
+ "a+"
241
+ );
242
+ }
243
+ async function withFile(path, use, flags = "r") {
244
+ return closingAfter(await open(path, flags), use);
245
+ }
246
+ async function withExistingFile(path, use) {
247
+ const handle = await open(path, "r").catch((error) => {
248
+ if (hasErrorCode(error, "ENOENT")) return null;
249
+ throw error;
250
+ });
251
+ return handle === null ? null : closingAfter(handle, use);
252
+ }
253
+ async function closingAfter(handle, use) {
254
+ try {
255
+ return await use(handle);
256
+ } finally {
257
+ await handle.close();
258
+ }
259
+ }
260
+ async function readAt(handle, position, length) {
261
+ const buffer = Buffer.alloc(length);
262
+ const { bytesRead } = await handle.read(buffer, 0, length, position);
263
+ return buffer.subarray(0, bytesRead);
264
+ }
265
+ function readFileAt(path, position, length) {
266
+ return withFile(path, (handle) => readAt(handle, position, length));
267
+ }
224
268
  function parseJson(text, schema) {
225
269
  try {
226
270
  const parsed = schema.safeParse(JSON.parse(text));
@@ -251,7 +295,7 @@ async function listDir(path, { recursive = false } = {}) {
251
295
  throw error;
252
296
  }
253
297
  }
254
- var REPLACE_RETRY_DELAYS_MS = [10, 20, 40, 80, 160, 320, 640];
298
+ var REPLACE_RETRY_DELAYS_MS = [10, 20, 40, 80, 160, 320, 640, 1e3, 1e3, 1e3, 1e3];
255
299
  var REPLACE_BLOCKED_CODES = ["EPERM", "EACCES", "EBUSY"];
256
300
  async function writeFileAtomic(path, content, mode) {
257
301
  await viaTemporaryFile(path, content, (temporary) => replaceFile(temporary, path), mode);
@@ -382,7 +426,7 @@ async function release({ lock, token }) {
382
426
 
383
427
  // src/core/store/runs.ts
384
428
  var RUNS_FILE = ".runs.jsonl";
385
- var RUNS_KEPT_DAYS = 30;
429
+ var RUNS_KEPT_DAYS = STATS_HISTORY_DAYS;
386
430
  var STALE_RUN_SLACK_DAYS = 1;
387
431
  var FIRST_LINE_BYTES = 4096;
388
432
  var cliRunSchema = z.object({
@@ -396,10 +440,7 @@ var cliRunSchema = z.object({
396
440
  async function appendRun(root, run) {
397
441
  await mkdir(root, { recursive: true });
398
442
  const path = join3(root, RUNS_FILE);
399
- await withFileLock(path, () => appendFile(path, toJsonLines([run]), "utf8"));
400
- }
401
- async function readRuns(root) {
402
- return (await readJsonLines(join3(root, RUNS_FILE), cliRunSchema)).values;
443
+ await withFileLock(path, () => appendJsonLines(path, [run]));
403
444
  }
404
445
  async function trimRuns(root, now) {
405
446
  const path = join3(root, RUNS_FILE);
@@ -419,20 +460,13 @@ async function trimRunsWhenStale(root, now) {
419
460
  return needsTrim ? trimRuns(root, now) : 0;
420
461
  }
421
462
  async function firstRun(path) {
422
- const file = await open(path, "r").catch((error) => {
423
- if (hasErrorCode(error, "ENOENT")) return null;
424
- throw error;
425
- });
426
- if (file === null) return "none";
427
- try {
428
- const { buffer, bytesRead } = await file.read(Buffer.alloc(FIRST_LINE_BYTES), 0, FIRST_LINE_BYTES, 0);
429
- const [firstLine = ""] = buffer.toString("utf8", 0, bytesRead).split("\n");
430
- if (firstLine.trim() === "") return "none";
431
- const run = parseJson(firstLine, cliRunSchema);
432
- return run === null ? "unparsable" : Date.parse(run.at);
433
- } finally {
434
- await file.close();
435
- }
463
+ return await withExistingFile(path, firstRunIn) ?? "none";
464
+ }
465
+ async function firstRunIn(handle) {
466
+ const [firstLine = ""] = (await readAt(handle, 0, FIRST_LINE_BYTES)).toString("utf8").split("\n");
467
+ if (firstLine.trim() === "") return "none";
468
+ const run = parseJson(firstLine, cliRunSchema);
469
+ return run === null ? "unparsable" : Date.parse(run.at);
436
470
  }
437
471
 
438
472
  // src/core/store/paths.ts
@@ -565,8 +599,8 @@ var cliEn = {
565
599
  installHookAdded: (settingsPath) => `Stop hook added to ${settingsPath}`,
566
600
  installHookUpdated: (settingsPath) => `Stop hook updated in ${settingsPath}`,
567
601
  codexHookApproval: "approve the hook in Codex: /hooks",
568
- installSettingsUnreadable: (settingsPath, detail) => `Could not read ${settingsPath} (${detail}), Stop hook not added.`,
569
- installSettingsInvalid: (settingsPath) => `${settingsPath} is not a JSON object, Stop hook not added. Fix the file and try again.`,
602
+ installHookConfigUnreadable: (path, detail) => `Could not read ${path} (${detail}), Stop hook not added.`,
603
+ installHookConfigInvalid: (path) => `${path} is not a JSON object, Stop hook not added. Fix the file and try again.`,
570
604
  agentNotFound: (dir) => `not found (${dir})`,
571
605
  pluginManages: (plugin) => `the ${plugin} plugin provides the skill and the hook. Remove the manual ones with: backlog setup --remove-manual`,
572
606
  pluginLanguageHint: (installed, wanted) => `the skill comes from the ${installed} plugin; for this language switch plugins: /plugin uninstall ${installed}, then /plugin install ${wanted}`,
@@ -576,12 +610,13 @@ var cliEn = {
576
610
  absent: (target) => `no skill link: ${target}`,
577
611
  foreign: (target) => `${target} is not a p-backlog link, left as is`
578
612
  },
613
+ manualSkillShared: (target, agent) => `skill link kept, ${agent} uses it: ${target}`,
579
614
  manualHookRemoval: {
580
615
  removed: (path) => `Stop hook removed from ${path}`,
581
616
  absent: (path) => `no p-backlog Stop hook in ${path}`
582
617
  },
583
- hookConfigUnreadable: (path, detail) => `Could not read ${path} (${detail}), the hook was not removed.`,
584
- hookConfigInvalid: (path) => `${path} is not a JSON object, the hook was not removed. Fix the file and try again.`,
618
+ removeHookConfigUnreadable: (path, detail) => `Could not read ${path} (${detail}), the hook was not removed.`,
619
+ removeHookConfigInvalid: (path) => `${path} is not a JSON object, the hook was not removed. Fix the file and try again.`,
585
620
  serviceInstalled: (file) => `Service installed: ${file}`,
586
621
  serviceLogs: (path) => `Logs: ${path}`,
587
622
  serviceUninstalled: "Service removed",
@@ -593,7 +628,7 @@ var cliEn = {
593
628
  taskNotFound: (id) => `Task ${id} not found`,
594
629
  fileConflict: (id) => `Task file ${id} changed while writing, retry the command`,
595
630
  statsTitle: (scopeName) => `${scopeName} \xB7 stats`,
596
- statsOpenLine: ({ open: open3, weight, net, created, closed }) => `Open: ${open3} (weight ${weight}) \xB7 this week: ${net} (created ${created}, closed ${closed})`,
631
+ statsOpenLine: ({ open: open2, weight, net, created, closed }) => `Open: ${open2} (weight ${weight}) \xB7 this week: ${net} (created ${created}, closed ${closed})`,
597
632
  statsAgeLine: (ageMedian, leadMedian, tail) => `Age, median: ${ageMedian} \xB7 time to close, median: ${leadMedian}${tail}`,
598
633
  statsP90Tail: (p903) => ` (90% \u2014 ${p903})`,
599
634
  statsForecastLine: (forecast3, tail) => `Forecast: ${forecast3} (${tail})`,
@@ -647,7 +682,7 @@ var cliEn = {
647
682
  "delete <id> --confirm <id> (deletes the project directory with all its tasks)"
648
683
  ],
649
684
  noProjects: "No projects",
650
- projectListLine: ({ id, name, prefix, statusWord: statusWord2, open: open3 }) => `${id} \xB7 ${name} \xB7 ${prefix} \xB7 ${statusWord2} \xB7 open ${open3}`,
685
+ projectListLine: ({ id, name, prefix, statusWord: statusWord2, open: open2 }) => `${id} \xB7 ${name} \xB7 ${prefix} \xB7 ${statusWord2} \xB7 open ${open2}`,
651
686
  confirmProjectDelete: (id) => `Confirm the deletion: backlog project delete ${id} --confirm ${id}`,
652
687
  projectDeleted: (id, taskCount) => `${id} deleted: tasks ${taskCount}`,
653
688
  projectActive: "active",
@@ -741,8 +776,8 @@ var cliRu = {
741
776
  installHookAdded: (settingsPath) => `\u0425\u0443\u043A Stop \u0434\u043E\u0431\u0430\u0432\u043B\u0435\u043D \u0432 ${settingsPath}`,
742
777
  installHookUpdated: (settingsPath) => `\u0425\u0443\u043A Stop \u043E\u0431\u043D\u043E\u0432\u043B\u0451\u043D \u0432 ${settingsPath}`,
743
778
  codexHookApproval: "\u043E\u0434\u043E\u0431\u0440\u0438\u0442\u0435 \u0445\u0443\u043A \u0432 Codex: /hooks",
744
- installSettingsUnreadable: (settingsPath, detail) => `${settingsPath} \u043D\u0435 \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u0442\u044C (${detail}), \u0445\u0443\u043A Stop \u043D\u0435 \u0434\u043E\u0431\u0430\u0432\u043B\u0435\u043D.`,
745
- installSettingsInvalid: (settingsPath) => `${settingsPath} \u2014 \u043D\u0435 \u043E\u0431\u044A\u0435\u043A\u0442 JSON, \u0445\u0443\u043A Stop \u043D\u0435 \u0434\u043E\u0431\u0430\u0432\u043B\u0435\u043D. \u0418\u0441\u043F\u0440\u0430\u0432\u044C\u0442\u0435 \u0444\u0430\u0439\u043B \u0438 \u043F\u043E\u0432\u0442\u043E\u0440\u0438\u0442\u0435.`,
779
+ installHookConfigUnreadable: (path, detail) => `${path} \u043D\u0435 \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u0442\u044C (${detail}), \u0445\u0443\u043A Stop \u043D\u0435 \u0434\u043E\u0431\u0430\u0432\u043B\u0435\u043D.`,
780
+ installHookConfigInvalid: (path) => `${path} \u2014 \u043D\u0435 \u043E\u0431\u044A\u0435\u043A\u0442 JSON, \u0445\u0443\u043A Stop \u043D\u0435 \u0434\u043E\u0431\u0430\u0432\u043B\u0435\u043D. \u0418\u0441\u043F\u0440\u0430\u0432\u044C\u0442\u0435 \u0444\u0430\u0439\u043B \u0438 \u043F\u043E\u0432\u0442\u043E\u0440\u0438\u0442\u0435.`,
746
781
  agentNotFound: (dir) => `\u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D (${dir})`,
747
782
  pluginManages: (plugin) => `\u0441\u043A\u0438\u043B\u043B \u0438 \u0445\u0443\u043A \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0430\u0435\u0442 \u043F\u043B\u0430\u0433\u0438\u043D ${plugin}. \u0420\u0443\u0447\u043D\u044B\u0435 \u043C\u043E\u0436\u043D\u043E \u0443\u0431\u0440\u0430\u0442\u044C: backlog setup --remove-manual`,
748
783
  pluginLanguageHint: (installed, wanted) => `\u0441\u043A\u0438\u043B\u043B \u0434\u0430\u0451\u0442 \u043F\u043B\u0430\u0433\u0438\u043D ${installed}; \u0434\u043B\u044F \u044D\u0442\u043E\u0433\u043E \u044F\u0437\u044B\u043A\u0430 \u043F\u043E\u0441\u0442\u0430\u0432\u044C\u0442\u0435 \u0434\u0440\u0443\u0433\u043E\u0439: /plugin uninstall ${installed}, \u0437\u0430\u0442\u0435\u043C /plugin install ${wanted}`,
@@ -752,12 +787,13 @@ var cliRu = {
752
787
  absent: (target) => `\u0441\u0441\u044B\u043B\u043A\u0438 \u043D\u0430 \u0441\u043A\u0438\u043B\u043B \u043D\u0435\u0442: ${target}`,
753
788
  foreign: (target) => `${target} \u2014 \u043D\u0435 \u0441\u0441\u044B\u043B\u043A\u0430 p-backlog, \u043D\u0435 \u0442\u0440\u043E\u043D\u0443\u0442`
754
789
  },
790
+ manualSkillShared: (target, agent) => `\u0441\u0441\u044B\u043B\u043A\u0430 \u043D\u0430 \u0441\u043A\u0438\u043B\u043B \u043E\u0441\u0442\u0430\u0432\u043B\u0435\u043D\u0430, \u0435\u044E \u043F\u043E\u043B\u044C\u0437\u0443\u0435\u0442\u0441\u044F ${agent}: ${target}`,
755
791
  manualHookRemoval: {
756
792
  removed: (path) => `\u0445\u0443\u043A Stop \u0441\u043D\u044F\u0442 \u0438\u0437 ${path}`,
757
793
  absent: (path) => `\u0445\u0443\u043A\u0430 Stop p-backlog \u0432 ${path} \u043D\u0435\u0442`
758
794
  },
759
- hookConfigUnreadable: (path, detail) => `\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u0442\u044C ${path} (${detail}), \u0445\u0443\u043A \u043D\u0435 \u0441\u043D\u044F\u0442.`,
760
- hookConfigInvalid: (path) => `${path} \u2014 \u043D\u0435 \u043E\u0431\u044A\u0435\u043A\u0442 JSON, \u0445\u0443\u043A \u043D\u0435 \u0441\u043D\u044F\u0442. \u0418\u0441\u043F\u0440\u0430\u0432\u044C\u0442\u0435 \u0444\u0430\u0439\u043B \u0438 \u043F\u043E\u0432\u0442\u043E\u0440\u0438\u0442\u0435.`,
795
+ removeHookConfigUnreadable: (path, detail) => `\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u0442\u044C ${path} (${detail}), \u0445\u0443\u043A \u043D\u0435 \u0441\u043D\u044F\u0442.`,
796
+ removeHookConfigInvalid: (path) => `${path} \u2014 \u043D\u0435 \u043E\u0431\u044A\u0435\u043A\u0442 JSON, \u0445\u0443\u043A \u043D\u0435 \u0441\u043D\u044F\u0442. \u0418\u0441\u043F\u0440\u0430\u0432\u044C\u0442\u0435 \u0444\u0430\u0439\u043B \u0438 \u043F\u043E\u0432\u0442\u043E\u0440\u0438\u0442\u0435.`,
761
797
  serviceInstalled: (file) => `\u0421\u043B\u0443\u0436\u0431\u0430 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u0430: ${file}`,
762
798
  serviceLogs: (path) => `\u041B\u043E\u0433\u0438: ${path}`,
763
799
  serviceUninstalled: "\u0421\u043B\u0443\u0436\u0431\u0430 \u0443\u0434\u0430\u043B\u0435\u043D\u0430",
@@ -769,7 +805,7 @@ var cliRu = {
769
805
  taskNotFound: (id) => `\u0417\u0430\u0434\u0430\u0447\u0430 ${id} \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u0430`,
770
806
  fileConflict: (id) => `\u0424\u0430\u0439\u043B \u0437\u0430\u0434\u0430\u0447\u0438 ${id} \u0438\u0437\u043C\u0435\u043D\u0438\u043B\u0441\u044F \u0432\u043E \u0432\u0440\u0435\u043C\u044F \u0437\u0430\u043F\u0438\u0441\u0438, \u043F\u043E\u0432\u0442\u043E\u0440\u0438\u0442\u0435 \u043A\u043E\u043C\u0430\u043D\u0434\u0443`,
771
807
  statsTitle: (scopeName) => `${scopeName} \xB7 \u0441\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u043A\u0430`,
772
- statsOpenLine: ({ open: open3, weight, net, created, closed }) => `\u041E\u0442\u043A\u0440\u044B\u0442\u043E: ${open3} (\u0432\u0435\u0441 ${weight}) \xB7 \u0437\u0430 \u043D\u0435\u0434\u0435\u043B\u044E: ${net} (\u0441\u043E\u0437\u0434\u0430\u043D\u043E ${created}, \u0437\u0430\u043A\u0440\u044B\u0442\u043E ${closed})`,
808
+ statsOpenLine: ({ open: open2, weight, net, created, closed }) => `\u041E\u0442\u043A\u0440\u044B\u0442\u043E: ${open2} (\u0432\u0435\u0441 ${weight}) \xB7 \u0437\u0430 \u043D\u0435\u0434\u0435\u043B\u044E: ${net} (\u0441\u043E\u0437\u0434\u0430\u043D\u043E ${created}, \u0437\u0430\u043A\u0440\u044B\u0442\u043E ${closed})`,
773
809
  statsAgeLine: (ageMedian, leadMedian, tail) => `\u0412\u043E\u0437\u0440\u0430\u0441\u0442, \u043C\u0435\u0434\u0438\u0430\u043D\u0430: ${ageMedian} \xB7 \u0434\u043E \u0437\u0430\u043A\u0440\u044B\u0442\u0438\u044F, \u043C\u0435\u0434\u0438\u0430\u043D\u0430: ${leadMedian}${tail}`,
774
810
  statsP90Tail: (p903) => ` (90% \u2014 ${p903})`,
775
811
  statsForecastLine: (forecast3, tail) => `\u041F\u0440\u043E\u0433\u043D\u043E\u0437: ${forecast3} (${tail})`,
@@ -834,8 +870,8 @@ var cliRu = {
834
870
  name,
835
871
  prefix,
836
872
  statusWord: statusWord2,
837
- open: open3
838
- }) => `${id} \xB7 ${name} \xB7 ${prefix} \xB7 ${statusWord2} \xB7 \u043E\u0442\u043A\u0440\u044B\u0442\u044B\u0445 ${open3}`,
873
+ open: open2
874
+ }) => `${id} \xB7 ${name} \xB7 ${prefix} \xB7 ${statusWord2} \xB7 \u043E\u0442\u043A\u0440\u044B\u0442\u044B\u0445 ${open2}`,
839
875
  confirmProjectDelete: (id) => `\u041F\u043E\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0435 \u0443\u0434\u0430\u043B\u0435\u043D\u0438\u0435: backlog project delete ${id} --confirm ${id}`,
840
876
  projectDeleted: (id, taskCount) => `${id} \u0443\u0434\u0430\u043B\u0451\u043D: \u0437\u0430\u0434\u0430\u0447 ${taskCount}`,
841
877
  projectActive: "\u0430\u043A\u0442\u0438\u0432\u0435\u043D",
@@ -1357,9 +1393,6 @@ function consecutivePeriods(starts, lastTo) {
1357
1393
  }
1358
1394
 
1359
1395
  // src/core/stats/weeks.ts
1360
- var STATS_WEEKS = 12;
1361
- var DAYS_PER_WEEK = 7;
1362
- var WEEK_MS = DAYS_PER_WEEK * DAY_MS;
1363
1396
  var MONDAY = 1;
1364
1397
  function weekStarts(now, count3) {
1365
1398
  const monday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - (now.getDay() + DAYS_PER_WEEK - MONDAY) % DAYS_PER_WEEK);
@@ -1371,15 +1404,18 @@ function statsPeriod(now) {
1371
1404
  function weekWindows(now, count3 = STATS_WEEKS) {
1372
1405
  return consecutivePeriods(weekStarts(now, count3), now.getTime());
1373
1406
  }
1374
- function weeklyFlow(histories, now) {
1407
+ function flowOver(periods, histories) {
1375
1408
  const closings = histories.flatMap(closingsOf);
1376
- return weekWindows(now).map((week) => ({
1377
- start: formatLocalIso(new Date(week.from)),
1378
- created: histories.filter((history) => week.contains(history.createdAt)).length,
1379
- closed: closings.filter((closing) => week.contains(closing.at)).length,
1380
- openAtEnd: histories.filter((history) => isOpenAt(history, week.to)).length
1409
+ return periods.map((span) => ({
1410
+ start: formatLocalIso(new Date(span.from)),
1411
+ created: histories.filter((history) => span.contains(history.createdAt)).length,
1412
+ closed: closings.filter((closing) => span.contains(closing.at)).length,
1413
+ openAtEnd: histories.filter((history) => isOpenAt(history, span.to)).length
1381
1414
  }));
1382
1415
  }
1416
+ function weeklyFlow(histories, now) {
1417
+ return flowOver(weekWindows(now), histories);
1418
+ }
1383
1419
 
1384
1420
  // src/core/messages/zod.ts
1385
1421
  var TYPE_WITNESSES = {
@@ -1471,8 +1507,8 @@ function p90(value) {
1471
1507
  if (value === null) return "\u2014";
1472
1508
  return value < 1 ? "within a day" : `within ${days(value)}`;
1473
1509
  }
1474
- function forecast({ open: open3, weeklyNet, weeks, until }) {
1475
- if (open3 === 0) return "No open tasks";
1510
+ function forecast({ open: open2, weeklyNet, weeks, until }) {
1511
+ if (open2 === 0) return "No open tasks";
1476
1512
  if (weeks !== null && until !== null) return `Debt clears in about ${weeks} wk. (by ${formatDayMonth("en", new Date(until))})`;
1477
1513
  if (weeklyNet === 0) return "Debt is not shrinking";
1478
1514
  const growth = roundToTenth(-weeklyNet);
@@ -1704,8 +1740,8 @@ function p902(value) {
1704
1740
  if (value === null) return "\u2014";
1705
1741
  return value < 1 ? "\u0431\u044B\u0441\u0442\u0440\u0435\u0435 \u0441\u0443\u0442\u043E\u043A" : `\u0437\u0430 ${days2(value)}`;
1706
1742
  }
1707
- function forecast2({ open: open3, weeklyNet, weeks, until }) {
1708
- if (open3 === 0) return "\u041E\u0442\u043A\u0440\u044B\u0442\u044B\u0445 \u0437\u0430\u0434\u0430\u0447 \u043D\u0435\u0442";
1743
+ function forecast2({ open: open2, weeklyNet, weeks, until }) {
1744
+ if (open2 === 0) return "\u041E\u0442\u043A\u0440\u044B\u0442\u044B\u0445 \u0437\u0430\u0434\u0430\u0447 \u043D\u0435\u0442";
1709
1745
  if (weeks !== null && until !== null) return `\u0414\u043E\u043B\u0433 \u0440\u0430\u0437\u0431\u0435\u0440\u0451\u0442\u0441\u044F \u043F\u0440\u0438\u043C\u0435\u0440\u043D\u043E \u0437\u0430 ${weeks}${NBSP}\u043D\u0435\u0434. (\u043A ${formatDayMonth("ru", new Date(until))})`;
1710
1746
  if (weeklyNet === 0) return "\u0414\u043E\u043B\u0433 \u043D\u0435 \u0443\u043C\u0435\u043D\u044C\u0448\u0430\u0435\u0442\u0441\u044F";
1711
1747
  const growth = roundToTenth(-weeklyNet);
@@ -2143,20 +2179,21 @@ function findBlockerCycle(start, resolve7) {
2143
2179
  }
2144
2180
 
2145
2181
  // src/core/store/journal.ts
2146
- import { appendFile as appendFile2 } from "node:fs/promises";
2147
2182
  import { join as join7 } from "node:path";
2148
2183
  var JOURNAL_FILE = "journal.jsonl";
2149
2184
  async function appendJournal(projectDir2, events, onError = reportToStderr) {
2150
2185
  if (events.length === 0) return;
2151
2186
  const path = join7(projectDir2, JOURNAL_FILE);
2152
2187
  try {
2153
- await appendFile2(path, toJsonLines(events), "utf8");
2188
+ await appendJsonLines(path, events);
2154
2189
  } catch (error) {
2155
2190
  onError(path, error);
2156
2191
  }
2157
2192
  }
2158
2193
  async function readJournal(projectDir2, projectId) {
2159
- const { values, invalidLines } = await readJsonLines(join7(projectDir2, JOURNAL_FILE), journalEventSchema);
2194
+ return projectJournal(projectId, await readJsonLines(join7(projectDir2, JOURNAL_FILE), journalEventSchema));
2195
+ }
2196
+ function projectJournal(projectId, { values, invalidLines }) {
2160
2197
  return { projectId, events: values, invalidLines };
2161
2198
  }
2162
2199
  async function readJournals(root, projectIds) {
@@ -2934,11 +2971,11 @@ function anchorState(task, facts) {
2934
2971
  return moved === null || movedAnchor === null ? { kind: "changed" } : { kind: "moved", source: moved, anchor: movedAnchor };
2935
2972
  }
2936
2973
  function findSimilarTask(draft, tasks) {
2937
- const open3 = tasks.filter((task) => task.type === "task" && !isClosed(task.status));
2974
+ const open2 = tasks.filter((task) => task.type === "task" && !isClosed(task.status));
2938
2975
  const { source } = draft;
2939
- const bySource = source === void 0 ? void 0 : open3.find((task) => task.source !== void 0 && samePlace(task.source, source));
2976
+ const bySource = source === void 0 ? void 0 : open2.find((task) => task.source !== void 0 && samePlace(task.source, source));
2940
2977
  if (bySource !== void 0) return { task: taskRef(bySource), match: "source" };
2941
- const byTitle = open3.find((task) => similarTitles(task.title, draft.title));
2978
+ const byTitle = open2.find((task) => similarTitles(task.title, draft.title));
2942
2979
  return byTitle === void 0 ? null : { task: taskRef(byTitle), match: "title" };
2943
2980
  }
2944
2981
  function duplicateCandidates(tasks, symbolOf = () => null) {
@@ -3934,23 +3971,21 @@ function claudeProjectsDir(env, home2) {
3934
3971
  // src/cli/agents/agent.ts
3935
3972
  var AGENTS = ["claude", "codex", "cursor"];
3936
3973
  var AGENT_LABELS = { claude: "Claude Code", codex: "Codex", cursor: "Cursor" };
3937
- function agentHomeDir(agent, env, home2) {
3974
+ function agentHomeDir(agent, { env, home: home2 }) {
3938
3975
  if (agent === "claude") return claudeDir(env, home2);
3939
3976
  if (agent === "codex") return env.CODEX_HOME || join16(home2, ".codex");
3940
3977
  return join16(home2, ".cursor");
3941
3978
  }
3942
- function agentSkillsDir(agent, env, home2) {
3943
- if (agent === "claude") return claudeSkillsDir(env, home2);
3944
- if (agent === "codex") return join16(home2, ".agents", "skills");
3945
- return join16(agentHomeDir(agent, env, home2), "skills");
3979
+ function agentSkillsDir(agent, places) {
3980
+ return agent === "claude" ? claudeSkillsDir(places.env, places.home) : join16(places.home, ".agents", "skills");
3946
3981
  }
3947
- function legacySkillsDirs(agent, env, home2) {
3948
- return agent === "codex" ? [join16(agentHomeDir(agent, env, home2), "skills")] : [];
3982
+ function legacySkillsDirs(agent, places) {
3983
+ return agent === "claude" ? [] : [join16(agentHomeDir(agent, places), "skills")];
3949
3984
  }
3950
- async function detectAgents(env, home2) {
3985
+ async function detectAgents(places) {
3951
3986
  const detection = { found: [], missing: [] };
3952
3987
  for (const agent of AGENTS) {
3953
- const dir = agentHomeDir(agent, env, home2);
3988
+ const dir = agentHomeDir(agent, places);
3954
3989
  const present = agent === "claude" || ((await stat4(dir).catch(() => null))?.isDirectory() ?? false);
3955
3990
  if (present) detection.found.push(agent);
3956
3991
  else detection.missing.push({ agent, dir });
@@ -3964,7 +3999,7 @@ var PLUGIN_BY_LANGUAGE = { en: "p-backlog", ru: "p-backlog-ru" };
3964
3999
  var PLUGIN_NAMES = new Set(Object.values(PLUGIN_BY_LANGUAGE));
3965
4000
  var MARKETPLACE_SEPARATOR = "@";
3966
4001
  var enabledPluginsSchema = z8.object({ enabledPlugins: z8.record(z8.string(), z8.unknown()).optional() });
3967
- async function agentPlugin(agent, env, home2) {
4002
+ async function agentPlugin(agent, { env, home: home2 }) {
3968
4003
  if (agent !== "claude") return null;
3969
4004
  const settings = await readJsonFile(claudeSettingsPath(env, home2), enabledPluginsSchema);
3970
4005
  const enabled = Object.entries(settings?.enabledPlugins ?? {}).find(([id, on]) => on === true && PLUGIN_NAMES.has(pluginName(id)));
@@ -4048,20 +4083,20 @@ async function runLanguage(positionals, io) {
4048
4083
  const language = parseChoice(io.language, value, LANGUAGES, cliMessages(io.language).optionLabel.language);
4049
4084
  await writeSettings(io.backlogRoot, { language });
4050
4085
  io.print(`${io.language} \u2192 ${language}`);
4051
- const { found } = await detectAgents(io.env, io.home);
4086
+ const { found } = await detectAgents(io);
4052
4087
  for (const agent of found) await relinkSkill(agent, language, io);
4053
4088
  return EXIT.ok;
4054
4089
  }
4055
4090
  async function relinkSkill(agent, language, io) {
4056
4091
  const messages = cliMessages(language);
4057
4092
  const label = AGENT_LABELS[agent];
4058
- const plugin = await agentPlugin(agent, io.env, io.home);
4093
+ const plugin = await agentPlugin(agent, io);
4059
4094
  if (plugin !== null) {
4060
4095
  const wanted = pluginToSwitchTo(plugin, language);
4061
4096
  if (wanted !== null) io.print(`${label}: ${messages.pluginLanguageHint(plugin, wanted)}`);
4062
4097
  return;
4063
4098
  }
4064
- const skillsDir = agentSkillsDir(agent, io.env, io.home);
4099
+ const skillsDir = agentSkillsDir(agent, io);
4065
4100
  const target = join18(skillsDir, "backlog");
4066
4101
  const options = { skillsDir, packageRoot: io.packageRoot, platform: io.platform };
4067
4102
  try {
@@ -4298,6 +4333,21 @@ function isWorkStatus(status) {
4298
4333
  return status === "in-progress" || status === "blocked";
4299
4334
  }
4300
4335
 
4336
+ // src/core/stats/days.ts
4337
+ var STATS_DAYS = 30;
4338
+ function dayRange(now, count3) {
4339
+ return dayStarts(now, count3).map(formatLocalDay);
4340
+ }
4341
+ function dailyFlow(histories, now) {
4342
+ return flowOver(dayWindows(now), histories);
4343
+ }
4344
+ function dayWindows(now, count3 = STATS_DAYS) {
4345
+ return consecutivePeriods(dayStarts(now, count3), now.getTime());
4346
+ }
4347
+ function dayStarts(now, count3) {
4348
+ return Array.from({ length: count3 }, (_, index) => new Date(now.getFullYear(), now.getMonth(), now.getDate() - (count3 - 1 - index)));
4349
+ }
4350
+
4301
4351
  // src/core/stats/quality/accuracy.ts
4302
4352
  function accuracy(histories, period2) {
4303
4353
  const episodes = histories.flatMap(
@@ -4310,15 +4360,21 @@ function accuracy(histories, period2) {
4310
4360
  });
4311
4361
  return [...byEvidence, { evidence: "total", ...outcomeCounts(episodes) }];
4312
4362
  }
4313
- function accuracyWeeks(histories, now) {
4314
- return weekWindows(now).map((week) => {
4363
+ function accuracyOver(periods, histories) {
4364
+ return periods.map((span) => {
4315
4365
  const decided = histories.flatMap(
4316
- (history) => history.candidates.filter((candidate) => week.contains(candidate.at) && candidate.evidence !== "no-source").map((candidate) => outcomeAfter(history, candidate.at)).filter((outcome) => outcome !== "open")
4366
+ (history) => history.candidates.filter((candidate) => span.contains(candidate.at) && candidate.evidence !== "no-source").map((candidate) => outcomeAfter(history, candidate.at)).filter((outcome) => outcome !== "open")
4317
4367
  );
4318
4368
  const closed = decided.filter((outcome) => outcome === "closed").length;
4319
- return { start: formatLocalIso(new Date(week.from)), decided: decided.length, precision: decided.length === 0 ? null : closed / decided.length };
4369
+ return { start: formatLocalIso(new Date(span.from)), decided: decided.length, precision: decided.length === 0 ? null : closed / decided.length };
4320
4370
  });
4321
4371
  }
4372
+ function accuracyWeeks(histories, now) {
4373
+ return accuracyOver(weekWindows(now), histories);
4374
+ }
4375
+ function accuracyDays(histories, now) {
4376
+ return accuracyOver(dayWindows(now), histories);
4377
+ }
4322
4378
  function methodAccuracy(histories, period2) {
4323
4379
  return splitAccuracy(histories, period2, { evidence: "source-changed", keys: RECORDED_METHODS, keyOf: (candidate) => candidate.method });
4324
4380
  }
@@ -4615,12 +4671,12 @@ async function rememberShown(projectDir2, shown, io) {
4615
4671
 
4616
4672
  // src/cli/describe.ts
4617
4673
  function describeTask(task, index) {
4618
- const open3 = openBlockers(task, index);
4674
+ const open2 = openBlockers(task, index);
4619
4675
  return {
4620
4676
  task,
4621
4677
  progress: taskProgress(task, index),
4622
- openBlockers: open3,
4623
- inactiveBlockerIds: task.blockedBy.filter((id) => !open3.some((blocker) => blocker.id === id)),
4678
+ openBlockers: open2,
4679
+ inactiveBlockerIds: task.blockedBy.filter((id) => !open2.some((blocker) => blocker.id === id)),
4624
4680
  blocks: dependentTasks(task, index),
4625
4681
  related: relatedTasks(task, index),
4626
4682
  epic: task.epic === void 0 ? void 0 : index.byId.get(task.epic),
@@ -4805,8 +4861,8 @@ async function listProjects(io) {
4805
4861
  }
4806
4862
  const index = buildIndex(loaded.tasks);
4807
4863
  for (const project of loaded.projects) {
4808
- const open3 = filterTasks(loaded.tasks, { projectId: project.id, statuses: OPEN_STATUSES }, index).length;
4809
- io.print(cli.projectListLine({ id: project.id, name: project.name, prefix: project.prefix, statusWord: statusWord(cli, project.active), open: open3 }));
4864
+ const open2 = filterTasks(loaded.tasks, { projectId: project.id, statuses: OPEN_STATUSES }, index).length;
4865
+ io.print(cli.projectListLine({ id: project.id, name: project.name, prefix: project.prefix, statusWord: statusWord(cli, project.active), open: open2 }));
4810
4866
  }
4811
4867
  return EXIT.ok;
4812
4868
  }
@@ -4987,7 +5043,7 @@ function listenFailure(error, port, messages) {
4987
5043
  // src/server/start.ts
4988
5044
  import { serve } from "@hono/node-server";
4989
5045
  import { mkdir as mkdir6, rm as rm4, writeFile as writeFile3 } from "node:fs/promises";
4990
- import { join as join27 } from "node:path";
5046
+ import { join as join28 } from "node:path";
4991
5047
 
4992
5048
  // src/core/store/sweep.ts
4993
5049
  import { dirname as dirname7, join as join22 } from "node:path";
@@ -5137,7 +5193,7 @@ async function reserveNumbers(projects, expired) {
5137
5193
  import { serveStatic } from "@hono/node-server/serve-static";
5138
5194
  import { Hono as Hono3 } from "hono";
5139
5195
  import { secureHeaders } from "hono/secure-headers";
5140
- import { join as join24 } from "node:path";
5196
+ import { join as join25 } from "node:path";
5141
5197
 
5142
5198
  // src/server/api.ts
5143
5199
  import { Hono as Hono2 } from "hono";
@@ -5267,17 +5323,22 @@ function previousOf(task) {
5267
5323
  function createReportCache({ ttlMs, now }) {
5268
5324
  let entries = /* @__PURE__ */ new Map();
5269
5325
  return {
5270
- get: (key, compute) => {
5326
+ get: (key, compute, tags = []) => {
5271
5327
  const cached = entries.get(key);
5272
5328
  if (cached !== void 0 && now() - cached.at <= ttlMs) return cached.value;
5273
- const generation = entries;
5274
5329
  const value = compute();
5275
- generation.set(key, { at: now(), value });
5276
- value.catch(() => generation.delete(key));
5330
+ entries.set(key, { at: now(), value, tags });
5331
+ value.catch(() => {
5332
+ if (entries.get(key)?.value === value) entries.delete(key);
5333
+ });
5277
5334
  return value;
5278
5335
  },
5279
5336
  clear: () => {
5280
5337
  entries = /* @__PURE__ */ new Map();
5338
+ },
5339
+ clearTagged: (tags) => {
5340
+ const tagged = new Set(tags);
5341
+ for (const [key, entry] of entries) if (entry.tags.some((tag) => tagged.has(tag))) entries.delete(key);
5281
5342
  }
5282
5343
  };
5283
5344
  }
@@ -5322,32 +5383,36 @@ async function markOf(path) {
5322
5383
 
5323
5384
  // src/server/stats-api.ts
5324
5385
  import { Hono } from "hono";
5386
+ import { relative as relative3, sep as sep4 } from "node:path";
5325
5387
 
5326
5388
  // src/core/code/code-cache.ts
5327
5389
  import { join as join23 } from "node:path";
5328
5390
  import { z as z15 } from "zod";
5329
5391
  var CODE_CACHE_FILE = ".code-cache.json";
5330
- var CODE_CACHE_VERSION = 1;
5331
- var repoCodeSchema = z15.object({
5332
- commits: z15.array(z15.array(z15.string())),
5333
- lines: z15.array(z15.object({ path: z15.string(), lines: z15.number() })),
5334
- units: z15.array(z15.object({ date: z15.string(), lines: z15.number() }))
5392
+ var CODE_CACHE_VERSION = 2;
5393
+ var repoScanSchema = z15.object({
5394
+ head: z15.string(),
5395
+ main: z15.string().nullable(),
5396
+ commits: z15.array(z15.object({ date: z15.string(), paths: z15.array(z15.string()) })),
5397
+ units: z15.array(z15.object({ date: z15.string(), lines: z15.number() })),
5398
+ lines: z15.array(z15.object({ path: z15.string(), lines: z15.number() }))
5335
5399
  });
5336
5400
  var fixCommitSchema = z15.object({ date: z15.string(), landedAt: z15.string().optional(), byAgent: z15.boolean(), lines: z15.number(), testLines: z15.number() });
5337
5401
  var snapshotSchema = z15.object({
5338
5402
  version: z15.literal(CODE_CACHE_VERSION),
5339
- repos: z15.record(z15.string(), z15.object({ key: z15.string(), code: repoCodeSchema })),
5340
- fixes: z15.record(z15.string(), fixCommitSchema)
5403
+ repos: z15.record(z15.string(), repoScanSchema),
5404
+ fixes: z15.record(z15.string(), fixCommitSchema),
5405
+ unsettled: z15.record(z15.string(), z15.string().nullable())
5341
5406
  });
5342
5407
  function emptyCodeCache() {
5343
- return { repos: {}, fixes: {} };
5408
+ return { repos: {}, fixes: {}, unsettled: {} };
5344
5409
  }
5345
5410
  function createCodeCacheFile(root) {
5346
5411
  const path = join23(root, CODE_CACHE_FILE);
5347
5412
  return {
5348
5413
  read: async () => {
5349
5414
  const snapshot = await readJsonFile(path, snapshotSchema);
5350
- return snapshot === null ? emptyCodeCache() : { repos: snapshot.repos, fixes: snapshot.fixes };
5415
+ return snapshot === null ? emptyCodeCache() : { repos: snapshot.repos, fixes: snapshot.fixes, unsettled: snapshot.unsettled };
5351
5416
  },
5352
5417
  write: (snapshot) => writeFileAtomic(path, JSON.stringify({ version: CODE_CACHE_VERSION, ...snapshot }))
5353
5418
  };
@@ -5355,6 +5420,9 @@ function createCodeCacheFile(root) {
5355
5420
 
5356
5421
  // src/core/code/code-window.ts
5357
5422
  var CHURN_DAYS = 90;
5423
+ function churnWindowStart(now) {
5424
+ return new Date(now.getTime() - CHURN_DAYS * DAY_MS);
5425
+ }
5358
5426
 
5359
5427
  // src/core/code/test-paths.ts
5360
5428
  var TEST_DIRECTORIES = /* @__PURE__ */ new Set(["test", "tests", "__tests__", "e2e", "spec"]);
@@ -5376,7 +5444,6 @@ var CHURN_EXCLUDES = [...LOCK_EXCLUDES, ...NON_CODE_EXTENSIONS.map((ext) => `:!*
5376
5444
  var HEAD = "HEAD";
5377
5445
  var MAIN_REFS = ["origin/HEAD", "main", "master"];
5378
5446
  var AGENT_TRAILER = /^claude/i;
5379
- var GREP_PREFIX = "HEAD:";
5380
5447
  var RENAME_ARROW = " => ";
5381
5448
  async function readRefs(git, repo) {
5382
5449
  const commits = await resolveCommits(git, repo, [HEAD, ...MAIN_REFS]) ?? /* @__PURE__ */ new Map();
@@ -5384,14 +5451,68 @@ async function readRefs(git, repo) {
5384
5451
  const main = MAIN_REFS.map((ref) => commits.get(ref)).find((commit) => commit !== void 0);
5385
5452
  return { head, main: main ?? head };
5386
5453
  }
5387
- async function readRepoCode(git, repo, since, mainCommit) {
5388
- const [log2, grep, units] = await Promise.all([
5389
- git(repo, ["log", `--since=${since.toISOString()}`, `--format=tformat:${RECORD}`, "--name-only", "-M", "--relative", "-z", "--", "."]),
5390
- git(repo, ["grep", "-I", "-c", "-z", "", "HEAD", "--", ".", ...LOCK_EXCLUDES]),
5391
- readUnits(git, repo, since, mainCommit)
5454
+ async function readCommitsSince(git, repo, since, range) {
5455
+ const output = await git(repo, [
5456
+ "log",
5457
+ revisionsOf(range),
5458
+ "--full-history",
5459
+ "--sparse",
5460
+ `--since=${since.toISOString()}`,
5461
+ `--format=tformat:${RECORD}%P${FIELD}%cI`,
5462
+ "--name-only",
5463
+ "-M",
5464
+ "--relative",
5465
+ "-z",
5466
+ "--",
5467
+ "."
5392
5468
  ]);
5393
- if (log2 === null || grep === null) return null;
5394
- return { commits: parseCommits(log2), lines: parseLines(grep), units };
5469
+ if (output === null) return null;
5470
+ const { after } = range;
5471
+ const commits = records(output).map((record) => {
5472
+ const [header = "", ...files] = record.split("\0");
5473
+ return { ...parseHistoryHeader(header), paths: files.map((file) => file.replace(/^\n/, "")).filter((file) => file !== "") };
5474
+ });
5475
+ return {
5476
+ entries: commits.filter(({ paths }) => paths.length > 0).map(({ date, paths }) => ({ date, paths })),
5477
+ reachesAfter: after === void 0 || commits.some(({ parents }) => parents.includes(after))
5478
+ };
5479
+ }
5480
+ async function readUnits(git, repo, since, range) {
5481
+ const output = await git(repo, [
5482
+ "log",
5483
+ revisionsOf(range),
5484
+ "--first-parent",
5485
+ "--sparse",
5486
+ "--diff-merges=first-parent",
5487
+ `--since=${since.toISOString()}`,
5488
+ `--format=tformat:${RECORD}%P${FIELD}%cI`,
5489
+ "--numstat",
5490
+ "--relative",
5491
+ "--",
5492
+ ".",
5493
+ ...CHURN_EXCLUDES
5494
+ ]);
5495
+ if (output === null) return null;
5496
+ const { after } = range;
5497
+ const units = records(output).map((record) => {
5498
+ const [header = "", ...rows] = record.split("\n");
5499
+ return { ...parseHistoryHeader(header), rows: rows.filter((row) => row.trim() !== "") };
5500
+ });
5501
+ return {
5502
+ entries: units.filter(({ rows }) => rows.length > 0).map(({ date, rows }) => ({ date, lines: numstatLines(rows) })),
5503
+ reachesAfter: after === void 0 || units.some(({ parents }) => parents[0] === after)
5504
+ };
5505
+ }
5506
+ async function readLines(git, repo, head) {
5507
+ const output = await git(repo, ["grep", "-I", "-c", "-z", "", head, "--", ".", ...LOCK_EXCLUDES]);
5508
+ return output === null ? null : parseLines(output, `${head}:`);
5509
+ }
5510
+ function revisionsOf({ tip, after }) {
5511
+ return after === void 0 ? tip : `${after}..${tip}`;
5512
+ }
5513
+ function parseHistoryHeader(header) {
5514
+ const [parents = "", date = ""] = header.split(FIELD);
5515
+ return { parents: parents.split(" ").filter((parent) => parent !== ""), date: date.trim() };
5395
5516
  }
5396
5517
  async function readFixCommits(git, repo, { hashes, mainCommit }) {
5397
5518
  const fullHashes = await resolveCommits(git, repo, hashes);
@@ -5455,29 +5576,6 @@ function parseFixStats(output) {
5455
5576
  function records(output) {
5456
5577
  return output.split(RECORD).filter((record) => record.trim() !== "");
5457
5578
  }
5458
- async function readUnits(git, repo, since, mainCommit) {
5459
- if (mainCommit === null) return [];
5460
- const output = await git(repo, [
5461
- "log",
5462
- mainCommit,
5463
- "--first-parent",
5464
- "--diff-merges=first-parent",
5465
- `--since=${since.toISOString()}`,
5466
- `--format=tformat:${RECORD}%cI`,
5467
- "--numstat",
5468
- "--relative",
5469
- "--",
5470
- ".",
5471
- ...CHURN_EXCLUDES
5472
- ]);
5473
- return output === null ? [] : parseUnits(output);
5474
- }
5475
- function parseUnits(output) {
5476
- return output.split(RECORD).filter((record) => record.trim() !== "").map((record) => {
5477
- const [date = "", ...rows] = record.split("\n");
5478
- return { date: date.trim(), lines: numstatLines(rows) };
5479
- });
5480
- }
5481
5579
  function numstatLines(rows) {
5482
5580
  return sum(rows.filter((row) => row.trim() !== "").map(numstatRowLines).filter((lines) => !Number.isNaN(lines)));
5483
5581
  }
@@ -5485,43 +5583,95 @@ function numstatRowLines(row) {
5485
5583
  const [added = "", deleted = ""] = row.split(" ");
5486
5584
  return Number(added) + Number(deleted);
5487
5585
  }
5488
- function parseCommits(output) {
5489
- return output.split(RECORD).map((record) => record.split("\0").map((file) => file.replace(/^\n/, "")).filter((file) => file !== "")).filter((files) => files.length > 0);
5490
- }
5491
- function parseLines(output) {
5492
- return output.split("\n").filter((record) => record.startsWith(GREP_PREFIX)).map((record) => {
5493
- const [path = "", count3 = ""] = record.slice(GREP_PREFIX.length).split("\0");
5586
+ function parseLines(output, prefix) {
5587
+ return output.split("\n").filter((record) => record.startsWith(prefix)).map((record) => {
5588
+ const [path = "", count3 = ""] = record.slice(prefix.length).split("\0");
5494
5589
  return { path, lines: Number(count3) };
5495
5590
  });
5496
5591
  }
5497
5592
 
5593
+ // src/core/code/repo-scan.ts
5594
+ async function scanRepo(git, repo, { refs, now, previous }) {
5595
+ const { head, main } = refs;
5596
+ if (head === null) return null;
5597
+ const since = churnWindowStart(now);
5598
+ const [commits, units, lines] = await Promise.all([
5599
+ extendHistory((range) => readCommitsSince(git, repo, since, range), head, historyOf2(previous?.head, previous?.commits)),
5600
+ main === null ? [] : extendHistory((range) => readUnits(git, repo, since, range), main, historyOf2(previous?.main, previous?.units)),
5601
+ previous?.head === head ? previous.lines : readLines(git, repo, head)
5602
+ ]);
5603
+ if (commits === null || units === null || lines === null) return null;
5604
+ const scan = { head, main, commits: windowOf2(commits, since), units: windowOf2(units, since), lines };
5605
+ return previous !== void 0 && isSameScan(previous, scan) ? previous : scan;
5606
+ }
5607
+ function repoCodeOf(scan) {
5608
+ return { commits: scan.commits.map(({ paths }) => paths), lines: scan.lines, units: scan.units };
5609
+ }
5610
+ function historyOf2(tip, entries) {
5611
+ return tip === void 0 || tip === null || entries === void 0 ? void 0 : { tip, entries };
5612
+ }
5613
+ async function extendHistory(read, tip, previous) {
5614
+ if (previous?.tip === tip) return previous.entries;
5615
+ if (previous !== void 0) {
5616
+ const added = await read({ tip, after: previous.tip });
5617
+ if (added?.reachesAfter === true) return [...added.entries, ...previous.entries];
5618
+ }
5619
+ return (await read({ tip }))?.entries ?? null;
5620
+ }
5621
+ function windowOf2(entries, since) {
5622
+ return entries.filter((entry) => Date.parse(entry.date) >= since.getTime()).sort((a, b) => Date.parse(b.date) - Date.parse(a.date));
5623
+ }
5624
+ function isSameScan(previous, scan) {
5625
+ return previous.head === scan.head && previous.main === scan.main && previous.commits.length === scan.commits.length && previous.units.length === scan.units.length;
5626
+ }
5627
+
5498
5628
  // src/core/code/code-source.ts
5499
5629
  function createCodeSource({ home: home2, git = runGit, store, onError = () => {
5500
5630
  } }) {
5501
5631
  const repoCache = /* @__PURE__ */ new Map();
5502
5632
  const fixCache = /* @__PURE__ */ new Map();
5633
+ const unsettledCheckedAt = /* @__PURE__ */ new Map();
5503
5634
  let changed = false;
5504
5635
  let restored = null;
5636
+ let retainedRepos = null;
5505
5637
  const restore = () => {
5506
5638
  restored ??= readSnapshot(store, (error) => onError("read", error)).then((snapshot) => {
5507
5639
  for (const [repo, entry] of Object.entries(snapshot.repos)) if (!repoCache.has(repo)) repoCache.set(repo, entry);
5508
5640
  for (const [key, commit] of Object.entries(snapshot.fixes)) if (!fixCache.has(key)) fixCache.set(key, commit);
5641
+ for (const [key, main] of Object.entries(snapshot.unsettled)) if (!unsettledCheckedAt.has(key)) unsettledCheckedAt.set(key, main);
5509
5642
  });
5510
5643
  return restored;
5511
5644
  };
5512
5645
  const dropStaleFixes = (now, requested) => {
5513
- const oldest = now.getTime() - CHURN_DAYS * DAY_MS;
5646
+ const oldest = churnWindowStart(now).getTime();
5514
5647
  for (const [key, commit] of fixCache) {
5515
5648
  if (requested.has(key) || Date.parse(commit.date) >= oldest) continue;
5516
5649
  fixCache.delete(key);
5650
+ unsettledCheckedAt.delete(key);
5651
+ changed = true;
5652
+ }
5653
+ };
5654
+ const dropUnretainedRepos = () => {
5655
+ if (retainedRepos === null) return;
5656
+ const kept = retainedRepos;
5657
+ for (const repo of repoCache.keys()) {
5658
+ if (kept.has(repo)) continue;
5659
+ repoCache.delete(repo);
5660
+ changed = true;
5661
+ }
5662
+ for (const key of /* @__PURE__ */ new Set([...fixCache.keys(), ...unsettledCheckedAt.keys()])) {
5663
+ if (kept.has(repoOfFixCacheKey(key))) continue;
5664
+ fixCache.delete(key);
5665
+ unsettledCheckedAt.delete(key);
5517
5666
  changed = true;
5518
5667
  }
5519
5668
  };
5520
5669
  let writing = Promise.resolve();
5521
5670
  const persist = () => {
5671
+ dropUnretainedRepos();
5522
5672
  if (store === void 0 || !changed) return writing;
5523
5673
  changed = false;
5524
- const snapshot = { repos: Object.fromEntries(repoCache), fixes: Object.fromEntries(fixCache) };
5674
+ const snapshot = { repos: Object.fromEntries(repoCache), fixes: Object.fromEntries(fixCache), unsettled: Object.fromEntries(unsettledCheckedAt) };
5525
5675
  writing = writing.then(
5526
5676
  () => store.write(snapshot).catch((error) => {
5527
5677
  changed = true;
@@ -5539,35 +5689,39 @@ function createCodeSource({ home: home2, git = runGit, store, onError = () => {
5539
5689
  return started;
5540
5690
  };
5541
5691
  const readRepoOnce = async (repo, now) => {
5542
- const refs = await readRefs(git, repo);
5543
- if (refs.head === null) return null;
5544
- const key = repoKey(refs, now);
5545
- const cached = repoCache.get(repo);
5546
- if (cached?.key === key) return { key, code: cached.code };
5547
- const code = await readRepoCode(git, repo, new Date(now.getTime() - CHURN_DAYS * DAY_MS), refs.main);
5548
- if (code === null) return null;
5549
- repoCache.set(repo, { key, code });
5692
+ const previous = repoCache.get(repo);
5693
+ const scan = await scanRepo(git, repo, { refs: await readRefs(git, repo), now, previous });
5694
+ if (scan === null) return null;
5695
+ if (scan !== previous) {
5696
+ repoCache.set(repo, scan);
5697
+ changed = true;
5698
+ }
5699
+ return repoCodeOf(scan);
5700
+ };
5701
+ const rememberUnsettled = (key, main, commit) => {
5702
+ const checkedAt = commit.landedAt === void 0 ? main : void 0;
5703
+ if (unsettledCheckedAt.get(key) === checkedAt) return;
5704
+ if (checkedAt === void 0) unsettledCheckedAt.delete(key);
5705
+ else unsettledCheckedAt.set(key, checkedAt);
5550
5706
  changed = true;
5551
- return { key, code };
5552
5707
  };
5553
- const unsettledCheckedAt = /* @__PURE__ */ new Map();
5554
5708
  const needsReading = (key, main) => {
5555
5709
  const cached = fixCache.get(key);
5556
5710
  return cached === void 0 || cached.landedAt === void 0 && unsettledCheckedAt.get(key) !== main;
5557
5711
  };
5558
5712
  const fixCommitsOf = async (repo, main, hashes) => {
5559
- const unsettled = hashes.filter((hash) => needsReading(`${repo} ${hash}`, main));
5713
+ const unsettled = hashes.filter((hash) => needsReading(fixCacheKey(repo, hash), main));
5560
5714
  if (unsettled.length === 0) return;
5561
5715
  const found = await readFixCommits(git, repo, { hashes: unsettled, mainCommit: main });
5562
5716
  for (const [hash, commit] of found ?? []) {
5563
- const key = `${repo} ${hash}`;
5564
- if (commit.landedAt === void 0) unsettledCheckedAt.set(key, main);
5717
+ const key = fixCacheKey(repo, hash);
5718
+ rememberUnsettled(key, main, commit);
5565
5719
  if (fixCache.has(key) && commit.landedAt === void 0) continue;
5566
5720
  fixCache.set(key, commit);
5567
5721
  changed = true;
5568
5722
  }
5569
5723
  };
5570
- const fixReposOf = async (projects, now) => {
5724
+ const fixReposOf = async (projects) => {
5571
5725
  const refsOf = /* @__PURE__ */ new Map();
5572
5726
  const refsOnce = (repo) => remembered(refsOf, repo, () => readRefs(git, repo));
5573
5727
  const entries = await Promise.all(
@@ -5576,7 +5730,7 @@ function createCodeSource({ home: home2, git = runGit, store, onError = () => {
5576
5730
  project.repos.map(async (repo) => {
5577
5731
  const expanded = expandHome(repo, home2);
5578
5732
  const refs = await refsOnce(expanded);
5579
- return refs.head === null ? [] : [{ repo: expanded, key: repoKey(refs, now), main: refs.main }];
5733
+ return refs.head === null ? [] : [{ repo: expanded, main: refs.main }];
5580
5734
  })
5581
5735
  );
5582
5736
  return [project.id, repos.flat()];
@@ -5585,6 +5739,9 @@ function createCodeSource({ home: home2, git = runGit, store, onError = () => {
5585
5739
  return new Map(entries);
5586
5740
  };
5587
5741
  return {
5742
+ retain: (backlogProjects) => {
5743
+ retainedRepos = new Set(backlogProjects.flatMap((project) => project.repos.map((repo) => expandHome(repo, home2))));
5744
+ },
5588
5745
  stateKey: async (projects) => {
5589
5746
  const repos = [...new Set(projects.flatMap((project) => project.repos.map((repo) => expandHome(repo, home2))))];
5590
5747
  const states = await Promise.all(repos.map(async (repo) => `${repo}@${refsKey(await readRefs(git, repo))}`));
@@ -5605,7 +5762,7 @@ function createCodeSource({ home: home2, git = runGit, store, onError = () => {
5605
5762
  const readable = [];
5606
5763
  for (const { repo, read } of repos) {
5607
5764
  if (read !== null) {
5608
- readable.push(read.code);
5765
+ readable.push(read);
5609
5766
  } else if (!seenUnavailable.has(repo)) {
5610
5767
  seenUnavailable.add(repo);
5611
5768
  unavailableRepos.push(repo);
@@ -5618,13 +5775,13 @@ function createCodeSource({ home: home2, git = runGit, store, onError = () => {
5618
5775
  },
5619
5776
  fixCommits: async (projects, requests, now) => {
5620
5777
  await restore();
5621
- const reposOf = await fixReposOf(projects, now);
5778
+ const reposOf = await fixReposOf(projects);
5622
5779
  await Promise.all(requests.flatMap(({ projectId, hashes }) => (reposOf.get(projectId) ?? []).map(({ repo, main }) => fixCommitsOf(repo, main, hashes))));
5623
5780
  const found = /* @__PURE__ */ new Map();
5624
5781
  const requested = /* @__PURE__ */ new Set();
5625
5782
  for (const { projectId, hashes } of requests) {
5626
5783
  for (const hash of hashes) {
5627
- const keys = (reposOf.get(projectId) ?? []).map(({ repo }) => `${repo} ${hash}`);
5784
+ const keys = (reposOf.get(projectId) ?? []).map(({ repo }) => fixCacheKey(repo, hash));
5628
5785
  for (const key of keys) requested.add(key);
5629
5786
  const commit = keys.map((key) => fixCache.get(key)).find((cached) => cached !== void 0);
5630
5787
  if (commit !== void 0) found.set(fixKey(projectId, hash), commit);
@@ -5636,12 +5793,15 @@ function createCodeSource({ home: home2, git = runGit, store, onError = () => {
5636
5793
  }
5637
5794
  };
5638
5795
  }
5796
+ function fixCacheKey(repo, hash) {
5797
+ return `${repo} ${hash}`;
5798
+ }
5799
+ function repoOfFixCacheKey(key) {
5800
+ return key.slice(0, key.lastIndexOf(" "));
5801
+ }
5639
5802
  function refsKey(refs) {
5640
5803
  return `${refs.head ?? ""} ${refs.main ?? ""}`;
5641
5804
  }
5642
- function repoKey(refs, now) {
5643
- return `${refsKey(refs)} ${formatLocalDay(now)}`;
5644
- }
5645
5805
  async function readSnapshot(store, onError) {
5646
5806
  try {
5647
5807
  return await store?.read() ?? emptyCodeCache();
@@ -5710,14 +5870,14 @@ function folderRows(project, openTasks) {
5710
5870
  (file) => file.lines
5711
5871
  );
5712
5872
  const ownSources = openTasks.flatMap((task) => task.projectId === project.projectId && task.source !== void 0 ? [task.source] : []);
5713
- const open3 = countBy(ownSources, folderOf);
5714
- return [...open3.entries()].flatMap(([label, count3]) => {
5873
+ const open2 = countBy(ownSources, folderOf);
5874
+ return [...open2.entries()].flatMap(([label, count3]) => {
5715
5875
  const folderLines = lines.get(label) ?? 0;
5716
5876
  return folderLines >= MIN_FOLDER_LINES ? [{ label, ...densityRow(folderLines, count3) }] : [];
5717
5877
  }).sort((a, b) => byDensity(a, b) || a.label.localeCompare(b.label)).slice(0, FOLDER_LIMIT);
5718
5878
  }
5719
- function densityRow(lines, open3) {
5720
- return { lines, open: open3, perKloc: lines === 0 ? null : open3 * LINES_PER_UNIT / lines };
5879
+ function densityRow(lines, open2) {
5880
+ return { lines, open: open2, perKloc: lines === 0 ? null : open2 * LINES_PER_UNIT / lines };
5721
5881
  }
5722
5882
  function byDensity(a, b) {
5723
5883
  return (b.perKloc ?? -1) - (a.perKloc ?? -1);
@@ -5830,26 +5990,6 @@ function costOf(model, tokens) {
5830
5990
  return total / 1e6;
5831
5991
  }
5832
5992
 
5833
- // src/core/stats/days.ts
5834
- var STATS_DAYS = 30;
5835
- function dayRange(now, count3) {
5836
- return dayStarts(now, count3).map(formatLocalDay);
5837
- }
5838
- function dailyIntake(histories, now) {
5839
- const created = /* @__PURE__ */ new Map();
5840
- for (const history of histories) {
5841
- const day = formatLocalDay(new Date(history.createdAt));
5842
- created.set(day, (created.get(day) ?? 0) + 1);
5843
- }
5844
- return dayRange(now, STATS_DAYS).map((day) => ({ day, created: created.get(day) ?? 0 }));
5845
- }
5846
- function dayWindows(now, count3 = STATS_DAYS) {
5847
- return consecutivePeriods(dayStarts(now, count3), now.getTime());
5848
- }
5849
- function dayStarts(now, count3) {
5850
- return Array.from({ length: count3 }, (_, index) => new Date(now.getFullYear(), now.getMonth(), now.getDate() - (count3 - 1 - index)));
5851
- }
5852
-
5853
5993
  // src/core/stats/cost/cost-report.ts
5854
5994
  var COST_TOTALS_DAYS = 7;
5855
5995
  function costReport({ buckets, runs, projectOf: projectOf2, projectId, now, scan }) {
@@ -5867,6 +6007,10 @@ function costReport({ buckets, runs, projectOf: projectOf2, projectId, now, scan
5867
6007
  since: sinceOf(scopedBuckets),
5868
6008
  totals: totalsOf(inDays(bucketsByDay, totalsDays), inDays(runsByDay, totalsDays)),
5869
6009
  days: days3.map((day) => dayRow(day, bucketsByDay.get(day) ?? [], runsByDay.get(day) ?? [])),
6010
+ weeks: weekWindows(now).map((week) => {
6011
+ const weekDays = daysOf(week);
6012
+ return periodRow(formatLocalIso(new Date(week.from)), inDays(bucketsByDay, weekDays), inDays(runsByDay, weekDays));
6013
+ }),
5870
6014
  models: modelsOf(scopedBuckets),
5871
6015
  commands: commandsOf(inDays(runsByDay, days3))
5872
6016
  };
@@ -5878,6 +6022,10 @@ function memoizedByCwd(projectOf2) {
5878
6022
  return known.get(cwd) ?? null;
5879
6023
  };
5880
6024
  }
6025
+ function daysOf(week) {
6026
+ const monday = new Date(week.from);
6027
+ return dayRange(new Date(monday.getFullYear(), monday.getMonth(), monday.getDate() + DAYS_PER_WEEK - 1), DAYS_PER_WEEK);
6028
+ }
5881
6029
  function localDay(at) {
5882
6030
  const moment = Date.parse(at);
5883
6031
  return Number.isNaN(moment) ? "" : formatLocalDay(new Date(moment));
@@ -5913,19 +6061,24 @@ function runCounts(runs) {
5913
6061
  const hookRuns = runs.filter((run) => run.command === HOOK_STOP_COMMAND).length;
5914
6062
  return { cliRuns: runs.length - hookRuns, hookRuns };
5915
6063
  }
5916
- function dayRow(day, dayBuckets, dayRuns) {
5917
- const hookBuckets = dayBuckets.filter((bucket) => bucket.kind === "hook");
5918
- const cliBuckets = dayBuckets.filter((bucket) => bucket.kind === "cli" || bucket.kind === "skill");
6064
+ function costRowNumbers(buckets, runs) {
6065
+ const hookBuckets = buckets.filter((bucket) => bucket.kind === "hook");
6066
+ const cliBuckets = buckets.filter((bucket) => bucket.kind === "cli" || bucket.kind === "skill");
5919
6067
  return {
5920
- day,
5921
6068
  hookTokens: tokensTotalOf(hookBuckets),
5922
6069
  cliTokens: tokensTotalOf(cliBuckets),
5923
- cost: costOfBuckets(dayBuckets),
5924
- hasUnpricedTokens: hasUnpricedTokens(dayBuckets),
5925
- hookTurns: sum(dayBuckets.map((bucket) => bucket.hookTurns)),
5926
- ...runCounts(dayRuns)
6070
+ cost: costOfBuckets(buckets),
6071
+ hasUnpricedTokens: hasUnpricedTokens(buckets),
6072
+ hookTurns: sum(buckets.map((bucket) => bucket.hookTurns)),
6073
+ ...runCounts(runs)
5927
6074
  };
5928
6075
  }
6076
+ function dayRow(day, dayBuckets, dayRuns) {
6077
+ return { day, ...costRowNumbers(dayBuckets, dayRuns) };
6078
+ }
6079
+ function periodRow(start, periodBuckets, periodRuns) {
6080
+ return { start, ...costRowNumbers(periodBuckets, periodRuns) };
6081
+ }
5929
6082
  function modelsOf(buckets) {
5930
6083
  return [...groupBy(buckets, (bucket) => bucket.model).entries()].map(([key, modelBuckets]) => ({ ...splitFastModel(key), tokens: tokensTotalOf(modelBuckets), cost: costOfBuckets(modelBuckets) })).filter((row) => row.tokens > 0).sort((a, b) => b.tokens - a.tokens);
5931
6084
  }
@@ -6022,8 +6175,8 @@ function sizeOf(samples) {
6022
6175
  }
6023
6176
  function totalsOf2(deferred, units, estimate) {
6024
6177
  const fixed = deferred.flatMap((item) => item.fixedLines === null ? [] : [item.fixedLines]);
6025
- const open3 = deferred.filter((item) => item.fixedLines === null);
6026
- const estimated = estimatedSizeOf(open3, estimate);
6178
+ const open2 = deferred.filter((item) => item.fixedLines === null);
6179
+ const estimated = estimatedSizeOf(open2, estimate);
6027
6180
  const estimatedLines = estimated?.lines ?? null;
6028
6181
  const realLines = sum(units.map((unit) => unit.lines));
6029
6182
  const fixedLines = sum(fixed);
@@ -6033,15 +6186,15 @@ function totalsOf2(deferred, units, estimate) {
6033
6186
  realLines,
6034
6187
  fixedTasks: fixed.length,
6035
6188
  fixedLines,
6036
- openTasks: open3.length,
6189
+ openTasks: open2.length,
6037
6190
  estimatedLines,
6038
6191
  deferredLines,
6039
6192
  deferredTestLines: sum(deferred.map((item) => item.fixedTestLines)) + (estimated?.testLines ?? 0),
6040
- noiseShare: realLines === 0 || estimatedLines === null && open3.length > 0 ? null : Math.min(1, deferredLines / denominator)
6193
+ noiseShare: realLines === 0 || estimatedLines === null && open2.length > 0 ? null : Math.min(1, deferredLines / denominator)
6041
6194
  };
6042
6195
  }
6043
- function estimatedSizeOf(open3, estimate) {
6044
- const estimates = open3.map((item) => estimate(item.history.category));
6196
+ function estimatedSizeOf(open2, estimate) {
6197
+ const estimates = open2.map((item) => estimate(item.history.category));
6045
6198
  if (estimates.some((size) => size === null)) return null;
6046
6199
  const sizes = estimates.filter((size) => size !== null);
6047
6200
  return { lines: sum(sizes.map((size) => size.lines)), testLines: sum(sizes.map((size) => size.testLines)) };
@@ -6149,6 +6302,7 @@ function qualityReport(input, base = reportBase(input), graphs = []) {
6149
6302
  ...base.head,
6150
6303
  accuracy: accuracy(histories, period2),
6151
6304
  accuracyWeeks: accuracyWeeks(histories, now),
6305
+ accuracyDays: accuracyDays(histories, now),
6152
6306
  methodAccuracy: methodAccuracy(histories, period2),
6153
6307
  matchAccuracy: matchAccuracy(histories, period2),
6154
6308
  graph: { projects: graphs, filter: graphFilterEffect(histories, period2) },
@@ -6168,7 +6322,7 @@ function statsReport(input, base = reportBase(input)) {
6168
6322
  ...base.head,
6169
6323
  totals: totals(histories, now, period2, base.scope.journalStart),
6170
6324
  weeks: weeklyFlow(histories, now),
6171
- days: dailyIntake(histories, now),
6325
+ days: dailyFlow(histories, now),
6172
6326
  hotspots: hotspots(openTasks, scopeLabel(projectId)),
6173
6327
  age: ageBreakdown(openTasks, now),
6174
6328
  closing: closingBreakdown(histories, period2)
@@ -6215,9 +6369,150 @@ function previousTotals(histories, nowMs, journalStart) {
6215
6369
  };
6216
6370
  }
6217
6371
 
6372
+ // src/server/stats-sources.ts
6373
+ import { join as join24 } from "node:path";
6374
+
6375
+ // src/core/store/jsonl-tail.ts
6376
+ import { createHash as createHash5 } from "node:crypto";
6377
+ var HEAD_FINGERPRINT_BYTES = 4096;
6378
+ var EMPTY_FINGERPRINT = createHash5("sha1").digest("hex");
6379
+ function createJsonlTail(path, schema) {
6380
+ let state = emptyState();
6381
+ let generation = 0;
6382
+ let queue = Promise.resolve();
6383
+ function read() {
6384
+ const result = queue.then(doRead);
6385
+ queue = result.catch(() => void 0);
6386
+ return result;
6387
+ }
6388
+ async function doRead() {
6389
+ const pending = await withExistingFile(path, consumeNew);
6390
+ if (pending === null) restart();
6391
+ const { values, invalidLines, bytes } = pending ?? nothingPending();
6392
+ return { values: values.length === 0 ? state.values : [...state.values, ...values], invalidLines: state.invalidLines + invalidLines, length: state.offset + bytes, generation };
6393
+ }
6394
+ async function consumeNew(handle) {
6395
+ const { size } = await handle.stat();
6396
+ if (size < state.offset || !await headMatches(handle)) restart();
6397
+ return size > state.offset ? consumeUpTo(handle, size) : nothingPending();
6398
+ }
6399
+ function restart() {
6400
+ if (state.offset === 0) return;
6401
+ generation += 1;
6402
+ state = emptyState();
6403
+ }
6404
+ async function headMatches(handle) {
6405
+ if (state.offset === 0) return true;
6406
+ return await headFingerprintAt(handle, state.offset) === state.headFingerprint;
6407
+ }
6408
+ async function consumeUpTo(handle, size) {
6409
+ const chunk = await readAt(handle, state.offset, size - state.offset);
6410
+ const completeLength = chunk.lastIndexOf(NEWLINE) + 1;
6411
+ if (completeLength > 0) {
6412
+ const parsed = parseJsonLines(chunk.subarray(0, completeLength).toString("utf8"), schema);
6413
+ const offset = state.offset + completeLength;
6414
+ state = { offset, headFingerprint: await headFingerprintAt(handle, offset), values: [...state.values, ...parsed.values], invalidLines: state.invalidLines + parsed.invalidLines };
6415
+ }
6416
+ const pending = chunk.subarray(completeLength);
6417
+ return { ...parseJsonLines(pending.toString("utf8"), schema), bytes: pending.length };
6418
+ }
6419
+ return { read };
6420
+ }
6421
+ function emptyState() {
6422
+ return { offset: 0, headFingerprint: EMPTY_FINGERPRINT, values: [], invalidLines: 0 };
6423
+ }
6424
+ function nothingPending() {
6425
+ return { values: [], invalidLines: 0, bytes: 0 };
6426
+ }
6427
+ async function headFingerprintAt(handle, offset) {
6428
+ const length = Math.min(offset, HEAD_FINGERPRINT_BYTES);
6429
+ if (length === 0) return EMPTY_FINGERPRINT;
6430
+ return createHash5("sha1").update(await readAt(handle, 0, length)).digest("hex");
6431
+ }
6432
+
6433
+ // src/server/stats-sources.ts
6434
+ var ALL_PROJECTS_SLOT = "*";
6435
+ function createStatsSources(root, computeCost) {
6436
+ const tails = /* @__PURE__ */ new Map();
6437
+ const bases = /* @__PURE__ */ new Map();
6438
+ const runsTail = createJsonlTail(join24(root, RUNS_FILE), cliRunSchema);
6439
+ const costMemos = /* @__PURE__ */ new Map();
6440
+ const snapshotIds = /* @__PURE__ */ new WeakMap();
6441
+ let lastSnapshotId = 0;
6442
+ const snapshotIdOf = (snapshot) => {
6443
+ const known = snapshotIds.get(snapshot);
6444
+ if (known !== void 0) return known;
6445
+ lastSnapshotId += 1;
6446
+ snapshotIds.set(snapshot, lastSnapshotId);
6447
+ return lastSnapshotId;
6448
+ };
6449
+ const tailOf = (projectId) => {
6450
+ const known = tails.get(projectId);
6451
+ if (known !== void 0) return known;
6452
+ const created = createJsonlTail(join24(root, projectId, JOURNAL_FILE), journalEventSchema);
6453
+ tails.set(projectId, created);
6454
+ return created;
6455
+ };
6456
+ const readTailed = async (projectId) => {
6457
+ const { length, generation, ...lines } = await tailOf(projectId).read();
6458
+ return { journal: projectJournal(projectId, lines), position: `${projectId}=${generation}:${length}` };
6459
+ };
6460
+ const rememberedBase = (snapshot, tailed) => (slot, input) => {
6461
+ const slotKey = `${slot}|${input.projectId ?? "*"}`;
6462
+ const positions = tailed.filter(({ journal }) => input.projectId === void 0 || journal.projectId === input.projectId).map(({ position }) => position);
6463
+ const key = `${snapshotIdOf(snapshot)}|${slotKey}|${positions.join(",")}`;
6464
+ const remembered2 = bases.get(slotKey);
6465
+ if (remembered2?.key === key) return remembered2.base;
6466
+ const base = reportBase(input);
6467
+ bases.set(slotKey, { projectId: input.projectId, key, base });
6468
+ return base;
6469
+ };
6470
+ return {
6471
+ read: async (snapshot, projectIds) => {
6472
+ const tailed = await Promise.all(projectIds.map(readTailed));
6473
+ return { journals: tailed.map(({ journal }) => journal), baseOf: rememberedBase(snapshot, tailed) };
6474
+ },
6475
+ retain: (projectIds) => {
6476
+ const kept = new Set(projectIds);
6477
+ pruneUnlessKept(tails, kept, (projectId) => projectId);
6478
+ pruneUnlessKept(bases, kept, (_, { projectId }) => projectId);
6479
+ pruneUnlessKept(costMemos, kept, (slot) => slot === ALL_PROJECTS_SLOT ? void 0 : slot);
6480
+ },
6481
+ costReport: async (request) => {
6482
+ const { values: runs, length, generation } = await runsTail.read();
6483
+ const inputs = { ...request, runs };
6484
+ const slot = inputs.scope.projectId ?? ALL_PROJECTS_SLOT;
6485
+ const keyParts = {
6486
+ usage: `${inputs.usage.revision}`,
6487
+ runs: `${generation}:${length}`,
6488
+ scope: `${snapshotIdOf(inputs.scope.snapshot)}/${slot}`,
6489
+ now: formatLocalDay(inputs.now)
6490
+ };
6491
+ const key = Object.values(keyParts).join("|");
6492
+ const remembered2 = costMemos.get(slot);
6493
+ if (remembered2?.key === key) return remembered2.report;
6494
+ const report = computeCost(inputs);
6495
+ costMemos.set(slot, { key, report });
6496
+ report.catch(() => {
6497
+ if (costMemos.get(slot)?.report === report) costMemos.delete(slot);
6498
+ });
6499
+ return report;
6500
+ }
6501
+ };
6502
+ }
6503
+ function pruneUnlessKept(map, kept, projectIdOf) {
6504
+ for (const [key, value] of map) {
6505
+ const projectId = projectIdOf(key, value);
6506
+ if (projectId !== void 0 && !kept.has(projectId)) map.delete(key);
6507
+ }
6508
+ }
6509
+
6218
6510
  // src/server/stats-api.ts
6219
6511
  var REPORT_TTL_MS = 5 * 60 * 1e3;
6220
- function createStatsApi({ root, readLanguage: readLanguage2, now, home: home2, usage, memory, warn: warn2, backlog }) {
6512
+ var ALL_PROJECTS_TAG = "project:*";
6513
+ var WHOLE_BACKLOG_TAG = "whole-backlog";
6514
+ var projectTag = (projectId) => `project:${projectId}`;
6515
+ function createStatsApi({ root, readLanguage: readLanguage2, now, home: home2, usage, memory, warn: warn2, backlog, graphHealth: graphHealth2 }) {
6221
6516
  const routes = new Hono();
6222
6517
  const reports = createReportCache({ ttlMs: REPORT_TTL_MS, now: () => now().getTime() });
6223
6518
  const onCodeSourceError = (kind, error) => void readLanguage2().then((language) => {
@@ -6227,68 +6522,95 @@ function createStatsApi({ root, readLanguage: readLanguage2, now, home: home2, u
6227
6522
  });
6228
6523
  const codeSource = createCodeSource({ home: home2, store: createCodeCacheFile(root), onError: onCodeSourceError });
6229
6524
  const lookupRepoRoot = cachedRepoRoots();
6525
+ const sources = createStatsSources(root, (inputs) => costOf2(inputs, home2, lookupRepoRoot));
6526
+ let knownProjectIds = [];
6527
+ let forgetCount = 0;
6230
6528
  const statsScopeOf = async (c, { wholeBacklog }) => {
6231
6529
  const projectId = c.req.query("project") || void 0;
6232
- const { projects, tasks, errors } = await backlog();
6530
+ const snapshot = await backlog();
6531
+ const { projects, tasks, errors } = snapshot;
6532
+ knownProjectIds = projects.map((project) => project.id);
6533
+ sources.retain(knownProjectIds);
6534
+ codeSource.retain(projects);
6233
6535
  if (projectId !== void 0 && !projects.some((project) => project.id === projectId)) {
6234
6536
  return c.json({ errors: [serverMessages(await readLanguage2()).projectNotFound(projectId)] }, 404);
6235
6537
  }
6236
- const included = (project) => project.id === projectId || (projectId === void 0 || wholeBacklog) && project.active;
6237
- const scoped = projects.filter(included);
6538
+ const scoped = projectsInScope(projects, projectId, { wholeBacklog });
6238
6539
  const scopedIds = new Set(scoped.map((project) => project.id));
6239
6540
  const inScope = (task) => scopedIds.has(task.projectId);
6240
- return { projectId, projects: scoped, tasks: tasks.filter(inScope), unparsedTasks: unparsedTasks(errors).filter(inScope) };
6541
+ return { projectId, projects: scoped, tasks: tasks.filter(inScope), unparsedTasks: unparsedTasks(errors).filter(inScope), snapshot };
6241
6542
  };
6242
6543
  const scopedStats = (name, report, { sourceKey, wholeBacklog = false } = {}) => async (c) => {
6544
+ const forgetCountAtRead = forgetCount;
6243
6545
  const scope = await statsScopeOf(c, { wholeBacklog });
6244
6546
  if (scope instanceof Response) return scope;
6245
6547
  const moment = now();
6246
6548
  const key = [name, scope.projectId ?? "*", formatLocalDay(moment), sourceKey === void 0 ? "" : await sourceKey(scope.projects)].join("|");
6247
- const result = await reports.get(key, async () => {
6248
- const journals = await readJournals(root, scope.projects.map((project) => project.id));
6549
+ const tags = wholeBacklog ? [WHOLE_BACKLOG_TAG] : [scope.projectId === void 0 ? ALL_PROJECTS_TAG : projectTag(scope.projectId)];
6550
+ const compute = async () => {
6551
+ const { journals, baseOf } = await sources.read(scope.snapshot, scope.projects.map((project) => project.id));
6249
6552
  const input = { tasks: scope.tasks, journals, now: moment, projectId: scope.projectId, unparsedTasks: scope.unparsedTasks };
6250
- return report(input, reportBase(input), scope.projects);
6251
- });
6252
- return c.json(result);
6553
+ const wholeBacklogBase = () => baseOf("backlog", { ...input, projectId: void 0 });
6554
+ return report({ input, base: baseOf("scoped", input), projects: scope.projects, snapshot: scope.snapshot, wholeBacklogBase });
6555
+ };
6556
+ const scopeOutdated = forgetCount !== forgetCountAtRead;
6557
+ return c.json(await (scopeOutdated ? compute() : reports.get(key, compute, tags)));
6253
6558
  };
6254
- const statsOfCode = async (input, base, projects) => codeReport({ ...input, code: await codeSource.collect(projects, input.now) }, base);
6255
- const statsOfEffect = async (input, base, projects) => {
6256
- const backlogBase = input.projectId === void 0 ? base : reportBase({ ...input, projectId: void 0 });
6559
+ const statsOfCode = async ({ input, base, projects }) => codeReport({ ...input, code: await codeSource.collect(projects, input.now) }, base);
6560
+ const statsOfEffect = async ({ input, base, projects, wholeBacklogBase }) => {
6561
+ const backlogBase = input.projectId === void 0 ? base : wholeBacklogBase();
6257
6562
  const scoped = projects.filter((project) => input.projectId === void 0 || project.id === input.projectId);
6258
6563
  const code = await codeSource.collect(scoped, input.now);
6259
6564
  const fixCommits = await codeSource.fixCommits(projects, codeFixRequests({ ...input, projectId: void 0 }, backlogBase), input.now);
6260
6565
  return effectReport({ ...input, code: { ...code, fixCommits } }, base, backlogBase);
6261
6566
  };
6262
- const statsOfQuality = async (input, base, projects) => qualityReport(input, base, await projectGraphs(projects, input.tasks, home2));
6263
- const statsOfCost = async (projectId, projects) => {
6264
- usage.ensureStarted();
6265
- const { cache, scan } = usage.snapshot();
6266
- const buckets = bucketsOf2(cache);
6267
- const runs = await readRuns(root);
6268
- const repoRoots = projectId === void 0 ? /* @__PURE__ */ new Map() : await resolveRepoRoots(lookupRepoRoot, [...buckets, ...runs].map((entry) => entry.cwd));
6269
- const projectOf2 = (cwd) => {
6270
- const roots = repoRoots.get(cwd) ?? null;
6271
- return roots === null ? null : findProjectForRoots(projects, roots, home2)?.id ?? null;
6272
- };
6273
- return costReport({ buckets, runs, projectOf: projectOf2, projectId, now: now(), scan });
6567
+ const statsOfQuality = async ({ input, base, projects, snapshot }) => {
6568
+ const graphs = await Promise.all(projects.map(async (project) => ({ projectId: project.id, name: project.name, ...await graphHealth2(snapshot, project) })));
6569
+ return qualityReport(input, base, graphs);
6274
6570
  };
6275
6571
  const codeState = { sourceKey: (projects) => codeSource.stateKey(projects) };
6276
- routes.get("/stats", scopedStats("stats", (input, base) => statsReport(input, base)));
6572
+ routes.get("/stats", scopedStats("stats", ({ input, base }) => statsReport(input, base)));
6277
6573
  routes.get("/stats/code", scopedStats("code", statsOfCode, codeState));
6278
6574
  routes.get("/stats/effect", scopedStats("effect", statsOfEffect, { ...codeState, wholeBacklog: true }));
6279
6575
  routes.get("/stats/quality", scopedStats("quality", statsOfQuality));
6280
- routes.get("/stats/signals", scopedStats("signals", (input, base) => ({ signals: statsSignals(input, base) })));
6576
+ routes.get("/stats/signals", scopedStats("signals", ({ input, base }) => ({ signals: statsSignals(input, base) })));
6281
6577
  routes.get("/stats/cost", async (c) => {
6282
6578
  const scope = await statsScopeOf(c, { wholeBacklog: true });
6283
- return scope instanceof Response ? scope : c.json(await statsOfCost(scope.projectId, scope.projects));
6579
+ if (scope instanceof Response) return scope;
6580
+ usage.ensureStarted();
6581
+ const { snapshot, projectId } = scope;
6582
+ return c.json(await sources.costReport({ usage: usage.snapshot(), scope: { snapshot, projectId }, now: now() }));
6284
6583
  });
6285
6584
  routes.get("/stats/memory", (c) => c.json({ samples: memory.samples() }));
6286
- return { routes, forget: () => reports.clear() };
6287
- }
6288
- async function projectGraphs(projects, tasks, home2) {
6289
- return Promise.all(
6290
- projects.map(async (project) => ({ projectId: project.id, name: project.name, ...await projectGraphHealth(project, tasks, home2) }))
6291
- );
6585
+ const projectIdOfPath = (path) => {
6586
+ const [first] = relative3(root, path).split(sep4);
6587
+ return knownProjectIds.find((id) => id === first);
6588
+ };
6589
+ const forget = (paths) => {
6590
+ forgetCount += 1;
6591
+ if (paths === void 0 || paths.length === 0) return void reports.clear();
6592
+ const changedProjectIds = /* @__PURE__ */ new Set();
6593
+ for (const path of paths) {
6594
+ const projectId = projectIdOfPath(path);
6595
+ if (projectId === void 0) return void reports.clear();
6596
+ changedProjectIds.add(projectId);
6597
+ }
6598
+ reports.clearTagged([ALL_PROJECTS_TAG, WHOLE_BACKLOG_TAG, ...[...changedProjectIds].map(projectTag)]);
6599
+ };
6600
+ return { routes, forget };
6601
+ }
6602
+ function projectsInScope(projects, projectId, { wholeBacklog }) {
6603
+ return projects.filter((project) => project.id === projectId || (projectId === void 0 || wholeBacklog) && project.active);
6604
+ }
6605
+ async function costOf2({ usage: { cache, scan }, runs, scope: { snapshot, projectId }, now }, home2, lookupRepoRoot) {
6606
+ const projects = projectsInScope(snapshot.projects, projectId, { wholeBacklog: true });
6607
+ const buckets = bucketsOf2(cache);
6608
+ const repoRoots = projectId === void 0 ? /* @__PURE__ */ new Map() : await resolveRepoRoots(lookupRepoRoot, [...buckets, ...runs].map((entry) => entry.cwd));
6609
+ const projectOf2 = (cwd) => {
6610
+ const roots = repoRoots.get(cwd) ?? null;
6611
+ return roots === null ? null : findProjectForRoots(projects, roots, home2)?.id ?? null;
6612
+ };
6613
+ return costReport({ buckets, runs, projectOf: projectOf2, projectId, now, scan });
6292
6614
  }
6293
6615
  function bucketsOf2(cache) {
6294
6616
  return Object.values(cache.files).flatMap((entry) => entry.buckets);
@@ -6311,30 +6633,35 @@ function createApi({ root, readLanguage: readLanguage2, changes, now, home: home
6311
6633
  });
6312
6634
  return snapshot;
6313
6635
  };
6314
- const stats = createStatsApi({ root, readLanguage: readLanguage2, now, home: home2, usage, memory, warn: warn2, backlog });
6315
- const graphStates = createReportCache({ ttlMs: GRAPH_STATE_TTL_MS, now: () => now().getTime() });
6316
- const forgetBacklog = () => {
6636
+ const graphHealthsBySnapshot = /* @__PURE__ */ new WeakMap();
6637
+ const graphHealth2 = (backlogSnapshot, project) => {
6638
+ let healths = graphHealthsBySnapshot.get(backlogSnapshot);
6639
+ if (healths === void 0) {
6640
+ healths = createReportCache({ ttlMs: GRAPH_STATE_TTL_MS, now: () => now().getTime() });
6641
+ graphHealthsBySnapshot.set(backlogSnapshot, healths);
6642
+ }
6643
+ return healths.get(project.id, () => projectGraphHealth(project, backlogSnapshot.tasks, home2));
6644
+ };
6645
+ const stats = createStatsApi({ root, readLanguage: readLanguage2, now, home: home2, usage, memory, warn: warn2, backlog, graphHealth: graphHealth2 });
6646
+ const forgetBacklog = (paths) => {
6317
6647
  snapshot = null;
6318
- stats.forget();
6319
- graphStates.clear();
6648
+ stats.forget(paths);
6320
6649
  };
6321
6650
  const recordOwnWrites = async (writes) => {
6322
6651
  if (writes.length > 0) await revisions.recordOwnWrites(writes);
6323
- forgetBacklog();
6652
+ forgetBacklog(writes.length > 0 ? writes.map((write) => write.path) : void 0);
6324
6653
  };
6325
6654
  const streams = /* @__PURE__ */ new Set();
6326
6655
  changes.subscribe(async (paths) => {
6327
- if (await revisions.settle(paths) === "foreign") forgetBacklog();
6328
- else stats.forget();
6656
+ if (await revisions.settle(paths) === "foreign") forgetBacklog(paths);
6657
+ else stats.forget(paths);
6329
6658
  const revision = revisions.current();
6330
6659
  for (const send of streams) send(revision);
6331
6660
  });
6332
6661
  api.get("/projects", async (c) => {
6333
- const { projects, tasks, revision } = await backlog();
6334
- const withGraph = async (project) => {
6335
- const codeGraph2 = await graphStates.get(project.id, async () => (await projectGraphHealth(project, tasks, home2)).state);
6336
- return { ...project, codeGraph: codeGraph2 };
6337
- };
6662
+ const loaded = await backlog();
6663
+ const { projects, revision } = loaded;
6664
+ const withGraph = async (project) => ({ ...project, codeGraph: (await graphHealth2(loaded, project)).state });
6338
6665
  return c.json({ projects: await Promise.all(projects.map(withGraph)), revision });
6339
6666
  });
6340
6667
  api.get("/tasks", async (c) => {
@@ -6488,13 +6815,13 @@ function createApp({ root, readLanguage: readLanguage2, changes, allowedHosts, h
6488
6815
  });
6489
6816
  if (staticDir !== void 0) {
6490
6817
  app.use("*", serveStatic({ root: staticDir }));
6491
- app.get("*", serveStatic({ path: join24(staticDir, "index.html") }));
6818
+ app.get("*", serveStatic({ path: join25(staticDir, "index.html") }));
6492
6819
  }
6493
6820
  return app;
6494
6821
  }
6495
6822
 
6496
6823
  // src/server/change-feed.ts
6497
- import { relative as relative3, sep as sep4 } from "node:path";
6824
+ import { relative as relative4, sep as sep5 } from "node:path";
6498
6825
  import { watch } from "chokidar";
6499
6826
  var CHANGE_DEBOUNCE_MS = 100;
6500
6827
  function createDebouncer(delayMs, run) {
@@ -6508,7 +6835,7 @@ function createDebouncer(delayMs, run) {
6508
6835
  };
6509
6836
  }
6510
6837
  function isHiddenPath(root, path) {
6511
- return relative3(root, path).split(sep4).some((segment) => segment.startsWith("."));
6838
+ return relative4(root, path).split(sep5).some((segment) => segment.startsWith("."));
6512
6839
  }
6513
6840
  function createChangeFeed({ root, debounceMs, messages, warn: warn2 }) {
6514
6841
  const listeners = /* @__PURE__ */ new Set();
@@ -6599,10 +6926,13 @@ function logReport({ closedEpics, reopenedEpics, blockingFiles, deleted, conflic
6599
6926
  }
6600
6927
  }
6601
6928
 
6929
+ // src/server/usage-scanner.ts
6930
+ import { isDeepStrictEqual } from "node:util";
6931
+
6602
6932
  // src/core/usage/transcripts.ts
6603
- import { createHash as createHash5 } from "node:crypto";
6604
- import { open as open2, stat as stat6 } from "node:fs/promises";
6605
- import { basename as basename10, join as join26 } from "node:path";
6933
+ import { createHash as createHash6 } from "node:crypto";
6934
+ import { stat as stat6 } from "node:fs/promises";
6935
+ import { basename as basename10, join as join27 } from "node:path";
6606
6936
 
6607
6937
  // src/core/stats/cost/attribute.ts
6608
6938
  import { z as z18 } from "zod";
@@ -6936,7 +7266,7 @@ function slotOf(timestamp) {
6936
7266
 
6937
7267
  // src/core/usage/usage-cache.ts
6938
7268
  import { mkdir as mkdir5 } from "node:fs/promises";
6939
- import { join as join25 } from "node:path";
7269
+ import { join as join26 } from "node:path";
6940
7270
  import { z as z19 } from "zod";
6941
7271
  var USAGE_CACHE_FILE = ".usage-cache.json";
6942
7272
  var USAGE_CACHE_VERSION = 6;
@@ -6956,33 +7286,32 @@ function emptyUsageCache() {
6956
7286
  return { version: USAGE_CACHE_VERSION, files: {} };
6957
7287
  }
6958
7288
  async function readUsageCache(root) {
6959
- return await readJsonFile(join25(root, USAGE_CACHE_FILE), usageCacheSchema) ?? emptyUsageCache();
7289
+ return await readJsonFile(join26(root, USAGE_CACHE_FILE), usageCacheSchema) ?? emptyUsageCache();
6960
7290
  }
6961
7291
  async function writeUsageCache(root, cache) {
6962
7292
  await mkdir5(root, { recursive: true });
6963
- await writeFileAtomic(join25(root, USAGE_CACHE_FILE), JSON.stringify(cache));
7293
+ await writeFileAtomic(join26(root, USAGE_CACHE_FILE), JSON.stringify(cache));
6964
7294
  }
6965
7295
 
6966
7296
  // src/core/usage/transcripts.ts
6967
- var NEWLINE = 10;
6968
7297
  var FINGERPRINT_BYTES = 256;
6969
7298
  var ABANDONED_LINE_MS = 10 * 60 * 1e3;
6970
- var EMPTY_FINGERPRINT = createHash5("sha1").digest("hex");
7299
+ var EMPTY_FINGERPRINT2 = createHash6("sha1").digest("hex");
6971
7300
  var COUNTED_LINE_MARKERS = ['"type":"assistant"', '"type":"user"'];
6972
7301
  async function listTranscripts(claudeProjectsDir2) {
6973
7302
  const files = [];
6974
7303
  for (const projectEntry of await listDir(claudeProjectsDir2)) {
6975
7304
  if (!projectEntry.isDirectory()) continue;
6976
- const projectDir2 = join26(claudeProjectsDir2, projectEntry.name);
7305
+ const projectDir2 = join27(claudeProjectsDir2, projectEntry.name);
6977
7306
  for (const entry of await listDir(projectDir2)) {
6978
- const entryPath = join26(projectDir2, entry.name);
7307
+ const entryPath = join27(projectDir2, entry.name);
6979
7308
  if (entry.isFile() && entry.name.endsWith(".jsonl")) {
6980
7309
  files.push(...await listedFile(entryPath));
6981
7310
  continue;
6982
7311
  }
6983
7312
  if (!entry.isDirectory()) continue;
6984
- for (const subagentEntry of await listDir(join26(entryPath, "subagents"), { recursive: true })) {
6985
- if (subagentEntry.isFile() && subagentEntry.name.endsWith(".jsonl")) files.push(...await listedFile(join26(subagentEntry.parentPath, subagentEntry.name)));
7313
+ for (const subagentEntry of await listDir(join27(entryPath, "subagents"), { recursive: true })) {
7314
+ if (subagentEntry.isFile() && subagentEntry.name.endsWith(".jsonl")) files.push(...await listedFile(join27(subagentEntry.parentPath, subagentEntry.name)));
6986
7315
  }
6987
7316
  }
6988
7317
  }
@@ -7018,7 +7347,7 @@ async function scanTranscripts({ files, cache, byteBudget, now }) {
7018
7347
  };
7019
7348
  }
7020
7349
  function deletedStillReported(cache, listedFiles, now) {
7021
- const reportStart = now.getTime() - COST_REPORT_DAYS * DAY_MS;
7350
+ const reportStart = now.getTime() - STATS_HISTORY_DAYS * DAY_MS;
7022
7351
  const listedSessions = new Set(Object.keys(listedFiles).map((path) => basename10(path)));
7023
7352
  return Object.fromEntries(
7024
7353
  Object.entries(cache.files).filter(([path, entry]) => !listedSessions.has(basename10(path)) && entry.buckets.some((bucket) => Date.parse(bucket.slot) >= reportStart))
@@ -7026,7 +7355,7 @@ function deletedStillReported(cache, listedFiles, now) {
7026
7355
  }
7027
7356
  async function scanChunk(file, start, chunkSize, { longestReadableLine, now }) {
7028
7357
  if (chunkSize <= 0) return { size: file.size, offset: start.offset, state: start.state, buckets: start.buckets };
7029
- const chunk = await readChunk(file.path, start.offset, chunkSize);
7358
+ const chunk = await readFileAt(file.path, start.offset, chunkSize);
7030
7359
  const tailAbandoned = start.offset + chunk.length === file.size && now.getTime() - file.mtimeMs >= ABANDONED_LINE_MS;
7031
7360
  const readableLength = tailAbandoned ? chunk.length : chunk.lastIndexOf(NEWLINE) + 1;
7032
7361
  if (readableLength === 0) {
@@ -7050,30 +7379,14 @@ async function continues(file, previous) {
7050
7379
  return await fingerprintOf(file.path, previous.offset) === previous.fingerprint;
7051
7380
  }
7052
7381
  async function fingerprintOf(path, offset) {
7053
- if (offset === 0) return EMPTY_FINGERPRINT;
7382
+ if (offset === 0) return EMPTY_FINGERPRINT2;
7054
7383
  const edge = Math.min(FINGERPRINT_BYTES, offset);
7055
7384
  return withFile(path, async (handle) => {
7056
7385
  const head = await readAt(handle, 0, edge);
7057
7386
  const tail = await readAt(handle, offset - edge, edge);
7058
- return createHash5("sha1").update(head).update(tail).digest("hex");
7387
+ return createHash6("sha1").update(head).update(tail).digest("hex");
7059
7388
  });
7060
7389
  }
7061
- function readChunk(path, position, length) {
7062
- return withFile(path, (handle) => readAt(handle, position, length));
7063
- }
7064
- async function withFile(path, use) {
7065
- const handle = await open2(path, "r");
7066
- try {
7067
- return await use(handle);
7068
- } finally {
7069
- await handle.close();
7070
- }
7071
- }
7072
- async function readAt(handle, position, length) {
7073
- const buffer = Buffer.alloc(length);
7074
- const { bytesRead } = await handle.read(buffer, 0, length, position);
7075
- return buffer.subarray(0, bytesRead);
7076
- }
7077
7390
  function parseLineOrNull(line) {
7078
7391
  try {
7079
7392
  return JSON.parse(line);
@@ -7112,17 +7425,24 @@ function createUsageScanner({
7112
7425
  }) {
7113
7426
  let cache = null;
7114
7427
  let scan = NOT_LISTED;
7428
+ let revision = 0;
7115
7429
  let inFlight = null;
7116
7430
  let timer = null;
7117
7431
  let running = false;
7118
7432
  let budgetExhausted = false;
7433
+ const setScan = (next) => {
7434
+ if (!scanEquals(scan, next)) revision += 1;
7435
+ scan = next;
7436
+ };
7119
7437
  const runPass = async () => {
7438
+ const published = cache ?? emptyUsageCache();
7120
7439
  const current = cache ?? await readUsageCache(root);
7121
7440
  const files = await listTranscripts(claudeProjectsDir2);
7122
- scan = progressBefore(files, current);
7441
+ setScan(progressBefore(files, current));
7123
7442
  const result = await scanTranscripts({ files, cache: current, byteBudget, now: now() });
7443
+ if (cacheChanged(published, result.cache)) revision += 1;
7124
7444
  cache = result.cache;
7125
- scan = { listed: true, filesTotal: files.length, filesDone: result.filesDone, bytesLeft: result.bytesLeft };
7445
+ setScan({ listed: true, filesTotal: files.length, filesDone: result.filesDone, bytesLeft: result.bytesLeft });
7126
7446
  budgetExhausted = result.bytesRead >= byteBudget;
7127
7447
  if (result.bytesRead > 0) await writeUsageCache(root, result.cache);
7128
7448
  };
@@ -7154,9 +7474,21 @@ function createUsageScanner({
7154
7474
  ensureStarted: () => {
7155
7475
  if (!scan.listed && inFlight === null) void scanOnce();
7156
7476
  },
7157
- snapshot: () => ({ cache: cache ?? emptyUsageCache(), scan })
7477
+ snapshot: () => ({ cache: cache ?? emptyUsageCache(), scan, revision })
7158
7478
  };
7159
7479
  }
7480
+ function scanEquals(a, b) {
7481
+ return a.listed === b.listed && a.filesTotal === b.filesTotal && a.filesDone === b.filesDone && a.bytesLeft === b.bytesLeft;
7482
+ }
7483
+ function cacheChanged(previous, next) {
7484
+ const previousPaths = Object.keys(previous.files);
7485
+ if (previousPaths.length !== Object.keys(next.files).length) return true;
7486
+ return previousPaths.some((path) => entryChanged(previous.files[path], next.files[path]));
7487
+ }
7488
+ function entryChanged(previous, next) {
7489
+ if (previous === void 0 || next === void 0) return previous !== next;
7490
+ return previous.size !== next.size || previous.offset !== next.offset || previous.fingerprint !== next.fingerprint || !isDeepStrictEqual(previous.buckets, next.buckets);
7491
+ }
7160
7492
  function progressBefore(files, cache) {
7161
7493
  const offsetOf = (file) => {
7162
7494
  const cached = cache.files[file.path];
@@ -7173,7 +7505,7 @@ function progressBefore(files, cache) {
7173
7505
  // src/server/start.ts
7174
7506
  var SWEEP_INTERVAL_MS = 60 * 60 * 1e3;
7175
7507
  var STOP_SIGNALS = ["SIGTERM", "SIGINT", "SIGHUP"];
7176
- var BUNDLED_WEB_DIR = join27(import.meta.dirname, "web");
7508
+ var BUNDLED_WEB_DIR = join28(import.meta.dirname, "web");
7177
7509
  var log = (line) => void process.stdout.write(`${line}
7178
7510
  `);
7179
7511
  var warn = (line) => void process.stderr.write(`${line}
@@ -7274,7 +7606,7 @@ async function runServe(args, io) {
7274
7606
 
7275
7607
  // src/cli/service/launchd.ts
7276
7608
  import { mkdir as mkdir7, rm as rm5, writeFile as writeFile4 } from "node:fs/promises";
7277
- import { dirname as dirname8, join as join28, posix } from "node:path";
7609
+ import { dirname as dirname8, join as join29, posix } from "node:path";
7278
7610
  import { setTimeout as sleep3 } from "node:timers/promises";
7279
7611
 
7280
7612
  // src/cli/service/service.ts
@@ -7347,7 +7679,7 @@ ${entries}
7347
7679
  `;
7348
7680
  }
7349
7681
  function launchdManager(context, delay = sleep3) {
7350
- const file = join28(context.home, "Library/LaunchAgents", `${LABEL}.plist`);
7682
+ const file = join29(context.home, "Library/LaunchAgents", `${LABEL}.plist`);
7351
7683
  const domain = `gui/${context.uid}`;
7352
7684
  const bootout = () => context.exec("launchctl", ["bootout", `${domain}/${LABEL}`]);
7353
7685
  const bootstrap = () => context.exec("launchctl", ["bootstrap", domain, file]);
@@ -7382,7 +7714,7 @@ function launchdManager(context, delay = sleep3) {
7382
7714
 
7383
7715
  // src/cli/service/startup-folder.ts
7384
7716
  import { mkdir as mkdir8, rm as rm6, writeFile as writeFile5 } from "node:fs/promises";
7385
- import { dirname as dirname9, join as join29, win32 } from "node:path";
7717
+ import { dirname as dirname9, join as join30, win32 } from "node:path";
7386
7718
  var SCRIPT_NAME = "p-backlog.vbs";
7387
7719
  var COMMAND_LINE_ARGUMENT = /"[^"]*"|\S+/g;
7388
7720
  var PROCESS_QUERY_TIMEOUT_MS = 15e3;
@@ -7390,16 +7722,16 @@ function vbsString(value) {
7390
7722
  return `"${value.replaceAll('"', '""')}"`;
7391
7723
  }
7392
7724
  function appDataDir(context) {
7393
- return join29(context.env.LOCALAPPDATA ?? join29(context.home, "AppData", "Local"), "p-backlog");
7725
+ return join30(context.env.LOCALAPPDATA ?? join30(context.home, "AppData", "Local"), "p-backlog");
7394
7726
  }
7395
7727
  function startupFolder(context) {
7396
- return join29(context.env.APPDATA ?? join29(context.home, "AppData", "Roaming"), "Microsoft", "Windows", "Start Menu", "Programs", "Startup");
7728
+ return join30(context.env.APPDATA ?? join30(context.home, "AppData", "Roaming"), "Microsoft", "Windows", "Start Menu", "Programs", "Startup");
7397
7729
  }
7398
7730
  function logPath2(context) {
7399
- return join29(appDataDir(context), "p-backlog.log");
7731
+ return join30(appDataDir(context), "p-backlog.log");
7400
7732
  }
7401
7733
  function pidFilePath(context) {
7402
- return join29(appDataDir(context), "server.pid");
7734
+ return join30(appDataDir(context), "server.pid");
7403
7735
  }
7404
7736
  var NODE_PATH_ENV = "P_BACKLOG_NODE";
7405
7737
  var CLI_PATH_ENV = "P_BACKLOG_CLI";
@@ -7450,7 +7782,7 @@ function sameWindowsPath(left, right) {
7450
7782
  return win32.normalize(left).toLowerCase() === win32.normalize(right).toLowerCase();
7451
7783
  }
7452
7784
  function startupFolderManager(context) {
7453
- const file = join29(startupFolder(context), SCRIPT_NAME);
7785
+ const file = join30(startupFolder(context), SCRIPT_NAME);
7454
7786
  return {
7455
7787
  file,
7456
7788
  logs: logPath2(context),
@@ -7477,7 +7809,7 @@ function startupFolderManager(context) {
7477
7809
 
7478
7810
  // src/cli/service/systemd.ts
7479
7811
  import { mkdir as mkdir9, rm as rm7, writeFile as writeFile6 } from "node:fs/promises";
7480
- import { dirname as dirname10, join as join30 } from "node:path";
7812
+ import { dirname as dirname10, join as join31 } from "node:path";
7481
7813
  var UNIT = "p-backlog.service";
7482
7814
  function quoted(value) {
7483
7815
  return `"${value.replace(/[\\"]/g, "\\$&").replaceAll("%", "%%")}"`;
@@ -7511,7 +7843,7 @@ async function systemctlSteps(exec, steps) {
7511
7843
  return "done";
7512
7844
  }
7513
7845
  function systemdManager(context) {
7514
- const file = join30(context.home, ".config/systemd/user", UNIT);
7846
+ const file = join31(context.home, ".config/systemd/user", UNIT);
7515
7847
  return {
7516
7848
  file,
7517
7849
  logs: "journalctl --user -u p-backlog",
@@ -7629,10 +7961,10 @@ function reportFailure({ failed, code, output }, io) {
7629
7961
  }
7630
7962
 
7631
7963
  // src/cli/commands/setup.ts
7632
- import { join as join32 } from "node:path";
7964
+ import { join as join33 } from "node:path";
7633
7965
 
7634
7966
  // src/cli/agents/agent-hooks.ts
7635
- import { join as join31 } from "node:path";
7967
+ import { join as join32 } from "node:path";
7636
7968
 
7637
7969
  // src/cli/agents/grouped-stop-hooks.ts
7638
7970
  import { z as z20 } from "zod";
@@ -7726,14 +8058,20 @@ function hasOurHook(group, isOurs) {
7726
8058
  }
7727
8059
 
7728
8060
  // src/cli/stop-hook.ts
7729
- var POSIX_COMMAND = "command -v backlog >/dev/null && backlog hook stop || true";
8061
+ var POSIX_COMMAND = guardedPosixCommand(HOOK_STOP_COMMAND);
7730
8062
  var POWERSHELL_COMMAND = "if (Get-Command backlog.cmd -ErrorAction SilentlyContinue) { backlog.cmd hook stop }";
7731
8063
  function stopHookFor(platform) {
7732
8064
  return platform === "win32" ? { type: "command", shell: "powershell", command: POWERSHELL_COMMAND } : { type: "command", command: POSIX_COMMAND };
7733
8065
  }
8066
+ function guardedPosixCommand(backlogArgs) {
8067
+ return `command -v backlog >/dev/null && backlog ${backlogArgs} || true`;
8068
+ }
8069
+ function hookCommand2(hook) {
8070
+ const command = typeof hook === "object" && hook !== null ? hook.command : void 0;
8071
+ return typeof command === "string" ? command : void 0;
8072
+ }
7734
8073
  function isOurStopHook(hook) {
7735
- if (typeof hook !== "object" || hook === null) return false;
7736
- const command = hook.command;
8074
+ const command = hookCommand2(hook);
7737
8075
  return command === POSIX_COMMAND || command === POWERSHELL_COMMAND;
7738
8076
  }
7739
8077
  function addStopHook(settingsPath, platform) {
@@ -7773,11 +8111,11 @@ async function removeCursorStopHook(path, isOurs) {
7773
8111
  // src/cli/agents/agent-hooks.ts
7774
8112
  var CODEX_HOOK_TIMEOUT_SECONDS = 30;
7775
8113
  var AGENT_HOOKS_FILE = "hooks.json";
7776
- function agentHookConfigPath(agent, env, home2) {
7777
- return agent === "claude" ? claudeSettingsPath(env, home2) : join31(agentHomeDir(agent, env, home2), AGENT_HOOKS_FILE);
8114
+ function agentHookConfigPath(agent, places) {
8115
+ return agent === "claude" ? claudeSettingsPath(places.env, places.home) : join32(agentHomeDir(agent, places), AGENT_HOOKS_FILE);
7778
8116
  }
7779
8117
  function installAgentHook(agent, site) {
7780
- const path = agentHookConfigPath(agent, site.env, site.home);
8118
+ const path = agentHookConfigPath(agent, site);
7781
8119
  switch (agent) {
7782
8120
  case "claude":
7783
8121
  return addStopHook(path, site.platform);
@@ -7792,7 +8130,7 @@ function installAgentHook(agent, site) {
7792
8130
  }
7793
8131
  }
7794
8132
  function removeAgentHook(agent, site) {
7795
- const path = agentHookConfigPath(agent, site.env, site.home);
8133
+ const path = agentHookConfigPath(agent, site);
7796
8134
  switch (agent) {
7797
8135
  case "claude":
7798
8136
  return removeStopHook(path);
@@ -7806,7 +8144,7 @@ function agentStopCommand(agent) {
7806
8144
  return `${HOOK_STOP_COMMAND} --agent ${agent}`;
7807
8145
  }
7808
8146
  function posixCommand(agent) {
7809
- return `command -v backlog >/dev/null && backlog ${agentStopCommand(agent)} || true`;
8147
+ return guardedPosixCommand(agentStopCommand(agent));
7810
8148
  }
7811
8149
  function windowsCommand(agent, cliPath2) {
7812
8150
  return `node "${cliPath2}" ${agentStopCommand(agent)}`;
@@ -7816,8 +8154,8 @@ function windowsCommandPattern(agent) {
7816
8154
  }
7817
8155
  function ourHookOf(agent, cliPath2) {
7818
8156
  return (hook) => {
7819
- const command = typeof hook === "object" && hook !== null ? hook.command : void 0;
7820
- if (typeof command !== "string") return false;
8157
+ const command = hookCommand2(hook);
8158
+ if (command === void 0) return false;
7821
8159
  return command === posixCommand(agent) || cliPath2 !== void 0 && command === windowsCommand(agent, cliPath2) || windowsCommandPattern(agent).test(command);
7822
8160
  };
7823
8161
  }
@@ -7838,16 +8176,15 @@ async function runSetup(args, io) {
7838
8176
  const options = parseOptions(io.language, args, { service: { type: "boolean" }, agent: { type: "string" }, "remove-manual": { type: "boolean" } });
7839
8177
  if (options["remove-manual"] && options.service) throw new UsageError(cliMessages(io.language).removeManualWithService);
7840
8178
  const agents = await targetAgents(options.agent, io);
7841
- const step = options["remove-manual"] ? removeManualSetup : setUpAgent;
7842
8179
  const outcomes = [];
7843
- for (const agent of agents) outcomes.push(await step(agent, io));
8180
+ for (const agent of agents) outcomes.push(options["remove-manual"] ? await removeManualSetup(agent, agents, io) : await setUpAgent(agent, io));
7844
8181
  const serviceCode = options.service ? await installService(io) : EXIT.ok;
7845
8182
  return outcomes.every(Boolean) ? serviceCode : EXIT.failed;
7846
8183
  }
7847
8184
  async function targetAgents(option, io) {
7848
8185
  const messages = cliMessages(io.language);
7849
8186
  if (option !== void 0) return [parseChoice(io.language, option, AGENTS, messages.optionLabel.agent)];
7850
- const detection = await detectAgents(io.env, io.home);
8187
+ const detection = await detectAgents(io);
7851
8188
  for (const { agent, dir } of detection.missing) agentVoice(agent, io).print(messages.agentNotFound(dir));
7852
8189
  return detection.found;
7853
8190
  }
@@ -7857,21 +8194,21 @@ function agentVoice(agent, io) {
7857
8194
  }
7858
8195
  async function setUpAgent(agent, io) {
7859
8196
  const voice = agentVoice(agent, io);
7860
- const plugin = await agentPlugin(agent, io.env, io.home);
8197
+ const plugin = await agentPlugin(agent, io);
7861
8198
  if (plugin !== null) {
7862
8199
  voice.print(cliMessages(io.language).pluginManages(plugin));
7863
8200
  return true;
7864
8201
  }
7865
8202
  if (!await linkAgentSkill(agent, io, voice)) return false;
7866
8203
  const hook = await installAgentHook(agent, io);
7867
- const reported = reportHook(hook, agentHookConfigPath(agent, io.env, io.home), io, voice);
8204
+ const reported = reportHook(hook, agentHookConfigPath(agent, io), io, voice);
7868
8205
  if (agent === "codex" && (hook === "added" || hook === "updated")) voice.print(cliMessages(io.language).codexHookApproval);
7869
8206
  return reported;
7870
8207
  }
7871
8208
  async function linkAgentSkill(agent, io, voice) {
7872
8209
  const messages = cliMessages(io.language);
7873
- const skillsDir = agentSkillsDir(agent, io.env, io.home);
7874
- const target = join32(skillsDir, "backlog");
8210
+ const skillsDir = agentSkillsDir(agent, io);
8211
+ const target = join33(skillsDir, "backlog");
7875
8212
  const source = skillSourceDir(io.packageRoot, io.language);
7876
8213
  let link2;
7877
8214
  try {
@@ -7885,8 +8222,14 @@ async function linkAgentSkill(agent, io, voice) {
7885
8222
  return false;
7886
8223
  }
7887
8224
  voice.print(link2 === "linked" ? messages.installSkillLinked(target, source) : messages.installSkillKept(target));
8225
+ await removeLegacySkillLinks(agent, io, voice);
7888
8226
  return true;
7889
8227
  }
8228
+ async function removeLegacySkillLinks(agent, io, voice) {
8229
+ for (const legacyDir of legacySkillsDirs(agent, io)) {
8230
+ if (await unlinkOurSkill(legacyDir) === "removed") voice.print(cliMessages(io.language).manualSkillRemoval.removed(join33(legacyDir, "backlog")));
8231
+ }
8232
+ }
7890
8233
  function reportHook(result, configPath, io, voice) {
7891
8234
  const messages = cliMessages(io.language);
7892
8235
  if (typeof result === "string") {
@@ -7895,21 +8238,26 @@ function reportHook(result, configPath, io, voice) {
7895
8238
  return true;
7896
8239
  }
7897
8240
  if (result.failed === "unreadable") {
7898
- voice.warn(messages.installSettingsUnreadable(configPath, result.code));
8241
+ voice.warn(messages.installHookConfigUnreadable(configPath, result.code));
7899
8242
  return false;
7900
8243
  }
7901
- voice.warn(messages.installSettingsInvalid(configPath));
8244
+ voice.warn(messages.installHookConfigInvalid(configPath));
7902
8245
  return false;
7903
8246
  }
7904
- async function removeManualSetup(agent, io) {
8247
+ async function removeManualSetup(agent, removing, io) {
7905
8248
  const voice = agentVoice(agent, io);
7906
8249
  const messages = cliMessages(io.language);
7907
- const skillsDir = agentSkillsDir(agent, io.env, io.home);
7908
- voice.print(messages.manualSkillRemoval[await unlinkOurSkill(skillsDir)](join32(skillsDir, "backlog")));
7909
- for (const legacyDir of legacySkillsDirs(agent, io.env, io.home)) {
7910
- if (await unlinkOurSkill(legacyDir) === "removed") voice.print(messages.manualSkillRemoval.removed(join32(legacyDir, "backlog")));
7911
- }
7912
- return reportHookRemoval(await removeAgentHook(agent, io), agentHookConfigPath(agent, io.env, io.home), io, voice);
8250
+ const skillsDir = agentSkillsDir(agent, io);
8251
+ const target = join33(skillsDir, "backlog");
8252
+ const sharer = await remainingSkillDirUser(agent, removing, io);
8253
+ voice.print(sharer === null ? messages.manualSkillRemoval[await unlinkOurSkill(skillsDir)](target) : messages.manualSkillShared(target, AGENT_LABELS[sharer]));
8254
+ await removeLegacySkillLinks(agent, io, voice);
8255
+ return reportHookRemoval(await removeAgentHook(agent, io), agentHookConfigPath(agent, io), io, voice);
8256
+ }
8257
+ async function remainingSkillDirUser(agent, removing, io) {
8258
+ const skillsDir = agentSkillsDir(agent, io);
8259
+ const { found } = await detectAgents(io);
8260
+ return found.find((other) => !removing.includes(other) && agentSkillsDir(other, io) === skillsDir) ?? null;
7913
8261
  }
7914
8262
  function reportHookRemoval(result, configPath, io, voice) {
7915
8263
  const messages = cliMessages(io.language);
@@ -7917,7 +8265,7 @@ function reportHookRemoval(result, configPath, io, voice) {
7917
8265
  voice.print(messages.manualHookRemoval[result](configPath));
7918
8266
  return true;
7919
8267
  }
7920
- voice.warn(result.failed === "unreadable" ? messages.hookConfigUnreadable(configPath, result.code) : messages.hookConfigInvalid(configPath));
8268
+ voice.warn(result.failed === "unreadable" ? messages.removeHookConfigUnreadable(configPath, result.code) : messages.removeHookConfigInvalid(configPath));
7921
8269
  return false;
7922
8270
  }
7923
8271
 
@@ -7955,7 +8303,7 @@ function backlogAgeWeeks(histories, now) {
7955
8303
  return firstCreated === null ? null : (now.getTime() - firstCreated) / WEEK_MS;
7956
8304
  }
7957
8305
  var clampedWeeks = (weeks) => Math.min(FORECAST_WINDOW_WEEKS, Math.max(MIN_WINDOW_WEEKS, weeks));
7958
- function flowForecast(histories, open3, now) {
8306
+ function flowForecast(histories, open2, now) {
7959
8307
  const ageWeeks = backlogAgeWeeks(histories, now);
7960
8308
  const windowWeeks = ageWeeks === null ? FORECAST_WINDOW_WEEKS : clampedWeeks(Math.floor(ageWeeks) + 1);
7961
8309
  const observedWeeks = ageWeeks === null ? FORECAST_WINDOW_WEEKS : clampedWeeks(ageWeeks);
@@ -7964,9 +8312,9 @@ function flowForecast(histories, open3, now) {
7964
8312
  const closed = histories.flatMap(closingsOf).filter((closing) => inWindow(closing.at)).length;
7965
8313
  const created = histories.filter((history) => inWindow(history.createdAt)).length;
7966
8314
  const weeklyNet = (closed - created) / observedWeeks;
7967
- const weeks = open3 > 0 && weeklyNet > 0 ? Math.ceil(open3 / weeklyNet) : null;
8315
+ const weeks = open2 > 0 && weeklyNet > 0 ? Math.ceil(open2 / weeklyNet) : null;
7968
8316
  const windowDays = Math.round(observedWeeks * DAYS_PER_WEEK);
7969
- return { closed, created, open: open3, weeklyNet, weeks, until: weeks === null ? null : formatLocalIso(weeksLater(now, weeks)), windowDays };
8317
+ return { closed, created, open: open2, weeklyNet, weeks, until: weeks === null ? null : formatLocalIso(weeksLater(now, weeks)), windowDays };
7970
8318
  }
7971
8319
  function weeksLater(now, weeks) {
7972
8320
  return new Date(now.getFullYear(), now.getMonth(), now.getDate() + DAYS_PER_WEEK * weeks, now.getHours(), now.getMinutes(), now.getSeconds());
@@ -8051,7 +8399,7 @@ var statusCommand = taskFieldCommand({
8051
8399
 
8052
8400
  // src/cli/commands/take.ts
8053
8401
  import { realpathSync as realpathSync3 } from "node:fs";
8054
- import { relative as relative4, resolve as resolve5, sep as sep5 } from "node:path";
8402
+ import { relative as relative5, resolve as resolve5, sep as sep6 } from "node:path";
8055
8403
  var takeCommand = {
8056
8404
  name: "take",
8057
8405
  usage: (language) => cliMessages(language).takeUsage(),
@@ -8126,7 +8474,7 @@ function isInside(file, path) {
8126
8474
  function repoRelativePath(io, project, path) {
8127
8475
  const roots = findGitRoots(io.cwd);
8128
8476
  if (roots === null || findProjectForDir([project], io.cwd, io.home) === void 0) return sourcePath(path);
8129
- const fromRoot = relative4(roots.worktree, resolve5(realpathSync3(io.cwd), sourcePath(path))).split(sep5).join("/");
8477
+ const fromRoot = relative5(roots.worktree, resolve5(realpathSync3(io.cwd), sourcePath(path))).split(sep6).join("/");
8130
8478
  const outsideRepo = fromRoot === ".." || fromRoot.startsWith("../");
8131
8479
  return outsideRepo ? sourcePath(path) : fromRoot;
8132
8480
  }