mainflow 0.23.0 → 0.25.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.
@@ -1,7 +1,7 @@
1
- import { A as findingSchema, B as taskSchema, C as SURVEY_LINES, D as WORK_STATUS, E as TASK_SECTIONS, F as parseOpsLine, G as describeRequest, I as parseRef, J as grantRepo, K as grantCovers, L as refExample, M as issueSchema, N as mainlineSchema, O as configSchema, P as opsEntrySchema, R as refMainline, S as SLUG, T as TASK_REQUIRED_SECTIONS, V as waitingForRef, Y as parseGrantTarget, _ as MERGE_STATES, a as DECISION_SECTIONS, b as RESERVED_HUB_NAMES, c as FINDING_SECTIONS, f as ISSUE_SECTIONS, g as MAINLINE_STATUS, h as MAINLINE_SECTIONS, i as DECISION_BASES, j as formatOpsEntry, k as decisionSchema, l as FINDING_STATUS, n as CLOSED_STATUS, q as grantForm, r as DECIDERS, s as DEFAULT_CATEGORIES, t as CHECKLIST_REF, w as SURVEY_SECTION, x as SESSION_STATUS, y as REF_EXAMPLES, z as sessionSchema } from "./dist-Dy45myuN.js";
1
+ import { $ as grantRepo, A as configSchema, B as parseUntil, C as SLUG, D as TASK_SECTIONS, E as TASK_REQUIRED_SECTIONS, F as mainlineSchema, G as waitingForAction, H as refMainline, I as opsEntrySchema, K as waitingForRef, L as parseActionRef, M as findingSchema, N as formatOpsEntry, P as issueSchema, Q as grantForm, R as parseOpsLine, S as SESSION_STATUS, T as SURVEY_SECTION, U as sessionSchema, V as refExample, W as taskSchema, X as describeRequest, Z as grantCovers, _ as MERGE_STATES, a as DECISION_SECTIONS, b as RESERVED_HUB_NAMES, c as FINDING_SECTIONS, et as parseGrantTarget, f as ISSUE_SECTIONS, g as MAINLINE_STATUS, h as MAINLINE_SECTIONS, i as DECISION_BASES, j as decisionSchema, k as WORK_STATUS, l as FINDING_STATUS, n as CLOSED_STATUS, r as DECIDERS, s as DEFAULT_CATEGORIES, t as CHECKLIST_REF, w as SURVEY_LINES, x as SESSION_ID, y as REF_EXAMPLES, z as parseRef } from "./dist-DlmecRLf.js";
2
2
  import { appendFileSync, closeSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, symlinkSync, writeFileSync, writeSync } from "node:fs";
3
3
  import { dirname, isAbsolute, join, parse, relative, resolve, sep } from "node:path";
4
- import { spawn, spawnSync } from "node:child_process";
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";
7
7
  //#region ../core/dist/paths.js
