mainflow 0.43.0 → 0.44.0-alpha.2

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/hooks.js CHANGED
@@ -1,8 +1,9 @@
1
- import { I as configSchema, Y as parseRef, ft as describeRequest, rt as taskSchema, tt as repoPaths } from "./dist-D5rZ0MQH.js";
2
- import { C as turnReminder, Dt as hubFormatVersion, Et as formatMismatch, F as headerLine, Ft as inRepositoryHubAt, Gt as nodeStorage, Ht as resolveHub, I as mayDecorate, Jt as hubPath, Kt as currentBranch, Lt as notRegisteredMessage, M as matchGrants, N as decorateReply, O as loadSessions, P as endsWithMarker, Pt as hubHoldingFile, Q as HubError, Qt as DATA_DIR, S as sessionBriefing, Xt as worktreesOf, Yt as realPath, dt as readFrontMatter, et as loadHub, ft as readYaml, i as hostName, j as grantScope, k as readSession, lt as taskFile, qt as dataFile } from "./host-BJvqZy-i.js";
3
- import { n as hubWriter, r as writtenLines } from "./operation-BhBbGauo.js";
1
+ import { I as configSchema, Y as parseRef, ft as describeRequest, rt as taskSchema, tt as repoPaths } from "./dist-CXrUfRQp.js";
2
+ import { $ as isOpenMainline, $t as realPath, Bt as notRegisteredMessage, C as turnReminder, F as headerLine, Gt as resolveHub, I as mayDecorate, Jt as nodeStorage, Lt as hubHoldingFile, M as matchGrants, N as decorateReply, Nt as SESSIONS_DIR, O as loadSessions, Ot as formatMismatch, P as endsWithMarker, Q as HubError, Qt as hubPath, Rt as inRepositoryHubAt, S as sessionBriefing, Xt as dataFile, Yt as currentBranch, dt as taskFile, en as worktreesOf, i as hostName, j as grantScope, k as readSession, kt as hubFormatVersion, mt as readYaml, nn as DATA_DIR, nt as loadMainlines, pt as readFrontMatter, tt as loadHub } from "./host-C74j2Qox.js";
3
+ import { n as hubWriter, r as writtenLines } from "./operation-CUeOFEaf.js";
4
4
  import { SCENARIO_LINES, SCENARIO_SKIP_KINDS } from "./facts.js";
5
5
  import { resolveFixed, scanCommand, scopedCwd } from "./grant-commands.js";
6
+ import { createHash } from "node:crypto";
6
7
  import { appendFileSync, existsSync, readFileSync } from "node:fs";
7
8
  import { isAbsolute, join, relative, resolve } from "node:path";
8
9
  //#region src/hooks.ts
@@ -124,11 +125,11 @@ const displayReply = (dirs, { payload }) => {
124
125
  };
125
126
  /** Mainline ids a reply mentions, e.g. m001 in `m001/t003`; not inside a longer word or path. */
