mainflow 0.26.0 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +58 -4
- package/dist/{dist-DlmecRLf.js → dist-CBGKXsoi.js} +10 -2
- package/dist/facts.d.ts +15 -5
- package/dist/facts.js +16 -5
- package/dist/grant-commands.d.ts +1 -1
- package/dist/grant-commands.js +1 -1
- package/dist/hooks.js +3 -3
- package/dist/{host-Dr0bCHd8.js → host-sSWrUJeC.js} +597 -22
- package/dist/host.js +1 -1
- package/dist/{index-D4IIwDGE.d.ts → index-bIFT1ORw.d.ts} +3 -1
- package/dist/{launch-lnD-Z3hb.js → launch-1i4OAsD6.js} +2 -2
- package/dist/launch.js +1 -1
- package/dist/{ui-Cz8suWal.js → ui-DcBLvgqY.js} +2 -2
- package/dist/web/assets/{index-xrrh30zr.js → index-ypKt0hMg.js} +2 -2
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { $ as
|
|
1
|
+
import { $ as grantForm, A as WORK_STATUS, B as parseRef, C as SESSION_STATUS, D as TASK_REQUIRED_SECTIONS, E as SURVEY_SECTION, F as issueSchema, G as taskSchema, H as refExample, I as mainlineSchema, K as waitingForAction, L as opsEntrySchema, M as decisionSchema, N as findingSchema, O as TASK_SECTIONS, P as formatOpsEntry, Q as grantCovers, R as parseActionRef, S as SESSION_ID, T as SURVEY_LINES, U as refMainline, V as parseUntil, W as sessionSchema, Z as describeRequest, _ as MERGE_STATES, a as DECISION_SECTIONS, b as RESERVED_HUB_NAMES, c as FINDING_SECTIONS, et as grantRepo, f as ISSUE_SECTIONS, g as MAINLINE_STATUS, h as MAINLINE_SECTIONS, i as DECISION_BASES, j as configSchema, l as FINDING_STATUS, n as CLOSED_STATUS, q as waitingForRef, r as DECIDERS, s as DEFAULT_CATEGORIES, tt as parseGrantTarget, w as SLUG, x as SERVES_REF, y as REF_EXAMPLES, z as parseOpsLine } from "./dist-CBGKXsoi.js";
|
|
2
2
|
import { appendFileSync, closeSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, symlinkSync, writeFileSync, writeSync } from "node:fs";
|
|
3
|
-
import { dirname, isAbsolute, join, parse, relative, resolve, sep } from "node:path";
|
|
3
|
+
import { basename, dirname, isAbsolute, join, matchesGlob, parse, posix, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { execFileSync, spawn, spawnSync } from "node:child_process";
|
|
5
5
|
import { createHash, randomBytes } from "node:crypto";
|
|
6
6
|
import { parse as parse$1, stringify } from "yaml";
|
|
@@ -419,6 +419,65 @@ function mainlineEdges(graph) {
|
|
|
419
419
|
return [...out.values()];
|
|
420
420
|
}
|
|
421
421
|
//#endregion
|
|
422
|
+
//#region ../core/dist/touches.js
|
|
423
|
+
/** Mainline statuses whose tasks are compared: work planned or under way. */
|
|
424
|
+
const COMPARED_MAINLINE = ["planning", "active"];
|
|
425
|
+
/** Parent directories of a path, nearest first, without the path itself. */
|
|
426
|
+
function ancestors(path) {
|
|
427
|
+
const out = [];
|
|
428
|
+
for (let dir = posix.dirname(path); dir !== "." && dir !== "/" && dir !== path; dir = posix.dirname(dir)) out.push(dir);
|
|
429
|
+
return out;
|
|
430
|
+
}
|
|
431
|
+
const normal = (path) => posix.normalize(path.trim()).replace(/\/+$/, "");
|
|
432
|
+
/**
|
|
433
|
+
* Whether a `touches` entry covers a path: the same path, a path under it, or one the glob matches directly or
|
|
434
|
+
* through a parent directory. A directory or a glob that matches a directory stands for everything under it.
|
|
435
|
+
*/
|
|
436
|
+
function covers(pattern, path) {
|
|
437
|
+
return [path, ...ancestors(path)].some((p) => p === pattern || matchesGlob(p, pattern));
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Whether two `touches` entries overlap: equal paths, or either one covers the other as a path. Two globs meet only
|
|
441
|
+
* when one matches the other's text, so `a/*.ts` and `?/b.ts` are not seen to share `a/b.ts`.
|
|
442
|
+
*/
|
|
443
|
+
function touchesOverlap(a, b) {
|
|
444
|
+
const x = normal(a);
|
|
445
|
+
const y = normal(b);
|
|
446
|
+
return x === y || covers(x, y) || covers(y, x);
|
|
447
|
+
}
|
|
448
|
+
/** Whether a dependency edge leaves `from` at the mainline or the task level and lands on `to` at either level. */
|
|
449
|
+
function dependsOn(edges, from, to) {
|
|
450
|
+
const fromRefs = [from.mainline, `${from.mainline}/${from.task.id}`];
|
|
451
|
+
const lands = (ref) => ref === to.mainline || ref.startsWith(`${to.mainline}#`) || ref === `${to.mainline}/${to.task.id}` || ref.startsWith(`${to.mainline}/${to.task.id}#`);
|
|
452
|
+
return edges.some((e) => fromRefs.includes(e.from) && lands(e.to));
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Open tasks of different open mainlines whose `touches` overlap in the same repo and which have no depends-on
|
|
456
|
+
* between them in either direction, written on the mainline or on the task. Tasks without a repo are not compared,
|
|
457
|
+
* since their paths have no root to share.
|
|
458
|
+
*/
|
|
459
|
+
function touchOverlaps(mainlines, edges) {
|
|
460
|
+
const open = mainlines.filter((m) => COMPARED_MAINLINE.includes(m.status)).flatMap((m) => m.tasks.filter((t) => !CLOSED_STATUS.task.includes(t.status) && t.repo && t.touches?.length).map((task) => ({
|
|
461
|
+
mainline: m.id,
|
|
462
|
+
task
|
|
463
|
+
})));
|
|
464
|
+
const overlaps = [];
|
|
465
|
+
open.forEach((a, i) => {
|
|
466
|
+
for (const b of open.slice(i + 1)) {
|
|
467
|
+
if (a.mainline === b.mainline || a.task.repo !== b.task.repo) continue;
|
|
468
|
+
const paths = a.task.touches.flatMap((x) => b.task.touches.filter((y) => touchesOverlap(x, y)).map((y) => [x, y]));
|
|
469
|
+
if (paths.length === 0 || dependsOn(edges, a, b) || dependsOn(edges, b, a)) continue;
|
|
470
|
+
overlaps.push({
|
|
471
|
+
tasks: [`${a.mainline}/${a.task.id}`, `${b.mainline}/${b.task.id}`],
|
|
472
|
+
repo: a.task.repo,
|
|
473
|
+
paths,
|
|
474
|
+
file: a.task.file
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
return overlaps;
|
|
479
|
+
}
|
|
480
|
+
//#endregion
|
|
422
481
|
//#region ../core/dist/templates.js
|
|
423
482
|
const body = (headings) => ["", ...headings.flatMap((h) => [`## ${h}`, ""])].join("\n");
|
|
424
483
|
const MAINLINE_BODY = body(MAINLINE_SECTIONS);
|
|
@@ -705,6 +764,14 @@ function loadHub(root, options = {}) {
|
|
|
705
764
|
message: `dependency cycle ${cycle.join(" → ")}; drop one of these dependencies`
|
|
706
765
|
});
|
|
707
766
|
}
|
|
767
|
+
for (const o of touchOverlaps(mainlines, deps.edges)) {
|
|
768
|
+
const pairs = o.paths.map(([a, b]) => a === b ? a : `${a} ~ ${b}`).join(", ");
|
|
769
|
+
warnings.push({
|
|
770
|
+
file: o.file,
|
|
771
|
+
field: "touches",
|
|
772
|
+
message: `${o.tasks[0]} and ${o.tasks[1]} both touch ${pairs} in repo ${o.repo}, and neither depends on the other; coordinate before both run, or add a depends-on link`
|
|
773
|
+
});
|
|
774
|
+
}
|
|
708
775
|
return {
|
|
709
776
|
root,
|
|
710
777
|
config,
|
|
@@ -1247,14 +1314,14 @@ function grantScope(root, session) {
|
|
|
1247
1314
|
reason: `${session.id} 同時有 ${doing.length} 個進行中的行動(${doing.map((a) => a.id).join("、")}),分不出授權範圍`
|
|
1248
1315
|
};
|
|
1249
1316
|
const action = doing[0];
|
|
1250
|
-
const
|
|
1317
|
+
const mainlineOf = (ref) => {
|
|
1251
1318
|
const parsed = parseRef(ref);
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1319
|
+
return parsed ? refMainline(parsed) : void 0;
|
|
1320
|
+
};
|
|
1321
|
+
const mainlines = [...new Set(action.serves.flatMap((ref) => mainlineOf(ref) ?? []))];
|
|
1255
1322
|
if (mainlines.length === 0) return {
|
|
1256
1323
|
ok: false,
|
|
1257
|
-
reason: `${action.id} 沒有服務任何 checklist
|
|
1324
|
+
reason: `${action.id} 沒有服務任何 checklist 項目或解決條件`
|
|
1258
1325
|
};
|
|
1259
1326
|
const grants = [];
|
|
1260
1327
|
const served = {};
|
|
@@ -1265,7 +1332,7 @@ function grantScope(root, session) {
|
|
|
1265
1332
|
closed.push(`${id} 是 ${mainline.status}`);
|
|
1266
1333
|
continue;
|
|
1267
1334
|
}
|
|
1268
|
-
served[id] = action.serves.find((ref) => ref
|
|
1335
|
+
served[id] = action.serves.find((ref) => mainlineOf(ref) === id);
|
|
1269
1336
|
for (const grant of mainline.grants ?? []) grants.push({
|
|
1270
1337
|
mainline: id,
|
|
1271
1338
|
grant,
|
|
@@ -1433,16 +1500,21 @@ function resolveAction(root, ref) {
|
|
|
1433
1500
|
}
|
|
1434
1501
|
}
|
|
1435
1502
|
/**
|
|
1436
|
-
* Every served checklist item must exist, so a session never points at work that is not in a
|
|
1437
|
-
* mainline
|
|
1438
|
-
*
|
|
1503
|
+
* Every served checklist item or criterion must exist, so a session never points at work that is not in a
|
|
1504
|
+
* mainline. A criterion stands in while the mainline has no task for it yet, as when it is still being surveyed
|
|
1505
|
+
* and planned. Its mainline must still be open: follow-up work on an ended one either reopens it with a criterion
|
|
1506
|
+
* or task for the follow-up, or starts a new mainline, so the follow-up is tracked instead of hanging off finished work.
|
|
1439
1507
|
*/
|
|
1440
1508
|
function requireServes(root, serves) {
|
|
1441
1509
|
for (const ref of serves) {
|
|
1442
1510
|
const parsed = parseRef(ref);
|
|
1443
|
-
if (!
|
|
1511
|
+
if (!SERVES_REF.test(ref) || parsed?.kind !== "checklistItem" && parsed?.kind !== "criterion") throw new WriteError(`--serves takes checklist items like m001/t002#c003 or criteria like m001#r001, got "${ref}"`);
|
|
1444
1512
|
const mainline = readFrontMatter(mainlineFile(root, parsed.mainline), mainlineSchema).data;
|
|
1445
1513
|
if (CLOSED_STATUS.mainline.includes(mainline.status)) throw new WriteError(`${parsed.mainline} is ${mainline.status}; to continue it, run "mainline set ${parsed.mainline} --status active" and add a criterion or task for the follow-up, or start a new mainline with "new mainline"`);
|
|
1514
|
+
if (parsed.kind === "criterion") {
|
|
1515
|
+
if (!mainline.criteria.some((r) => r.id === parsed.criterion)) throw new WriteError(`criterion ${ref} not found`);
|
|
1516
|
+
continue;
|
|
1517
|
+
}
|
|
1446
1518
|
if (!readFrontMatter(taskFile(root, parsed.mainline, parsed.task), taskSchema).data.checklist.some((c) => c.id === parsed.item)) throw new WriteError(`checklist item ${ref} not found`);
|
|
1447
1519
|
}
|
|
1448
1520
|
}
|
|
@@ -1695,9 +1767,18 @@ function updateAction(root, ref, patch) {
|
|
|
1695
1767
|
//#region ../core/dist/briefing.js
|
|
1696
1768
|
function describe(root, ref) {
|
|
1697
1769
|
const parsed = parseRef(ref);
|
|
1698
|
-
if (parsed?.kind !== "checklistItem") return null;
|
|
1770
|
+
if (parsed?.kind !== "checklistItem" && parsed?.kind !== "criterion") return null;
|
|
1699
1771
|
try {
|
|
1700
1772
|
const mainline = readFrontMatter(mainlineFile(root, parsed.mainline), mainlineSchema).data;
|
|
1773
|
+
if (parsed.kind === "criterion") {
|
|
1774
|
+
const criterion = mainline.criteria.find((r) => r.id === parsed.criterion);
|
|
1775
|
+
return criterion ? {
|
|
1776
|
+
ref,
|
|
1777
|
+
text: criterion.text,
|
|
1778
|
+
checked: criterion.done,
|
|
1779
|
+
where: `${mainline.id} ${mainline.title} › 解決條件`
|
|
1780
|
+
} : null;
|
|
1781
|
+
}
|
|
1701
1782
|
const task = readFrontMatter(taskFile(root, parsed.mainline, parsed.task), taskSchema).data;
|
|
1702
1783
|
const item = task.checklist.find((c) => c.id === parsed.item);
|
|
1703
1784
|
if (!item) return null;
|
|
@@ -1711,6 +1792,32 @@ function describe(root, ref) {
|
|
|
1711
1792
|
return null;
|
|
1712
1793
|
}
|
|
1713
1794
|
}
|
|
1795
|
+
/** The criteria a served checklist item pushes forward, as m{NNN}#r{NNN}; empty for a criterion or an item that cannot be read. */
|
|
1796
|
+
function criteriaOf(root, ref) {
|
|
1797
|
+
const parsed = parseRef(ref);
|
|
1798
|
+
if (parsed?.kind !== "checklistItem") return [];
|
|
1799
|
+
try {
|
|
1800
|
+
return (readFrontMatter(taskFile(root, parsed.mainline, parsed.task), taskSchema).data.checklist.find((c) => c.id === parsed.item)?.serves ?? []).map((r) => `${parsed.mainline}#${r}`);
|
|
1801
|
+
} catch {
|
|
1802
|
+
return [];
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
/**
|
|
1806
|
+
* Where two served lists meet: a reference both serve, or a criterion one serves that a checklist item the other
|
|
1807
|
+
* serves pushes forward, named as the item with its criterion. Two items serving the same criterion do not meet,
|
|
1808
|
+
* since splitting a criterion into items is how work on it runs in parallel.
|
|
1809
|
+
*/
|
|
1810
|
+
function sharedServes(root, mine, theirs) {
|
|
1811
|
+
const shared = mine.filter((ref) => theirs.includes(ref));
|
|
1812
|
+
const pushes = (items, criteria) => items.flatMap((item) => criteriaOf(root, item).filter((r) => criteria.includes(r)).map((r) => `${item}(推進 ${r})`));
|
|
1813
|
+
const mineCriteria = mine.filter((ref) => parseRef(ref)?.kind === "criterion");
|
|
1814
|
+
const theirCriteria = theirs.filter((ref) => parseRef(ref)?.kind === "criterion");
|
|
1815
|
+
return [.../* @__PURE__ */ new Set([
|
|
1816
|
+
...shared,
|
|
1817
|
+
...pushes(theirs, mineCriteria),
|
|
1818
|
+
...pushes(mine, theirCriteria)
|
|
1819
|
+
])];
|
|
1820
|
+
}
|
|
1714
1821
|
/** Problems in a session's queue that the executor should fix before going on, each with the command that fixes it. */
|
|
1715
1822
|
function queueWarnings(root, session, cli) {
|
|
1716
1823
|
const warnings = [];
|
|
@@ -1718,8 +1825,8 @@ function queueWarnings(root, session, cli) {
|
|
|
1718
1825
|
if (doing.length > 1) warnings.push(`同時有 ${doing.length} 個進行中的行動(${doing.map((a) => a.id).join("、")})。一次只推進一個,其餘改回 todo。`);
|
|
1719
1826
|
for (const action of doing) {
|
|
1720
1827
|
const served = action.serves.map((ref) => describe(root, ref));
|
|
1721
|
-
if (served.some((s) => s === null)) warnings.push(`${action.id} 服務的項目有的已不存在,用 ${cli} action set ${action.id} --serves <m/t#c> 改正。`);
|
|
1722
|
-
else if (served.length > 0 && served.every((s) => s.checked)) warnings.push(`${action.id}
|
|
1828
|
+
if (served.some((s) => s === null)) warnings.push(`${action.id} 服務的項目有的已不存在,用 ${cli} action set ${action.id} --serves <m/t#c 或 m#r> 改正。`);
|
|
1829
|
+
else if (served.length > 0 && served.every((s) => s.checked)) warnings.push(`${action.id} 服務的項目都已勾選或達成。做完了就執行 ${cli} action set ${action.id} --status done。`);
|
|
1723
1830
|
}
|
|
1724
1831
|
const next = session.actions.find((a) => a.status === "todo");
|
|
1725
1832
|
if (doing.length === 0 && next) warnings.push(`下一個行動是 ${next.id}。開始前先執行 ${cli} action set ${next.id} --status doing。`);
|
|
@@ -1794,8 +1901,8 @@ function waiterLines(root, session) {
|
|
|
1794
1901
|
return lines;
|
|
1795
1902
|
}
|
|
1796
1903
|
/**
|
|
1797
|
-
* One reminder for each action in progress in another session that may still be running and serves
|
|
1798
|
-
*
|
|
1904
|
+
* One reminder for each action in progress in another session that may still be running and serves what this
|
|
1905
|
+
* action also serves: the same item or criterion, or a criterion and an item that pushes it forward. Sessions in the same delegation tree are left out: a subagent working on the item
|
|
1799
1906
|
* its parent handed it is not a second claim.
|
|
1800
1907
|
*/
|
|
1801
1908
|
function duplicateClaimLines(root, sessionId, actionId) {
|
|
@@ -1807,7 +1914,7 @@ function duplicateClaimLines(root, sessionId, actionId) {
|
|
|
1807
1914
|
for (const other of sessions) {
|
|
1808
1915
|
if (related.has(other.id) || !mayBeRunning(root, other)) continue;
|
|
1809
1916
|
for (const theirs of other.actions.filter((a) => a.status === "doing")) {
|
|
1810
|
-
const shared =
|
|
1917
|
+
const shared = sharedServes(root, action.serves, theirs.serves);
|
|
1811
1918
|
if (shared.length > 0) lines.push(`⚠ ${other.id}#${theirs.id}(15 分鐘內有活動)也在做 ${shared.join("、")},先協調再開始`);
|
|
1812
1919
|
}
|
|
1813
1920
|
}
|
|
@@ -1824,8 +1931,8 @@ function handoffLines(root, session, cli) {
|
|
|
1824
1931
|
}
|
|
1825
1932
|
const servedLine = (root, action) => action.serves.map((ref) => {
|
|
1826
1933
|
const s = describe(root, ref);
|
|
1827
|
-
return s ? `${ref} ${s.text}(${s.where})` : `${ref}
|
|
1828
|
-
}).join(";") || "沒有指定服務的 checklist
|
|
1934
|
+
return s ? `${ref} ${s.text}(${s.where})` : `${ref}(找不到這個項目或條件)`;
|
|
1935
|
+
}).join(";") || "沒有指定服務的 checklist 項目或解決條件";
|
|
1829
1936
|
/** Full briefing for the start of a session, after resuming, clearing or compacting. */
|
|
1830
1937
|
function sessionBriefing(root, session, options) {
|
|
1831
1938
|
const { cli } = options;
|
|
@@ -1853,7 +1960,7 @@ function sessionBriefing(root, session, options) {
|
|
|
1853
1960
|
if (!session.actions.some(isOpen)) lines.push(...handoffLines(root, session, cli));
|
|
1854
1961
|
const warnings = queueWarnings(root, session, cli);
|
|
1855
1962
|
if (warnings.length > 0) lines.push("需要處理:", ...warnings.map((w) => `- ${w}`));
|
|
1856
|
-
lines.push("工作方式:", `- 開始一件新工作前,先列進佇列:${cli} action add "<做法>" --serves <m/t#c>。`, `- 開始做標 doing,做完標 done:${cli} action set <a編號> --status doing。`, `- 勾選 checklist 要附證據:${cli} item check <m/t#c> --evidence <commit、檔案:行號或指令輸出>。`, `- 內容直接寫進檔案:意圖或需求寫進 mainline.md 的內文,任務的目標與完成定義寫進 task.md,選擇寫成決策(${cli} new decision <m> <slug> --title <標題> 建檔後再寫內文)。`, `- 改完 .mainflow/ 的檔案跑 ${cli} check,把它列出的問題修到沒有。`, "- 用完其他 skill 或子流程後,回到上面的目前行動再繼續。", "- 要開主脈絡、拆任務、記決策、延後或收尾時,用 mainflow skill,它依序帶過並告訴你資訊該寫在哪裡。");
|
|
1963
|
+
lines.push("工作方式:", `- 開始一件新工作前,先列進佇列:${cli} action add "<做法>" --serves <m/t#c>。主脈絡還沒拆任務時(查核與規劃階段),--serves 指向解決條件 <m#r>。`, `- 開始做標 doing,做完標 done:${cli} action set <a編號> --status doing。`, `- 勾選 checklist 要附證據:${cli} item check <m/t#c> --evidence <commit、檔案:行號或指令輸出>。`, `- 內容直接寫進檔案:意圖或需求寫進 mainline.md 的內文,任務的目標與完成定義寫進 task.md,選擇寫成決策(${cli} new decision <m> <slug> --title <標題> 建檔後再寫內文)。`, `- 改完 .mainflow/ 的檔案跑 ${cli} check,把它列出的問題修到沒有。`, "- 用完其他 skill 或子流程後,回到上面的目前行動再繼續。", "- 要開主脈絡、拆任務、記決策、延後或收尾時,用 mainflow skill,它依序帶過並告訴你資訊該寫在哪裡。");
|
|
1857
1964
|
return lines.join("\n");
|
|
1858
1965
|
}
|
|
1859
1966
|
/** One or two lines for every user turn; null when the session has no actions and the hub has no open mainline. */
|
|
@@ -2314,6 +2421,474 @@ function setArchivedMany(root, kind, ids, archived) {
|
|
|
2314
2421
|
return result;
|
|
2315
2422
|
}
|
|
2316
2423
|
//#endregion
|
|
2424
|
+
//#region ../core/dist/retro.js
|
|
2425
|
+
/** Where the prices come from and when they were read; every figure is USD per million tokens. */
|
|
2426
|
+
const PRICING_SOURCE = {
|
|
2427
|
+
url: "https://platform.claude.com/docs/en/about-claude/pricing",
|
|
2428
|
+
date: "2026-09-30",
|
|
2429
|
+
unit: "USD/MTok"
|
|
2430
|
+
};
|
|
2431
|
+
const price = (input, cacheWrite5m, cacheWrite1h, cacheRead, output) => ({
|
|
2432
|
+
input,
|
|
2433
|
+
cacheWrite5m,
|
|
2434
|
+
cacheWrite1h,
|
|
2435
|
+
cacheRead,
|
|
2436
|
+
output
|
|
2437
|
+
});
|
|
2438
|
+
/**
|
|
2439
|
+
* List prices by model id prefix. A model matches a prefix when the rest of its id is empty, a date suffix
|
|
2440
|
+
* (-20251001) or a bracketed variant ([1m]); any other model has no known price, so its cost is not guessed.
|
|
2441
|
+
*/
|
|
2442
|
+
const MODEL_PRICES = [
|
|
2443
|
+
{
|
|
2444
|
+
prefix: "claude-opus-5-5",
|
|
2445
|
+
price: price(4, 5, 8, .2, 20)
|
|
2446
|
+
},
|
|
2447
|
+
{
|
|
2448
|
+
prefix: "claude-opus-5",
|
|
2449
|
+
price: price(5, 6.25, 10, .5, 25)
|
|
2450
|
+
},
|
|
2451
|
+
{
|
|
2452
|
+
prefix: "claude-opus-4-8",
|
|
2453
|
+
price: price(5, 6.25, 10, .5, 25)
|
|
2454
|
+
},
|
|
2455
|
+
{
|
|
2456
|
+
prefix: "claude-sonnet-5-5",
|
|
2457
|
+
price: price(2, 2.5, 4, .2, 10)
|
|
2458
|
+
},
|
|
2459
|
+
{
|
|
2460
|
+
prefix: "claude-sonnet-5",
|
|
2461
|
+
price: price(2, 2.5, 4, .2, 10)
|
|
2462
|
+
},
|
|
2463
|
+
{
|
|
2464
|
+
prefix: "claude-sonnet-4-6",
|
|
2465
|
+
price: price(3, 3.75, 6, .3, 15)
|
|
2466
|
+
},
|
|
2467
|
+
{
|
|
2468
|
+
prefix: "claude-haiku-4-5",
|
|
2469
|
+
price: price(1, 1.25, 2, .1, 5)
|
|
2470
|
+
},
|
|
2471
|
+
{
|
|
2472
|
+
prefix: "claude-fable-5-1",
|
|
2473
|
+
price: price(10, 12.5, 20, .25, 50)
|
|
2474
|
+
}
|
|
2475
|
+
];
|
|
2476
|
+
/**
|
|
2477
|
+
* Prices at speed "fast", keyed by the MODEL_PRICES prefix. The cache multipliers stack on the fast input
|
|
2478
|
+
* price (5m write 1.25x, 1h write 2x, read 0.05x for Opus 5.5 and 0.1x otherwise), as with standard speed.
|
|
2479
|
+
*/
|
|
2480
|
+
const FAST_PRICES = {
|
|
2481
|
+
"claude-opus-5-5": price(8, 10, 16, .4, 40),
|
|
2482
|
+
"claude-opus-5": price(10, 12.5, 20, 1, 50),
|
|
2483
|
+
"claude-opus-4-8": price(10, 12.5, 20, 1, 50)
|
|
2484
|
+
};
|
|
2485
|
+
/** Inference kept in the US (inference_geo "us") is billed at 1.1 times the list price. */
|
|
2486
|
+
const US_GEO_MULTIPLIER = 1.1;
|
|
2487
|
+
const PRICE_SUFFIX = /^(?:-\d{8})?(?:\[[^\]]*\])?$/;
|
|
2488
|
+
/** The price entry a model id falls under: the longest prefix whose remainder is only a date or a variant. */
|
|
2489
|
+
function priceEntry(model) {
|
|
2490
|
+
return MODEL_PRICES.filter((p) => model.startsWith(p.prefix) && PRICE_SUFFIX.test(model.slice(p.prefix.length))).sort((a, b) => b.prefix.length - a.prefix.length)[0];
|
|
2491
|
+
}
|
|
2492
|
+
const priceOf = (model) => priceEntry(model)?.price;
|
|
2493
|
+
const emptyUsage = () => ({
|
|
2494
|
+
input: 0,
|
|
2495
|
+
cacheWrite5m: 0,
|
|
2496
|
+
cacheWrite1h: 0,
|
|
2497
|
+
cacheRead: 0,
|
|
2498
|
+
output: 0
|
|
2499
|
+
});
|
|
2500
|
+
const addUsage = (to, from) => {
|
|
2501
|
+
for (const key of Object.keys(to)) to[key] += from[key];
|
|
2502
|
+
};
|
|
2503
|
+
/** Everything the model read before answering: fresh input, cache writes and cache reads. */
|
|
2504
|
+
const contextTokens = (u) => u.input + u.cacheWrite5m + u.cacheWrite1h + u.cacheRead;
|
|
2505
|
+
/** Share of the context served from the cache; null when there was no context at all. */
|
|
2506
|
+
const cacheHitRate = (u) => contextTokens(u) > 0 ? u.cacheRead / contextTokens(u) : null;
|
|
2507
|
+
const num = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : 0;
|
|
2508
|
+
/** Read message.usage; cache writes without the 5m and 1h split count as 5m, the default cache lifetime. */
|
|
2509
|
+
function readUsage(raw) {
|
|
2510
|
+
const created = num(raw.cache_creation_input_tokens);
|
|
2511
|
+
const split = raw.cache_creation ?? {};
|
|
2512
|
+
const w1h = num(split.ephemeral_1h_input_tokens);
|
|
2513
|
+
const w5m = split.ephemeral_5m_input_tokens === void 0 ? created - w1h : num(split.ephemeral_5m_input_tokens);
|
|
2514
|
+
return {
|
|
2515
|
+
input: num(raw.input_tokens),
|
|
2516
|
+
cacheWrite5m: Math.max(0, w5m),
|
|
2517
|
+
cacheWrite1h: w1h,
|
|
2518
|
+
cacheRead: num(raw.cache_read_input_tokens),
|
|
2519
|
+
output: num(raw.output_tokens)
|
|
2520
|
+
};
|
|
2521
|
+
}
|
|
2522
|
+
/** The estimated cost of one message in USD, or null when the model has no known price. */
|
|
2523
|
+
function messageCost(model, usage, speed, geo) {
|
|
2524
|
+
const entry = priceEntry(model);
|
|
2525
|
+
if (!entry) return null;
|
|
2526
|
+
const p = (speed === "fast" ? FAST_PRICES[entry.prefix] : void 0) ?? entry.price;
|
|
2527
|
+
const usd = (usage.input * p.input + usage.cacheWrite5m * p.cacheWrite5m + usage.cacheWrite1h * p.cacheWrite1h + usage.cacheRead * p.cacheRead + usage.output * p.output) / 1e6;
|
|
2528
|
+
return geo === "us" ? usd * US_GEO_MULTIPLIER : usd;
|
|
2529
|
+
}
|
|
2530
|
+
const isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
2531
|
+
const blocks = (content) => Array.isArray(content) ? content.filter(isObject) : [];
|
|
2532
|
+
/** The text a hook attachment hands to the model, empty when it hands none. */
|
|
2533
|
+
function hookText(a) {
|
|
2534
|
+
const parts = [];
|
|
2535
|
+
if (typeof a.content === "string") parts.push(a.content);
|
|
2536
|
+
if (Array.isArray(a.content)) parts.push(...a.content.filter((c) => typeof c === "string"));
|
|
2537
|
+
const blocking = a.blockingError;
|
|
2538
|
+
if (typeof blocking === "string") parts.push(blocking);
|
|
2539
|
+
if (isObject(blocking) && typeof blocking.blockingError === "string") parts.push(blocking.blockingError);
|
|
2540
|
+
return parts.join("");
|
|
2541
|
+
}
|
|
2542
|
+
const hookEvent = (a) => typeof a.hookEvent === "string" && a.hookEvent ? a.hookEvent : typeof a.hookName === "string" && a.hookName ? a.hookName.split(":")[0] : "unknown";
|
|
2543
|
+
const emptyStats = (file, found) => ({
|
|
2544
|
+
file,
|
|
2545
|
+
found,
|
|
2546
|
+
versions: [],
|
|
2547
|
+
lines: 0,
|
|
2548
|
+
unreadable: 0,
|
|
2549
|
+
messages: [],
|
|
2550
|
+
toolUses: 0,
|
|
2551
|
+
toolErrors: {},
|
|
2552
|
+
bashRuns: {},
|
|
2553
|
+
hooks: [],
|
|
2554
|
+
misfires: {},
|
|
2555
|
+
gaps: []
|
|
2556
|
+
});
|
|
2557
|
+
function readTranscript(file) {
|
|
2558
|
+
const stats = emptyStats(file, existsSync(file));
|
|
2559
|
+
if (!stats.found) return stats;
|
|
2560
|
+
const versions = /* @__PURE__ */ new Set();
|
|
2561
|
+
const byId = /* @__PURE__ */ new Map();
|
|
2562
|
+
const toolNames = /* @__PURE__ */ new Map();
|
|
2563
|
+
const hooks = /* @__PURE__ */ new Map();
|
|
2564
|
+
/** Injections still waiting for the next assistant message, with the message they came after */
|
|
2565
|
+
let pending = [];
|
|
2566
|
+
const bound = [];
|
|
2567
|
+
let lastAssistant;
|
|
2568
|
+
let lastAt;
|
|
2569
|
+
let lastAtText = "";
|
|
2570
|
+
let anonymous = 0;
|
|
2571
|
+
for (const text of readFileSync(file, "utf8").split("\n")) {
|
|
2572
|
+
if (!text.trim()) continue;
|
|
2573
|
+
stats.lines += 1;
|
|
2574
|
+
let line;
|
|
2575
|
+
try {
|
|
2576
|
+
const parsed = JSON.parse(text);
|
|
2577
|
+
if (!isObject(parsed)) throw new Error("not an object");
|
|
2578
|
+
line = parsed;
|
|
2579
|
+
} catch {
|
|
2580
|
+
stats.unreadable += 1;
|
|
2581
|
+
continue;
|
|
2582
|
+
}
|
|
2583
|
+
if (typeof line.version === "string") versions.add(line.version);
|
|
2584
|
+
const at = typeof line.timestamp === "string" ? Date.parse(line.timestamp) : NaN;
|
|
2585
|
+
if (!Number.isNaN(at)) {
|
|
2586
|
+
if (lastAt !== void 0 && at - lastAt > 3e5) {
|
|
2587
|
+
const turnEnded = lastAssistant !== void 0 && byId.get(lastAssistant)?.toolUses === 0;
|
|
2588
|
+
stats.gaps.push({
|
|
2589
|
+
from: lastAtText,
|
|
2590
|
+
to: line.timestamp,
|
|
2591
|
+
minutes: Math.round((at - lastAt) / 6e4),
|
|
2592
|
+
waitingForUser: turnEnded
|
|
2593
|
+
});
|
|
2594
|
+
}
|
|
2595
|
+
if (lastAt === void 0 || at > lastAt) {
|
|
2596
|
+
lastAt = at;
|
|
2597
|
+
lastAtText = line.timestamp;
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
if (line.type === "assistant") {
|
|
2601
|
+
const m = line.message;
|
|
2602
|
+
if (!isObject(m)) {
|
|
2603
|
+
stats.unreadable += 1;
|
|
2604
|
+
continue;
|
|
2605
|
+
}
|
|
2606
|
+
const id = typeof m.id === "string" && m.id ? m.id : `(no id ${++anonymous})`;
|
|
2607
|
+
let message = byId.get(id);
|
|
2608
|
+
if (!message) {
|
|
2609
|
+
message = {
|
|
2610
|
+
id,
|
|
2611
|
+
model: typeof m.model === "string" ? m.model : "unknown",
|
|
2612
|
+
usage: emptyUsage(),
|
|
2613
|
+
toolUses: 0,
|
|
2614
|
+
...typeof line.timestamp === "string" ? { at: line.timestamp } : {}
|
|
2615
|
+
};
|
|
2616
|
+
byId.set(id, message);
|
|
2617
|
+
stats.messages.push(message);
|
|
2618
|
+
}
|
|
2619
|
+
if (isObject(m.usage)) {
|
|
2620
|
+
const usage = readUsage(m.usage);
|
|
2621
|
+
for (const key of Object.keys(usage)) message.usage[key] = Math.max(message.usage[key], usage[key]);
|
|
2622
|
+
if (typeof m.usage.speed === "string") message.speed = m.usage.speed;
|
|
2623
|
+
if (typeof m.usage.inference_geo === "string") message.geo = m.usage.inference_geo;
|
|
2624
|
+
}
|
|
2625
|
+
for (const block of blocks(m.content)) {
|
|
2626
|
+
if (block.type !== "tool_use") continue;
|
|
2627
|
+
message.toolUses += 1;
|
|
2628
|
+
stats.toolUses += 1;
|
|
2629
|
+
const name = typeof block.name === "string" ? block.name : "unknown";
|
|
2630
|
+
if (typeof block.id === "string") toolNames.set(block.id, name);
|
|
2631
|
+
const input = isObject(block.input) ? block.input : {};
|
|
2632
|
+
if (name === "Bash" && typeof input.command === "string") {
|
|
2633
|
+
const command = input.command.trim();
|
|
2634
|
+
stats.bashRuns[command] = (stats.bashRuns[command] ?? 0) + 1;
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
if (id !== lastAssistant) {
|
|
2638
|
+
for (const p of pending.filter((x) => x.after !== id)) bound.push({
|
|
2639
|
+
event: p.event,
|
|
2640
|
+
message: id
|
|
2641
|
+
});
|
|
2642
|
+
pending = pending.filter((x) => x.after === id);
|
|
2643
|
+
}
|
|
2644
|
+
lastAssistant = id;
|
|
2645
|
+
continue;
|
|
2646
|
+
}
|
|
2647
|
+
if (line.type === "user" && line.message !== void 0) {
|
|
2648
|
+
for (const block of blocks(line.message?.content)) {
|
|
2649
|
+
if (block.type !== "tool_result" || block.is_error !== true) continue;
|
|
2650
|
+
const name = typeof block.tool_use_id === "string" && toolNames.get(block.tool_use_id) || "unknown";
|
|
2651
|
+
stats.toolErrors[name] = (stats.toolErrors[name] ?? 0) + 1;
|
|
2652
|
+
}
|
|
2653
|
+
continue;
|
|
2654
|
+
}
|
|
2655
|
+
if (line.type === "attachment") {
|
|
2656
|
+
const a = line.attachment;
|
|
2657
|
+
if (!isObject(a)) {
|
|
2658
|
+
stats.unreadable += 1;
|
|
2659
|
+
continue;
|
|
2660
|
+
}
|
|
2661
|
+
const kind = typeof a.type === "string" ? a.type : "";
|
|
2662
|
+
if (!kind.includes("hook")) continue;
|
|
2663
|
+
const event = hookEvent(a);
|
|
2664
|
+
const hook = hooks.get(event) ?? {
|
|
2665
|
+
event,
|
|
2666
|
+
injections: 0,
|
|
2667
|
+
chars: 0,
|
|
2668
|
+
blocked: 0
|
|
2669
|
+
};
|
|
2670
|
+
hooks.set(event, hook);
|
|
2671
|
+
if (kind.includes("blocking")) hook.blocked += 1;
|
|
2672
|
+
const injected = hookText(a);
|
|
2673
|
+
if (injected.length > 0) {
|
|
2674
|
+
hook.injections += 1;
|
|
2675
|
+
hook.chars += [...injected].length;
|
|
2676
|
+
pending.push({
|
|
2677
|
+
event,
|
|
2678
|
+
after: lastAssistant
|
|
2679
|
+
});
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
2683
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2684
|
+
for (const b of bound) {
|
|
2685
|
+
const key = `${b.event}\u0000${b.message}`;
|
|
2686
|
+
if (seen.has(key)) continue;
|
|
2687
|
+
seen.add(key);
|
|
2688
|
+
if (byId.get(b.message)?.toolUses === 0) stats.misfires[b.event] = (stats.misfires[b.event] ?? 0) + 1;
|
|
2689
|
+
}
|
|
2690
|
+
stats.versions = [...versions].sort();
|
|
2691
|
+
stats.hooks = [...hooks.values()];
|
|
2692
|
+
return stats;
|
|
2693
|
+
}
|
|
2694
|
+
/** Whether any action of the session serves the mainline, through a checklist item or a criterion. */
|
|
2695
|
+
const servesMainline = (session, mainline) => session.actions.some((a) => a.serves.some((ref) => {
|
|
2696
|
+
const parsed = parseRef(ref);
|
|
2697
|
+
return parsed !== null && parsed.hub === void 0 && refMainline(parsed) === mainline;
|
|
2698
|
+
}));
|
|
2699
|
+
/** Subagent transcripts Claude Code keeps beside a session's: <dir>/<session>/subagents/*.jsonl. */
|
|
2700
|
+
function subagentFiles(transcript) {
|
|
2701
|
+
const dir = join(transcript.replace(/\.jsonl$/, ""), "subagents");
|
|
2702
|
+
try {
|
|
2703
|
+
return readdirSync(dir).filter((n) => n.endsWith(".jsonl")).sort().map((n) => join(dir, n));
|
|
2704
|
+
} catch {
|
|
2705
|
+
return [];
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2708
|
+
function readMeta(file) {
|
|
2709
|
+
try {
|
|
2710
|
+
const meta = JSON.parse(readFileSync(file.replace(/\.jsonl$/, ".meta.json"), "utf8"));
|
|
2711
|
+
if (!isObject(meta)) return {};
|
|
2712
|
+
return {
|
|
2713
|
+
...typeof meta.agentType === "string" ? { agentType: meta.agentType } : {},
|
|
2714
|
+
...typeof meta.description === "string" ? { description: meta.description } : {}
|
|
2715
|
+
};
|
|
2716
|
+
} catch {
|
|
2717
|
+
return {};
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
function summarize(stats) {
|
|
2721
|
+
const usage = emptyUsage();
|
|
2722
|
+
let cost = 0;
|
|
2723
|
+
const unknown = /* @__PURE__ */ new Set();
|
|
2724
|
+
const priced = stats.messages.filter((m) => m.model !== "<synthetic>");
|
|
2725
|
+
for (const m of priced) {
|
|
2726
|
+
addUsage(usage, m.usage);
|
|
2727
|
+
const c = messageCost(m.model, m.usage, m.speed, m.geo);
|
|
2728
|
+
if (c === null) unknown.add(m.model);
|
|
2729
|
+
else cost += c;
|
|
2730
|
+
}
|
|
2731
|
+
const first = priced.find((m) => contextTokens(m.usage) > 0);
|
|
2732
|
+
return {
|
|
2733
|
+
file: stats.file,
|
|
2734
|
+
found: stats.found,
|
|
2735
|
+
calls: priced.length,
|
|
2736
|
+
usage,
|
|
2737
|
+
cost,
|
|
2738
|
+
unknownPriceModels: [...unknown].sort(),
|
|
2739
|
+
models: [...new Set(priced.map((m) => m.model))].sort(),
|
|
2740
|
+
openingContext: first ? contextTokens(first.usage) : null,
|
|
2741
|
+
cacheHitRate: cacheHitRate(usage)
|
|
2742
|
+
};
|
|
2743
|
+
}
|
|
2744
|
+
/** Lines of a task's 紀錄 section that record a completion check that did not pass. */
|
|
2745
|
+
const FAILED_CHECK = (line) => line.includes("完成檢查") && line.includes("未過");
|
|
2746
|
+
/**
|
|
2747
|
+
* Read the transcripts of every session with an action serving the mainline, and their subagents', into cost
|
|
2748
|
+
* figures and candidates for where the work went wrong. Each transcript is read once, even when a subagent session
|
|
2749
|
+
* serves the mainline as well.
|
|
2750
|
+
*/
|
|
2751
|
+
function retroReport(root, mainlineId) {
|
|
2752
|
+
const mainline = loadHub(root, { remotes: false }).mainlines.find((m) => m.id === mainlineId);
|
|
2753
|
+
if (!mainline) throw new WriteError(`mainline ${mainlineId} not found; run "mainflow status --all" to list mainlines`);
|
|
2754
|
+
const serving = loadSessions(root).sessions.filter((s) => servesMainline(s, mainlineId));
|
|
2755
|
+
const read = /* @__PURE__ */ new Map();
|
|
2756
|
+
const stats = (file) => {
|
|
2757
|
+
let s = read.get(file);
|
|
2758
|
+
if (!s) read.set(file, s = readTranscript(file));
|
|
2759
|
+
return s;
|
|
2760
|
+
};
|
|
2761
|
+
const sessions = [];
|
|
2762
|
+
const subagents = [];
|
|
2763
|
+
const labels = /* @__PURE__ */ new Map();
|
|
2764
|
+
const subagentFilesSeen = /* @__PURE__ */ new Set();
|
|
2765
|
+
const addSubagent = (file, parent) => {
|
|
2766
|
+
if (subagentFilesSeen.has(file)) return;
|
|
2767
|
+
subagentFilesSeen.add(file);
|
|
2768
|
+
labels.set(file, `${parent} › ${basename(file, ".jsonl")}`);
|
|
2769
|
+
subagents.push({
|
|
2770
|
+
...summarize(stats(file)),
|
|
2771
|
+
parent,
|
|
2772
|
+
...readMeta(file)
|
|
2773
|
+
});
|
|
2774
|
+
};
|
|
2775
|
+
for (const s of [...serving].sort((a, b) => Number(Boolean(a.delegatedFrom)) - Number(Boolean(b.delegatedFrom)))) {
|
|
2776
|
+
const file = s.transcript ? transcriptPath(root, s.transcript) : void 0;
|
|
2777
|
+
if (s.delegatedFrom) {
|
|
2778
|
+
if (file) addSubagent(file, s.delegatedFrom);
|
|
2779
|
+
continue;
|
|
2780
|
+
}
|
|
2781
|
+
if (!file) {
|
|
2782
|
+
sessions.push({
|
|
2783
|
+
...summarize(emptyStats("", false)),
|
|
2784
|
+
session: s.id,
|
|
2785
|
+
missing: "the session records no transcript"
|
|
2786
|
+
});
|
|
2787
|
+
continue;
|
|
2788
|
+
}
|
|
2789
|
+
labels.set(file, s.id);
|
|
2790
|
+
const summary = summarize(stats(file));
|
|
2791
|
+
sessions.push({
|
|
2792
|
+
...summary,
|
|
2793
|
+
session: s.id,
|
|
2794
|
+
...summary.found ? {} : { missing: `${file} does not exist` }
|
|
2795
|
+
});
|
|
2796
|
+
for (const sub of subagentFiles(file)) addSubagent(sub, s.id);
|
|
2797
|
+
}
|
|
2798
|
+
const all = [...read.values()];
|
|
2799
|
+
const models = /* @__PURE__ */ new Map();
|
|
2800
|
+
const total = emptyUsage();
|
|
2801
|
+
for (const m of all.flatMap((t) => t.messages).filter((m) => m.model !== "<synthetic>")) {
|
|
2802
|
+
const entry = models.get(m.model) ?? {
|
|
2803
|
+
model: m.model,
|
|
2804
|
+
calls: 0,
|
|
2805
|
+
usage: emptyUsage(),
|
|
2806
|
+
cost: priceOf(m.model) ? 0 : null
|
|
2807
|
+
};
|
|
2808
|
+
models.set(m.model, entry);
|
|
2809
|
+
entry.calls += 1;
|
|
2810
|
+
addUsage(entry.usage, m.usage);
|
|
2811
|
+
addUsage(total, m.usage);
|
|
2812
|
+
const c = messageCost(m.model, m.usage, m.speed, m.geo);
|
|
2813
|
+
if (c !== null && entry.cost !== null) entry.cost += c;
|
|
2814
|
+
}
|
|
2815
|
+
const hooks = /* @__PURE__ */ new Map();
|
|
2816
|
+
for (const h of all.flatMap((t) => t.hooks)) {
|
|
2817
|
+
const entry = hooks.get(h.event) ?? {
|
|
2818
|
+
event: h.event,
|
|
2819
|
+
injections: 0,
|
|
2820
|
+
chars: 0,
|
|
2821
|
+
blocked: 0
|
|
2822
|
+
};
|
|
2823
|
+
hooks.set(h.event, entry);
|
|
2824
|
+
entry.injections += h.injections;
|
|
2825
|
+
entry.chars += h.chars;
|
|
2826
|
+
entry.blocked += h.blocked;
|
|
2827
|
+
}
|
|
2828
|
+
const sum = (records) => {
|
|
2829
|
+
const out = {};
|
|
2830
|
+
for (const r of records) for (const [k, v] of Object.entries(r)) out[k] = (out[k] ?? 0) + v;
|
|
2831
|
+
return out;
|
|
2832
|
+
};
|
|
2833
|
+
const count = (r) => Object.values(r).reduce((a, b) => a + b, 0);
|
|
2834
|
+
const toolErrors = sum(all.map((t) => t.toolErrors));
|
|
2835
|
+
const misfires = sum(all.map((t) => t.misfires));
|
|
2836
|
+
const blocked = Object.fromEntries([...hooks.values()].filter((h) => h.blocked > 0).map((h) => [h.event, h.blocked]));
|
|
2837
|
+
const reruns = all.flatMap((t) => Object.entries(t.bashRuns).filter(([, runs]) => runs > 1).map(([command, runs]) => ({
|
|
2838
|
+
command,
|
|
2839
|
+
runs,
|
|
2840
|
+
session: labels.get(t.file) ?? t.file
|
|
2841
|
+
}))).sort((a, b) => b.runs - a.runs);
|
|
2842
|
+
const modelList = [...models.values()].sort((a, b) => b.calls - a.calls || a.model.localeCompare(b.model));
|
|
2843
|
+
return {
|
|
2844
|
+
mainline: {
|
|
2845
|
+
id: mainline.id,
|
|
2846
|
+
title: mainline.title,
|
|
2847
|
+
status: mainline.status,
|
|
2848
|
+
criteria: mainline.criteria.length,
|
|
2849
|
+
tasks: mainline.tasks.length
|
|
2850
|
+
},
|
|
2851
|
+
versions: [...new Set(all.flatMap((t) => t.versions))].sort(),
|
|
2852
|
+
unreadable: all.reduce((n, t) => n + t.unreadable, 0),
|
|
2853
|
+
pricing: PRICING_SOURCE,
|
|
2854
|
+
cost: {
|
|
2855
|
+
total: modelList.reduce((n, m) => n + (m.cost ?? 0), 0),
|
|
2856
|
+
unknownPriceModels: modelList.filter((m) => m.cost === null).map((m) => m.model),
|
|
2857
|
+
cacheHitRate: cacheHitRate(total),
|
|
2858
|
+
models: modelList,
|
|
2859
|
+
sessions,
|
|
2860
|
+
subagents,
|
|
2861
|
+
hooks: [...hooks.values()].sort((a, b) => b.injections - a.injections || a.event.localeCompare(b.event))
|
|
2862
|
+
},
|
|
2863
|
+
precision: {
|
|
2864
|
+
toolErrors: {
|
|
2865
|
+
total: count(toolErrors),
|
|
2866
|
+
byTool: toolErrors
|
|
2867
|
+
},
|
|
2868
|
+
bashReruns: {
|
|
2869
|
+
total: reruns.reduce((n, r) => n + r.runs - 1, 0),
|
|
2870
|
+
commands: reruns
|
|
2871
|
+
},
|
|
2872
|
+
misfireCandidates: {
|
|
2873
|
+
total: count(misfires),
|
|
2874
|
+
byEvent: misfires
|
|
2875
|
+
},
|
|
2876
|
+
blocked: {
|
|
2877
|
+
total: count(blocked),
|
|
2878
|
+
byEvent: blocked
|
|
2879
|
+
},
|
|
2880
|
+
gaps: all.flatMap((t) => t.gaps.map((g) => ({
|
|
2881
|
+
...g,
|
|
2882
|
+
session: labels.get(t.file) ?? t.file
|
|
2883
|
+
}))),
|
|
2884
|
+
failedChecks: mainline.tasks.flatMap((t) => (sectionText(t.body, "紀錄") ?? "").split("\n").filter(FAILED_CHECK).map((line) => ({
|
|
2885
|
+
task: `${mainline.id}/${t.id}`,
|
|
2886
|
+
line: line.trim()
|
|
2887
|
+
})))
|
|
2888
|
+
}
|
|
2889
|
+
};
|
|
2890
|
+
}
|
|
2891
|
+
//#endregion
|
|
2317
2892
|
//#region ../core/dist/migrate.js
|
|
2318
2893
|
const drop = (o, ...keys) => keys.forEach((k) => delete o[k]);
|
|
2319
2894
|
const dropNull = (o, ...keys) => keys.forEach((k) => o[k] === null && delete o[k]);
|
|
@@ -2597,4 +3172,4 @@ function openInApp(hub, target, closeAfterMs = 150) {
|
|
|
2597
3172
|
return true;
|
|
2598
3173
|
}
|
|
2599
3174
|
//#endregion
|
|
2600
|
-
export {
|
|
3175
|
+
export { createFinding as $, duplicateClaimLines as A, setSessionStatus as B, setCriterion as C, supersedeDecision as D, setMainlineTitle as E, handoffSession as F, grantScope as G, startSession as H, loadSessions as I, endsWithMarker as J, matchGrants as K, readSession as L, turnReminder as M, addAction as N, updateTask as O, ensureSession as P, closeIssue as Q, setSessionArchived as R, setChecklistItem as S, setMainlineStatus as T, transcriptPath as U, settleSession as V, updateAction as W, mayDecorate as X, headerLine as Y, appendOps as Z, createMainline as _, DATA_DIR as _t, openInApp as a, setIssueArchived as at, initHub as b, worktreesOf as bt, syncHost as c, dependencyGraph as ct, ARCHIVE_KINDS as d, refIndex as dt, createIssue as et, setArchivedMany as f, MAINLINE_FILE as ft, createDecision as g, readYaml as gt, addRepo as h, WriteError as ht, hostName as i, setFindingStatus as it, sessionBriefing as j, untilState as k, migrateHub as l, mainlineEdges as lt, addHub as m, ConflictError as mt, DEFAULT_UI_PORT as n, reopenIssue as nt, removeHostLink as o, HubError as ot, acceptDecision as p, TASK_FILE as pt, decorateReply as q, caddySites as r, setFindingArchived as rt, siteBlock as s, loadHub as st, APP_BUNDLE_ID as t, planIssue as tt, retroReport as u, linksInto as ut, createTask as v, currentBranch as vt, setCriterionText as w, setArchived as x, dropChecklistItem as y, findHubRoot as yt, setSessionInfo as z };
|