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.
- package/CHANGELOG.md +24 -0
- package/README.md +9 -1
- package/README.ru.md +10 -1
- package/dist/cli.js +670 -322
- package/dist/web/assets/{CodeTab-BU7UU1i4.js → CodeTab-D3I5lIJJ.js} +1 -1
- package/dist/web/assets/CostTab-8f2AC5ER.js +1 -0
- package/dist/web/assets/EffectTab-1jObLK9a.js +1 -0
- package/dist/web/assets/Figure-BHBCd7Dt.js +1 -0
- package/dist/web/assets/OverviewTab-DjNyxzJq.js +1 -0
- package/dist/web/assets/QualityTab-W2NdYBhI.js +1 -0
- package/dist/web/assets/StatsPage-rJH3k8Ye.js +1 -0
- package/dist/web/assets/StatsTabState-BoimPoKm.js +1 -0
- package/dist/web/assets/{StatsTable-C0coWtNM.js → StatsTable-B0tbx68M.js} +1 -1
- package/dist/web/assets/{UnavailableRepos-DDHmBHg0.js → UnavailableRepos-SpC6Dzwu.js} +1 -1
- package/dist/web/assets/cx-DDl6tKXq.js +58 -0
- package/dist/web/assets/{index-DN4lSwOL.js → index-C8xqRmo-.js} +2 -2
- package/dist/web/assets/start-YlC0KTe5.js +38 -0
- package/dist/web/assets/use-grain-series-Bx70IZvb.js +55 -0
- package/dist/web/assets/value-dot-B7BsbWBD.js +1 -0
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/dist/web/assets/ChartTooltip-b-yZtIRd.js +0 -55
- package/dist/web/assets/CostTab-Be8xR9DU.js +0 -1
- package/dist/web/assets/EffectTab-D0kaZiSl.js +0 -1
- package/dist/web/assets/Figure-B6Ry1zXo.js +0 -1
- package/dist/web/assets/OverviewTab-DqU7o9Ug.js +0 -1
- package/dist/web/assets/QualityTab-DtnTgxXj.js +0 -1
- package/dist/web/assets/StatsPage-BLZKtSL6.js +0 -1
- package/dist/web/assets/StatsTabState-CYkDWYp7.js +0 -1
- package/dist/web/assets/cx-Cfec8QGz.js +0 -58
- package/dist/web/assets/rolldown-runtime-hePW80VL.js +0 -1
- package/dist/web/assets/start-sbZVqXmP.js +0 -38
- package/dist/web/assets/value-dot-CfFRNT_m.js +0 -1
- /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 {
|
|
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
|
-
|
|
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 =
|
|
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, () =>
|
|
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
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
if (
|
|
427
|
-
|
|
428
|
-
|
|
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
|
-
|
|
569
|
-
|
|
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
|
-
|
|
584
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
745
|
-
|
|
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
|
-
|
|
760
|
-
|
|
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:
|
|
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:
|
|
838
|
-
}) => `${id} \xB7 ${name} \xB7 ${prefix} \xB7 ${statusWord2} \xB7 \u043E\u0442\u043A\u0440\u044B\u0442\u044B\u0445 ${
|
|
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
|
|
1407
|
+
function flowOver(periods, histories) {
|
|
1375
1408
|
const closings = histories.flatMap(closingsOf);
|
|
1376
|
-
return
|
|
1377
|
-
start: formatLocalIso(new Date(
|
|
1378
|
-
created: histories.filter((history) =>
|
|
1379
|
-
closed: closings.filter((closing) =>
|
|
1380
|
-
openAtEnd: histories.filter((history) => isOpenAt(history,
|
|
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:
|
|
1475
|
-
if (
|
|
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:
|
|
1708
|
-
if (
|
|
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
|
|
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
|
-
|
|
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
|
|
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 :
|
|
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 =
|
|
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,
|
|
3943
|
-
|
|
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,
|
|
3948
|
-
return agent === "
|
|
3982
|
+
function legacySkillsDirs(agent, places) {
|
|
3983
|
+
return agent === "claude" ? [] : [join16(agentHomeDir(agent, places), "skills")];
|
|
3949
3984
|
}
|
|
3950
|
-
async function detectAgents(
|
|
3985
|
+
async function detectAgents(places) {
|
|
3951
3986
|
const detection = { found: [], missing: [] };
|
|
3952
3987
|
for (const agent of AGENTS) {
|
|
3953
|
-
const dir = agentHomeDir(agent,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
4314
|
-
return
|
|
4363
|
+
function accuracyOver(periods, histories) {
|
|
4364
|
+
return periods.map((span) => {
|
|
4315
4365
|
const decided = histories.flatMap(
|
|
4316
|
-
(history) => history.candidates.filter((candidate) =>
|
|
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(
|
|
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
|
|
4674
|
+
const open2 = openBlockers(task, index);
|
|
4619
4675
|
return {
|
|
4620
4676
|
task,
|
|
4621
4677
|
progress: taskProgress(task, index),
|
|
4622
|
-
openBlockers:
|
|
4623
|
-
inactiveBlockerIds: task.blockedBy.filter((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
|
|
4809
|
-
io.print(cli.projectListLine({ id: project.id, name: project.name, prefix: project.prefix, statusWord: statusWord(cli, project.active), open:
|
|
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
|
|
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
|
|
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
|
-
|
|
5276
|
-
value.catch(() =>
|
|
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 =
|
|
5331
|
-
var
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
|
|
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(),
|
|
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
|
|
5388
|
-
const
|
|
5389
|
-
|
|
5390
|
-
|
|
5391
|
-
|
|
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 (
|
|
5394
|
-
|
|
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
|
|
5489
|
-
return output.split(
|
|
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()
|
|
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
|
|
5543
|
-
|
|
5544
|
-
|
|
5545
|
-
|
|
5546
|
-
|
|
5547
|
-
|
|
5548
|
-
|
|
5549
|
-
|
|
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(
|
|
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 =
|
|
5564
|
-
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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 }) =>
|
|
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
|
|
5714
|
-
return [...
|
|
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,
|
|
5720
|
-
return { lines, open:
|
|
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
|
|
5917
|
-
const hookBuckets =
|
|
5918
|
-
const cliBuckets =
|
|
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(
|
|
5924
|
-
hasUnpricedTokens: hasUnpricedTokens(
|
|
5925
|
-
hookTurns: sum(
|
|
5926
|
-
...runCounts(
|
|
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
|
|
6026
|
-
const estimated = estimatedSizeOf(
|
|
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:
|
|
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 &&
|
|
6193
|
+
noiseShare: realLines === 0 || estimatedLines === null && open2.length > 0 ? null : Math.min(1, deferredLines / denominator)
|
|
6041
6194
|
};
|
|
6042
6195
|
}
|
|
6043
|
-
function estimatedSizeOf(
|
|
6044
|
-
const estimates =
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
6248
|
-
|
|
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
|
-
|
|
6251
|
-
|
|
6252
|
-
|
|
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 :
|
|
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
|
|
6263
|
-
|
|
6264
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6287
|
-
|
|
6288
|
-
|
|
6289
|
-
|
|
6290
|
-
|
|
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
|
|
6315
|
-
const
|
|
6316
|
-
|
|
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
|
|
6334
|
-
const
|
|
6335
|
-
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
6604
|
-
import {
|
|
6605
|
-
import { basename as basename10, join as
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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 =
|
|
7305
|
+
const projectDir2 = join27(claudeProjectsDir2, projectEntry.name);
|
|
6977
7306
|
for (const entry of await listDir(projectDir2)) {
|
|
6978
|
-
const entryPath =
|
|
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(
|
|
6985
|
-
if (subagentEntry.isFile() && subagentEntry.name.endsWith(".jsonl")) files.push(...await listedFile(
|
|
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() -
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
7725
|
+
return join30(context.env.LOCALAPPDATA ?? join30(context.home, "AppData", "Local"), "p-backlog");
|
|
7394
7726
|
}
|
|
7395
7727
|
function startupFolder(context) {
|
|
7396
|
-
return
|
|
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
|
|
7731
|
+
return join30(appDataDir(context), "p-backlog.log");
|
|
7400
7732
|
}
|
|
7401
7733
|
function pidFilePath(context) {
|
|
7402
|
-
return
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
7964
|
+
import { join as join33 } from "node:path";
|
|
7633
7965
|
|
|
7634
7966
|
// src/cli/agents/agent-hooks.ts
|
|
7635
|
-
import { join as
|
|
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 =
|
|
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
|
-
|
|
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,
|
|
7777
|
-
return agent === "claude" ? claudeSettingsPath(env,
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
7820
|
-
if (
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
7874
|
-
const target =
|
|
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.
|
|
8241
|
+
voice.warn(messages.installHookConfigUnreadable(configPath, result.code));
|
|
7899
8242
|
return false;
|
|
7900
8243
|
}
|
|
7901
|
-
voice.warn(messages.
|
|
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
|
|
7908
|
-
|
|
7909
|
-
|
|
7910
|
-
|
|
7911
|
-
|
|
7912
|
-
return reportHookRemoval(await removeAgentHook(agent, io), agentHookConfigPath(agent, io
|
|
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.
|
|
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,
|
|
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 =
|
|
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:
|
|
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
|
|
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 =
|
|
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
|
}
|