mainflow 0.26.0 → 0.28.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 +64 -5
- package/dist/{dist-DlmecRLf.js → dist-BbamrIPW.js} +12 -2
- package/dist/facts.d.ts +22 -6
- package/dist/facts.js +24 -7
- package/dist/grant-commands.d.ts +1 -1
- package/dist/grant-commands.js +1 -1
- package/dist/hooks.js +52 -6
- package/dist/{host-Dr0bCHd8.js → host-CeN3dvXg.js} +712 -44
- 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-BKKoCyTj.js} +2 -2
- package/dist/launch.js +1 -1
- package/dist/{ui-Cz8suWal.js → ui-D_9x6XNs.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-BbamrIPW.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,76 @@ 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
|
+
/** Unfinished tasks of planning or active mainlines that list `touches`, each with the mainline it sits in. */
|
|
455
|
+
function openTouching(mainlines) {
|
|
456
|
+
return mainlines.filter((m) => COMPARED_MAINLINE.includes(m.status)).flatMap((m) => m.tasks.filter((t) => !CLOSED_STATUS.task.includes(t.status) && t.touches?.length).map((task) => ({
|
|
457
|
+
mainline: m.id,
|
|
458
|
+
task
|
|
459
|
+
})));
|
|
460
|
+
}
|
|
461
|
+
/** Unfinished tasks that list `touches` but no `repo`: their paths have no root, so touchOverlaps skips them. */
|
|
462
|
+
function touchesWithoutRepo(mainlines) {
|
|
463
|
+
return openTouching(mainlines).filter((p) => !p.task.repo).map((p) => ({
|
|
464
|
+
task: `${p.mainline}/${p.task.id}`,
|
|
465
|
+
file: p.task.file
|
|
466
|
+
}));
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Open tasks of different open mainlines whose `touches` overlap in the same repo and which have no depends-on
|
|
470
|
+
* between them in either direction, written on the mainline or on the task. Tasks without a repo are not compared,
|
|
471
|
+
* since their paths have no root to share; touchesWithoutRepo names them.
|
|
472
|
+
*/
|
|
473
|
+
function touchOverlaps(mainlines, edges) {
|
|
474
|
+
const open = openTouching(mainlines).filter((p) => p.task.repo);
|
|
475
|
+
const overlaps = [];
|
|
476
|
+
open.forEach((a, i) => {
|
|
477
|
+
for (const b of open.slice(i + 1)) {
|
|
478
|
+
if (a.mainline === b.mainline || a.task.repo !== b.task.repo) continue;
|
|
479
|
+
const paths = a.task.touches.flatMap((x) => b.task.touches.filter((y) => touchesOverlap(x, y)).map((y) => [x, y]));
|
|
480
|
+
if (paths.length === 0 || dependsOn(edges, a, b) || dependsOn(edges, b, a)) continue;
|
|
481
|
+
overlaps.push({
|
|
482
|
+
tasks: [`${a.mainline}/${a.task.id}`, `${b.mainline}/${b.task.id}`],
|
|
483
|
+
repo: a.task.repo,
|
|
484
|
+
paths,
|
|
485
|
+
file: a.task.file
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
return overlaps;
|
|
490
|
+
}
|
|
491
|
+
//#endregion
|
|
422
492
|
//#region ../core/dist/templates.js
|
|
423
493
|
const body = (headings) => ["", ...headings.flatMap((h) => [`## ${h}`, ""])].join("\n");
|
|
424
494
|
const MAINLINE_BODY = body(MAINLINE_SECTIONS);
|
|
@@ -656,11 +726,18 @@ function loadHub(root, options = {}) {
|
|
|
656
726
|
});
|
|
657
727
|
const hubIssues = hubDocs(join(dataDir, "issues"), issueSchema);
|
|
658
728
|
const taskRefs = new Set(mainlines.flatMap((m) => m.tasks.map((t) => `${m.id}/${t.id}`)));
|
|
659
|
-
for (const issue of hubIssues)
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
729
|
+
for (const issue of hubIssues) {
|
|
730
|
+
if (issue.plannedIn && !taskRefs.has(issue.plannedIn)) problems.push({
|
|
731
|
+
file: issue.file,
|
|
732
|
+
field: "plannedIn",
|
|
733
|
+
message: `task ${issue.plannedIn} does not exist`
|
|
734
|
+
});
|
|
735
|
+
if (issue.status !== "closed" && !issue.archivedAt && !issue.labels?.length) warnings.push({
|
|
736
|
+
file: issue.file,
|
|
737
|
+
field: "labels",
|
|
738
|
+
message: `issue ${issue.id} is ${issue.status} but has no labels; add labels so it can be found by topic`
|
|
739
|
+
});
|
|
740
|
+
}
|
|
664
741
|
const findings = hubDocs(join(dataDir, "findings"), findingSchema);
|
|
665
742
|
const ops = [];
|
|
666
743
|
const opsPath = join(dataDir, "ops");
|
|
@@ -705,6 +782,19 @@ function loadHub(root, options = {}) {
|
|
|
705
782
|
message: `dependency cycle ${cycle.join(" → ")}; drop one of these dependencies`
|
|
706
783
|
});
|
|
707
784
|
}
|
|
785
|
+
for (const o of touchOverlaps(mainlines, deps.edges)) {
|
|
786
|
+
const pairs = o.paths.map(([a, b]) => a === b ? a : `${a} ~ ${b}`).join(", ");
|
|
787
|
+
warnings.push({
|
|
788
|
+
file: o.file,
|
|
789
|
+
field: "touches",
|
|
790
|
+
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`
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
for (const t of touchesWithoutRepo(mainlines)) warnings.push({
|
|
794
|
+
file: t.file,
|
|
795
|
+
field: "repo",
|
|
796
|
+
message: `${t.task} lists touches but no repo, so its paths are not compared with other tasks; add the repo they are relative to`
|
|
797
|
+
});
|
|
708
798
|
return {
|
|
709
799
|
root,
|
|
710
800
|
config,
|
|
@@ -1026,7 +1116,8 @@ function createIssue(root, input) {
|
|
|
1026
1116
|
title: input.title,
|
|
1027
1117
|
status: "open",
|
|
1028
1118
|
created: today(),
|
|
1029
|
-
...input.origin ? { origin: input.origin } : {}
|
|
1119
|
+
...input.origin ? { origin: input.origin } : {},
|
|
1120
|
+
...input.labels?.length ? { labels: input.labels } : {}
|
|
1030
1121
|
}, "issue");
|
|
1031
1122
|
const file = join(dir, `${id}-${input.slug}.md`);
|
|
1032
1123
|
writeAtomic(file, frontMatterText(issue, ISSUE_BODY));
|
|
@@ -1247,14 +1338,14 @@ function grantScope(root, session) {
|
|
|
1247
1338
|
reason: `${session.id} 同時有 ${doing.length} 個進行中的行動(${doing.map((a) => a.id).join("、")}),分不出授權範圍`
|
|
1248
1339
|
};
|
|
1249
1340
|
const action = doing[0];
|
|
1250
|
-
const
|
|
1341
|
+
const mainlineOf = (ref) => {
|
|
1251
1342
|
const parsed = parseRef(ref);
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1343
|
+
return parsed ? refMainline(parsed) : void 0;
|
|
1344
|
+
};
|
|
1345
|
+
const mainlines = [...new Set(action.serves.flatMap((ref) => mainlineOf(ref) ?? []))];
|
|
1255
1346
|
if (mainlines.length === 0) return {
|
|
1256
1347
|
ok: false,
|
|
1257
|
-
reason: `${action.id} 沒有服務任何 checklist
|
|
1348
|
+
reason: `${action.id} 沒有服務任何 checklist 項目或解決條件`
|
|
1258
1349
|
};
|
|
1259
1350
|
const grants = [];
|
|
1260
1351
|
const served = {};
|
|
@@ -1265,7 +1356,7 @@ function grantScope(root, session) {
|
|
|
1265
1356
|
closed.push(`${id} 是 ${mainline.status}`);
|
|
1266
1357
|
continue;
|
|
1267
1358
|
}
|
|
1268
|
-
served[id] = action.serves.find((ref) => ref
|
|
1359
|
+
served[id] = action.serves.find((ref) => mainlineOf(ref) === id);
|
|
1269
1360
|
for (const grant of mainline.grants ?? []) grants.push({
|
|
1270
1361
|
mainline: id,
|
|
1271
1362
|
grant,
|
|
@@ -1433,23 +1524,30 @@ function resolveAction(root, ref) {
|
|
|
1433
1524
|
}
|
|
1434
1525
|
}
|
|
1435
1526
|
/**
|
|
1436
|
-
* Every served checklist item must exist, so a session never points at work that is not in a
|
|
1437
|
-
* mainline
|
|
1438
|
-
*
|
|
1527
|
+
* Every served checklist item or criterion must exist, so a session never points at work that is not in a
|
|
1528
|
+
* mainline. A criterion stands in while the mainline has no task for it yet, as when it is still being surveyed
|
|
1529
|
+
* and planned. Its mainline must still be open: follow-up work on an ended one either reopens it with a criterion
|
|
1530
|
+
* or task for the follow-up, or starts a new mainline, so the follow-up is tracked instead of hanging off finished work.
|
|
1439
1531
|
*/
|
|
1440
1532
|
function requireServes(root, serves) {
|
|
1441
1533
|
for (const ref of serves) {
|
|
1442
1534
|
const parsed = parseRef(ref);
|
|
1443
|
-
if (!
|
|
1535
|
+
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
1536
|
const mainline = readFrontMatter(mainlineFile(root, parsed.mainline), mainlineSchema).data;
|
|
1445
1537
|
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"`);
|
|
1538
|
+
if (parsed.kind === "criterion") {
|
|
1539
|
+
if (!mainline.criteria.some((r) => r.id === parsed.criterion)) throw new WriteError(`criterion ${ref} not found`);
|
|
1540
|
+
continue;
|
|
1541
|
+
}
|
|
1446
1542
|
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
1543
|
}
|
|
1448
1544
|
}
|
|
1449
1545
|
const isOpen$1 = (a) => a.status === "todo" || a.status === "doing";
|
|
1546
|
+
const isCarried$1 = (a) => isOpen$1(a) || a.status === "blocked";
|
|
1450
1547
|
/**
|
|
1451
1548
|
* The previous session's unfinished actions, numbered after `existing`, with the previous session marked
|
|
1452
|
-
* handed-off.
|
|
1549
|
+
* handed-off. Blocked actions go along with their `blockedBy`, since the wait is still on and whoever is
|
|
1550
|
+
* waited for keeps seeing it. Each carried action is dropped in the previous session with a note naming its new reference,
|
|
1453
1551
|
* so the work stays open in one place only and the old session still shows where it went. Runs inside the
|
|
1454
1552
|
* caller's lock.
|
|
1455
1553
|
*/
|
|
@@ -1460,7 +1558,7 @@ function takeUnfinished(root, from, to, existing) {
|
|
|
1460
1558
|
const carried = [];
|
|
1461
1559
|
const at = nowMinute();
|
|
1462
1560
|
const left = previous.actions.map((a) => {
|
|
1463
|
-
if (!
|
|
1561
|
+
if (!isCarried$1(a)) return a;
|
|
1464
1562
|
const id = nextId("a", ids);
|
|
1465
1563
|
ids.push(id);
|
|
1466
1564
|
carried.push({
|
|
@@ -1468,8 +1566,9 @@ function takeUnfinished(root, from, to, existing) {
|
|
|
1468
1566
|
id
|
|
1469
1567
|
});
|
|
1470
1568
|
const handedOff = handedOffNote(`${to}#${id}`);
|
|
1569
|
+
const { blockedBy: _moved, ...rest } = a;
|
|
1471
1570
|
return {
|
|
1472
|
-
...
|
|
1571
|
+
...rest,
|
|
1473
1572
|
status: "dropped",
|
|
1474
1573
|
note: a.note ? `${a.note}; ${handedOff}` : handedOff,
|
|
1475
1574
|
doneAt: at
|
|
@@ -1545,7 +1644,7 @@ function handoffSession(root, id, from) {
|
|
|
1545
1644
|
if (session.handoffFrom && session.handoffFrom !== from) throw new WriteError(`session ${id} already took over from ${session.handoffFrom}`);
|
|
1546
1645
|
const open = session.actions.filter(isOpen$1);
|
|
1547
1646
|
if (open.length > 0) throw new WriteError(`session ${id} has unfinished actions (${open.map((a) => a.id).join(", ")}); finish or drop them before taking over from ${from}`);
|
|
1548
|
-
if (!readYaml(requireSession(root, from), sessionSchema).data.actions.some(
|
|
1647
|
+
if (!readYaml(requireSession(root, from), sessionSchema).data.actions.some(isCarried$1)) throw new WriteError(`session ${from} has no unfinished actions to take over`);
|
|
1549
1648
|
const carried = takeUnfinished(root, from, id, session.actions.map((a) => a.id));
|
|
1550
1649
|
const next = validate(sessionSchema, {
|
|
1551
1650
|
...session,
|
|
@@ -1566,6 +1665,13 @@ function setSessionInfo(root, id, info) {
|
|
|
1566
1665
|
for (const [key, value] of Object.entries(info)) if (value !== void 0 && value !== null && value !== "") s[key] = value;
|
|
1567
1666
|
});
|
|
1568
1667
|
}
|
|
1668
|
+
/** Remember the digest of what the last turn reminder listed in full; null forgets it, so the next reminder lists everything again. */
|
|
1669
|
+
function setReminderDigest(root, id, digest) {
|
|
1670
|
+
mutateYaml(root, requireSession(root, id), sessionSchema, (s) => {
|
|
1671
|
+
if (digest === null) delete s.reminderDigest;
|
|
1672
|
+
else s.reminderDigest = digest;
|
|
1673
|
+
});
|
|
1674
|
+
}
|
|
1569
1675
|
function setSessionStatus(root, id, status) {
|
|
1570
1676
|
const value = requireOneOf(status, SESSION_STATUS, "session status");
|
|
1571
1677
|
return mutateYaml(root, requireSession(root, id), sessionSchema, (s) => {
|
|
@@ -1662,6 +1768,8 @@ function updateAction(root, ref, patch) {
|
|
|
1662
1768
|
const index = s.actions.findIndex((a) => a.id === action);
|
|
1663
1769
|
if (index < 0) throw new WriteError(`action ${ref} not found`);
|
|
1664
1770
|
const [current] = s.actions.splice(index, 1);
|
|
1771
|
+
const movedTo = HANDED_OFF.exec(current.note ?? "")?.[1];
|
|
1772
|
+
if (movedTo && current.status === "dropped" && status !== void 0 && status !== "dropped") throw new WriteError(`${ref} was handed off to ${movedTo}, and references to ${ref} follow its note only while it stays dropped; change ${movedTo} instead`);
|
|
1665
1773
|
const handedTo = patch.note ? HANDED_OFF.exec(current.note ?? "")?.[1] : void 0;
|
|
1666
1774
|
if (handedTo) throw new WriteError(`${ref} was handed off to ${handedTo}, and references to ${ref} follow its note there, so the note stays as it is; add the note to ${handedTo} instead`);
|
|
1667
1775
|
const next = {
|
|
@@ -1695,9 +1803,18 @@ function updateAction(root, ref, patch) {
|
|
|
1695
1803
|
//#region ../core/dist/briefing.js
|
|
1696
1804
|
function describe(root, ref) {
|
|
1697
1805
|
const parsed = parseRef(ref);
|
|
1698
|
-
if (parsed?.kind !== "checklistItem") return null;
|
|
1806
|
+
if (parsed?.kind !== "checklistItem" && parsed?.kind !== "criterion") return null;
|
|
1699
1807
|
try {
|
|
1700
1808
|
const mainline = readFrontMatter(mainlineFile(root, parsed.mainline), mainlineSchema).data;
|
|
1809
|
+
if (parsed.kind === "criterion") {
|
|
1810
|
+
const criterion = mainline.criteria.find((r) => r.id === parsed.criterion);
|
|
1811
|
+
return criterion ? {
|
|
1812
|
+
ref,
|
|
1813
|
+
text: criterion.text,
|
|
1814
|
+
checked: criterion.done,
|
|
1815
|
+
where: `${mainline.id} ${mainline.title} › 解決條件`
|
|
1816
|
+
} : null;
|
|
1817
|
+
}
|
|
1701
1818
|
const task = readFrontMatter(taskFile(root, parsed.mainline, parsed.task), taskSchema).data;
|
|
1702
1819
|
const item = task.checklist.find((c) => c.id === parsed.item);
|
|
1703
1820
|
if (!item) return null;
|
|
@@ -1711,6 +1828,32 @@ function describe(root, ref) {
|
|
|
1711
1828
|
return null;
|
|
1712
1829
|
}
|
|
1713
1830
|
}
|
|
1831
|
+
/** The criteria a served checklist item pushes forward, as m{NNN}#r{NNN}; empty for a criterion or an item that cannot be read. */
|
|
1832
|
+
function criteriaOf(root, ref) {
|
|
1833
|
+
const parsed = parseRef(ref);
|
|
1834
|
+
if (parsed?.kind !== "checklistItem") return [];
|
|
1835
|
+
try {
|
|
1836
|
+
return (readFrontMatter(taskFile(root, parsed.mainline, parsed.task), taskSchema).data.checklist.find((c) => c.id === parsed.item)?.serves ?? []).map((r) => `${parsed.mainline}#${r}`);
|
|
1837
|
+
} catch {
|
|
1838
|
+
return [];
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
/**
|
|
1842
|
+
* Where two served lists meet: a reference both serve, or a criterion one serves that a checklist item the other
|
|
1843
|
+
* serves pushes forward, named as the item with its criterion. Two items serving the same criterion do not meet,
|
|
1844
|
+
* since splitting a criterion into items is how work on it runs in parallel.
|
|
1845
|
+
*/
|
|
1846
|
+
function sharedServes(root, mine, theirs) {
|
|
1847
|
+
const shared = mine.filter((ref) => theirs.includes(ref));
|
|
1848
|
+
const pushes = (items, criteria) => items.flatMap((item) => criteriaOf(root, item).filter((r) => criteria.includes(r)).map((r) => `${item}(推進 ${r})`));
|
|
1849
|
+
const mineCriteria = mine.filter((ref) => parseRef(ref)?.kind === "criterion");
|
|
1850
|
+
const theirCriteria = theirs.filter((ref) => parseRef(ref)?.kind === "criterion");
|
|
1851
|
+
return [.../* @__PURE__ */ new Set([
|
|
1852
|
+
...shared,
|
|
1853
|
+
...pushes(theirs, mineCriteria),
|
|
1854
|
+
...pushes(mine, theirCriteria)
|
|
1855
|
+
])];
|
|
1856
|
+
}
|
|
1714
1857
|
/** Problems in a session's queue that the executor should fix before going on, each with the command that fixes it. */
|
|
1715
1858
|
function queueWarnings(root, session, cli) {
|
|
1716
1859
|
const warnings = [];
|
|
@@ -1718,8 +1861,8 @@ function queueWarnings(root, session, cli) {
|
|
|
1718
1861
|
if (doing.length > 1) warnings.push(`同時有 ${doing.length} 個進行中的行動(${doing.map((a) => a.id).join("、")})。一次只推進一個,其餘改回 todo。`);
|
|
1719
1862
|
for (const action of doing) {
|
|
1720
1863
|
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}
|
|
1864
|
+
if (served.some((s) => s === null)) warnings.push(`${action.id} 服務的項目有的已不存在,用 ${cli} action set ${action.id} --serves <m/t#c 或 m#r> 改正。`);
|
|
1865
|
+
else if (served.length > 0 && served.every((s) => s.checked)) warnings.push(`${action.id} 服務的項目都已勾選或達成。做完了就執行 ${cli} action set ${action.id} --status done。`);
|
|
1723
1866
|
}
|
|
1724
1867
|
const next = session.actions.find((a) => a.status === "todo");
|
|
1725
1868
|
if (doing.length === 0 && next) warnings.push(`下一個行動是 ${next.id}。開始前先執行 ${cli} action set ${next.id} --status doing。`);
|
|
@@ -1727,6 +1870,8 @@ function queueWarnings(root, session, cli) {
|
|
|
1727
1870
|
}
|
|
1728
1871
|
const current = (session) => session.actions.find((a) => a.status === "doing");
|
|
1729
1872
|
const isOpen = (a) => a.status === "todo" || a.status === "doing";
|
|
1873
|
+
/** What a handoff takes over: open actions and blocked ones, which are still unfinished. */
|
|
1874
|
+
const isCarried = (a) => isOpen(a) || a.status === "blocked";
|
|
1730
1875
|
/** A blocked action and what it waits for: the reason, the other party and the condition, as `action set --status blocked` recorded them. */
|
|
1731
1876
|
function waitingLine(a) {
|
|
1732
1877
|
const block = a.blockedBy;
|
|
@@ -1794,8 +1939,8 @@ function waiterLines(root, session) {
|
|
|
1794
1939
|
return lines;
|
|
1795
1940
|
}
|
|
1796
1941
|
/**
|
|
1797
|
-
* One reminder for each action in progress in another session that may still be running and serves
|
|
1798
|
-
*
|
|
1942
|
+
* One reminder for each action in progress in another session that may still be running and serves what this
|
|
1943
|
+
* 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
1944
|
* its parent handed it is not a second claim.
|
|
1800
1945
|
*/
|
|
1801
1946
|
function duplicateClaimLines(root, sessionId, actionId) {
|
|
@@ -1807,7 +1952,7 @@ function duplicateClaimLines(root, sessionId, actionId) {
|
|
|
1807
1952
|
for (const other of sessions) {
|
|
1808
1953
|
if (related.has(other.id) || !mayBeRunning(root, other)) continue;
|
|
1809
1954
|
for (const theirs of other.actions.filter((a) => a.status === "doing")) {
|
|
1810
|
-
const shared =
|
|
1955
|
+
const shared = sharedServes(root, action.serves, theirs.serves);
|
|
1811
1956
|
if (shared.length > 0) lines.push(`⚠ ${other.id}#${theirs.id}(15 分鐘內有活動)也在做 ${shared.join("、")},先協調再開始`);
|
|
1812
1957
|
}
|
|
1813
1958
|
}
|
|
@@ -1818,14 +1963,32 @@ function duplicateClaimLines(root, sessionId, actionId) {
|
|
|
1818
1963
|
* over into this one. Subagent sessions are left out: their work returns to the session that delegated it.
|
|
1819
1964
|
*/
|
|
1820
1965
|
function handoffLines(root, session, cli) {
|
|
1821
|
-
const others = loadSessions(root).sessions.filter((s) => s.id !== session.id && !s.delegatedFrom && !s.archivedAt && (s.status === "active" || s.status === "paused") && s.actions.some(
|
|
1966
|
+
const others = loadSessions(root).sessions.filter((s) => s.id !== session.id && !s.delegatedFrom && !s.archivedAt && (s.status === "active" || s.status === "paused") && s.actions.some(isCarried));
|
|
1822
1967
|
if (others.length === 0) return [];
|
|
1823
|
-
return [`其他會話還有沒做完的行動。要接續的是其中一場已經停下的工作時,先用 ${cli} session handoff <會話> 把它的行動接進這場會話,再開始做:`, ...others.map((s) => `- ${s.id}${mayBeRunning(root, s) ? "(15 分鐘內還有寫入對話紀錄,可能還在執行,不要接手)" : ""}:${s.actions.filter(
|
|
1968
|
+
return [`其他會話還有沒做完的行動。要接續的是其中一場已經停下的工作時,先用 ${cli} session handoff <會話> 把它的行動接進這場會話,再開始做:`, ...others.map((s) => `- ${s.id}${mayBeRunning(root, s) ? "(15 分鐘內還有寫入對話紀錄,可能還在執行,不要接手)" : ""}:${s.actions.filter(isCarried).map((a) => `${a.id} ${a.do}(${a.status},服務 ${a.serves.join("、") || "未指定"})`).join(";")}`)];
|
|
1824
1969
|
}
|
|
1825
1970
|
const servedLine = (root, action) => action.serves.map((ref) => {
|
|
1826
1971
|
const s = describe(root, ref);
|
|
1827
|
-
return s ? `${ref} ${s.text}(${s.where})` : `${ref}
|
|
1828
|
-
}).join(";") || "沒有指定服務的 checklist
|
|
1972
|
+
return s ? `${ref} ${s.text}(${s.where})` : `${ref}(找不到這個項目或條件)`;
|
|
1973
|
+
}).join(";") || "沒有指定服務的 checklist 項目或解決條件";
|
|
1974
|
+
/**
|
|
1975
|
+
* Lines for each pair of overlapping touches that involves a mainline an action in progress serves: the other
|
|
1976
|
+
* mainline and task, and the paths they share. Empty when the session serves nothing that overlaps.
|
|
1977
|
+
*/
|
|
1978
|
+
function overlapLines(root, session) {
|
|
1979
|
+
const mine = new Set(session.actions.filter((a) => a.status === "doing").flatMap((a) => a.serves.map((ref) => parseRef(ref))).flatMap((parsed) => parsed && "mainline" in parsed ? [parsed.mainline] : []));
|
|
1980
|
+
if (mine.size === 0) return [];
|
|
1981
|
+
const hub = loadHub(root, { remotes: false });
|
|
1982
|
+
const lines = [];
|
|
1983
|
+
for (const o of touchOverlaps(hub.mainlines, dependencyGraph(hub).edges)) {
|
|
1984
|
+
const [own, other] = mine.has(o.tasks[0].split("/")[0]) ? [0, 1] : mine.has(o.tasks[1].split("/")[0]) ? [1, 0] : [-1, -1];
|
|
1985
|
+
if (own < 0) continue;
|
|
1986
|
+
const paths = o.paths.map(([a, b]) => own === 0 ? [a, b] : [b, a]).map(([a, b]) => a === b ? a : `${a} ~ ${b}`).join("、");
|
|
1987
|
+
const title = hub.mainlines.find((m) => m.id === o.tasks[other].split("/")[0])?.title;
|
|
1988
|
+
lines.push(`- ${o.tasks[own]} 和 ${o.tasks[other]}${title ? `(${title})` : ""} 都會動到 ${paths}(倉庫 ${o.repo}),兩邊沒有 depends-on`);
|
|
1989
|
+
}
|
|
1990
|
+
return lines;
|
|
1991
|
+
}
|
|
1829
1992
|
/** Full briefing for the start of a session, after resuming, clearing or compacting. */
|
|
1830
1993
|
function sessionBriefing(root, session, options) {
|
|
1831
1994
|
const { cli } = options;
|
|
@@ -1850,13 +2013,18 @@ function sessionBriefing(root, session, options) {
|
|
|
1850
2013
|
if (blocked.length > 0) lines.push(`等外部條件:${blocked.map(waitingLine).join(";")}`);
|
|
1851
2014
|
}
|
|
1852
2015
|
lines.push(...waiterLines(root, session));
|
|
2016
|
+
const overlaps = overlapLines(root, session);
|
|
2017
|
+
if (overlaps.length > 0) lines.push("目前行動服務的主脈絡和其他進行中的主脈絡預計動到同樣的路徑,開始改之前先和對方協調,或加 depends-on:", ...overlaps);
|
|
1853
2018
|
if (!session.actions.some(isOpen)) lines.push(...handoffLines(root, session, cli));
|
|
1854
2019
|
const warnings = queueWarnings(root, session, cli);
|
|
1855
2020
|
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,它依序帶過並告訴你資訊該寫在哪裡。");
|
|
2021
|
+
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
2022
|
return lines.join("\n");
|
|
1858
2023
|
}
|
|
1859
|
-
/**
|
|
2024
|
+
/**
|
|
2025
|
+
* One or two lines for every user turn; null when the session has no actions and the hub has no open mainline.
|
|
2026
|
+
* Records a digest of the grants, waits and warnings it lists in the session file, only when the digest changes.
|
|
2027
|
+
*/
|
|
1860
2028
|
function turnReminder(root, session, options) {
|
|
1861
2029
|
if (session.actions.length === 0) return openMainlines(root).length > 0 ? `[mainflow] ${session.id} 還沒有行動。這一輪要推進主脈絡時,先用 ${options.cli} action add 列出行動並標為 doing,或用 ${options.cli} session handoff <會話> 接手上一場沒做完的行動,再開始做。回覆結尾的標頭只看這場會話進行中的行動。` : null;
|
|
1862
2030
|
const doing = current(session);
|
|
@@ -1866,13 +2034,28 @@ function turnReminder(root, session, options) {
|
|
|
1866
2034
|
const header = headerLine(session);
|
|
1867
2035
|
const marker = header ? [`- 這一輪最後一則回覆的最後一行寫「${header}」,回覆範例等其他內容都放在它前面。有顯示 hook 時,它會換成標題與連結。`] : [];
|
|
1868
2036
|
const grants = grantLines(root, session);
|
|
1869
|
-
const
|
|
2037
|
+
const waiters = waiterLines(root, session);
|
|
2038
|
+
const warnings = queueWarnings(root, session, options.cli);
|
|
2039
|
+
const digest = contentHash(JSON.stringify([
|
|
2040
|
+
doing?.id ?? null,
|
|
2041
|
+
grants,
|
|
2042
|
+
waiters,
|
|
2043
|
+
warnings
|
|
2044
|
+
]));
|
|
2045
|
+
const repeated = session.reminderDigest === digest;
|
|
2046
|
+
if (!repeated && (session.reminderDigest !== void 0 || grants.length + waiters.length + warnings.length > 0) && readSession(root, session.id)) try {
|
|
2047
|
+
setReminderDigest(root, session.id, digest);
|
|
2048
|
+
} catch {}
|
|
2049
|
+
const once = (lines, summary, full) => lines.length === 0 ? [] : repeated ? [summary] : full;
|
|
2050
|
+
const granted = once(grants, `- 預先授權同上一輪,共 ${grants.length} 條。`, ["- 目前行動服務的主脈絡預先授權了這些動作:", ...grants.map((g) => ` ${g}`)]);
|
|
2051
|
+
const waiting = once(waiters, `- 等待行同上一輪,共 ${waiters.length} 條。`, waiters.map((w) => `- ${w}`));
|
|
2052
|
+
const warned = once(warnings, `- 佇列提醒同上一輪,共 ${warnings.length} 條。`, warnings.map((w) => `- ${w}`));
|
|
1870
2053
|
return [
|
|
1871
2054
|
head,
|
|
1872
|
-
...
|
|
2055
|
+
...waiting,
|
|
1873
2056
|
...granted,
|
|
1874
2057
|
...marker,
|
|
1875
|
-
...
|
|
2058
|
+
...warned
|
|
1876
2059
|
].join("\n");
|
|
1877
2060
|
}
|
|
1878
2061
|
//#endregion
|
|
@@ -2096,7 +2279,7 @@ function setCriterion(root, refInput, done, evidence, expectedVersion) {
|
|
|
2096
2279
|
return mutateFrontMatter(root, mainlineFile(root, ref.mainline), mainlineSchema, (m) => {
|
|
2097
2280
|
const criterion = m.criteria.find((c) => c.id === ref.criterion);
|
|
2098
2281
|
if (!criterion) throw new WriteError(`criterion ${refInput} not found`);
|
|
2099
|
-
setListItem(criterion, done, evidence);
|
|
2282
|
+
setListItem(criterion, done, evidence, `${refInput} is dropped; remove dropped and supersededBy in ${MAINLINE_FILE} to bring it back before checking it`);
|
|
2100
2283
|
}, expectedVersion);
|
|
2101
2284
|
}
|
|
2102
2285
|
/** Rewrite a criterion's text or doneWhen, leaving done and evidence as they are. */
|
|
@@ -2110,14 +2293,17 @@ function setCriterionText(root, refInput, patch, expectedVersion) {
|
|
|
2110
2293
|
if (patch.doneWhen !== void 0) criterion.doneWhen = patch.doneWhen;
|
|
2111
2294
|
}, expectedVersion);
|
|
2112
2295
|
}
|
|
2113
|
-
/**
|
|
2114
|
-
|
|
2296
|
+
/**
|
|
2297
|
+
* Check or uncheck a criterion or checklist item. A dropped item cannot be checked, because a drop is a
|
|
2298
|
+
* ruling that the item is no longer needed; `droppedHint` says how to bring it back. Neither direction
|
|
2299
|
+
* touches dropped or supersededBy.
|
|
2300
|
+
*/
|
|
2301
|
+
function setListItem(item, done, evidence, droppedHint) {
|
|
2302
|
+
if (done && item.dropped) throw new WriteError(droppedHint);
|
|
2115
2303
|
item.done = done;
|
|
2116
2304
|
if (done) {
|
|
2117
2305
|
item.evidence = evidence;
|
|
2118
2306
|
item.doneAt = today();
|
|
2119
|
-
delete item.dropped;
|
|
2120
|
-
delete item.supersededBy;
|
|
2121
2307
|
} else {
|
|
2122
2308
|
delete item.evidence;
|
|
2123
2309
|
delete item.doneAt;
|
|
@@ -2192,7 +2378,7 @@ function setChecklistItem(root, refInput, done, evidence, expectedVersion) {
|
|
|
2192
2378
|
return mutateFrontMatter(root, taskFile(root, ref.mainline, ref.task), taskSchema, (t) => {
|
|
2193
2379
|
const item = t.checklist.find((c) => c.id === ref.item);
|
|
2194
2380
|
if (!item) throw new WriteError(`checklist item ${refInput} not found`);
|
|
2195
|
-
setListItem(item, done, evidence);
|
|
2381
|
+
setListItem(item, done, evidence, `${refInput} is dropped; run item undrop ${refInput} to bring it back before checking it`);
|
|
2196
2382
|
}, expectedVersion);
|
|
2197
2383
|
}
|
|
2198
2384
|
/** Mark an item as no longer needed, naming what replaced it when something did. */
|
|
@@ -2207,6 +2393,20 @@ function dropChecklistItem(root, refInput, input = {}, expectedVersion) {
|
|
|
2207
2393
|
if (input.note) item.note = input.note;
|
|
2208
2394
|
}, expectedVersion);
|
|
2209
2395
|
}
|
|
2396
|
+
/** Bring a dropped item back as an open one: clears dropped and supersededBy, keeps the note. */
|
|
2397
|
+
function undropChecklistItem(root, refInput, expectedVersion) {
|
|
2398
|
+
const ref = requireRef(refInput, "checklistItem");
|
|
2399
|
+
return mutateFrontMatter(root, taskFile(root, ref.mainline, ref.task), taskSchema, (t) => {
|
|
2400
|
+
const item = t.checklist.find((c) => c.id === ref.item);
|
|
2401
|
+
if (!item) throw new WriteError(`checklist item ${refInput} not found`);
|
|
2402
|
+
if (!item.dropped) throw new WriteError(`${refInput} is not dropped`);
|
|
2403
|
+
delete item.dropped;
|
|
2404
|
+
delete item.supersededBy;
|
|
2405
|
+
item.done = false;
|
|
2406
|
+
delete item.evidence;
|
|
2407
|
+
delete item.doneAt;
|
|
2408
|
+
}, expectedVersion);
|
|
2409
|
+
}
|
|
2210
2410
|
function createDecision(root, mainline, input) {
|
|
2211
2411
|
requireRef(mainline, "mainline");
|
|
2212
2412
|
requireSlug(input.slug);
|
|
@@ -2314,6 +2514,474 @@ function setArchivedMany(root, kind, ids, archived) {
|
|
|
2314
2514
|
return result;
|
|
2315
2515
|
}
|
|
2316
2516
|
//#endregion
|
|
2517
|
+
//#region ../core/dist/retro.js
|
|
2518
|
+
/** Where the prices come from and when they were read; every figure is USD per million tokens. */
|
|
2519
|
+
const PRICING_SOURCE = {
|
|
2520
|
+
url: "https://platform.claude.com/docs/en/about-claude/pricing",
|
|
2521
|
+
date: "2026-09-30",
|
|
2522
|
+
unit: "USD/MTok"
|
|
2523
|
+
};
|
|
2524
|
+
const price = (input, cacheWrite5m, cacheWrite1h, cacheRead, output) => ({
|
|
2525
|
+
input,
|
|
2526
|
+
cacheWrite5m,
|
|
2527
|
+
cacheWrite1h,
|
|
2528
|
+
cacheRead,
|
|
2529
|
+
output
|
|
2530
|
+
});
|
|
2531
|
+
/**
|
|
2532
|
+
* List prices by model id prefix. A model matches a prefix when the rest of its id is empty, a date suffix
|
|
2533
|
+
* (-20251001) or a bracketed variant ([1m]); any other model has no known price, so its cost is not guessed.
|
|
2534
|
+
*/
|
|
2535
|
+
const MODEL_PRICES = [
|
|
2536
|
+
{
|
|
2537
|
+
prefix: "claude-opus-5-5",
|
|
2538
|
+
price: price(4, 5, 8, .2, 20)
|
|
2539
|
+
},
|
|
2540
|
+
{
|
|
2541
|
+
prefix: "claude-opus-5",
|
|
2542
|
+
price: price(5, 6.25, 10, .5, 25)
|
|
2543
|
+
},
|
|
2544
|
+
{
|
|
2545
|
+
prefix: "claude-opus-4-8",
|
|
2546
|
+
price: price(5, 6.25, 10, .5, 25)
|
|
2547
|
+
},
|
|
2548
|
+
{
|
|
2549
|
+
prefix: "claude-sonnet-5-5",
|
|
2550
|
+
price: price(2, 2.5, 4, .2, 10)
|
|
2551
|
+
},
|
|
2552
|
+
{
|
|
2553
|
+
prefix: "claude-sonnet-5",
|
|
2554
|
+
price: price(2, 2.5, 4, .2, 10)
|
|
2555
|
+
},
|
|
2556
|
+
{
|
|
2557
|
+
prefix: "claude-sonnet-4-6",
|
|
2558
|
+
price: price(3, 3.75, 6, .3, 15)
|
|
2559
|
+
},
|
|
2560
|
+
{
|
|
2561
|
+
prefix: "claude-haiku-4-5",
|
|
2562
|
+
price: price(1, 1.25, 2, .1, 5)
|
|
2563
|
+
},
|
|
2564
|
+
{
|
|
2565
|
+
prefix: "claude-fable-5-1",
|
|
2566
|
+
price: price(10, 12.5, 20, .25, 50)
|
|
2567
|
+
}
|
|
2568
|
+
];
|
|
2569
|
+
/**
|
|
2570
|
+
* Prices at speed "fast", keyed by the MODEL_PRICES prefix. The cache multipliers stack on the fast input
|
|
2571
|
+
* price (5m write 1.25x, 1h write 2x, read 0.05x for Opus 5.5 and 0.1x otherwise), as with standard speed.
|
|
2572
|
+
*/
|
|
2573
|
+
const FAST_PRICES = {
|
|
2574
|
+
"claude-opus-5-5": price(8, 10, 16, .4, 40),
|
|
2575
|
+
"claude-opus-5": price(10, 12.5, 20, 1, 50),
|
|
2576
|
+
"claude-opus-4-8": price(10, 12.5, 20, 1, 50)
|
|
2577
|
+
};
|
|
2578
|
+
/** Inference kept in the US (inference_geo "us") is billed at 1.1 times the list price. */
|
|
2579
|
+
const US_GEO_MULTIPLIER = 1.1;
|
|
2580
|
+
const PRICE_SUFFIX = /^(?:-\d{8})?(?:\[[^\]]*\])?$/;
|
|
2581
|
+
/** The price entry a model id falls under: the longest prefix whose remainder is only a date or a variant. */
|
|
2582
|
+
function priceEntry(model) {
|
|
2583
|
+
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];
|
|
2584
|
+
}
|
|
2585
|
+
const priceOf = (model) => priceEntry(model)?.price;
|
|
2586
|
+
const emptyUsage = () => ({
|
|
2587
|
+
input: 0,
|
|
2588
|
+
cacheWrite5m: 0,
|
|
2589
|
+
cacheWrite1h: 0,
|
|
2590
|
+
cacheRead: 0,
|
|
2591
|
+
output: 0
|
|
2592
|
+
});
|
|
2593
|
+
const addUsage = (to, from) => {
|
|
2594
|
+
for (const key of Object.keys(to)) to[key] += from[key];
|
|
2595
|
+
};
|
|
2596
|
+
/** Everything the model read before answering: fresh input, cache writes and cache reads. */
|
|
2597
|
+
const contextTokens = (u) => u.input + u.cacheWrite5m + u.cacheWrite1h + u.cacheRead;
|
|
2598
|
+
/** Share of the context served from the cache; null when there was no context at all. */
|
|
2599
|
+
const cacheHitRate = (u) => contextTokens(u) > 0 ? u.cacheRead / contextTokens(u) : null;
|
|
2600
|
+
const num = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : 0;
|
|
2601
|
+
/** Read message.usage; cache writes without the 5m and 1h split count as 5m, the default cache lifetime. */
|
|
2602
|
+
function readUsage(raw) {
|
|
2603
|
+
const created = num(raw.cache_creation_input_tokens);
|
|
2604
|
+
const split = raw.cache_creation ?? {};
|
|
2605
|
+
const w1h = num(split.ephemeral_1h_input_tokens);
|
|
2606
|
+
const w5m = split.ephemeral_5m_input_tokens === void 0 ? created - w1h : num(split.ephemeral_5m_input_tokens);
|
|
2607
|
+
return {
|
|
2608
|
+
input: num(raw.input_tokens),
|
|
2609
|
+
cacheWrite5m: Math.max(0, w5m),
|
|
2610
|
+
cacheWrite1h: w1h,
|
|
2611
|
+
cacheRead: num(raw.cache_read_input_tokens),
|
|
2612
|
+
output: num(raw.output_tokens)
|
|
2613
|
+
};
|
|
2614
|
+
}
|
|
2615
|
+
/** The estimated cost of one message in USD, or null when the model has no known price. */
|
|
2616
|
+
function messageCost(model, usage, speed, geo) {
|
|
2617
|
+
const entry = priceEntry(model);
|
|
2618
|
+
if (!entry) return null;
|
|
2619
|
+
const p = (speed === "fast" ? FAST_PRICES[entry.prefix] : void 0) ?? entry.price;
|
|
2620
|
+
const usd = (usage.input * p.input + usage.cacheWrite5m * p.cacheWrite5m + usage.cacheWrite1h * p.cacheWrite1h + usage.cacheRead * p.cacheRead + usage.output * p.output) / 1e6;
|
|
2621
|
+
return geo === "us" ? usd * US_GEO_MULTIPLIER : usd;
|
|
2622
|
+
}
|
|
2623
|
+
const isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
2624
|
+
const blocks = (content) => Array.isArray(content) ? content.filter(isObject) : [];
|
|
2625
|
+
/** The text a hook attachment hands to the model, empty when it hands none. */
|
|
2626
|
+
function hookText(a) {
|
|
2627
|
+
const parts = [];
|
|
2628
|
+
if (typeof a.content === "string") parts.push(a.content);
|
|
2629
|
+
if (Array.isArray(a.content)) parts.push(...a.content.filter((c) => typeof c === "string"));
|
|
2630
|
+
const blocking = a.blockingError;
|
|
2631
|
+
if (typeof blocking === "string") parts.push(blocking);
|
|
2632
|
+
if (isObject(blocking) && typeof blocking.blockingError === "string") parts.push(blocking.blockingError);
|
|
2633
|
+
return parts.join("");
|
|
2634
|
+
}
|
|
2635
|
+
const hookEvent = (a) => typeof a.hookEvent === "string" && a.hookEvent ? a.hookEvent : typeof a.hookName === "string" && a.hookName ? a.hookName.split(":")[0] : "unknown";
|
|
2636
|
+
const emptyStats = (file, found) => ({
|
|
2637
|
+
file,
|
|
2638
|
+
found,
|
|
2639
|
+
versions: [],
|
|
2640
|
+
lines: 0,
|
|
2641
|
+
unreadable: 0,
|
|
2642
|
+
messages: [],
|
|
2643
|
+
toolUses: 0,
|
|
2644
|
+
toolErrors: {},
|
|
2645
|
+
bashRuns: {},
|
|
2646
|
+
hooks: [],
|
|
2647
|
+
misfires: {},
|
|
2648
|
+
gaps: []
|
|
2649
|
+
});
|
|
2650
|
+
function readTranscript(file) {
|
|
2651
|
+
const stats = emptyStats(file, existsSync(file));
|
|
2652
|
+
if (!stats.found) return stats;
|
|
2653
|
+
const versions = /* @__PURE__ */ new Set();
|
|
2654
|
+
const byId = /* @__PURE__ */ new Map();
|
|
2655
|
+
const toolNames = /* @__PURE__ */ new Map();
|
|
2656
|
+
const hooks = /* @__PURE__ */ new Map();
|
|
2657
|
+
/** Injections still waiting for the next assistant message, with the message they came after */
|
|
2658
|
+
let pending = [];
|
|
2659
|
+
const bound = [];
|
|
2660
|
+
let lastAssistant;
|
|
2661
|
+
let lastAt;
|
|
2662
|
+
let lastAtText = "";
|
|
2663
|
+
let anonymous = 0;
|
|
2664
|
+
for (const text of readFileSync(file, "utf8").split("\n")) {
|
|
2665
|
+
if (!text.trim()) continue;
|
|
2666
|
+
stats.lines += 1;
|
|
2667
|
+
let line;
|
|
2668
|
+
try {
|
|
2669
|
+
const parsed = JSON.parse(text);
|
|
2670
|
+
if (!isObject(parsed)) throw new Error("not an object");
|
|
2671
|
+
line = parsed;
|
|
2672
|
+
} catch {
|
|
2673
|
+
stats.unreadable += 1;
|
|
2674
|
+
continue;
|
|
2675
|
+
}
|
|
2676
|
+
if (typeof line.version === "string") versions.add(line.version);
|
|
2677
|
+
const at = typeof line.timestamp === "string" ? Date.parse(line.timestamp) : NaN;
|
|
2678
|
+
if (!Number.isNaN(at)) {
|
|
2679
|
+
if (lastAt !== void 0 && at - lastAt > 3e5) {
|
|
2680
|
+
const turnEnded = lastAssistant !== void 0 && byId.get(lastAssistant)?.toolUses === 0;
|
|
2681
|
+
stats.gaps.push({
|
|
2682
|
+
from: lastAtText,
|
|
2683
|
+
to: line.timestamp,
|
|
2684
|
+
minutes: Math.round((at - lastAt) / 6e4),
|
|
2685
|
+
waitingForUser: turnEnded
|
|
2686
|
+
});
|
|
2687
|
+
}
|
|
2688
|
+
if (lastAt === void 0 || at > lastAt) {
|
|
2689
|
+
lastAt = at;
|
|
2690
|
+
lastAtText = line.timestamp;
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
if (line.type === "assistant") {
|
|
2694
|
+
const m = line.message;
|
|
2695
|
+
if (!isObject(m)) {
|
|
2696
|
+
stats.unreadable += 1;
|
|
2697
|
+
continue;
|
|
2698
|
+
}
|
|
2699
|
+
const id = typeof m.id === "string" && m.id ? m.id : `(no id ${++anonymous})`;
|
|
2700
|
+
let message = byId.get(id);
|
|
2701
|
+
if (!message) {
|
|
2702
|
+
message = {
|
|
2703
|
+
id,
|
|
2704
|
+
model: typeof m.model === "string" ? m.model : "unknown",
|
|
2705
|
+
usage: emptyUsage(),
|
|
2706
|
+
toolUses: 0,
|
|
2707
|
+
...typeof line.timestamp === "string" ? { at: line.timestamp } : {}
|
|
2708
|
+
};
|
|
2709
|
+
byId.set(id, message);
|
|
2710
|
+
stats.messages.push(message);
|
|
2711
|
+
}
|
|
2712
|
+
if (isObject(m.usage)) {
|
|
2713
|
+
const usage = readUsage(m.usage);
|
|
2714
|
+
for (const key of Object.keys(usage)) message.usage[key] = Math.max(message.usage[key], usage[key]);
|
|
2715
|
+
if (typeof m.usage.speed === "string") message.speed = m.usage.speed;
|
|
2716
|
+
if (typeof m.usage.inference_geo === "string") message.geo = m.usage.inference_geo;
|
|
2717
|
+
}
|
|
2718
|
+
for (const block of blocks(m.content)) {
|
|
2719
|
+
if (block.type !== "tool_use") continue;
|
|
2720
|
+
message.toolUses += 1;
|
|
2721
|
+
stats.toolUses += 1;
|
|
2722
|
+
const name = typeof block.name === "string" ? block.name : "unknown";
|
|
2723
|
+
if (typeof block.id === "string") toolNames.set(block.id, name);
|
|
2724
|
+
const input = isObject(block.input) ? block.input : {};
|
|
2725
|
+
if (name === "Bash" && typeof input.command === "string") {
|
|
2726
|
+
const command = input.command.trim();
|
|
2727
|
+
stats.bashRuns[command] = (stats.bashRuns[command] ?? 0) + 1;
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
if (id !== lastAssistant) {
|
|
2731
|
+
for (const p of pending.filter((x) => x.after !== id)) bound.push({
|
|
2732
|
+
event: p.event,
|
|
2733
|
+
message: id
|
|
2734
|
+
});
|
|
2735
|
+
pending = pending.filter((x) => x.after === id);
|
|
2736
|
+
}
|
|
2737
|
+
lastAssistant = id;
|
|
2738
|
+
continue;
|
|
2739
|
+
}
|
|
2740
|
+
if (line.type === "user" && line.message !== void 0) {
|
|
2741
|
+
for (const block of blocks(line.message?.content)) {
|
|
2742
|
+
if (block.type !== "tool_result" || block.is_error !== true) continue;
|
|
2743
|
+
const name = typeof block.tool_use_id === "string" && toolNames.get(block.tool_use_id) || "unknown";
|
|
2744
|
+
stats.toolErrors[name] = (stats.toolErrors[name] ?? 0) + 1;
|
|
2745
|
+
}
|
|
2746
|
+
continue;
|
|
2747
|
+
}
|
|
2748
|
+
if (line.type === "attachment") {
|
|
2749
|
+
const a = line.attachment;
|
|
2750
|
+
if (!isObject(a)) {
|
|
2751
|
+
stats.unreadable += 1;
|
|
2752
|
+
continue;
|
|
2753
|
+
}
|
|
2754
|
+
const kind = typeof a.type === "string" ? a.type : "";
|
|
2755
|
+
if (!kind.includes("hook")) continue;
|
|
2756
|
+
const event = hookEvent(a);
|
|
2757
|
+
const hook = hooks.get(event) ?? {
|
|
2758
|
+
event,
|
|
2759
|
+
injections: 0,
|
|
2760
|
+
chars: 0,
|
|
2761
|
+
blocked: 0
|
|
2762
|
+
};
|
|
2763
|
+
hooks.set(event, hook);
|
|
2764
|
+
if (kind.includes("blocking")) hook.blocked += 1;
|
|
2765
|
+
const injected = hookText(a);
|
|
2766
|
+
if (injected.length > 0) {
|
|
2767
|
+
hook.injections += 1;
|
|
2768
|
+
hook.chars += [...injected].length;
|
|
2769
|
+
pending.push({
|
|
2770
|
+
event,
|
|
2771
|
+
after: lastAssistant
|
|
2772
|
+
});
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2777
|
+
for (const b of bound) {
|
|
2778
|
+
const key = `${b.event}\u0000${b.message}`;
|
|
2779
|
+
if (seen.has(key)) continue;
|
|
2780
|
+
seen.add(key);
|
|
2781
|
+
if (byId.get(b.message)?.toolUses === 0) stats.misfires[b.event] = (stats.misfires[b.event] ?? 0) + 1;
|
|
2782
|
+
}
|
|
2783
|
+
stats.versions = [...versions].sort();
|
|
2784
|
+
stats.hooks = [...hooks.values()];
|
|
2785
|
+
return stats;
|
|
2786
|
+
}
|
|
2787
|
+
/** Whether any action of the session serves the mainline, through a checklist item or a criterion. */
|
|
2788
|
+
const servesMainline = (session, mainline) => session.actions.some((a) => a.serves.some((ref) => {
|
|
2789
|
+
const parsed = parseRef(ref);
|
|
2790
|
+
return parsed !== null && parsed.hub === void 0 && refMainline(parsed) === mainline;
|
|
2791
|
+
}));
|
|
2792
|
+
/** Subagent transcripts Claude Code keeps beside a session's: <dir>/<session>/subagents/*.jsonl. */
|
|
2793
|
+
function subagentFiles(transcript) {
|
|
2794
|
+
const dir = join(transcript.replace(/\.jsonl$/, ""), "subagents");
|
|
2795
|
+
try {
|
|
2796
|
+
return readdirSync(dir).filter((n) => n.endsWith(".jsonl")).sort().map((n) => join(dir, n));
|
|
2797
|
+
} catch {
|
|
2798
|
+
return [];
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
2801
|
+
function readMeta(file) {
|
|
2802
|
+
try {
|
|
2803
|
+
const meta = JSON.parse(readFileSync(file.replace(/\.jsonl$/, ".meta.json"), "utf8"));
|
|
2804
|
+
if (!isObject(meta)) return {};
|
|
2805
|
+
return {
|
|
2806
|
+
...typeof meta.agentType === "string" ? { agentType: meta.agentType } : {},
|
|
2807
|
+
...typeof meta.description === "string" ? { description: meta.description } : {}
|
|
2808
|
+
};
|
|
2809
|
+
} catch {
|
|
2810
|
+
return {};
|
|
2811
|
+
}
|
|
2812
|
+
}
|
|
2813
|
+
function summarize(stats) {
|
|
2814
|
+
const usage = emptyUsage();
|
|
2815
|
+
let cost = 0;
|
|
2816
|
+
const unknown = /* @__PURE__ */ new Set();
|
|
2817
|
+
const priced = stats.messages.filter((m) => m.model !== "<synthetic>");
|
|
2818
|
+
for (const m of priced) {
|
|
2819
|
+
addUsage(usage, m.usage);
|
|
2820
|
+
const c = messageCost(m.model, m.usage, m.speed, m.geo);
|
|
2821
|
+
if (c === null) unknown.add(m.model);
|
|
2822
|
+
else cost += c;
|
|
2823
|
+
}
|
|
2824
|
+
const first = priced.find((m) => contextTokens(m.usage) > 0);
|
|
2825
|
+
return {
|
|
2826
|
+
file: stats.file,
|
|
2827
|
+
found: stats.found,
|
|
2828
|
+
calls: priced.length,
|
|
2829
|
+
usage,
|
|
2830
|
+
cost,
|
|
2831
|
+
unknownPriceModels: [...unknown].sort(),
|
|
2832
|
+
models: [...new Set(priced.map((m) => m.model))].sort(),
|
|
2833
|
+
openingContext: first ? contextTokens(first.usage) : null,
|
|
2834
|
+
cacheHitRate: cacheHitRate(usage)
|
|
2835
|
+
};
|
|
2836
|
+
}
|
|
2837
|
+
/** Lines of a task's 紀錄 section that record a completion check that did not pass. */
|
|
2838
|
+
const FAILED_CHECK = (line) => line.includes("完成檢查") && line.includes("未過");
|
|
2839
|
+
/**
|
|
2840
|
+
* Read the transcripts of every session with an action serving the mainline, and their subagents', into cost
|
|
2841
|
+
* figures and candidates for where the work went wrong. Each transcript is read once, even when a subagent session
|
|
2842
|
+
* serves the mainline as well.
|
|
2843
|
+
*/
|
|
2844
|
+
function retroReport(root, mainlineId) {
|
|
2845
|
+
const mainline = loadHub(root, { remotes: false }).mainlines.find((m) => m.id === mainlineId);
|
|
2846
|
+
if (!mainline) throw new WriteError(`mainline ${mainlineId} not found; run "mainflow status --all" to list mainlines`);
|
|
2847
|
+
const serving = loadSessions(root).sessions.filter((s) => servesMainline(s, mainlineId));
|
|
2848
|
+
const read = /* @__PURE__ */ new Map();
|
|
2849
|
+
const stats = (file) => {
|
|
2850
|
+
let s = read.get(file);
|
|
2851
|
+
if (!s) read.set(file, s = readTranscript(file));
|
|
2852
|
+
return s;
|
|
2853
|
+
};
|
|
2854
|
+
const sessions = [];
|
|
2855
|
+
const subagents = [];
|
|
2856
|
+
const labels = /* @__PURE__ */ new Map();
|
|
2857
|
+
const subagentFilesSeen = /* @__PURE__ */ new Set();
|
|
2858
|
+
const addSubagent = (file, parent) => {
|
|
2859
|
+
if (subagentFilesSeen.has(file)) return;
|
|
2860
|
+
subagentFilesSeen.add(file);
|
|
2861
|
+
labels.set(file, `${parent} › ${basename(file, ".jsonl")}`);
|
|
2862
|
+
subagents.push({
|
|
2863
|
+
...summarize(stats(file)),
|
|
2864
|
+
parent,
|
|
2865
|
+
...readMeta(file)
|
|
2866
|
+
});
|
|
2867
|
+
};
|
|
2868
|
+
for (const s of [...serving].sort((a, b) => Number(Boolean(a.delegatedFrom)) - Number(Boolean(b.delegatedFrom)))) {
|
|
2869
|
+
const file = s.transcript ? transcriptPath(root, s.transcript) : void 0;
|
|
2870
|
+
if (s.delegatedFrom) {
|
|
2871
|
+
if (file) addSubagent(file, s.delegatedFrom);
|
|
2872
|
+
continue;
|
|
2873
|
+
}
|
|
2874
|
+
if (!file) {
|
|
2875
|
+
sessions.push({
|
|
2876
|
+
...summarize(emptyStats("", false)),
|
|
2877
|
+
session: s.id,
|
|
2878
|
+
missing: "the session records no transcript"
|
|
2879
|
+
});
|
|
2880
|
+
continue;
|
|
2881
|
+
}
|
|
2882
|
+
labels.set(file, s.id);
|
|
2883
|
+
const summary = summarize(stats(file));
|
|
2884
|
+
sessions.push({
|
|
2885
|
+
...summary,
|
|
2886
|
+
session: s.id,
|
|
2887
|
+
...summary.found ? {} : { missing: `${file} does not exist` }
|
|
2888
|
+
});
|
|
2889
|
+
for (const sub of subagentFiles(file)) addSubagent(sub, s.id);
|
|
2890
|
+
}
|
|
2891
|
+
const all = [...read.values()];
|
|
2892
|
+
const models = /* @__PURE__ */ new Map();
|
|
2893
|
+
const total = emptyUsage();
|
|
2894
|
+
for (const m of all.flatMap((t) => t.messages).filter((m) => m.model !== "<synthetic>")) {
|
|
2895
|
+
const entry = models.get(m.model) ?? {
|
|
2896
|
+
model: m.model,
|
|
2897
|
+
calls: 0,
|
|
2898
|
+
usage: emptyUsage(),
|
|
2899
|
+
cost: priceOf(m.model) ? 0 : null
|
|
2900
|
+
};
|
|
2901
|
+
models.set(m.model, entry);
|
|
2902
|
+
entry.calls += 1;
|
|
2903
|
+
addUsage(entry.usage, m.usage);
|
|
2904
|
+
addUsage(total, m.usage);
|
|
2905
|
+
const c = messageCost(m.model, m.usage, m.speed, m.geo);
|
|
2906
|
+
if (c !== null && entry.cost !== null) entry.cost += c;
|
|
2907
|
+
}
|
|
2908
|
+
const hooks = /* @__PURE__ */ new Map();
|
|
2909
|
+
for (const h of all.flatMap((t) => t.hooks)) {
|
|
2910
|
+
const entry = hooks.get(h.event) ?? {
|
|
2911
|
+
event: h.event,
|
|
2912
|
+
injections: 0,
|
|
2913
|
+
chars: 0,
|
|
2914
|
+
blocked: 0
|
|
2915
|
+
};
|
|
2916
|
+
hooks.set(h.event, entry);
|
|
2917
|
+
entry.injections += h.injections;
|
|
2918
|
+
entry.chars += h.chars;
|
|
2919
|
+
entry.blocked += h.blocked;
|
|
2920
|
+
}
|
|
2921
|
+
const sum = (records) => {
|
|
2922
|
+
const out = {};
|
|
2923
|
+
for (const r of records) for (const [k, v] of Object.entries(r)) out[k] = (out[k] ?? 0) + v;
|
|
2924
|
+
return out;
|
|
2925
|
+
};
|
|
2926
|
+
const count = (r) => Object.values(r).reduce((a, b) => a + b, 0);
|
|
2927
|
+
const toolErrors = sum(all.map((t) => t.toolErrors));
|
|
2928
|
+
const misfires = sum(all.map((t) => t.misfires));
|
|
2929
|
+
const blocked = Object.fromEntries([...hooks.values()].filter((h) => h.blocked > 0).map((h) => [h.event, h.blocked]));
|
|
2930
|
+
const reruns = all.flatMap((t) => Object.entries(t.bashRuns).filter(([, runs]) => runs > 1).map(([command, runs]) => ({
|
|
2931
|
+
command,
|
|
2932
|
+
runs,
|
|
2933
|
+
session: labels.get(t.file) ?? t.file
|
|
2934
|
+
}))).sort((a, b) => b.runs - a.runs);
|
|
2935
|
+
const modelList = [...models.values()].sort((a, b) => b.calls - a.calls || a.model.localeCompare(b.model));
|
|
2936
|
+
return {
|
|
2937
|
+
mainline: {
|
|
2938
|
+
id: mainline.id,
|
|
2939
|
+
title: mainline.title,
|
|
2940
|
+
status: mainline.status,
|
|
2941
|
+
criteria: mainline.criteria.length,
|
|
2942
|
+
tasks: mainline.tasks.length
|
|
2943
|
+
},
|
|
2944
|
+
versions: [...new Set(all.flatMap((t) => t.versions))].sort(),
|
|
2945
|
+
unreadable: all.reduce((n, t) => n + t.unreadable, 0),
|
|
2946
|
+
pricing: PRICING_SOURCE,
|
|
2947
|
+
cost: {
|
|
2948
|
+
total: modelList.reduce((n, m) => n + (m.cost ?? 0), 0),
|
|
2949
|
+
unknownPriceModels: modelList.filter((m) => m.cost === null).map((m) => m.model),
|
|
2950
|
+
cacheHitRate: cacheHitRate(total),
|
|
2951
|
+
models: modelList,
|
|
2952
|
+
sessions,
|
|
2953
|
+
subagents,
|
|
2954
|
+
hooks: [...hooks.values()].sort((a, b) => b.injections - a.injections || a.event.localeCompare(b.event))
|
|
2955
|
+
},
|
|
2956
|
+
precision: {
|
|
2957
|
+
toolErrors: {
|
|
2958
|
+
total: count(toolErrors),
|
|
2959
|
+
byTool: toolErrors
|
|
2960
|
+
},
|
|
2961
|
+
bashReruns: {
|
|
2962
|
+
total: reruns.reduce((n, r) => n + r.runs - 1, 0),
|
|
2963
|
+
commands: reruns
|
|
2964
|
+
},
|
|
2965
|
+
misfireCandidates: {
|
|
2966
|
+
total: count(misfires),
|
|
2967
|
+
byEvent: misfires
|
|
2968
|
+
},
|
|
2969
|
+
blocked: {
|
|
2970
|
+
total: count(blocked),
|
|
2971
|
+
byEvent: blocked
|
|
2972
|
+
},
|
|
2973
|
+
gaps: all.flatMap((t) => t.gaps.map((g) => ({
|
|
2974
|
+
...g,
|
|
2975
|
+
session: labels.get(t.file) ?? t.file
|
|
2976
|
+
}))),
|
|
2977
|
+
failedChecks: mainline.tasks.flatMap((t) => (sectionText(t.body, "紀錄") ?? "").split("\n").filter(FAILED_CHECK).map((line) => ({
|
|
2978
|
+
task: `${mainline.id}/${t.id}`,
|
|
2979
|
+
line: line.trim()
|
|
2980
|
+
})))
|
|
2981
|
+
}
|
|
2982
|
+
};
|
|
2983
|
+
}
|
|
2984
|
+
//#endregion
|
|
2317
2985
|
//#region ../core/dist/migrate.js
|
|
2318
2986
|
const drop = (o, ...keys) => keys.forEach((k) => delete o[k]);
|
|
2319
2987
|
const dropNull = (o, ...keys) => keys.forEach((k) => o[k] === null && delete o[k]);
|
|
@@ -2597,4 +3265,4 @@ function openInApp(hub, target, closeAfterMs = 150) {
|
|
|
2597
3265
|
return true;
|
|
2598
3266
|
}
|
|
2599
3267
|
//#endregion
|
|
2600
|
-
export {
|
|
3268
|
+
export { appendOps as $, untilState as A, setSessionArchived as B, setCriterion as C, supersedeDecision as D, setMainlineTitle as E, ensureSession as F, transcriptPath as G, setSessionStatus as H, handoffSession as I, matchGrants as J, updateAction as K, loadSessions as L, sessionBriefing as M, turnReminder as N, undropChecklistItem as O, addAction as P, mayDecorate as Q, readSession as R, setChecklistItem as S, worktreesOf as St, setMainlineStatus as T, settleSession as U, setSessionInfo as V, startSession as W, endsWithMarker as X, decorateReply as Y, headerLine as Z, createMainline as _, WriteError as _t, openInApp as a, setFindingArchived as at, initHub as b, currentBranch as bt, syncHost as c, HubError as ct, ARCHIVE_KINDS as d, mainlineEdges as dt, closeIssue as et, setArchivedMany as f, linksInto as ft, createDecision as g, ConflictError as gt, addRepo as h, TASK_FILE as ht, hostName as i, reopenIssue as it, duplicateClaimLines as j, updateTask as k, migrateHub as l, loadHub as lt, addHub as m, MAINLINE_FILE as mt, DEFAULT_UI_PORT as n, createIssue as nt, removeHostLink as o, setFindingStatus as ot, acceptDecision as p, refIndex as pt, grantScope as q, caddySites as r, planIssue as rt, siteBlock as s, setIssueArchived as st, APP_BUNDLE_ID as t, createFinding as tt, retroReport as u, dependencyGraph as ut, createTask as v, readYaml as vt, setCriterionText as w, setArchived as x, findHubRoot as xt, dropChecklistItem as y, DATA_DIR as yt, setReminderDigest as z };
|