mainflow 0.35.0-beta.1 → 0.36.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 CHANGED
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
- import { G as parseRef, K as parseUntil, N as UNTIL_FORMS, et as stampMs, n as CLOSED_STATUS, q as refExample } from "./dist-BdvWGTsb.js";
3
- import { B as applyOperation, C as readSession, H as HubError, Q as nodeStorage, S as loadSessions, V as checkHubName, W as loadHub, X as nowMinute, _ as initHub, _t as DATA_DIR, b as sessionBriefing, d as retroReport, dt as TASK_FILE, f as retroReportFromReceiver, ft as requireSlug, m as RETRY_RULES, mt as findHubRoot, nt as MAX_FILE_BYTES, p as receiverFromEnv, u as migrateHub, ut as MAINLINE_FILE, v as untilState, vt as WriteError, y as duplicateClaimLines } from "./host-bnHYIwuY.js";
2
+ import { G as parseRef, K as parseUntil, N as UNTIL_FORMS, et as stampMs, n as CLOSED_STATUS, q as refExample } from "./dist-ud09dzZo.js";
3
+ import { $ as nowMinute, B as applyOperation, C as readSession, H as HubError, Q as unavailableHubs, S as loadSessions, V as checkHubName, W as loadHub, Y as memberOfGroup, Z as remoteMembers, _ as initHub, _t as findHubRoot, at as MAX_FILE_BYTES, b as sessionBriefing, bt as DATA_DIR, d as retroReport, f as retroReportFromReceiver, ht as requireSlug, m as RETRY_RULES, mt as TASK_FILE, p as receiverFromEnv, pt as MAINLINE_FILE, tt as nodeStorage, u as migrateHub, v as untilState, xt as WriteError, y as duplicateClaimLines } from "./host-BauijAdm.js";
4
4
  import { t as listProgress } from "./progress-1BfPg2zN.js";
5
- import { C as ServerError, D as syncSettings, E as hubName, O as SETTINGS_FILE, S as ServerApi, T as earlierToken, _ as readDaemon, a as remoteMode, b as readRemote, c as planAnnounce, d as toolCallOf, f as startDaemon, g as probeDaemon, h as callDaemon, i as queuedCount, j as syncPaths, l as planFetch, m as DaemonUnavailable, n as hubWriter, o as fetchDenial, p as DaemonRunning, r as writtenLines, s as hookAnswer, t as CONFIRM_MS, u as planFlush, v as silentHint, w as ENV, x as oversizedFiles } from "./operation-DVcJgKM9.js";
5
+ import { C as ServerError, D as syncSettings, E as hubName, O as SETTINGS_FILE, S as ServerApi, T as earlierToken, _ as readDaemon, a as remoteMode, b as readRemote, c as planAnnounce, d as toolCallOf, f as startDaemon, g as probeDaemon, h as callDaemon, i as queuedCount, j as syncPaths, l as planFetch, m as DaemonUnavailable, n as hubWriter, o as fetchDenial, p as DaemonRunning, r as writtenLines, s as hookAnswer, t as CONFIRM_MS, u as planFlush, v as silentHint, w as ENV, x as oversizedFiles } from "./operation-DGRH4iBE.js";
6
6
  import { COMMAND_SPECS, OPTIONS } from "./facts.js";
7
7
  import { hookFallback, parsePayload, runHook } from "./hooks.js";
8
- import { f as CLI_VERSION, i as launchUi, l as startUiInBackground, r as exitCodeFor, u as watchHubGone } from "./launch-D80CYPcm.js";
8
+ import { f as CLI_VERSION, i as launchUi, l as startUiInBackground, r as exitCodeFor, u as watchHubGone } from "./launch-n2ZDF0BF.js";
9
9
  import { randomBytes } from "node:crypto";
10
10
  import { existsSync, readFileSync, readdirSync, realpathSync, rmSync, statSync } from "node:fs";
11
11
  import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";
@@ -524,6 +524,18 @@ function retroText(report) {
524
524
  //#endregion
525
525
  //#region src/cli.ts
526
526
  const write = (spec) => spec;
527
+ /**
528
+ * A group of another hub (`other-hub:w001`) must exist there when that hub is on this machine; the operation only
529
+ * checks that the hub is registered, since a hub kept on another machine cannot be read from here.
530
+ */
531
+ const checkGroupOption = ({ root, opts }) => {
532
+ const ref = opts.group ? parseRef(opts.group) : null;
533
+ if (ref?.kind !== "group" || ref.hub === void 0) return;
534
+ const hub = loadHub(root);
535
+ if (ref.hub === hub.config.hub) return;
536
+ const remote = hub.remotes.find((r) => r.name === ref.hub);
537
+ if (remote?.hub && !remote.hub.groups.some((g) => g.id === ref.group)) throw new WriteError(`group ${ref.group} not found in hub "${ref.hub}"`);
538
+ };
527
539
  /** Where a document an operation created or changed is, as the commands print it: .mainflow/... */
528
540
  const inData = (...parts) => join(DATA_DIR, ...parts);
529
541
  const WRITES = {
@@ -651,6 +663,7 @@ const WRITES = {
651
663
  }
652
664
  }),