@@ -637,6 +637,7 @@ function loadHub(root, options = {}) {
637
637
  mainlines.push({
638
638
  ...mainline,
639
639
  dir: rel(mainlineDir),
640
+ artifacts: listFiles(join(mainlineDir, "artifacts")).map(rel),
640
641
  views: listFiles(join(mainlineDir, "views")).map(rel),
641
642
  decisions,
642
643
  tasks
@@ -1387,6 +1388,50 @@ function splitActionRef(ref) {
1387
1388
  action
1388
1389
  };
1389
1390
  }
1391
+ /** The note a handoff leaves on the dropped action, naming where the work went. */
1392
+ const handedOffNote = (to) => `handed off to ${to}`;
1393
+ const HANDED_OFF = /(?:^|; )handed off to ([a-z0-9][a-z0-9-]*#a\d{3})$/;
1394
+ /**
1395
+ * Find the action a reference names. A handoff drops the action and names its new place in the note, so the
1396
+ * reference is followed there: a wait on the old reference keeps working after the work moved to another session.
1397
+ */
1398
+ function resolveAction(root, ref) {
1399
+ const seen = /* @__PURE__ */ new Set();
1400
+ let current = ref;
1401
+ for (;;) {
1402
+ const parsed = parseActionRef(current);
1403
+ if (!parsed) return {
1404
+ ref: current,
1405
+ missing: `"${current}" is not an action reference like claude-code-1a2b3c4d#a003`
1406
+ };
1407
+ let session;
1408
+ try {
1409
+ session = readSession(root, parsed.session);
1410
+ } catch (error) {
1411
+ return {
1412
+ ref: current,
1413
+ missing: error.message
1414
+ };
1415
+ }
1416
+ if (!session) return {
1417
+ ref: current,
1418
+ missing: `session ${parsed.session} not found`
1419
+ };
1420
+ const action = session.actions.find((a) => a.id === parsed.action);
1421
+ if (!action) return {
1422
+ ref: current,
1423
+ missing: `action ${current} not found`
1424
+ };
1425
+ const next = action.status === "dropped" ? HANDED_OFF.exec(action.note ?? "")?.[1] : void 0;
1426
+ if (!next || seen.has(next)) return {
1427
+ ref: current,
1428
+ session,
1429
+ action
1430
+ };
1431
+ seen.add(current);
1432
+ current = next;
1433
+ }
1434
+ }
1390
1435
  /**
1391
1436
  * Every served checklist item must exist, so a session never points at work that is not in a mainline. Its
1392
1437
  * mainline must still be open: follow-up work on an ended one either reopens it with a criterion or task for the
@@ -1422,7 +1467,7 @@ function takeUnfinished(root, from, to, existing) {
1422
1467
  ...a,
1423
1468
  id
1424
1469
  });
1425
- const handedOff = `handed off to ${to}#${id}`;
1470
+ const handedOff = handedOffNote(`${to}#${id}`);
1426
1471
  return {
1427
1472
  ...a,
1428
1473
  status: "dropped",
@@ -1443,7 +1488,7 @@ function takeUnfinished(root, from, to, existing) {
1443
1488
  */
1444
1489
  function startSession(root, input) {
1445
1490
  const id = input.id ?? `${input.agent}-${randomBytes(4).toString("hex")}`;
1446
- if (!/^[a-z0-9][a-z0-9-]*$/.test(id)) throw new WriteError(`session id "${id}" must be lowercase letters, digits and hyphens`);
1491
+ if (!SESSION_ID.test(id)) throw new WriteError(`session id "${id}" must be lowercase letters, digits and hyphens`);
1447
1492
  return withLock(root, () => {
1448
1493
  const file = sessionFile(root, id);
1449
1494
  if (existsSync(file)) throw new WriteError(`session ${id} already exists`);
@@ -1585,16 +1630,40 @@ function addAction(root, sessionId, input) {
1585
1630
  });
1586
1631
  return `${sessionId}#${created}`;
1587
1632
  }
1633
+ /**
1634
+ * What a blocked action waits for must be checkable: an action it names has to exist and, followed through
1635
+ * handoffs, cannot be the action itself, and an `until` has to be one of the condition forms. An old reference
1636
+ * to work that has since been handed off to this very action would otherwise wait forever.
1637
+ */
1638
+ function requireBlock(root, ref, block) {
1639
+ const targets = [waitingForAction(block.waitingFor), block.until === void 0 ? void 0 : untilTarget(block.until)];
1640
+ for (const target of targets) {
1641
+ if (!target) continue;
1642
+ const found = resolveAction(root, target);
1643
+ if ("missing" in found) throw new WriteError(`${found.missing}; --waiting-for and --until action-done take an existing action like claude-code-1a2b3c4d#a003`);
1644
+ if (found.ref === ref) throw new WriteError(`action ${ref} cannot wait for itself${target === ref ? "" : ` (${target} was handed off to it)`}`);
1645
+ }
1646
+ }
1647
+ /** The action an `until` waits for; throws when the condition matches none of the forms. */
1648
+ function untilTarget(until) {
1649
+ const parsed = parseUntil(until);
1650
+ if (!parsed) throw new WriteError(`--until takes a condition like commit:{branch}:{text}, action-done:{sessionId}#a{NNN} or file:{relative path}, got "${until}"`);
1651
+ return parsed.kind === "action-done" ? `${parsed.session}#${parsed.action}` : void 0;
1652
+ }
1588
1653
  function updateAction(root, ref, patch) {
1589
1654
  const { session, action } = splitActionRef(ref);
1590
1655
  const status = patch.status === void 0 ? void 0 : requireOneOf(patch.status, WORK_STATUS, "action status");
1591
1656
  if (patch.serves) requireServes(root, patch.serves);
1592
1657
  if (patch.before === action) throw new WriteError(`action ${action} cannot be placed before itself`);
1658
+ if (status === "blocked" && !patch.blockedBy?.reason) throw new WriteError(`blocking ${ref} needs a reason, e.g. --reason "等 schema 定案" --waiting-for claude-code-1a2b3c4d#a003 --until action-done:claude-code-1a2b3c4d#a003`);
1659
+ if (patch.blockedBy) requireBlock(root, `${session}#${action}`, patch.blockedBy);
1593
1660
  let updated;
1594
1661
  mutateYaml(root, requireSession(root, session), sessionSchema, (s) => {
1595
1662
  const index = s.actions.findIndex((a) => a.id === action);
1596
1663
  if (index < 0) throw new WriteError(`action ${ref} not found`);
1597
1664
  const [current] = s.actions.splice(index, 1);
1665
+ const handedTo = patch.note ? HANDED_OFF.exec(current.note ?? "")?.[1] : void 0;
1666
+ 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`);
1598
1667
  const next = {
1599
1668
  ...current,
1600
1669
  ...status && { status },
@@ -1602,6 +1671,11 @@ function updateAction(root, ref, patch) {
1602
1671
  ...patch.serves && { serves: patch.serves },
1603
1672
  ...patch.note && { note: patch.note }
1604
1673
  };
1674
+ if (patch.blockedBy) {
1675
+ if (next.status !== "blocked") throw new WriteError(`--reason, --waiting-for and --until go with --status blocked; ${ref} is ${next.status}`);
1676
+ next.blockedBy = patch.blockedBy;
1677
+ }
1678
+ if (next.status !== "blocked") delete next.blockedBy;
1605
1679
  if (status === "doing" && current.status !== "doing") {
1606
1680
  next.startedAt = nowMinute();
1607
1681
  delete next.doneAt;
@@ -1653,8 +1727,16 @@ function queueWarnings(root, session, cli) {
1653
1727
  }
1654
1728
  const current = (session) => session.actions.find((a) => a.status === "doing");
1655
1729
  const isOpen = (a) => a.status === "todo" || a.status === "doing";
1656
- /** A blocked action and what it waits for; the note is where `action set --note` records the wait. */
1657
- const waitingLine = (a) => `${a.id} ${a.do}${a.note ? `(${a.note})` : ""}`;
1730
+ /** A blocked action and what it waits for: the reason, the other party and the condition, as `action set --status blocked` recorded them. */
1731
+ function waitingLine(a) {
1732
+ const block = a.blockedBy;
1733
+ const parts = block ? [
1734
+ block.reason,
1735
+ ...block.waitingFor ? [`等 ${block.waitingFor}`] : [],
1736
+ ...block.until ? [`條件 ${block.until}`] : []
1737
+ ] : [];
1738
+ return `${a.id} ${a.do}${parts.length > 0 ? `(${parts.join(";")})` : ""}`;
1739
+ }
1658
1740
  /** Mainlines a session could be pushing forward. */
1659
1741
  const openMainlines = (root) => loadHub(root).mainlines.filter((m) => !m.archivedAt && (m.status === "active" || m.status === "planning"));
1660
1742
  /** How recently a session's transcript must have been written for the session to count as possibly still running. */
@@ -1668,6 +1750,69 @@ function mayBeRunning(root, session) {
1668
1750
  return false;
1669
1751
  }
1670
1752
  }
1753
+ /** The session and every session in its delegation tree: the sessions that delegated to it, and theirs, and all their subagents. */
1754
+ function family(sessions, id) {
1755
+ const byId = new Map(sessions.map((s) => [s.id, s]));
1756
+ const chain = /* @__PURE__ */ new Set([id]);
1757
+ let top = id;
1758
+ for (let up = byId.get(id)?.delegatedFrom; up && byId.has(up) && !chain.has(up); up = byId.get(up)?.delegatedFrom) {
1759
+ chain.add(up);
1760
+ top = up;
1761
+ }
1762
+ const members = /* @__PURE__ */ new Set([top]);
1763
+ for (let grew = true; grew;) {
1764
+ grew = false;
1765
+ for (const s of sessions) if (s.delegatedFrom && members.has(s.delegatedFrom) && !members.has(s.id)) {
1766
+ members.add(s.id);
1767
+ grew = true;
1768
+ }
1769
+ }
1770
+ return members.add(id);
1771
+ }
1772
+ /** The actions in this session that a blocked action elsewhere waits for, named by `waitingFor` or by an `until` of kind action-done. */
1773
+ function waitedFor(root, blocked) {
1774
+ const until = blocked.blockedBy?.until ? parseUntil(blocked.blockedBy.until) : null;
1775
+ const refs = [waitingForAction(blocked.blockedBy?.waitingFor), until?.kind === "action-done" ? `${until.session}#${until.action}` : void 0];
1776
+ return [...new Set(refs.filter((r) => r !== void 0).map((r) => resolveAction(root, r).ref))];
1777
+ }
1778
+ /**
1779
+ * One line for each blocked action in another session that waits for an action of this one, followed through
1780
+ * handoffs, so the session being waited for sees it without the other side having to send a message. An action
1781
+ * already done or dropped has nothing left to deliver, so a wait on it is not shown.
1782
+ */
1783
+ function waiterLines(root, session) {
1784
+ const lines = [];
1785
+ for (const other of loadSessions(root).sessions) {
1786
+ if (other.id === session.id || other.archivedAt || other.status === "handed-off") continue;
1787
+ for (const blocked of other.actions.filter((a) => a.status === "blocked")) for (const ref of waitedFor(root, blocked)) {
1788
+ const mine = ref.startsWith(`${session.id}#`) ? session.actions.find((a) => `${session.id}#${a.id}` === ref) : void 0;
1789
+ if (!mine || mine.status === "done" || mine.status === "dropped") continue;
1790
+ const until = blocked.blockedBy?.until ? `。條件:${blocked.blockedBy.until}` : "";
1791
+ lines.push(`有會話在等你:${other.id}#${blocked.id} 等 ${mine.id} ${mine.do}(${blocked.blockedBy.reason})${until}`);
1792
+ }
1793
+ }
1794
+ return lines;
1795
+ }
1796
+ /**
1797
+ * One reminder for each action in progress in another session that may still be running and serves an item
1798
+ * this action also serves. Sessions in the same delegation tree are left out: a subagent working on the item
1799
+ * its parent handed it is not a second claim.
1800
+ */
1801
+ function duplicateClaimLines(root, sessionId, actionId) {
1802
+ const { sessions } = loadSessions(root);
1803
+ const action = sessions.find((s) => s.id === sessionId)?.actions.find((a) => a.id === actionId);
1804
+ if (!action || action.serves.length === 0) return [];
1805
+ const related = family(sessions, sessionId);
1806
+ const lines = [];
1807
+ for (const other of sessions) {
1808
+ if (related.has(other.id) || !mayBeRunning(root, other)) continue;
1809
+ for (const theirs of other.actions.filter((a) => a.status === "doing")) {
1810
+ const shared = theirs.serves.filter((ref) => action.serves.includes(ref));
1811
+ if (shared.length > 0) lines.push(`⚠ ${other.id}#${theirs.id}(15 分鐘內有活動)也在做 ${shared.join("、")},先協調再開始`);
1812
+ }
1813
+ }
1814
+ return lines;
1815
+ }
1671
1816
  /**
1672
1817
  * Lines naming the other top-level sessions that still hold unfinished actions, with the command that takes them
1673
1818
  * over into this one. Subagent sessions are left out: their work returns to the session that delegated it.
@@ -1704,6 +1849,7 @@ function sessionBriefing(root, session, options) {
1704
1849
  const blocked = session.actions.filter((a) => a.status === "blocked");
1705
1850
  if (blocked.length > 0) lines.push(`等外部條件:${blocked.map(waitingLine).join(";")}`);
1706
1851
  }
1852
+ lines.push(...waiterLines(root, session));
1707
1853
  if (!session.actions.some(isOpen)) lines.push(...handoffLines(root, session, cli));
1708
1854
  const warnings = queueWarnings(root, session, cli);
1709
1855
  if (warnings.length > 0) lines.push("需要處理:", ...warnings.map((w) => `- ${w}`));
@@ -1716,18 +1862,115 @@ function turnReminder(root, session, options) {
1716
1862
  const doing = current(session);
1717
1863
  const open = session.actions.filter(isOpen);
1718
1864
  const blocked = session.actions.filter((a) => a.status === "blocked");
1719
- 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(";")}。條件出現後用 ${options.cli} action set <a編號> --status doing 繼續;要先做別的事,用 ${options.cli} action add 列出行動。` : `[mainflow] ${session.id} 的行動都已結束。繼續工作前先用 ${options.cli} action add 列出下一個行動。`;
1865
+ 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 列出下一個行動。`;
1720
1866
  const header = headerLine(session);
1721
1867
  const marker = header ? [`- 這一輪最後一則回覆的最後一行寫「${header}」,回覆範例等其他內容都放在它前面。有顯示 hook 時,它會換成標題與連結。`] : [];
1722
1868
  const grants = grantLines(root, session);
1869
+ const granted = grants.length > 0 ? ["- 目前行動服務的主脈絡預先授權了這些動作:", ...grants.map((g) => ` ${g}`)] : [];
1723
1870
  return [
1724
1871
  head,
1725
- ...grants.length > 0 ? ["- 目前行動服務的主脈絡預先授權了這些動作:", ...grants.map((g) => ` ${g}`)] : [],
1872
+ ...waiterLines(root, session).map((w) => `- ${w}`),
1873
+ ...granted,
1726
1874
  ...marker,
1727
1875
  ...queueWarnings(root, session, options.cli).map((w) => `- ${w}`)
1728
1876
  ].join("\n");
1729
1877
  }
1730
1878
  //#endregion
1879
+ //#region ../core/dist/waiting.js
1880
+ /** Separates commits in the git log output (%x1e); commit messages do not use this control character. */
1881
+ const RECORD = "";
1882
+ /** Whether git finds a repository at the hub root; false as well when git itself cannot run. */
1883
+ function inGitRepository(root) {
1884
+ try {
1885
+ execFileSync("git", ["rev-parse", "--git-dir"], {
1886
+ cwd: root,
1887
+ stdio: "ignore"
1888
+ });
1889
+ return true;
1890
+ } catch {
1891
+ return false;
1892
+ }
1893
+ }
1894
+ /** Whether a commit on the branch has the text in its message; git runs at the hub root, so the branch is the hub repository's. */
1895
+ function commitState(root, branch, text) {
1896
+ let log;
1897
+ try {
1898
+ log = execFileSync("git", [
1899
+ "log",
1900
+ "--format=%h %s%n%b%x1e",
1901
+ branch,
1902
+ "--"
1903
+ ], {
1904
+ cwd: root,
1905
+ encoding: "utf8",
1906
+ stdio: [
1907
+ "ignore",
1908
+ "pipe",
1909
+ "pipe"
1910
+ ],
1911
+ maxBuffer: 268435456
1912
+ });
1913
+ } catch {
1914
+ if (!inGitRepository(root)) return {
1915
+ met: false,
1916
+ unreachable: true,
1917
+ detail: `the hub root ${root} is not in a git repository, so no commit can appear there`
1918
+ };
1919
+ return {
1920
+ met: false,
1921
+ detail: `branch ${branch} is not in the repository at the hub root yet`
1922
+ };
1923
+ }
1924
+ for (const record of log.split(RECORD)) {
1925
+ const entry = record.replace(/^\n/, "");
1926
+ const space = entry.indexOf(" ");
1927
+ const message = entry.slice(space + 1);
1928
+ if (space > 0 && message.includes(text)) return {
1929
+ met: true,
1930
+ detail: `commit ${entry.slice(0, space)} on ${branch} contains "${text}": ${message.split("\n")[0]}`
1931
+ };
1932
+ }
1933
+ return {
1934
+ met: false,
1935
+ detail: `no commit on ${branch} contains "${text}"`
1936
+ };
1937
+ }
1938
+ function actionState(root, ref) {
1939
+ const found = resolveAction(root, ref);
1940
+ if ("missing" in found) return {
1941
+ met: false,
1942
+ detail: found.missing
1943
+ };
1944
+ const via = found.ref === ref ? "" : ` (handed off to ${found.ref})`;
1945
+ if (found.action.status === "done") return {
1946
+ met: true,
1947
+ detail: `${ref}${via} is done`
1948
+ };
1949
+ if (found.action.status === "dropped") return {
1950
+ met: false,
1951
+ unreachable: true,
1952
+ detail: `${ref}${via} was dropped without a handoff, so it will never be done`
1953
+ };
1954
+ return {
1955
+ met: false,
1956
+ detail: `${ref}${via} is ${found.action.status}`
1957
+ };
1958
+ }
1959
+ /** Check a wait condition once. */
1960
+ function untilState(root, until) {
1961
+ switch (until.kind) {
1962
+ case "commit": return commitState(root, until.branch, until.text);
1963
+ case "action-done": return actionState(root, `${until.session}#${until.action}`);
1964
+ case "file": return existsSync(resolve(root, until.path)) ? {
1965
+ met: true,
1966
+ detail: `${until.path} exists`
1967
+ } : {
1968
+ met: false,
1969
+ detail: `${until.path} does not exist yet`
1970
+ };
1971
+ }
1972
+ }
1973
+ //#endregion
1731
1974
  //#region ../core/dist/write.js
1732
1975
  function checkTaskLinks(root, mainline, task) {
1733
1976
  if (task.repo && !(task.repo in readConfig(root).repos)) throw new WriteError(`repo "${task.repo}" is not listed in .mainflow/config.yaml; add it under repos first`);
@@ -2354,4 +2597,4 @@ function openInApp(hub, target, closeAfterMs = 150) {
2354
2597
  return true;
2355
2598
  }
2356
2599
  //#endregion
2357
- export { reopenIssue as $, addAction as A, transcriptPath as B, setCriterionText as C, updateTask as D, supersedeDecision as E, setSessionArchived as F, endsWithMarker as G, grantScope as H, setSessionInfo as I, appendOps as J, headerLine as K, setSessionStatus as L, handoffSession as M, loadSessions as N, sessionBriefing as O, readSession as P, planIssue as Q, settleSession as R, setCriterion as S, setMainlineTitle as T, matchGrants as U, updateAction as V, decorateReply as W, createFinding as X, closeIssue as Y, createIssue as Z, createTask as _, worktreesOf as _t, openInApp as a, dependencyGraph as at, setArchived as b, syncHost as c, refIndex as ct, setArchivedMany as d, ConflictError as dt, setFindingArchived as et, acceptDecision as f, WriteError as ft, createMainline as g, findHubRoot as gt, createDecision as h, currentBranch as ht, hostName as i, loadHub as it, ensureSession as j, turnReminder as k, migrateHub as l, MAINLINE_FILE as lt, addRepo as m, DATA_DIR as mt, DEFAULT_UI_PORT as n, setIssueArchived as nt, removeHostLink as o, mainlineEdges as ot, addHub as p, readYaml as pt, mayDecorate as q, caddySites as r, HubError as rt, siteBlock as s, linksInto as st, APP_BUNDLE_ID as t, setFindingStatus as tt, ARCHIVE_KINDS as u, TASK_FILE as ut, dropChecklistItem as v, setMainlineStatus as w, setChecklistItem as x, initHub as y, startSession as z };
2600
+ export { createIssue as $, sessionBriefing as A, settleSession as B, setCriterionText as C, updateTask as D, supersedeDecision as E, loadSessions as F, matchGrants as G, transcriptPath as H, readSession as I, headerLine as J, decorateReply as K, setSessionArchived as L, addAction as M, ensureSession as N, untilState as O, handoffSession as P, createFinding as Q, setSessionInfo as R, setCriterion as S, setMainlineTitle as T, updateAction as U, startSession as V, grantScope as W, appendOps as X, mayDecorate as Y, closeIssue as Z, createTask as _, currentBranch as _t, openInApp as a, HubError as at, setArchived as b, syncHost as c, mainlineEdges as ct, setArchivedMany as d, MAINLINE_FILE as dt, planIssue as et, acceptDecision as f, TASK_FILE as ft, createMainline as g, DATA_DIR as gt, createDecision as h, readYaml as ht, hostName as i, setIssueArchived as it, turnReminder as j, duplicateClaimLines as k, migrateHub as l, linksInto as lt, addRepo as m, WriteError as mt, DEFAULT_UI_PORT as n, setFindingArchived as nt, removeHostLink as o, loadHub as ot, addHub as p, ConflictError as pt, endsWithMarker as q, caddySites as r, setFindingStatus as rt, siteBlock as s, dependencyGraph as st, APP_BUNDLE_ID as t, reopenIssue as tt, ARCHIVE_KINDS as u, refIndex as ut, dropChecklistItem as v, findHubRoot as vt, setMainlineStatus as w, setChecklistItem as x, initHub as y, worktreesOf as yt, setSessionStatus as z };
package/dist/host.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as openInApp, c as syncHost, i as hostName, n as DEFAULT_UI_PORT, o as removeHostLink, r as caddySites, s as siteBlock, t as APP_BUNDLE_ID } from "./host-DkaCYnnc.js";
1
+ import { a as openInApp, c as syncHost, i as hostName, n as DEFAULT_UI_PORT, o as removeHostLink, r as caddySites, s as siteBlock, t as APP_BUNDLE_ID } from "./host-Dr0bCHd8.js";
2
2
  export { APP_BUNDLE_ID, DEFAULT_UI_PORT, caddySites, hostName, openInApp, removeHostLink, siteBlock, syncHost };
@@ -23,5 +23,22 @@ interface SurveyLine {
23
23
  /** Text a checked line must contain, e.g. the environment a spike ran in. */
24
24
  requires?: string;
25
25
  }
26
+ /**
27
+ * A condition a program can check, written `{kind}:{argument}`. `commit` holds once a commit message on the
28
+ * branch contains the text, `action-done` once the action is done, `file` once the path exists under the hub
29
+ * root. Git branch names cannot contain a colon, so the text after the branch may.
30
+ */
31
+ type Until = {
32
+ kind: 'commit';
33
+ branch: string;
34
+ text: string;
35
+ } | {
36
+ kind: 'action-done';
37
+ session: string;
38
+ action: string;
39
+ } | {
40
+ kind: 'file';
41
+ path: string;
42
+ };
26
43
  //#endregion
27
- export { SurveyLine as a, SLUG as i, FORMAT_VERSION as n, ID_PATTERNS as r, CHECKLIST_REF as t };
44
+ export { SurveyLine as a, SLUG as i, FORMAT_VERSION as n, Until as o, ID_PATTERNS as r, CHECKLIST_REF as t };
@@ -1,4 +1,4 @@
1
- import { c as syncHost, ft as WriteError, i as hostName, mt as DATA_DIR, n as DEFAULT_UI_PORT, o as removeHostLink } from "./host-DkaCYnnc.js";
1
+ import { c as syncHost, gt as DATA_DIR, i as hostName, mt as WriteError, n as DEFAULT_UI_PORT, o as removeHostLink } from "./host-Dr0bCHd8.js";
2
2
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { spawn, spawnSync } from "node:child_process";
@@ -176,7 +176,7 @@ async function launchUi(root, hub, configured, options) {
176
176
  }
177
177
  let server;
178
178
  if (!reuse) {
179
- const { serveUi } = await import("./ui-Do7fLlUC.js");
179
+ const { serveUi } = await import("./ui-Cz8suWal.js");
180
180
  server = await serveUi(root, choice.port);
181
181
  writeUiRecord(root, {
182
182
  port: choice.port,
package/dist/launch.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as portCandidates, c as watchHubGone, i as networkProbe, n as choosePort, o as readUiRecord, r as launchUi, s as startUiInBackground, t as PORT_SPAN } from "./launch-DOX1TTGx.js";
1
+ import { a as portCandidates, c as watchHubGone, i as networkProbe, n as choosePort, o as readUiRecord, r as launchUi, s as startUiInBackground, t as PORT_SPAN } from "./launch-lnD-Z3hb.js";
2
2
  export { PORT_SPAN, choosePort, launchUi, networkProbe, portCandidates, readUiRecord, startUiInBackground, watchHubGone };
@@ -1,6 +1,6 @@
1
- import { B as transcriptPath, D as updateTask, F as setSessionArchived, N as loadSessions, P as readSession, S as setCriterion, T as setMainlineTitle, a as openInApp, at as dependencyGraph, b as setArchived, ct as refIndex, d as setArchivedMany, dt as ConflictError, et as setFindingArchived, ft as WriteError, it as loadHub, mt as DATA_DIR, nt as setIssueArchived, ot as mainlineEdges, st as linksInto, u as ARCHIVE_KINDS, w as setMainlineStatus, x as setChecklistItem } from "./host-DkaCYnnc.js";
1
+ import { D as updateTask, F as loadSessions, H as transcriptPath, I as readSession, L as setSessionArchived, S as setCriterion, T as setMainlineTitle, a as openInApp, b as setArchived, ct as mainlineEdges, d as setArchivedMany, gt as DATA_DIR, it as setIssueArchived, lt as linksInto, mt as WriteError, nt as setFindingArchived, ot as loadHub, pt as ConflictError, st as dependencyGraph, u as ARCHIVE_KINDS, ut as refIndex, w as setMainlineStatus, x as setChecklistItem } from "./host-Dr0bCHd8.js";
2
2
  import { t as listProgress } from "./progress-1BfPg2zN.js";
3
- import { l as CLI_VERSION } from "./launch-DOX1TTGx.js";
3
+ import { l as CLI_VERSION } from "./launch-lnD-Z3hb.js";
4
4
  import { createRequire } from "node:module";
5
5
  import { closeSync, existsSync, openSync, readFileSync, readSync, statSync, watch } from "node:fs";
6
6
  import { basename, dirname, extname, join, normalize, relative, sep } from "node:path";