126
127
  const MAINLINE_MENTION = /(?<![\w/#.[-])(m\d{3})(?![\w\]])/g;
127
- /** The open mainlines the text mentions; loads the hub only when the text mentions one. */
128
+ /** The open mainlines the text mentions; reads the mainlines' mainline.md only when the text mentions one. */
128
129
  function openMainlinesIn(dirs, text) {
129
130
  const mentioned = new Set([...text.matchAll(MAINLINE_MENTION)].map((m) => m[1]));
130
131
  if (mentioned.size === 0) return [];
131
- return loadHub(dirs, { remotes: false }).mainlines.filter((m) => mentioned.has(m.id) && !m.archivedAt && (m.status === "active" || m.status === "planning")).map((m) => m.id);
132
+ return loadMainlines(dirs).filter((m) => mentioned.has(m.id) && isOpenMainline(m)).map((m) => m.id);
132
133
  }
133
134
  /** Tools whose `file_path` input writes a file. */
134
135
  const WRITE_TOOLS = /* @__PURE__ */ new Set([
@@ -219,6 +220,10 @@ const CLAUDE_CODE_EVENTS = {
219
220
  id,
220
221
  digest: null
221
222
  });
223
+ if (session.checkReported?.length) await write("session.checked", {
224
+ id,
225
+ reported: null
226
+ });
222
227
  if (envFile) appendFileSync(envFile, `export MAINFLOW_SESSION=${id}\n`);
223
228
  startUi?.(dirs);
224
229
  return sessionBriefing(dirs, session, {
@@ -423,10 +428,50 @@ function decideStartGate(dirs, { payload, cli, env }) {
423
428
  permissionDecisionReason: `[mainflow] ${reason}`
424
429
  } });
425
430
  }
426
- /** How many problems outside the written file the reminder lists before it only gives the count. */
427
- const OTHER_PROBLEMS_SHOWN = 5;
431
+ /** How many problems the reminder lists when the session is unknown, before it only gives the count. */
432
+ const UNSCOPED_PROBLEMS_SHOWN = 5;
428
433
  /** A problem as the reminder lists it, its file as the absolute path the model opens. */
429
434
  const problemLine = (dirs, p) => `- ${dataFile(dirs, p.file)}${p.field ? ` › ${p.field}` : ""}:${p.message}`;
435
+ /** What a reported problem is remembered by: the same file, field and message is the same problem. */
436
+ const problemKey = (p) => createHash("sha1").update(`${p.file}\0${p.field}\0${p.message}`).digest("hex").slice(0, 16);
437
+ /** The mainlines the session's unfinished actions serve, as the ids their refs start with. */
438
+ function servedMainlines(session) {
439
+ const ids = /* @__PURE__ */ new Set();
440
+ for (const a of session.actions) {
441
+ if (a.status === "done" || a.status === "dropped") continue;
442
+ for (const ref of a.serves) {
443
+ const id = /^m\d+/.exec(ref)?.[0];
444
+ if (id) ids.add(id);
445
+ }
446
+ }
447
+ return ids;
448
+ }
449
+ /**
450
+ * Whether a problem's file is the session's to fix: a mainline its unfinished actions serve, the session's own file,
451
+ * or a file of the hub itself (issues, findings, groups, config.yaml and the like), which no mainline owns.
452
+ */
453
+ function inSessionScope(file, session) {
454
+ const mainlines = `${DATA_DIR}/mainlines/`;
455
+ const sessions = `${DATA_DIR}/${SESSIONS_DIR}/`;
456
+ if (file.startsWith(mainlines)) return [...servedMainlines(session)].some((m) => file.startsWith(`${mainlines}${m}-`));
457
+ if (file.startsWith(sessions)) return file === `${sessions}${session.id}.yaml`;
458
+ return true;
459
+ }
460
+ /**
461
+ * What a list of in-scope problems lies in, for the line that heads it: only the kinds it holds, so a problem of the
462
+ * session's own file alone is not called one of a mainline or of the hub's own files.
463
+ */
464
+ function scopeLabel(problems) {
465
+ const kindOf = (file) => file.startsWith(`.mainflow/mainlines/`) ? 0 : file.startsWith(`.mainflow/local/sessions/`) ? 2 : 1;
466
+ const kinds = new Set(problems.map((p) => kindOf(p.file)));
467
+ const parts = [
468
+ "目前行動服務的主脈絡",
469
+ "hub 層的檔",
470
+ "這場會話的會話檔"
471
+ ].filter((_, i) => kinds.has(i));
472
+ const last = parts.at(-1) ?? "";
473
+ return parts.length > 1 ? `${parts.slice(0, -1).join("、")}與${/^[A-Za-z0-9]/.test(last) ? " " : ""}${last}` : last;
474
+ }
430
475
  /**
431
476
  * After a tool writes into a hub's data directory, run the same checks as `mainflow check` and hand the problems
432
477
  * to the model in the same turn. Edit, Write and MultiEdit name the file; Bash only counts when its command mentions
@@ -434,8 +479,16 @@ const problemLine = (dirs, p) => `- ${dataFile(dirs, p.file)}${p.field ? ` › $
434
479
  * write elsewhere, adds nothing. The hub a written file belongs to is the registered hub whose data directory
435
480
  * holds it, found from the file itself rather than from the session's directory, because a session may write
436
481
  * into another registered hub.
482
+ *
483
+ * With the session known, only the problems of the written file, of the mainlines its unfinished actions serve, of the
484
+ * hub's own files (everything outside `mainlines/` and `local/sessions/`) and of the session's own file are listed, the
485
+ * rest counted in one line, and a problem handed over once is not handed over again while it is still there: other
486
+ * sessions' mainlines and session files are theirs to fix, and the model still holds what an earlier write reported.
487
+ * SessionStart forgets what was reported, since a resume, clear or compaction drops it. A subagent's write (the
488
+ * payload has agent_id) is scoped by the main session's actions but neither skips nor records what was reported,
489
+ * because the subagent has not seen what the main session was told.
437
490
  */
438
- function checkAfterWrite(dirs, { payload, cli, env }) {
491
+ async function checkAfterWrite(dirs, { payload, cli, env, write }) {
439
492
  const input = payload.tool_input ?? {};
440
493
  let hub = dirs;
441
494
  let written = null;
@@ -463,15 +516,50 @@ function checkAfterWrite(dirs, { payload, cli, env }) {
463
516
  message: error.message
464
517
  }];
465
518
  }
466
- if (problems.length === 0) return null;
467
- const own = written ? problems.filter((p) => p.file === written) : [];
468
- const others = problems.filter((p) => !own.includes(p));
519
+ const sessionId = payload.session_id ? claudeCodeSessionId(payload.session_id) : null;
520
+ let session = null;
521
+ try {
522
+ session = sessionId ? readSession(hub, sessionId) : null;
523
+ } catch {}
469
524
  const line = (p) => problemLine(hub, p);
525
+ const own = written ? problems.filter((p) => p.file === written) : [];
526
+ if (!session) {
527
+ if (problems.length === 0) return null;
528
+ const others = problems.filter((p) => !own.includes(p));
529
+ const lines = [
530
+ written && own.length ? `[mainflow] 剛寫入的 ${dataFile(hub, written)} 有 ${own.length} 個問題,改好再繼續:` : `[mainflow] 寫入 hub 的資料目錄 ${hub.data} 後,check 找到 ${problems.length} 個問題:`,
531
+ ...own.map(line),
532
+ ...others.length ? [...own.length ? [`其他檔案還有 ${others.length} 個問題:`] : [], ...others.slice(0, UNSCOPED_PROBLEMS_SHOWN).map(line)] : [],
533
+ ...others.length > UNSCOPED_PROBLEMS_SHOWN ? [`還有 ${others.length - UNSCOPED_PROBLEMS_SHOWN} 個沒列出,跑 ${cli} check 看全部。`] : []
534
+ ];
535
+ return JSON.stringify({ hookSpecificOutput: {
536
+ hookEventName: "PostToolUse",
537
+ additionalContext: lines.join("\n")
538
+ } });
539
+ }
540
+ const ofScope = problems.filter((p) => !own.includes(p) && inSessionScope(p.file, session));
541
+ const relevant = [...own, ...ofScope];
542
+ const subagent = Boolean(payload.agent_id);
543
+ const reported = new Set(subagent ? [] : session.checkReported ?? []);
544
+ const fresh = (list) => list.filter((p) => !reported.has(problemKey(p)));
545
+ const newOwn = fresh(own);
546
+ const newScope = fresh(ofScope);
547
+ if (!subagent) {
548
+ const present = new Set(problems.map(problemKey));
549
+ const keys = [.../* @__PURE__ */ new Set([...[...reported].filter((k) => present.has(k)), ...[...newOwn, ...newScope].map(problemKey)])].sort();
550
+ if (keys.length !== reported.size || keys.some((k) => !reported.has(k))) await write("session.checked", {
551
+ id: session.id,
552
+ reported: keys.length ? keys : null
553
+ }).catch(() => void 0);
554
+ }
555
+ if (newOwn.length + newScope.length === 0) return null;
556
+ const elsewhere = problems.length - relevant.length;
470
557
  const lines = [
471
- written && own.length ? `[mainflow] 剛寫入的 ${dataFile(hub, written)} 有 ${own.length} 個問題,改好再繼續:` : `[mainflow] 寫入 hub 的資料目錄 ${hub.data} 後,check 找到 ${problems.length} 個問題:`,
472
- ...own.map(line),
473
- ...others.length ? [...own.length ? [`其他檔案還有 ${others.length} 個問題:`] : [], ...others.slice(0, OTHER_PROBLEMS_SHOWN).map(line)] : [],
474
- ...others.length > OTHER_PROBLEMS_SHOWN ? [`還有 ${others.length - OTHER_PROBLEMS_SHOWN} 個沒列出,跑 ${cli} check 看全部。`] : []
558
+ newOwn.length ? `[mainflow] 剛寫入的 ${dataFile(hub, written)} 有 ${newOwn.length} 個問題,改好再繼續:` : `[mainflow] 寫入 hub 的資料目錄 ${hub.data} 後,${scopeLabel(newScope)}有 ${newScope.length} 個問題:`,
559
+ ...newOwn.map(line),
560
+ ...newOwn.length && newScope.length ? [`${scopeLabel(newScope)}還有 ${newScope.length} 個問題:`] : [],
561
+ ...newScope.map(line),
562
+ ...elsewhere > 0 ? [`其他還有 ${elsewhere} 條,屬於別的主脈絡或別的會話的會話檔,跑 ${cli} check 看全部。`] : []
475
563
  ];
476
564
  return JSON.stringify({ hookSpecificOutput: {
477
565
  hookEventName: "PostToolUse",
@@ -1,4 +1,4 @@
1
- import { $ as removedUntil, A as SURVEY_LINES, B as groupSchema, C as PRIORITIES, D as SESSION_ID, E as SERVES_REF, F as WORK_STATUS, G as mainlineSchema, H as isMainflowRange, I as configSchema, J as parseOpsLine, K as opsEntrySchema, L as decisionSchema, M as TASK_REQUIRED_SECTIONS, N as TASK_SECTIONS, O as SESSION_STATUS, P as UNTIL_FORMS, Q as refMainline, R as findingSchema, S as MERGE_STATES, T as RESERVED_HUB_NAMES, U as issueSchema, V as hubRegistrySchema, W as listedRepos, X as parseUntil, Y as parseRef, Z as refExample, a as DECISION_SECTIONS, at as waitingForAction, b as MAINLINE_SECTIONS, c as FINDING_SECTIONS, ct as stampMs, d as GROUP_SECTIONS, et as repoPath, f as GROUP_STATUS, ft as describeRequest, g as ISSUE_SECTIONS, gt as parseGrantTarget, ht as grantRepo, i as DECISION_BASES, j as SURVEY_SECTION, k as SLUG, l as FINDING_STATUS, m as HUB_NAME, mt as grantForm, n as CLOSED_STATUS, nt as sessionSchema, ot as waitingForRef, pt as grantCovers, q as parseActionRef, r as DECIDERS, rt as taskSchema, s as DEFAULT_CATEGORIES, st as OFFSET, w as REF_EXAMPLES, x as MAINLINE_STATUS, y as MAINFLOW_RANGE, z as formatOpsEntry } from "./dist-D5rZ0MQH.js";
1
+ import { $ as removedUntil, A as SURVEY_LINES, B as groupSchema, C as PRIORITIES, D as SESSION_ID, E as SERVES_REF, F as WORK_STATUS, G as mainlineSchema, H as isMainflowRange, I as configSchema, J as parseOpsLine, K as opsEntrySchema, L as decisionSchema, M as TASK_REQUIRED_SECTIONS, N as TASK_SECTIONS, O as SESSION_STATUS, P as UNTIL_FORMS, Q as refMainline, R as findingSchema, S as MERGE_STATES, T as RESERVED_HUB_NAMES, U as issueSchema, V as hubRegistrySchema, W as listedRepos, X as parseUntil, Y as parseRef, Z as refExample, _t as parseGrantTarget, a as DECISION_SECTIONS, at as waitingForAction, b as MAINLINE_SECTIONS, c as FINDING_SECTIONS, ct as stampMs, d as GROUP_SECTIONS, et as repoPath, f as GROUP_STATUS, ft as describeRequest, g as ISSUE_SECTIONS, ht as grantRepo, i as DECISION_BASES, j as SURVEY_SECTION, k as SLUG, l as FINDING_STATUS, m as HUB_NAME, mt as grantForm, n as CLOSED_STATUS, nt as sessionSchema, ot as waitingForRef, pt as grantCovers, q as parseActionRef, r as DECIDERS, rt as taskSchema, s as DEFAULT_CATEGORIES, st as OFFSET, vt as pathInPatterns, w as REF_EXAMPLES, x as MAINLINE_STATUS, y as MAINFLOW_RANGE, z as formatOpsEntry } from "./dist-CXrUfRQp.js";
2
2
  import { a as LABEL_ROLES, i as FINDING_SEVERITIES, n as FINDING_CLASSES, r as FINDING_FIELDS, t as FINDINGS_FENCE } from "./retro-formats-ECy0gGOL.js";
3
3
  import { createHash } from "node:crypto";
4
4
  import { appendFileSync, closeSync, cpSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, symlinkSync, writeFileSync, writeSync } from "node:fs";
@@ -1566,10 +1566,15 @@ function mainlineOf(ref) {
1566
1566
  const m = parsed ? refMainline(parsed) : void 0;
1567
1567
  return m && (parsed.hub ? `${parsed.hub}:${m}` : m);
1568
1568
  }
1569
- /** Dependencies lifted to mainlines, for a map of how mainlines wait for one another. */
1569
+ /**
1570
+ * The order mainlines are pushed forward in, for a map of how mainlines wait for one another. Only a mainline's
1571
+ * own `depends-on` counts: a task waiting on another mainline's task is a detail of that task, and lifting it
1572
+ * would bury the order under every file two tasks happen to share. The target may still be a task or criterion.
1573
+ */
1570
1574
  function mainlineEdges(graph) {
1571
1575
  const out = /* @__PURE__ */ new Map();
1572
1576
  for (const e of graph.edges) {
1577
+ if (parseRef(e.from)?.kind !== "mainline") continue;
1573
1578
  const from = mainlineOf(e.from);
1574
1579
  const to = mainlineOf(e.to);
1575
1580
  if (!from || !to || from === to) continue;
@@ -1753,6 +1758,30 @@ function loadConfig(dirs) {
1753
1758
  if (mismatch) throw new HubError(mismatch, []);
1754
1759
  return config;
1755
1760
  }
1761
+ /**
1762
+ * The mainlines loadHub lists, in the same order, with only what each mainline.md's front matter holds: no tasks,
1763
+ * decisions, artifacts or checks. For the callers that only need ids, titles and statuses, such as the hooks that run
1764
+ * on every turn, since loadHub reads and parses every document of the hub. Like loadHub it throws when config.yaml
1765
+ * cannot be used, and it leaves out a mainline whose mainline.md is missing, has front matter that does not parse, or
1766
+ * does not fit the schema. Every call reads the files again, so a status written in between is what it returns.
1767
+ */
1768
+ function loadMainlines(dirs) {
1769
+ loadConfig(dirs);
1770
+ return subdirs$1(join(dirs.data, "mainlines")).flatMap((dir) => {
1771
+ const text = readText$1(join(dir, MAINLINE_FILE));
1772
+ if (text === null) return [];
1773
+ let data;
1774
+ try {
1775
+ data = splitFrontMatter(text).data;
1776
+ } catch {
1777
+ return [];
1778
+ }
1779
+ const result = mainlineSchema.safeParse(data);
1780
+ return result.success ? [result.data] : [];
1781
+ });
1782
+ }
1783
+ /** Whether a session could be pushing the mainline forward: it is active or planning, and not archived. */
1784
+ const isOpenMainline = (m) => !m.archivedAt && (m.status === "active" || m.status === "planning");
1756
1785
  function loadHub(dirs, options = {}) {
1757
1786
  const problems = [];
1758
1787
  const warnings = [];
@@ -1877,6 +1906,14 @@ function loadHub(dirs, options = {}) {
1877
1906
  const taskIds = new Set(tasks.map((t) => t.id));
1878
1907
  const criterionIds = new Set(mainline.criteria.map((c) => c.id));
1879
1908
  const itemRefs = new Set(tasks.flatMap((t) => t.checklist.map((c) => `${mainline.id}/${t.id}#${c.id}`)));
1909
+ mainline.grants?.forEach((grant, i) => {
1910
+ const task = grant.scope?.task;
1911
+ if (task && !taskIds.has(task) && !unreadable.has(`${mainline.id}/${task}`)) problems.push({
1912
+ file: mainline.file,
1913
+ field: `grants.${i}.scope.task`,
1914
+ message: `task ${mainline.id}/${task} does not exist; a grant's scope.task names a task of its own mainline`
1915
+ });
1916
+ });
1880
1917
  for (const task of tasks) {
1881
1918
  if (task.parent && !taskIds.has(task.parent)) problems.push({
1882
1919
  file: task.file,
@@ -1944,7 +1981,16 @@ function loadHub(dirs, options = {}) {
1944
1981
  const findings = hubDocs(join(dataDir, "findings"), findingSchema);
1945
1982
  const groups = hubDocs(join(dataDir, "groups"), groupSchema);
1946
1983
  problems.push(...groupProblems(config.hub, groups, mainlines, hubIssues, unreadable));
1947
- for (const g of groups) problems.push(...grantProblems(g, config), ...listGaps(g.file, "criteria", g.criteria ?? [], g.status === "open"));
1984
+ for (const g of groups) {
1985
+ problems.push(...grantProblems(g, config), ...listGaps(g.file, "criteria", g.criteria ?? [], g.status === "open"));
1986
+ g.grants?.forEach((grant, i) => {
1987
+ if (grant.scope?.task) problems.push({
1988
+ file: g.file,
1989
+ field: `grants.${i}.scope.task`,
1990
+ message: `a group has no tasks, so ${g.id}#${grant.id} would never apply; limit it with session or paths, or move it to the mainline that holds ${grant.scope.task}`
1991
+ });
1992
+ });
1993
+ }
1948
1994
  const ops = [];
1949
1995
  const opsPath = join(dataDir, "ops");
1950
1996
  for (const name of existsSync(opsPath) ? readdirSync(opsPath).filter((n) => n.endsWith(".log")).sort() : []) readFileSync(join(opsPath, name), "utf8").split("\n").forEach((line, index) => {
@@ -3462,7 +3508,7 @@ function setSessionInfo(hub, input) {
3462
3508
  for (const [key, value] of Object.entries(input.info)) if (value !== void 0 && value !== null && value !== "") s[key] = value;
3463
3509
  }));
3464
3510
  }
3465
- /** Remember the digest of what the last turn reminder listed in full; null forgets it, so the next reminder lists everything again. */
3511
+ /** Remember the digest of the whole turn reminder last sent; null forgets it, so the next reminder is sent again. */
3466
3512
  function setReminderDigest$1(hub, input) {
3467
3513
  const draft = new Draft(hub);
3468
3514
  return draft.result(editYaml(draft, requireSession(draft, input.id), sessionSchema, (s) => {
@@ -3470,6 +3516,14 @@ function setReminderDigest$1(hub, input) {
3470
3516
  else s.reminderDigest = input.digest;
3471
3517
  }));
3472
3518
  }
3519
+ /** Remember the digests of the check problems last handed over after a write; null or an empty list forgets them, so every problem is reported again. */
3520
+ function setCheckReported(hub, input) {
3521
+ const draft = new Draft(hub);
3522
+ return draft.result(editYaml(draft, requireSession(draft, input.id), sessionSchema, (s) => {
3523
+ if (!input.reported?.length) delete s.checkReported;
3524
+ else s.checkReported = input.reported;
3525
+ }));
3526
+ }
3473
3527
  function setSessionStatus(hub, input) {
3474
3528
  const value = requireOneOf(input.status, SESSION_STATUS, "session status");
3475
3529
  const draft = new Draft(hub);
@@ -3919,6 +3973,7 @@ const OPERATIONS = {
3919
3973
  "session.handoff": handoffSession,
3920
3974
  "session.info": setSessionInfo,
3921
3975
  "session.reminder": setReminderDigest$1,
3976
+ "session.checked": setCheckReported,
3922
3977
  "session.set": setSessionStatus,
3923
3978
  "session.settle": settleSession,
3924
3979
  "session.archive": setSessionArchived,
@@ -4506,7 +4561,7 @@ function grantScope(dirs, session) {
4506
4561
  ok: false,
4507
4562
  reason: `${action.id} 沒有服務任何 checklist 項目或解決條件`
4508
4563
  };
4509
- const grants = [];
4564
+ const all = [];
4510
4565
  const served = {};
4511
4566
  const closed = [];
4512
4567
  const hubName = readConfig(dirs).hub;
@@ -4519,7 +4574,7 @@ function grantScope(dirs, session) {
4519
4574
  continue;
4520
4575
  }
4521
4576
  served[id] = action.serves.find((ref) => mainlineOf(ref) === id);
4522
- for (const grant of mainline.grants ?? []) grants.push({
4577
+ for (const grant of mainline.grants ?? []) all.push({
4523
4578
  owner: id,
4524
4579
  grant,
4525
4580
  ref: `${id}#${grant.id}`
@@ -4543,7 +4598,7 @@ function grantScope(dirs, session) {
4543
4598
  continue;
4544
4599
  }
4545
4600
  served[id] = via;
4546
- for (const grant of group.grants ?? []) grants.push({
4601
+ for (const grant of group.grants ?? []) all.push({
4547
4602
  owner: id,
4548
4603
  grant,
4549
4604
  ref: `${id}#${grant.id}`
@@ -4556,27 +4611,74 @@ function grantScope(dirs, session) {
4556
4611
  reason: `${action.id} 服務的${what}已不在進行中(${closed.join("、")}),授權不再生效`
4557
4612
  };
4558
4613
  }
4614
+ const grants = [];
4615
+ const outOfScope = [];
4616
+ for (const g of all) {
4617
+ const why = limitMiss(g, session.id, action);
4618
+ if (why) outOfScope.push({
4619
+ grant: g,
4620
+ why
4621
+ });
4622
+ else grants.push(g);
4623
+ }
4559
4624
  return {
4560
4625
  ok: true,
4561
4626
  scope: {
4562
4627
  action,
4563
4628
  grants,
4564
- served
4629
+ outOfScope,
4630
+ served,
4631
+ root: realPath(dirs.root) ?? dirs.root
4565
4632
  }
4566
4633
  };
4567
4634
  }
4568
- /** Every request must be covered by some grant in scope; the first one that is not is named in the reason. */
4635
+ /** Why a grant's scope leaves out this session or action, or null when its task and session hold. */
4636
+ function limitMiss({ owner, grant }, sessionId, action) {
4637
+ const limits = grant.scope;
4638
+ if (!limits) return null;
4639
+ if (limits.session !== void 0 && limits.session !== sessionId) return `限定會話 ${limits.session},這次是 ${sessionId}`;
4640
+ if (limits.task !== void 0) {
4641
+ if (!action.serves.some((ref) => {
4642
+ const parsed = parseRef(ref);
4643
+ return parsed?.kind === "checklistItem" && parsed.mainline === owner && parsed.task === limits.task;
4644
+ })) return `限定行動服務 ${owner}/${limits.task} 的項目,${action.id} 沒有服務它`;
4645
+ }
4646
+ return null;
4647
+ }
4648
+ /** Why a grant's paths leave out the request's directory, or null when it has none or the directory lies in them. */
4649
+ function pathsMiss(grant, request, root) {
4650
+ const paths = grant.scope?.paths;
4651
+ if (!paths) return null;
4652
+ if (request.dir === void 0) return `限定路徑 ${paths.join("、")},這次沒有可比對的目錄`;
4653
+ const rel = relative(root, request.dir).split(sep).join("/");
4654
+ return pathInPatterns(paths, rel) ? null : `限定路徑 ${paths.join("、")},這次的目錄是 ${request.dir}`;
4655
+ }
4656
+ /**
4657
+ * Every request must be covered by some grant in scope whose paths, if it has any, hold the request's directory; the
4658
+ * first request that is not covered is named in the reason, with the grants whose scope left it out.
4659
+ */
4569
4660
  function matchGrants(scope, requests) {
4570
4661
  const matches = [];
4662
+ const covers = (g, request) => {
4663
+ const parsed = parseGrantTarget(g.grant.action, g.grant.target);
4664
+ return parsed.ok && grantCovers(parsed.target, request);
4665
+ };
4571
4666
  for (const request of requests) {
4667
+ const misses = [];
4572
4668
  const grant = scope.grants.find((g) => {
4573
- const parsed = parseGrantTarget(g.grant.action, g.grant.target);
4574
- return parsed.ok && grantCovers(parsed.target, request);
4669
+ if (!covers(g, request)) return false;
4670
+ const why = pathsMiss(g.grant, request, scope.root);
4671
+ if (why) misses.push(`${g.ref} ${why}`);
4672
+ return why === null;
4575
4673
  });
4576
- if (!grant) return {
4577
- ok: false,
4578
- reason: `${Object.keys(scope.served).join("、")} 沒有涵蓋 ${describeRequest(request)} 的授權`
4579
- };
4674
+ if (!grant) {
4675
+ const owners = Object.keys(scope.served).join("、");
4676
+ misses.push(...scope.outOfScope.filter((o) => covers(o.grant, request)).map((o) => `${o.grant.ref} ${o.why}`));
4677
+ return {
4678
+ ok: false,
4679
+ reason: `${owners} 沒有涵蓋 ${describeRequest(request)} 的授權${misses.length ? `(${misses.join(";")})` : ""}`
4680
+ };
4681
+ }
4580
4682
  matches.push({
4581
4683
  request,
4582
4684
  grant
@@ -4599,7 +4701,13 @@ function grantLines(dirs, session) {
4599
4701
  return result.scope.grants.map((g) => {
4600
4702
  const parsed = parseGrantTarget(g.grant.action, g.grant.target);
4601
4703
  const form = parsed.ok ? grantForm(parsed.target) : void 0;
4602
- return `- ${g.ref} ${g.grant.action} ${g.grant.target}${form ? `:${form}` : ""}`;
4704
+ const limits = g.grant.scope;
4705
+ const limited = limits ? [
4706
+ limits.task && `任務 ${g.owner}/${limits.task}`,
4707
+ limits.session && `會話 ${limits.session}`,
4708
+ limits.paths && `路徑 ${limits.paths.join("、")}`
4709
+ ].filter(Boolean).join("、") : "";
4710
+ return `- ${g.ref} ${g.grant.action} ${g.grant.target}${limited ? `(限${limited})` : ""}${form ? `:${form}` : ""}`;
4603
4711
  });
4604
4712
  } catch {
4605
4713
  return [];
@@ -4625,7 +4733,7 @@ function resolveAction(dirs, ref) {
4625
4733
  function transcriptPath(dirs, transcript) {
4626
4734
  return isAbsolute(transcript) ? transcript : join(dirs.root, transcript);
4627
4735
  }
4628
- /** Remember the digest of what the last turn reminder listed in full; null forgets it, so the next reminder lists everything again. */
4736
+ /** Remember the digest of the whole turn reminder last sent; null forgets it, so the next reminder is sent again. */
4629
4737
  function setReminderDigest(dirs, id, digest) {
4630
4738
  runOp(dirs, (hub) => setReminderDigest$1(hub, {
4631
4739
  id,
@@ -4878,8 +4986,8 @@ function waitingLine(a) {
4878
4986
  ] : [];
4879
4987
  return `${a.id} ${a.do}${parts.length > 0 ? `(${parts.join(";")})` : ""}`;
4880
4988
  }
4881
- /** Mainlines a session could be pushing forward. */
4882
- const openMainlines = (dirs) => loadHub(dirs).mainlines.filter((m) => !m.archivedAt && (m.status === "active" || m.status === "planning"));
4989
+ /** Mainlines a session could be pushing forward; only their mainline.md is read, as the turn hooks need nothing else. */
4990
+ const openMainlines = (dirs) => loadMainlines(dirs).filter(isOpenMainline);
4883
4991
  /** How recently a session's transcript must have been written for the session to count as possibly still running. */
4884
4992
  const RUNNING_WINDOW_MS = 9e5;
4885
4993
  /** Whether the session's transcript was written within the running window; false when no transcript is recorded or readable. */
@@ -5025,7 +5133,7 @@ function sessionBriefing(dirs, session, options) {
5025
5133
  }
5026
5134
  lines.push(...waiterLines(dirs, session));
5027
5135
  const overlaps = overlapLines(dirs, session);
5028
- if (overlaps.length > 0) lines.push("目前行動服務的主脈絡和其他進行中的主脈絡預計動到同樣的路徑,開始改之前先和對方協調,或加 depends-on:", ...overlaps);
5136
+ if (overlaps.length > 0) lines.push("目前行動服務的主脈絡和其他進行中的主脈絡預計動到同樣的路徑,開始改之前先和對方協調先後:", ...overlaps);
5029
5137
  if (!session.actions.some(isOpen)) lines.push(...handoffLines(dirs, session, cli));
5030
5138
  const warnings = queueWarnings(dirs, session, cli);
5031
5139
  if (warnings.length > 0) lines.push("需要處理:", ...warnings.map((w) => `- ${w}`));
@@ -5034,36 +5142,33 @@ function sessionBriefing(dirs, session, options) {
5034
5142
  return lines.join("\n");
5035
5143
  }
5036
5144
  /**
5037
- * One or two lines for every user turn; null when the session has no actions and the hub has no open mainline.
5038
- * Records a digest of the grants, waits and warnings it lists in the session file, only when the digest changes;
5145
+ * The reminder for every user turn; null when there is nothing to say, or when it would be the same text as the
5146
+ * reminder the model was last given. Records a digest of the whole text in the session file, only when it changes;
5039
5147
  * `saveDigest` records it instead of writing the file here, as a host hook in remote mode sends it to the sync server.
5040
5148
  */
5041
5149
  function turnReminder(dirs, session, options) {
5042
- if (session.actions.length === 0) return openMainlines(dirs).length > 0 ? `[mainflow] ${session.id} 還沒有行動。這一輪要推進主脈絡時,先用 ${options.cli} action add 列出行動並標為 doing,或用 ${options.cli} session handoff <會話> 接手上一場沒做完的行動,再開始做。回覆結尾的標頭只看這場會話進行中的行動。` : null;
5150
+ const text = reminderText(dirs, session, options.cli);
5151
+ if (text === null) return null;
5152
+ const digest = contentHash(text);
5153
+ if (session.reminderDigest === digest) return null;
5154
+ if (readSession(dirs, session.id)) try {
5155
+ if (options.saveDigest) options.saveDigest(digest);
5156
+ else setReminderDigest(dirs, session.id, digest);
5157
+ } catch {}
5158
+ return text;
5159
+ }
5160
+ function reminderText(dirs, session, cli) {
5161
+ if (session.actions.length === 0) return openMainlines(dirs).length > 0 ? `[mainflow] ${session.id} 還沒有行動。這一輪要推進主脈絡時,先用 ${cli} action add 列出行動並標為 doing,或用 ${cli} session handoff <會話> 接手上一場沒做完的行動,再開始做。回覆結尾的標頭只看這場會話進行中的行動。` : null;
5043
5162
  const doing = current(session);
5044
5163
  const open = session.actions.filter(isOpen);
5045
5164
  const blocked = session.actions.filter((a) => a.status === "blocked");
5046
- const head = doing ? `[mainflow] 目前行動 ${doing.id} ${doing.do},服務 ${doing.serves.join("、") || "(未指定)"}。這一輪的工作若不屬於它,先調整佇列再做。` : open.length > 0 ? `[mainflow] ${session.id} 目前沒有進行中的行動。` : blocked.length > 0 ? `[mainflow] ${session.id} 的行動在等外部條件:${blocked.map(waitingLine).join(";")}。有條件(until)的可以在背景執行 ${options.cli} wait <a編號> 等它成立。條件出現後用 ${options.cli} action set <a編號> --status doing 繼續;要先做別的事,用 ${options.cli} action add 列出行動。` : `[mainflow] ${session.id} 的行動都已結束。繼續工作前先用 ${options.cli} action add 列出下一個行動。`;
5165
+ const head = doing ? `[mainflow] 目前行動 ${doing.id} ${doing.do},服務 ${doing.serves.join("、") || "(未指定)"}。這一輪的工作若不屬於它,先調整佇列再做。` : open.length > 0 ? `[mainflow] ${session.id} 目前沒有進行中的行動。` : blocked.length > 0 ? `[mainflow] ${session.id} 的行動在等外部條件:${blocked.map(waitingLine).join(";")}。有條件(until)的可以在背景執行 ${cli} wait <a編號> 等它成立。條件出現後用 ${cli} action set <a編號> --status doing 繼續;要先做別的事,用 ${cli} action add 列出行動。` : `[mainflow] ${session.id} 的行動都已結束。繼續工作前先用 ${cli} action add 列出下一個行動。`;
5047
5166
  const header = headerLine(session);
5048
5167
  const marker = header ? [`- 這一輪最後一則回覆的最後一行寫「${header}」,回覆範例等其他內容都放在它前面。有顯示 hook 時,它會換成標題與連結。`] : [];
5049
5168
  const grants = grantLines(dirs, session);
5050
- const waiters = waiterLines(dirs, session);
5051
- const warnings = queueWarnings(dirs, session, options.cli);
5052
- const digest = contentHash(JSON.stringify([
5053
- doing?.id ?? null,
5054
- grants,
5055
- waiters,
5056
- warnings
5057
- ]));
5058
- const repeated = session.reminderDigest === digest;
5059
- if (!repeated && (session.reminderDigest !== void 0 || grants.length + waiters.length + warnings.length > 0) && readSession(dirs, session.id)) try {
5060
- if (options.saveDigest) options.saveDigest(digest);
5061
- else setReminderDigest(dirs, session.id, digest);
5062
- } catch {}
5063
- const once = (lines, summary, full) => lines.length === 0 ? [] : repeated ? [summary] : full;
5064
- const granted = once(grants, `- 預先授權同上一輪,共 ${grants.length} 條。`, ["- 目前行動服務的主脈絡與所屬的組預先授權了這些動作:", ...grants.map((g) => ` ${g}`)]);
5065
- const waiting = once(waiters, `- 等待行同上一輪,共 ${waiters.length} 條。`, waiters.map((w) => `- ${w}`));
5066
- const warned = once(warnings, `- 佇列提醒同上一輪,共 ${warnings.length} 條。`, warnings.map((w) => `- ${w}`));
5169
+ const granted = grants.length > 0 ? ["- 目前行動服務的主脈絡與所屬的組預先授權了這些動作:", ...grants.map((g) => ` ${g}`)] : [];
5170
+ const waiting = waiterLines(dirs, session).map((w) => `- ${w}`);
5171
+ const warned = queueWarnings(dirs, session, cli).map((w) => `- ${w}`);
5067
5172
  return [
5068
5173
  head,
5069
5174
  ...waiting,
@@ -8404,6 +8509,7 @@ function resolveTurns(session, t) {
8404
8509
  };
8405
8510
  const first = t.turns.findIndex((x) => x.head !== void 0);
8406
8511
  const out = [];
8512
+ let latest;
8407
8513
  t.turns.forEach((turn, i) => {
8408
8514
  const span = {
8409
8515
  from: turn.at,
@@ -8421,39 +8527,50 @@ function resolveTurns(session, t) {
8421
8527
  });
8422
8528
  return;
8423
8529
  }
8424
- const head = turn.head;
8425
- if (head && "action" in head) {
8530
+ if (turn.head) latest = turn.head;
8531
+ const head = latest;
8532
+ const started = turn.started.find((id) => byId.has(id));
8533
+ const next = turn.handoff ? nextNamed(i) : void 0;
8534
+ const call = turn.head || turn.reportsOn === void 0 ? void 0 : t.agentCalls.get(turn.reportsOn);
8535
+ const spawner = call === void 0 ? void 0 : [...out].reverse().find((r) => r.from <= call);
8536
+ const switchedFrom = (mainline) => mainline !== null && turn.started.some((id) => {
8537
+ const a = byId.get(id);
8538
+ return a !== void 0 && primaryMainline(a.serves) !== mainline;
8539
+ });
8540
+ if (spawner && ("action" in head || !(started || next))) {
8541
+ out.push({
8542
+ ...span,
8543
+ mainline: spawner.mainline,
8544
+ source: "report",
8545
+ shared: spawner.shared,
8546
+ switched: switchedFrom(spawner.mainline)
8547
+ });
8548
+ return;
8549
+ }
8550
+ if ("action" in head) {
8426
8551
  serves = servesOf(head);
8427
- source = "head";
8428
- } else if (head) {
8429
- const started = turn.started.find((id) => byId.has(id));
8430
- const next = turn.handoff ? nextNamed(i) : void 0;
8552
+ source = turn.head ? "head" : "previous";
8553
+ } else if (turn.head || started || next) {
8431
8554
  serves = started ? byId.get(started).serves : next ? servesOf(next) : void 0;
8432
8555
  source = started ? "started" : next ? "handoff" : "none";
8433
8556
  } else {
8434
- const call = turn.reportsOn ? t.agentCalls.get(turn.reportsOn) : void 0;
8435
- const spawner = call === void 0 ? void 0 : [...out].reverse().find((r) => r.from <= call);
8436
- const from = spawner ?? out.at(-1);
8557
+ const from = out.at(-1);
8437
8558
  out.push({
8438
8559
  ...span,
8439
8560
  mainline: from.mainline,
8440
- source: spawner ? "report" : "previous",
8561
+ source: "previous",
8441
8562
  shared: from.shared,
8442
8563
  switched: false
8443
8564
  });
8444
8565
  return;
8445
8566
  }
8446
8567
  const mainline = serves ? primaryMainline(serves) ?? null : null;
8447
- const switched = mainline !== null && turn.started.some((id) => {
8448
- const a = byId.get(id);
8449
- return a !== void 0 && primaryMainline(a.serves) !== mainline;
8450
- });
8451
8568
  out.push({
8452
8569
  ...span,
8453
8570
  mainline,
8454
8571
  source,
8455
8572
  shared: serves !== void 0 && mainlinesOf(serves).length > 1,
8456
- switched
8573
+ switched: switchedFrom(mainline)
8457
8574
  });
8458
8575
  });
8459
8576
  return out;
@@ -9482,4 +9599,4 @@ function openInApp(hub, target, closeAfterMs = 150) {
9482
9599
  return true;
9483
9600
  }
9484
9601
  //#endregion
9485
- export { loadConfig as $, WriteError as $t, transcriptPath as A, MAINLINE_FILE as At, applyOffline as B, registeredName as Bt, turnReminder as C, readsAsText as Ct, readNotices as D, hubFormatVersion as Dt, parseWarn as E, formatMismatch as Et, headerLine as F, inRepositoryHubAt as Ft, scopeOfPath as G, nodeStorage as Gt, renumberFiles as H, resolveHub as Ht, mayDecorate as I, listHubs as It, syncedDir as J, hubPath as Jt, hubDigest as K, currentBranch as Kt, attributeTrace as L, notRegisteredMessage as Lt, matchGrants as M, requireSlug as Mt, decorateReply as N, hubAtRoot as Nt, loadSessions as O, hubRequirements as Ot, endsWithMarker as P, hubHoldingFile as Pt, HubError as Q, DATA_DIR as Qt, traceTranscript as R, readRegistry as Rt, sessionBriefing as S, exceedsFileLimit as St, markNoticesHandled as T, textBytes as Tt, renumberPath as U, unregisterHub as Ut, renumberRequest as V, registryFile as Vt, renumberText as W, userDataDir as Wt, applyOperation as X, worktreesOf as Xt, syncedPath as Y, realPath as Yt, checkHubName as Z, ConflictError as Zt, ARCHIVE_KINDS as _, MAX_FILE_BYTES as _t, openInApp as a, memberOfGroup as at, untilState as b, VERSION_HEADER as bt, siteBlock as c, unavailableHubs as ct, migrateHubData as d, readFrontMatter as dt, contentHash as en, loadHub as et, migrateHub as f, readYaml as ft, RETRY_RULES as g, MAX_CHANGE_PAGE as gt, receiverFromEnv as h, MAX_BLOB_BYTES as ht, hostName as i, linksInto as it, grantScope as j, TASK_FILE as jt, readSession as k, versionMismatch as kt, syncHost as l, taskFile as lt, retroReportFromReceiver as m, BLOB_HASH as mt, DEFAULT_UI_PORT as n, dependencyGraph as nt, removeDanglingLinks as o, refIndex as ot, retroReport as p, ACCESS_HEADERS as pt, mayHoldSynced as q, dataFile as qt, caddySites as r, mainlineEdges as rt, removeHostLink as s, remoteMembers as st, APP_BUNDLE_ID as t, splitFrontMatter as tn, sectionText as tt, groupPlan as u, nowMinute as ut, setArchivedMany as v, ROUTES as vt, HANDLED_FILE as w, requestTime as wt, duplicateClaimLines as x, comparePaths as xt, initHub as y, SOCKET_MAX_AGE_CLOSE as yt, transcriptTitle as z, registerHub as zt };
9602
+ export { isOpenMainline as $, realPath as $t, transcriptPath as A, hubRequirements as At, applyOffline as B, notRegisteredMessage as Bt, turnReminder as C, comparePaths as Ct, readNotices as D, textBytes as Dt, parseWarn as E, requestTime as Et, headerLine as F, requireSlug as Ft, scopeOfPath as G, resolveHub as Gt, renumberFiles as H, registerHub as Ht, mayDecorate as I, hubAtRoot as It, syncedDir as J, nodeStorage as Jt, hubDigest as K, unregisterHub as Kt, attributeTrace as L, hubHoldingFile as Lt, matchGrants as M, MAINLINE_FILE as Mt, decorateReply as N, SESSIONS_DIR as Nt, loadSessions as O, formatMismatch as Ot, endsWithMarker as P, TASK_FILE as Pt, HubError as Q, hubPath as Qt, traceTranscript as R, inRepositoryHubAt as Rt, sessionBriefing as S, VERSION_HEADER as St, markNoticesHandled as T, readsAsText as Tt, renumberPath as U, registeredName as Ut, renumberRequest as V, readRegistry as Vt, renumberText as W, registryFile as Wt, applyOperation as X, dataFile as Xt, syncedPath as Y, currentBranch as Yt, checkHubName as Z, gitCheckout as Zt, ARCHIVE_KINDS as _, MAX_BLOB_BYTES as _t, openInApp as a, splitFrontMatter as an, mainlineEdges as at, untilState as b, ROUTES as bt, siteBlock as c, refIndex as ct, migrateHubData as d, taskFile as dt, worktreesOf as en, loadConfig as et, migrateHub as f, nowMinute as ft, RETRY_RULES as g, BLOB_HASH as gt, receiverFromEnv as h, ACCESS_HEADERS as ht, hostName as i, contentHash as in, dependencyGraph as it, grantScope as j, versionMismatch as jt, readSession as k, hubFormatVersion as kt, syncHost as l, remoteMembers as lt, retroReportFromReceiver as m, readYaml as mt, DEFAULT_UI_PORT as n, DATA_DIR as nn, loadMainlines as nt, removeDanglingLinks as o, linksInto as ot, retroReport as p, readFrontMatter as pt, mayHoldSynced as q, userDataDir as qt, caddySites as r, WriteError as rn, sectionText as rt, removeHostLink as s, memberOfGroup as st, APP_BUNDLE_ID as t, ConflictError as tn, loadHub as tt, groupPlan as u, unavailableHubs as ut, setArchivedMany as v, MAX_CHANGE_PAGE as vt, HANDLED_FILE as w, exceedsFileLimit as wt, duplicateClaimLines as x, SOCKET_MAX_AGE_CLOSE as xt, initHub as y, MAX_FILE_BYTES as yt, transcriptTitle as z, listHubs as zt };
package/dist/host.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { o as HubDirs } from "./index-DRpamPge.js";
1
+ import { o as HubDirs } from "./index-jq7lN8V8.js";
2
2
  //#region src/host.d.ts
3
3
  export declare const DEFAULT_UI_PORT = 4320;
4
4
  export declare const hostName: (hub: string) => string;
package/dist/host.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as openInApp, c as siteBlock, i as hostName, l as syncHost, n as DEFAULT_UI_PORT, o as removeDanglingLinks, r as caddySites, s as removeHostLink, t as APP_BUNDLE_ID } from "./host-BJvqZy-i.js";
1
+ import { a as openInApp, c as siteBlock, i as hostName, l as syncHost, n as DEFAULT_UI_PORT, o as removeDanglingLinks, r as caddySites, s as removeHostLink, t as APP_BUNDLE_ID } from "./host-C74j2Qox.js";
2
2
  export { APP_BUNDLE_ID, DEFAULT_UI_PORT, caddySites, hostName, openInApp, removeDanglingLinks, removeHostLink, siteBlock, syncHost };
@@ -1,4 +1,4 @@
1
- import { _ as Task, a as Decision, c as Group, d as Mainline, f as OpsEntry, h as Session, i as ChecklistItem, n as ActionBlock, s as Finding, t as Action, u as Issue } from "./index-BqPQp6TF.js";
1
+ import { _ as Task, a as Decision, c as Group, d as Mainline, f as OpsEntry, h as Session, i as ChecklistItem, n as ActionBlock, s as Finding, t as Action, u as Issue } from "./index-l8QUG80k.js";
2
2
  import { z } from "zod";
3
3
  //#region ../core/dist/ids/allocators.d.ts
4
4
  /** The letters ids start with. */
@@ -437,11 +437,16 @@ declare function setSessionInfo(hub: HubReader, input: {
437
437
  id: string;
438
438
  info: SessionInfo;
439
439
  }): OpResult<Session>;
440
- /** Remember the digest of what the last turn reminder listed in full; null forgets it, so the next reminder lists everything again. */
440
+ /** Remember the digest of the whole turn reminder last sent; null forgets it, so the next reminder is sent again. */
441
441
  declare function setReminderDigest(hub: HubReader, input: {
442
442
  id: string;
443
443
  digest: string | null;
444
444
  }): OpResult<Session>;
445
+ /** Remember the digests of the check problems last handed over after a write; null or an empty list forgets them, so every problem is reported again. */
446
+ declare function setCheckReported(hub: HubReader, input: {
447
+ id: string;
448
+ reported: string[] | null;
449
+ }): OpResult<Session>;
445
450
  declare function setSessionStatus(hub: HubReader, input: {
446
451
  id: string;
447
452
  status: string;
@@ -490,7 +495,7 @@ declare function updateAction(hub: HubReader, input: {
490
495
  * (`item check` and `item uncheck` send `item.check`, with done true or false). The operations no command sends
491
496
  * are listed in COMMANDLESS_OPERATIONS with who sends them.
492
497
  */
493
- declare const OPERATION_NAMES: readonly ['repo.add', 'hub.add', 'mainline.new', 'mainline.set', 'mainline.title', 'mainline.archive', 'criterion.add', 'criterion.check', 'criterion.set', 'task.new', 'task.set', 'item.add', 'item.check', 'item.drop', 'item.undrop', 'decision.new', 'decision.accept', 'decision.supersede', 'issue.new', 'issue.set', 'issue.plan', 'issue.close', 'issue.reopen', 'issue.archive', 'finding.new', 'finding.set', 'finding.archive', 'group.new', 'group.set', 'ops.add', 'section.append', 'session.start', 'session.ensure', 'session.handoff', 'session.info', 'session.reminder', 'session.set', 'session.settle', 'session.archive', 'action.add', 'action.set', 'file.remove'];
498
+ declare const OPERATION_NAMES: readonly ['repo.add', 'hub.add', 'mainline.new', 'mainline.set', 'mainline.title', 'mainline.archive', 'criterion.add', 'criterion.check', 'criterion.set', 'task.new', 'task.set', 'item.add', 'item.check', 'item.drop', 'item.undrop', 'decision.new', 'decision.accept', 'decision.supersede', 'issue.new', 'issue.set', 'issue.plan', 'issue.close', 'issue.reopen', 'issue.archive', 'finding.new', 'finding.set', 'finding.archive', 'group.new', 'group.set', 'ops.add', 'section.append', 'session.start', 'session.ensure', 'session.handoff', 'session.info', 'session.reminder', 'session.checked', 'session.set', 'session.settle', 'session.archive', 'action.add', 'action.set', 'file.remove'];
494
499
  type OperationName = (typeof OPERATION_NAMES)[number];
495
500
  //#endregion
496
501
  //#region ../core/dist/ops/operations.d.ts
@@ -537,6 +542,7 @@ declare const OPERATIONS: {
537
542
  readonly 'session.handoff': typeof handoffSession;
538
543
  readonly 'session.info': typeof setSessionInfo;
539
544
  readonly 'session.reminder': typeof setReminderDigest;
545
+ readonly 'session.checked': typeof setCheckReported;
540
546
  readonly 'session.set': typeof setSessionStatus;
541
547
  readonly 'session.settle': typeof settleSession;
542
548
  readonly 'session.archive': typeof setSessionArchived;