653
665
  "new mainline": write({
666
+ check: checkGroupOption,
654
667
  op: "mainline.new",
655
668
  input: ({ args, opts }) => ({
656
669
  slug: args[0],
@@ -687,6 +700,7 @@ const WRITES = {
687
700
  print: (_, { ref, file }) => done(`created ${ref} ${inData(file)}`)
688
701
  }),
689
702
  "new issue": write({
703
+ check: checkGroupOption,
690
704
  op: "issue.new",
691
705
  input: ({ args, opts }) => ({
692
706
  slug: args[0],
@@ -734,6 +748,7 @@ const WRITES = {
734
748
  print: (_, { id, file }) => done(`created ${id} ${inData(file)}`)
735
749
  }),
736
750
  "mainline set": write({
751
+ check: checkGroupOption,
737
752
  op: "mainline.set",
738
753
  input: ({ args, opts }) => ({
739
754
  id: args[0],
@@ -891,6 +906,7 @@ const WRITES = {
891
906
  print: (_, i) => done(`closed ${i.id}`)
892
907
  }),
893
908
  "issue set": write({
909
+ check: checkGroupOption,
894
910
  op: "issue.set",
895
911
  input: ({ args, opts }) => ({
896
912
  id: args[0],
@@ -1353,14 +1369,23 @@ function printStatus(hub, all) {
1353
1369
  */
1354
1370
  function printGroups(hub, all) {
1355
1371
  const rank = (p) => (p ?? "P2").slice(1);
1372
+ const remoteByGroup = remoteMembers(hub.remotes, hub.config.hub);
1356
1373
  const shown = hub.groups.filter((g) => all || g.status !== "done").sort((a, b) => rank(a.priority).localeCompare(rank(b.priority)) || a.id.localeCompare(b.id));
1357
1374
  for (const g of shown) {
1358
1375
  console.log(` ${g.id} ${g.status.padEnd(4)} ${g.priority ?? "P2"} ${g.title}${g.trigger ? ` (時機:${g.trigger})` : ""}`);
1359
- const mainlines = hub.mainlines.filter((m) => m.group === g.id && (all || !m.archivedAt && !CLOSED_STATUS.mainline.includes(m.status)));
1360
- const issues = hub.issues.filter((i) => i.group === g.id && (all || !i.archivedAt && i.status !== "closed"));
1376
+ const mainlines = hub.mainlines.filter((m) => memberOfGroup(m.group, g.id, hub.config.hub) && (all || !m.archivedAt && !CLOSED_STATUS.mainline.includes(m.status)));
1377
+ const issues = hub.issues.filter((i) => memberOfGroup(i.group, g.id, hub.config.hub) && (all || !i.archivedAt && i.status !== "closed"));
1361
1378
  for (const m of [...mainlines.filter((m) => m.role === "plan"), ...mainlines.filter((m) => m.role !== "plan")]) console.log(` ${m.id} ${(m.role ?? "impl").padEnd(4)} ${m.status.padEnd(8)} ${m.title}`);
1362
1379
  for (const i of issues) console.log(` ${i.id} issue ${i.status.padEnd(8)} ${i.title}`);
1380
+ const remote = (remoteByGroup.get(g.id) ?? []).filter((m) => all || !m.archived && m.open);
1381
+ for (const m of [
1382
+ ...remote.filter((m) => m.role === "plan"),
1383
+ ...remote.filter((m) => m.kind === "mainline" && m.role !== "plan"),
1384
+ ...remote.filter((m) => m.kind === "issue")
1385
+ ]) console.log(` ${m.ref} ${(m.kind === "issue" ? "issue" : m.role).padEnd(4)} ${m.status.padEnd(8)} ${m.title}`);
1363
1386
  }
1387
+ const missing = unavailableHubs(hub.remotes);
1388
+ if (missing.length) console.log(` note: hub ${missing.join(", ")} not loaded, so their members are not listed`);
1364
1389
  return done(`${shown.length} groups`);
1365
1390
  }
1366
1391
  function printProblems(problems, mark = "✗") {
@@ -699,8 +699,11 @@ const documentShape = {
699
699
  links: z.array(linkSchema).optional()
700
700
  };
701
701
  const priority = z.enum(PRIORITIES).optional();
702
- /** The hub-level group a mainline or an issue belongs to, at most one; work that goes together, apart from what depends on what */
703
- const group = id("group").optional();
702
+ /**
703
+ * The hub-level group a mainline or an issue belongs to, at most one; work that goes together, apart from what depends on what.
704
+ * It is a group of this hub (`w001`) or of another one (`other-hub:w001`), so work in one hub can join a group of another.
705
+ */
706
+ const group = z.string().refine((s) => parseRef(s)?.kind === "group", `expected a group like ${refExample("group")}, or one of another hub's like other-hub:${refExample("group")}`).optional();
704
707
  const evidenceRequired = (item, ctx) => {
705
708
  if (item.done && !item.evidence) ctx.addIssue({
706
709
  code: "custom",
@@ -889,6 +892,11 @@ const UNTIL_FORMS = [
889
892
  kind: "file",
890
893
  format: "file:{relative path}",
891
894
  example: "file:packages/cli/dist/cli.js"
895
+ },
896
+ {
897
+ kind: "mainline-done",
898
+ format: "mainline-done:m{NNN} or mainline-done:{hub}:m{NNN}",
899
+ example: "mainline-done:other-hub:m031"
892
900
  }
893
901
  ];
894
902
  const UNTIL_EXAMPLES = UNTIL_FORMS.map((f) => f.format).join(", ");
@@ -922,6 +930,10 @@ function parseUntil(input) {
922
930
  kind,
923
931
  path: argument
924
932
  } : null;
933
+ case "mainline-done": return parseRef(argument)?.kind === "mainline" ? {
934
+ kind,
935
+ mainline: argument
936
+ } : null;
925
937
  default: return null;
926
938
  }
927
939
  }
package/dist/facts.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { g as SurveyLine, l as ID_PATTERNS, m as SLUG, o as FORMAT_VERSION, p as SERVES_REF, r as CHECKLIST_REF, v as Until } from "./index-DgYB5u1f.js";
1
+ import { g as SurveyLine, l as ID_PATTERNS, m as SLUG, o as FORMAT_VERSION, p as SERVES_REF, r as CHECKLIST_REF, v as Until } from "./index-B32s8PHu.js";
2
2
  //#region src/facts.d.ts
3
3
  export interface CommandSpec {
4
4
  usage: string;
@@ -125,7 +125,7 @@ export declare const COMMAND_SPECS: {
125
125
  };
126
126
  readonly 'new mainline': {
127
127
  readonly usage: 'new mainline <slug> --title <title> [--group <w>] [--role plan|impl] [--priority P0|P1|P2|P3]';
128
- readonly summary: 'Create mainline.md with the next id and the section template; write the rest in the file. --group puts it in an existing group; --role plan makes it the mainline that splits the work, coordinates it and makes the final check (without --role it is an impl mainline)';
128
+ readonly summary: 'Create mainline.md with the next id and the section template; write the rest in the file. --group puts it in an existing group, or in one of another registered hub as {hub}:w001; --role plan makes it the mainline that splits the work, coordinates it and makes the final check (without --role it is an impl mainline)';
129
129
  readonly args: 1;
130
130
  readonly needsHub: true;
131
131
  };
@@ -143,7 +143,7 @@ export declare const COMMAND_SPECS: {
143
143
  };
144
144
  readonly 'new issue': {
145
145
  readonly usage: 'new issue <slug> --title <title> [--origin <ref>] [--labels a,b] [--group <w>] [--priority P0|P1|P2|P3]';
146
- readonly summary: 'Create an issue file for a follow-up that must outlive its mainline; --group puts it with the work it is taken up with';
146
+ readonly summary: 'Create an issue file for a follow-up that must outlive its mainline; --group puts it with the work it is taken up with, in this hub or another registered one ({hub}:w001)';
147
147
  readonly args: 1;
148
148
  readonly needsHub: true;
149
149
  };
@@ -167,13 +167,13 @@ export declare const COMMAND_SPECS: {
167
167
  };
168
168
  readonly 'group list': {
169
169
  readonly usage: 'group list [--all]';
170
- readonly summary: 'List open groups by priority with their trigger and members, plan mainlines first (--all includes done groups)';
170
+ readonly summary: 'List open groups by priority with their trigger and members, plan mainlines first; members in other hubs read {hub}:{id}, and a registered hub that is not loaded is noted (--all includes done groups)';
171
171
  readonly args: 0;
172
172
  readonly needsHub: true;
173
173
  };
174
174
  readonly 'mainline set': {
175
175
  readonly usage: 'mainline set <m> [--status <status>] [--group <w|none>] [--role plan|impl] [--priority P0|P1|P2|P3]';
176
- readonly summary: 'Change status, group, role or priority; done needs every criterion met and no proposed decision. A planning mainline becomes active by itself when work on it starts, so use --status for paused, done and dropped. --group none leaves the group';
176
+ readonly summary: 'Change status, group, role or priority; done needs every criterion met and no proposed decision. A planning mainline becomes active by itself when work on it starts, so use --status for paused, done and dropped. --group none leaves the group; {hub}:w001 joins a group of another registered hub';
177
177
  readonly args: 1;
178
178
  readonly needsHub: true;
179
179
  };
@@ -293,7 +293,7 @@ export declare const COMMAND_SPECS: {
293
293
  };
294
294
  readonly 'issue set': {
295
295
  readonly usage: 'issue set <i> [--group <w|none>] [--priority P0|P1|P2|P3]';
296
- readonly summary: 'Put an issue in a group, or take it out with --group none, and change its priority';
296
+ readonly summary: 'Put an issue in a group (of this hub, or {hub}:w001 of another registered one), or take it out with --group none, and change its priority';
297
297
  readonly args: 1;
298
298
  readonly needsHub: true;
299
299
  };
@@ -620,7 +620,7 @@ export declare const RETRO_FORMATS: {
620
620
  };
621
621
  /**
622
622
  * Conditions a blocked action's `until` takes and `wait` checks, one example each: a commit on a branch whose
623
- * message contains the text (git log runs at the hub root), an action that is done, a path relative to the hub root that exists (`..` reaches sibling repositories).
623
+ * message contains the text (git log runs at the hub root), an action that is done, a path relative to the hub root that exists (`..` reaches sibling repositories), a mainline of this hub or a registered hub that is done.
624
624
  */
625
625
  export declare const UNTIL: readonly {
626
626
  kind: Until['kind'];
package/dist/facts.js CHANGED
@@ -1,4 +1,4 @@
1
- import { A as SURVEY_SECTION, D as SESSION_STATUS, M as TASK_SECTIONS, N as UNTIL_FORMS, O as SLUG, P as WORK_STATUS, S as PRIORITIES, T as SERVES_REF, _ as LINK_RELS, a as DECISION_SECTIONS, b as MAINLINE_STATUS, c as DEFAULT_ROLE, d as FORMAT_VERSION, f as GROUP_SECTIONS, g as ISSUE_STATUS, h as ISSUE_SECTIONS, i as DECISION_BASES, j as TASK_REQUIRED_SECTIONS, k as SURVEY_LINES, l as FINDING_SECTIONS, m as ID_PATTERNS, n as CLOSED_STATUS, nt as GRANT_FORMS, o as DECISION_STATUS, p as GROUP_STATUS, r as DECIDERS, rt as GRANT_TARGET_FORMS, s as DEFAULT_CATEGORIES, t as CHECKLIST_REF, tt as GRANT_ACTIONS, u as FINDING_STATUS, v as MAINLINE_ROLES, x as MERGE_STATES, y as MAINLINE_SECTIONS } from "./dist-BdvWGTsb.js";
1
+ import { A as SURVEY_SECTION, D as SESSION_STATUS, M as TASK_SECTIONS, N as UNTIL_FORMS, O as SLUG, P as WORK_STATUS, S as PRIORITIES, T as SERVES_REF, _ as LINK_RELS, a as DECISION_SECTIONS, b as MAINLINE_STATUS, c as DEFAULT_ROLE, d as FORMAT_VERSION, f as GROUP_SECTIONS, g as ISSUE_STATUS, h as ISSUE_SECTIONS, i as DECISION_BASES, j as TASK_REQUIRED_SECTIONS, k as SURVEY_LINES, l as FINDING_SECTIONS, m as ID_PATTERNS, n as CLOSED_STATUS, nt as GRANT_FORMS, o as DECISION_STATUS, p as GROUP_STATUS, r as DECIDERS, rt as GRANT_TARGET_FORMS, s as DEFAULT_CATEGORIES, t as CHECKLIST_REF, tt as GRANT_ACTIONS, u as FINDING_STATUS, v as MAINLINE_ROLES, x as MERGE_STATES, y as MAINLINE_SECTIONS } from "./dist-ud09dzZo.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
  //#region src/facts.ts
4
4
  const COMMAND_SPECS = {
@@ -115,7 +115,7 @@ const COMMAND_SPECS = {
115
115
  },
116
116
  "new mainline": {
117
117
  usage: "new mainline <slug> --title <title> [--group <w>] [--role plan|impl] [--priority P0|P1|P2|P3]",
118
- summary: "Create mainline.md with the next id and the section template; write the rest in the file. --group puts it in an existing group; --role plan makes it the mainline that splits the work, coordinates it and makes the final check (without --role it is an impl mainline)",
118
+ summary: "Create mainline.md with the next id and the section template; write the rest in the file. --group puts it in an existing group, or in one of another registered hub as {hub}:w001; --role plan makes it the mainline that splits the work, coordinates it and makes the final check (without --role it is an impl mainline)",
119
119
  args: 1,
120
120
  needsHub: true
121
121
  },
@@ -133,7 +133,7 @@ const COMMAND_SPECS = {
133
133
  },
134
134
  "new issue": {
135
135
  usage: "new issue <slug> --title <title> [--origin <ref>] [--labels a,b] [--group <w>] [--priority P0|P1|P2|P3]",
136
- summary: "Create an issue file for a follow-up that must outlive its mainline; --group puts it with the work it is taken up with",
136
+ summary: "Create an issue file for a follow-up that must outlive its mainline; --group puts it with the work it is taken up with, in this hub or another registered one ({hub}:w001)",
137
137
  args: 1,
138
138
  needsHub: true
139
139
  },
@@ -157,13 +157,13 @@ const COMMAND_SPECS = {
157
157
  },
158
158
  "group list": {
159
159
  usage: "group list [--all]",
160
- summary: "List open groups by priority with their trigger and members, plan mainlines first (--all includes done groups)",
160
+ summary: "List open groups by priority with their trigger and members, plan mainlines first; members in other hubs read {hub}:{id}, and a registered hub that is not loaded is noted (--all includes done groups)",
161
161
  args: 0,
162
162
  needsHub: true
163
163
  },
164
164
  "mainline set": {
165
165
  usage: "mainline set <m> [--status <status>] [--group <w|none>] [--role plan|impl] [--priority P0|P1|P2|P3]",
166
- summary: "Change status, group, role or priority; done needs every criterion met and no proposed decision. A planning mainline becomes active by itself when work on it starts, so use --status for paused, done and dropped. --group none leaves the group",
166
+ summary: "Change status, group, role or priority; done needs every criterion met and no proposed decision. A planning mainline becomes active by itself when work on it starts, so use --status for paused, done and dropped. --group none leaves the group; {hub}:w001 joins a group of another registered hub",
167
167
  args: 1,
168
168
  needsHub: true
169
169
  },
@@ -283,7 +283,7 @@ const COMMAND_SPECS = {
283
283
  },
284
284
  "issue set": {
285
285
  usage: "issue set <i> [--group <w|none>] [--priority P0|P1|P2|P3]",
286
- summary: "Put an issue in a group, or take it out with --group none, and change its priority",
286
+ summary: "Put an issue in a group (of this hub, or {hub}:w001 of another registered one), or take it out with --group none, and change its priority",
287
287
  args: 1,
288
288
  needsHub: true
289
289
  },
@@ -538,7 +538,7 @@ const RETRO_FORMATS = {
538
538
  };
539
539
  /**
540
540
  * Conditions a blocked action's `until` takes and `wait` checks, one example each: a commit on a branch whose
541
- * message contains the text (git log runs at the hub root), an action that is done, a path relative to the hub root that exists (`..` reaches sibling repositories).
541
+ * message contains the text (git log runs at the hub root), an action that is done, a path relative to the hub root that exists (`..` reaches sibling repositories), a mainline of this hub or a registered hub that is done.
542
542
  */
543
543
  const UNTIL = UNTIL_FORMS;
544
544
  /** Relation types a document's links take; readers derive the reverse direction. */
@@ -654,7 +654,7 @@ const LIST_ITEM = [
654
654
  const FIELDS = {
655
655
  mainline: [
656
656
  ...shared(...EVERY),
657
- field("group", false, "所屬的組 w{NNN},一條主脈絡最多一組;組和依賴無關"),
657
+ field("group", false, "所屬的組 w{NNN},或其他 hub 的組 {hub}:w{NNN},一條主脈絡最多一組;組和依賴無關"),
658
658
  field("role", false, "plan 是拆分顆粒、整合調度與最終確認的規劃型,impl 是執行的實作型;沒填視為 impl"),
659
659
  field("due", false, "預計完成的日期"),
660
660
  field("archivedAt", false, "封存的日期,和狀態分開"),
@@ -690,7 +690,7 @@ const FIELDS = {
690
690
  ],
691
691
  issue: [
692
692
  ...shared(...EVERY),
693
- field("group", false, "所屬的組 w{NNN},一筆待辦最多一組;同組的待辦在同一個時機一起做"),
693
+ field("group", false, "所屬的組 w{NNN},或其他 hub 的組 {hub}:w{NNN},一筆待辦最多一組;同組的待辦在同一個時機一起做"),
694
694
  field("order", false, "待辦之間的順序"),
695
695
  field("due", false, "預計完成的日期"),
696
696
  field("plannedIn", false, "排進的任務,planned 時必填"),
@@ -1,4 +1,4 @@
1
- import "./index-DgYB5u1f.js";
1
+ import "./index-B32s8PHu.js";
2
2
  //#region ../schema/dist/grants.d.ts
3
3
  /** An operation about to run, resolved to what a grant is written against. */
4
4
  type GrantRequest = {
@@ -1,4 +1,4 @@
1
- import { nt as GRANT_FORMS } from "./dist-BdvWGTsb.js";
1
+ import { nt as GRANT_FORMS } from "./dist-ud09dzZo.js";
2
2
  import { closeSync, constants, fstatSync, openSync, readFileSync, realpathSync } from "node:fs";
3
3
  import { isAbsolute, join, relative, resolve } from "node:path";
4
4
  import { spawnSync } from "node:child_process";
package/dist/hooks.d.ts CHANGED
@@ -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-DgYB5u1f.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-B32s8PHu.js";
2
2
  import { z } from "zod";
3
3
  //#region ../core/dist/ids/allocators.d.ts
4
4
  /** The letters ids start with. */
package/dist/hooks.js CHANGED
@@ -1,6 +1,6 @@
1
- import { F as configSchema, it as describeRequest } from "./dist-BdvWGTsb.js";
2
- import { A as mayDecorate, C as readSession, D as decorateReply, E as matchGrants, H as HubError, O as endsWithMarker, S as loadSessions, T as grantScope, W as loadHub, Z as readYaml, _t as DATA_DIR, b as sessionBriefing, ht as worktreesOf, i as hostName, k as headerLine, mt as findHubRoot, pt as currentBranch, x as turnReminder } from "./host-bnHYIwuY.js";
3
- import { n as hubWriter, r as writtenLines } from "./operation-DVcJgKM9.js";
1
+ import { F as configSchema, it as describeRequest } from "./dist-ud09dzZo.js";
2
+ import { A as mayDecorate, C as readSession, D as decorateReply, E as matchGrants, H as HubError, O as endsWithMarker, S as loadSessions, T as grantScope, W as loadHub, _t as findHubRoot, b as sessionBriefing, bt as DATA_DIR, et as readYaml, gt as currentBranch, i as hostName, k as headerLine, vt as worktreesOf, x as turnReminder } from "./host-BauijAdm.js";
3
+ import { n as hubWriter, r as writtenLines } from "./operation-DGRH4iBE.js";
4
4
  import { resolveFixed, scanCommand, scopedCwd } from "./grant-commands.js";
5
5
  import { appendFileSync, existsSync, readFileSync } from "node:fs";
6
6
  import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
@@ -1,4 +1,4 @@
1
- import { $ as OFFSET, A as SURVEY_SECTION, B as issueSchema, C as REF_EXAMPLES, D as SESSION_STATUS, E as SESSION_ID, F as configSchema, G as parseRef, H as opsEntrySchema, I as decisionSchema, J as refMainline, K as parseUntil, L as findingSchema, M as TASK_SECTIONS, O as SLUG, P as WORK_STATUS, Q as waitingForRef, R as formatOpsEntry, S as PRIORITIES, T as SERVES_REF, U as parseActionRef, V as mainlineSchema, W as parseOpsLine, X as taskSchema, Y as sessionSchema, Z as waitingForAction, a as DECISION_SECTIONS, at as grantCovers, b as MAINLINE_STATUS, ct as parseGrantTarget, et as stampMs, f as GROUP_SECTIONS, h as ISSUE_SECTIONS, i as DECISION_BASES, it as describeRequest, j as TASK_REQUIRED_SECTIONS, k as SURVEY_LINES, l as FINDING_SECTIONS, n as CLOSED_STATUS, ot as grantForm, p as GROUP_STATUS, q as refExample, r as DECIDERS, s as DEFAULT_CATEGORIES, st as grantRepo, u as FINDING_STATUS, v as MAINLINE_ROLES, w as RESERVED_HUB_NAMES, x as MERGE_STATES, y as MAINLINE_SECTIONS, z as groupSchema } from "./dist-BdvWGTsb.js";
1
+ import { $ as OFFSET, A as SURVEY_SECTION, B as issueSchema, C as REF_EXAMPLES, D as SESSION_STATUS, E as SESSION_ID, F as configSchema, G as parseRef, H as opsEntrySchema, I as decisionSchema, J as refMainline, K as parseUntil, L as findingSchema, M as TASK_SECTIONS, N as UNTIL_FORMS, O as SLUG, P as WORK_STATUS, Q as waitingForRef, R as formatOpsEntry, S as PRIORITIES, T as SERVES_REF, U as parseActionRef, V as mainlineSchema, W as parseOpsLine, X as taskSchema, Y as sessionSchema, Z as waitingForAction, a as DECISION_SECTIONS, at as grantCovers, b as MAINLINE_STATUS, ct as parseGrantTarget, et as stampMs, f as GROUP_SECTIONS, h as ISSUE_SECTIONS, i as DECISION_BASES, it as describeRequest, j as TASK_REQUIRED_SECTIONS, k as SURVEY_LINES, l as FINDING_SECTIONS, n as CLOSED_STATUS, ot as grantForm, p as GROUP_STATUS, q as refExample, r as DECIDERS, s as DEFAULT_CATEGORIES, st as grantRepo, u as FINDING_STATUS, v as MAINLINE_ROLES, w as RESERVED_HUB_NAMES, x as MERGE_STATES, y as MAINLINE_SECTIONS, z as groupSchema } from "./dist-ud09dzZo.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, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, symlinkSync, writeFileSync, writeSync } from "node:fs";
@@ -618,6 +618,7 @@ const mainlineDir = (root, id) => onDisk(root, mainlineDirIn(nodeStorage(root),
618
618
  const taskDir = (root, mainline, task) => onDisk(root, taskDirIn(nodeStorage(root), mainline, task));
619
619
  const mainlineFile = (root, id) => join(mainlineDir(root, id), MAINLINE_FILE);
620
620
  const taskFile = (root, mainline, task) => join(taskDir(root, mainline, task), TASK_FILE);
621
+ const readConfig = (root) => readConfigIn(nodeStorage(root));
621
622
  //#endregion
622
623
  //#region ../core/dist/refs.js
623
624
  const itemStatus = (item) => item.done ? "done" : item.dropped ? "dropped" : "open";
@@ -706,6 +707,64 @@ function linksInto(hub, hubName) {
706
707
  for (const g of hub.groups) add(g.id, g.links);
707
708
  return out;
708
709
  }
710
+ /**
711
+ * Where a member's `group` value points: `id` is the group's id, and `hub` is the other hub's name, or undefined
712
+ * for a group of the member's own hub (no prefix, or the member's own hub name as prefix).
713
+ */
714
+ function splitGroup(value, ownHub) {
715
+ const colon = value.indexOf(":");
716
+ if (colon < 0) return { id: value };
717
+ const hub = value.slice(0, colon);
718
+ const id = value.slice(colon + 1);
719
+ return hub === ownHub ? { id } : {
720
+ hub,
721
+ id
722
+ };
723
+ }
724
+ /** Whether a member's `group` value names the group `groupId` of its own hub `ownHub`: `w001` or `{ownHub}:w001`. */
725
+ function memberOfGroup(group, groupId, ownHub) {
726
+ if (group === void 0) return false;
727
+ const target = splitGroup(group, ownHub);
728
+ return target.hub === void 0 && target.id === groupId;
729
+ }
730
+ /**
731
+ * The mainlines and issues of the registered hubs whose `group` names one of `hubName`'s groups, by group id. Only
732
+ * the hubs' own documents are read, one level, so a hub that is not on this machine contributes nothing.
733
+ */
734
+ function remoteMembers(remotes, hubName) {
735
+ const out = /* @__PURE__ */ new Map();
736
+ const add = (group, member) => {
737
+ if (!group) return;
738
+ const target = splitGroup(group, member.hub);
739
+ if (target.hub !== hubName) return;
740
+ out.set(target.id, [...out.get(target.id) ?? [], member]);
741
+ };
742
+ for (const r of remotes) {
743
+ if (!r.hub) continue;
744
+ for (const m of r.hub.mainlines) add(m.group, {
745
+ ref: `${r.name}:${m.id}`,
746
+ hub: r.name,
747
+ kind: "mainline",
748
+ title: m.title,
749
+ status: m.status,
750
+ role: m.role ?? "impl",
751
+ archived: Boolean(m.archivedAt),
752
+ open: !CLOSED_STATUS.mainline.includes(m.status)
753
+ });
754
+ for (const i of r.hub.issues) add(i.group, {
755
+ ref: `${r.name}:${i.id}`,
756
+ hub: r.name,
757
+ kind: "issue",
758
+ title: i.title,
759
+ status: i.status,
760
+ archived: Boolean(i.archivedAt),
761
+ open: i.status !== "closed"
762
+ });
763
+ }
764
+ return out;
765
+ }
766
+ /** The registered hubs that could not be loaded, for a reader that must say its member list may be incomplete. */
767
+ const unavailableHubs = (remotes) => remotes.filter((r) => r.unavailable).map((r) => r.name);
709
768
  //#endregion
710
769
  //#region ../core/dist/deps.js
711
770
  const MET = /* @__PURE__ */ new Set([
@@ -1165,8 +1224,7 @@ function loadHub(root, options = {}) {
1165
1224
  }
1166
1225
  const findings = hubDocs(join(dataDir, "findings"), findingSchema);
1167
1226
  const groups = hubDocs(join(dataDir, "groups"), groupSchema);
1168
- problems.push(...groupProblems(groups, mainlines, hubIssues, unreadable));
1169
- warnings.push(...groupWarnings(groups, mainlines, hubIssues));
1227
+ problems.push(...groupProblems(config.hub, groups, mainlines, hubIssues, unreadable));
1170
1228
  const ops = [];
1171
1229
  const opsPath = join(dataDir, "ops");
1172
1230
  for (const name of existsSync(opsPath) ? readdirSync(opsPath).filter((n) => n.endsWith(".log")).sort() : []) readFileSync(join(opsPath, name), "utf8").split("\n").forEach((line, index) => {
@@ -1185,6 +1243,12 @@ function loadHub(root, options = {}) {
1185
1243
  });
1186
1244
  });
1187
1245
  const remotes = options.remotes === false ? [] : loadRemotes(root, config, problems, warnings);
1246
+ warnings.push(...groupWarnings(config.hub, groups, mainlines, hubIssues, remotes));
1247
+ for (const doc of [...mainlines, ...hubIssues]) if (doc.group && splitGroup(doc.group, config.hub).hub !== void 0) pending.push({
1248
+ ref: doc.group,
1249
+ file: doc.file,
1250
+ field: "group"
1251
+ });
1188
1252
  checkReferences({
1189
1253
  config,
1190
1254
  mainlines,
@@ -1238,20 +1302,32 @@ function loadHub(root, options = {}) {
1238
1302
  remotes
1239
1303
  };
1240
1304
  }
1241
- /** Members name their group; one that names a group this hub does not have is reported on the member. */
1242
- function groupProblems(groups, mainlines, issues, unreadable) {
1305
+ /** Members name their group; one that names a group this hub does not have is reported on the member. A group of another hub is checked with the other references. */
1306
+ function groupProblems(hubName, groups, mainlines, issues, unreadable) {
1243
1307
  const known = new Set(groups.map((g) => g.id));
1244
- return [...mainlines, ...issues].flatMap((doc) => doc.group && !known.has(doc.group) && !unreadable.has(doc.group) ? [{
1245
- file: doc.file,
1246
- field: "group",
1247
- message: `group ${doc.group} does not exist; create it with "mainflow new group <slug> --title <title>" or name an existing one`
1248
- }] : []);
1308
+ return [...mainlines, ...issues].flatMap((doc) => {
1309
+ if (!doc.group) return [];
1310
+ const { hub, id } = splitGroup(doc.group, hubName);
1311
+ return hub === void 0 && !known.has(id) && !unreadable.has(id) ? [{
1312
+ file: doc.file,
1313
+ field: "group",
1314
+ message: `group ${doc.group} does not exist; create it with "mainflow new group <slug> --title <title>" or name an existing one`
1315
+ }] : [];
1316
+ });
1249
1317
  }
1250
- /** A group marked done while some of its members are still open: a plan to settle, not a broken file. */
1251
- function groupWarnings(groups, mainlines, issues) {
1318
+ /**
1319
+ * A group marked done while some of its members are still open: a plan to settle, not a broken file. Members in
1320
+ * other hubs count too, when those hubs are on this machine.
1321
+ */
1322
+ function groupWarnings(hubName, groups, mainlines, issues, remotes) {
1323
+ const remote = remoteMembers(remotes, hubName);
1252
1324
  return groups.flatMap((g) => {
1253
1325
  if (g.status !== "done") return [];
1254
- const open = [...mainlines.filter((m) => m.group === g.id && isOpen$2("mainline", m.status)).map((m) => m.id), ...issues.filter((i) => i.group === g.id && i.status !== "closed").map((i) => i.id)];
1326
+ const open = [
1327
+ ...mainlines.filter((m) => memberOfGroup(m.group, g.id, hubName) && isOpen$2("mainline", m.status)).map((m) => m.id),
1328
+ ...issues.filter((i) => memberOfGroup(i.group, g.id, hubName) && i.status !== "closed").map((i) => i.id),
1329
+ ...(remote.get(g.id) ?? []).filter((m) => m.open).map((m) => m.ref)
1330
+ ];
1255
1331
  return open.length ? [{
1256
1332
  file: g.file,
1257
1333
  field: "status",
@@ -1648,12 +1724,18 @@ function requireExisting(hub, ref) {
1648
1724
  /** A priority as a command gives it, checked against P0 to P3. */
1649
1725
  const requirePriority = (value) => requireOneOf(value, PRIORITIES, "priority");
1650
1726
  /**
1651
- * The group a member names, which must exist in this hub. `none` clears the membership; `null` is how a patch says
1652
- * the same, so a command can pass the word and an interface the value.
1727
+ * The group a member names: one of this hub, which must exist, or one of another hub (`other-hub:w001`), which
1728
+ * only needs that hub registered here; the commands check the group itself when that hub is on this machine.
1729
+ * The hub's own name as prefix gives the group without it. `none` clears the membership; `null` is how a patch
1730
+ * says the same, so a command can pass the word and an interface the value.
1653
1731
  */
1654
1732
  function requireGroup(hub, value) {
1655
1733
  const ref = requireRef(value, "group");
1656
- if (ref.hub !== void 0) throw new WriteError(`a group is one of this hub's, like w001; got "${value}"`);
1734
+ const config = readConfigIn(hub);
1735
+ if (ref.hub !== void 0 && ref.hub !== config.hub) {
1736
+ if (!config.hubs?.[ref.hub]) throw new WriteError(`hub "${ref.hub}" is not registered; run "mainflow hub add ${ref.hub} <path>" first`);
1737
+ return `${ref.hub}:${ref.group}`;
1738
+ }
1657
1739
  entryFile(hub, GROUPS_DIR, ref.group, "group");
1658
1740
  return ref.group;
1659
1741
  }
@@ -2692,6 +2774,7 @@ function addAction(hub, input, ctx) {
2692
2774
  * to work that has since been handed off to this very action would otherwise wait forever.
2693
2775
  */
2694
2776
  function requireBlock(hub, ref, block) {
2777
+ if (block.until !== void 0) requireMainlineTarget(hub, block.until);
2695
2778
  const targets = [waitingForAction(block.waitingFor), block.until === void 0 ? void 0 : untilTarget(block.until)];
2696
2779
  for (const target of targets) {
2697
2780
  if (!target) continue;
@@ -2700,10 +2783,23 @@ function requireBlock(hub, ref, block) {
2700
2783
  if (found.ref === ref) throw new WriteError(`action ${ref} cannot wait for itself${target === ref ? "" : ` (${target} was handed off to it)`}`);
2701
2784
  }
2702
2785
  }
2786
+ /** A `mainline-done` condition has to name a mainline in this hub, or a hub registered here. */
2787
+ function requireMainlineTarget(hub, until) {
2788
+ const parsed = parseUntil(until);
2789
+ if (parsed?.kind !== "mainline-done") return;
2790
+ const ref = parseRef(parsed.mainline);
2791
+ if (ref?.kind !== "mainline") return;
2792
+ const config = readConfigIn(hub);
2793
+ if (ref.hub !== void 0 && ref.hub !== config.hub) {
2794
+ if (!config.hubs?.[ref.hub]) throw new WriteError(`hub "${ref.hub}" is not registered; run "mainflow hub add ${ref.hub} <path>" first`);
2795
+ return;
2796
+ }
2797
+ mainlineDirIn(hub, ref.mainline);
2798
+ }
2703
2799
  /** The action an `until` waits for; throws when the condition matches none of the forms. */
2704
2800
  function untilTarget(until) {
2705
2801
  const parsed = parseUntil(until);
2706
- if (!parsed) throw new WriteError(`--until takes a condition like commit:{branch}:{text}, action-done:{sessionId}#a{NNN} or file:{relative path}, got "${until}"`);
2802
+ if (!parsed) throw new WriteError(`--until takes a condition like ${UNTIL_FORMS.map((f) => f.format).join(", ")}, got "${until}"`);
2707
2803
  return parsed.kind === "action-done" ? `${parsed.session}#${parsed.action}` : void 0;
2708
2804
  }
2709
2805
  function updateAction(hub, input, ctx) {
@@ -3917,11 +4013,78 @@ function actionState(root, ref) {
3917
4013
  detail: `${ref}${via} is ${found.action.status}`
3918
4014
  };
3919
4015
  }
4016
+ /**
4017
+ * Whether a mainline is done. It can no longer come true when the mainline was dropped, does not exist, or its
4018
+ * hub is not registered here. A registered hub that is missing on this machine or fails to load may return, so
4019
+ * that case keeps waiting. A prefix equal to this hub's own name reads the local hub.
4020
+ */
4021
+ function mainlineState(root, text) {
4022
+ const ref = parseRef(text);
4023
+ if (ref?.kind !== "mainline") return {
4024
+ met: false,
4025
+ unreachable: true,
4026
+ detail: `"${text}" is not a mainline reference like m001 or other-hub:m001`
4027
+ };
4028
+ let target = root;
4029
+ try {
4030
+ const config = readConfig(root);
4031
+ if (ref.hub !== void 0 && ref.hub !== config.hub) {
4032
+ const path = config.hubs?.[ref.hub];
4033
+ if (path === void 0) return {
4034
+ met: false,
4035
+ unreachable: true,
4036
+ detail: `hub "${ref.hub}" is not registered, so ${text} can never be read; run "mainflow hub add ${ref.hub} <path>"`
4037
+ };
4038
+ target = remoteRoot(root, path);
4039
+ if (!isHubRoot(target)) return {
4040
+ met: false,
4041
+ detail: `hub "${ref.hub}" is not at ${path} on this machine, so ${text} cannot be read yet`
4042
+ };
4043
+ }
4044
+ } catch (error) {
4045
+ return {
4046
+ met: false,
4047
+ detail: `the hub config cannot be read, so ${text} cannot be checked: ${error.message}`
4048
+ };
4049
+ }
4050
+ try {
4051
+ mainlineDir(target, ref.mainline);
4052
+ } catch {
4053
+ return {
4054
+ met: false,
4055
+ unreachable: true,
4056
+ detail: `mainline ${text} does not exist, so it will never be done`
4057
+ };
4058
+ }
4059
+ let status;
4060
+ try {
4061
+ status = readFrontMatter(mainlineFile(target, ref.mainline), mainlineSchema).data.status;
4062
+ } catch (error) {
4063
+ return {
4064
+ met: false,
4065
+ detail: `mainline ${text} cannot be read yet: ${error.message}`
4066
+ };
4067
+ }
4068
+ if (status === "done") return {
4069
+ met: true,
4070
+ detail: `mainline ${text} is done`
4071
+ };
4072
+ if (status === "dropped") return {
4073
+ met: false,
4074
+ unreachable: true,
4075
+ detail: `mainline ${text} was dropped, so it will never be done`
4076
+ };
4077
+ return {
4078
+ met: false,
4079
+ detail: `mainline ${text} is ${status}`
4080
+ };
4081
+ }
3920
4082
  /** Check a wait condition once. */
3921
4083
  function untilState(root, until) {
3922
4084
  switch (until.kind) {
3923
4085
  case "commit": return commitState(root, until.branch, until.text);
3924
4086
  case "action-done": return actionState(root, `${until.session}#${until.action}`);
4087
+ case "mainline-done": return mainlineState(root, until.mainline);
3925
4088
  case "file": return existsSync(resolve(root, until.path)) ? {
3926
4089
  met: true,
3927
4090
  detail: `${until.path} exists`
@@ -7939,4 +8102,4 @@ function openInApp(hub, target, closeAfterMs = 150) {
7939
8102
  return true;
7940
8103
  }
7941
8104
  //#endregion
7942
- export { ACCESS_HEADERS as $, mayDecorate as A, applyOperation as B, readSession as C, decorateReply as D, matchGrants as E, renumberRequest as F, sectionText as G, HubError as H, renumberFiles as I, linksInto as J, dependencyGraph as K, renumberPath as L, traceTranscript as M, transcriptTitle as N, endsWithMarker as O, applyOffline as P, nodeStorage as Q, renumberText as R, loadSessions as S, grantScope as T, loadConfig as U, checkHubName as V, loadHub as W, nowMinute as X, refIndex as Y, readYaml as Z, initHub as _, DATA_DIR as _t, openInApp as a, comparePaths as at, sessionBriefing as b, splitFrontMatter as bt, siteBlock as c, requestTime as ct, retroReport as d, TASK_FILE as dt, BLOB_HASH as et, retroReportFromReceiver as f, requireSlug as ft, setArchivedMany as g, ConflictError as gt, ARCHIVE_KINDS as h, worktreesOf as ht, hostName as i, SOCKET_MAX_AGE_CLOSE as it, attributeTrace as j, headerLine as k, syncHost as l, textBytes as lt, RETRY_RULES as m, findHubRoot as mt, DEFAULT_UI_PORT as n, MAX_FILE_BYTES as nt, removeDanglingLinks as o, exceedsFileLimit as ot, receiverFromEnv as p, currentBranch as pt, mainlineEdges as q, caddySites as r, ROUTES as rt, removeHostLink as s, readsAsText as st, APP_BUNDLE_ID as t, MAX_BLOB_BYTES as tt, migrateHub as u, MAINLINE_FILE as ut, untilState as v, WriteError as vt, transcriptPath as w, turnReminder as x, duplicateClaimLines as y, contentHash as yt, scopeOfPath as z };
8105
+ export { nowMinute as $, mayDecorate as A, applyOperation as B, readSession as C, splitFrontMatter as Ct, decorateReply as D, matchGrants as E, renumberRequest as F, sectionText as G, HubError as H, renumberFiles as I, linksInto as J, dependencyGraph as K, renumberPath as L, traceTranscript as M, transcriptTitle as N, endsWithMarker as O, applyOffline as P, unavailableHubs as Q, renumberText as R, loadSessions as S, contentHash as St, grantScope as T, loadConfig as U, checkHubName as V, loadHub as W, refIndex as X, memberOfGroup as Y, remoteMembers as Z, initHub as _, findHubRoot as _t, openInApp as a, MAX_FILE_BYTES as at, sessionBriefing as b, DATA_DIR as bt, siteBlock as c, comparePaths as ct, retroReport as d, requestTime as dt, readYaml as et, retroReportFromReceiver as f, textBytes as ft, setArchivedMany as g, currentBranch as gt, ARCHIVE_KINDS as h, requireSlug as ht, hostName as i, MAX_BLOB_BYTES as it, attributeTrace as j, headerLine as k, syncHost as l, exceedsFileLimit as lt, RETRY_RULES as m, TASK_FILE as mt, DEFAULT_UI_PORT as n, ACCESS_HEADERS as nt, removeDanglingLinks as o, ROUTES as ot, receiverFromEnv as p, MAINLINE_FILE as pt, mainlineEdges as q, caddySites as r, BLOB_HASH as rt, removeHostLink as s, SOCKET_MAX_AGE_CLOSE as st, APP_BUNDLE_ID as t, nodeStorage as tt, migrateHub as u, readsAsText as ut, untilState as v, worktreesOf as vt, transcriptPath as w, turnReminder as x, WriteError as xt, duplicateClaimLines as y, ConflictError as yt, scopeOfPath as z };
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-bnHYIwuY.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-BauijAdm.js";
2
2
  export { APP_BUNDLE_ID, DEFAULT_UI_PORT, caddySites, hostName, openInApp, removeDanglingLinks, removeHostLink, siteBlock, syncHost };
@@ -250,7 +250,8 @@ type Task = z.infer<typeof taskSchema>;
250
250
  /**
251
251
  * A condition a program can check, written `{kind}:{argument}`. `commit` holds once a commit message on the
252
252
  * branch contains the text, `action-done` once the action is done, `file` once the path exists under the hub
253
- * root. Git branch names cannot contain a colon, so the text after the branch may.
253
+ * root, `mainline-done` once the mainline (`m{NNN}` or `{hub}:m{NNN}`) is done. Git branch names cannot contain a
254
+ * colon, so the text after the branch may.
254
255
  */
255
256
  type Until = {
256
257
  kind: 'commit';
@@ -263,6 +264,9 @@ type Until = {
263
264
  } | {
264
265
  kind: 'file';
265
266
  path: string;
267
+ } | {
268
+ kind: 'mainline-done';
269
+ mainline: string;
266
270
  };
267
271
  /**
268
272
  * Why a blocked action cannot move, what it waits for, and the condition a program checks to end the wait.
@@ -1,4 +1,4 @@
1
- import { _t as DATA_DIR, i as hostName, l as syncHost, n as DEFAULT_UI_PORT, s as removeHostLink, vt as WriteError } from "./host-bnHYIwuY.js";
1
+ import { bt as DATA_DIR, i as hostName, l as syncHost, n as DEFAULT_UI_PORT, s as removeHostLink, xt as WriteError } from "./host-BauijAdm.js";
2
2
  import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
3
3
  import { join, resolve } from "node:path";
4
4
  import { spawn, spawnSync } from "node:child_process";
@@ -276,7 +276,7 @@ async function launchUi(root, hub, configured, options) {
276
276
  }
277
277
  let server;
278
278
  if (!reuse) {
279
- const { serveUi } = await import("./ui-C4h4iLDu.js");
279
+ const { serveUi } = await import("./ui-C-laSyqU.js");
280
280
  server = await serveUi(root, choice.port);
281
281
  writeUiRecord(root, {
282
282
  port: choice.port,
package/dist/launch.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as networkProbe, c as realRoot, d as withUiLock, i as launchUi, l as startUiInBackground, n as choosePort, o as portCandidates, r as exitCodeFor, s as readUiRecord, t as PORT_SPAN, u as watchHubGone } from "./launch-D80CYPcm.js";
1
+ import { a as networkProbe, c as realRoot, d as withUiLock, i as launchUi, l as startUiInBackground, n as choosePort, o as portCandidates, r as exitCodeFor, s as readUiRecord, t as PORT_SPAN, u as watchHubGone } from "./launch-n2ZDF0BF.js";
2
2
  export { PORT_SPAN, choosePort, exitCodeFor, launchUi, networkProbe, portCandidates, readUiRecord, realRoot, startUiInBackground, watchHubGone, withUiLock };