mainflow 0.41.0 → 0.43.0-alpha.1

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,3 +1,4 @@
1
+ import validRange from "semver/ranges/valid.js";
1
2
  import { z } from "zod";
2
3
  //#region ../schema/dist/grants.js
3
4
  /** What a mainline can authorize ahead of time. */
@@ -43,7 +44,7 @@ const GRANT_TARGET_FORMS = {
43
44
  };
44
45
  const KEBAB = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
45
46
  /** A git branch name, `*` standing for any run of characters. */
46
- const BRANCH$1 = /^(?!\/)(?!.*\/\/)(?!.*\.\.)[A-Za-z0-9._/*-]+(?<!\/)$/;
47
+ const BRANCH = /^(?!\/)(?!.*\/\/)(?!.*\.\.)[A-Za-z0-9._/*-]+(?<!\/)$/;
47
48
  /** npm package names: lowercase, optionally scoped. */
48
49
  const PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._~-]*\/)?[a-z0-9][a-z0-9._~-]*$/;
49
50
  /** A tag pattern: no whitespace, `*` and `?` as in a shell glob. */
@@ -62,7 +63,7 @@ function parseGrantTarget(action, target) {
62
63
  const repo = colon < 0 ? target : target.slice(0, colon);
63
64
  const branch = colon < 0 ? void 0 : target.slice(colon + 1);
64
65
  if (!repo) return fail(action, "the repo name is missing");
65
- if (branch !== void 0 && !BRANCH$1.test(branch)) return fail(action, `"${branch}" is not a branch name`);
66
+ if (branch !== void 0 && !BRANCH.test(branch)) return fail(action, `"${branch}" is not a branch name`);
66
67
  return {
67
68
  ok: true,
68
69
  target: {
@@ -666,17 +667,68 @@ const SURVEY_LINES = [
666
667
  requires: "環境:"
667
668
  }
668
669
  ];
670
+ /**
671
+ * How a version is compared with config.yaml's `mainflow`: a prerelease satisfies a range by its place in semver order,
672
+ * so 0.44.0-beta.1 satisfies ">=0.43.0" and 0.43.0-beta.1 does not.
673
+ */
674
+ const MAINFLOW_RANGE = { includePrerelease: true };
675
+ /** Whether `range` is a range config.yaml's `mainflow` may hold: one the semver package reads, and not blank, which it would read as any version. */
676
+ const isMainflowRange = (range) => range.trim() !== "" && validRange(range, MAINFLOW_RANGE) !== null;
677
+ /** A repository under `repos`: a path relative to the hub root, which is a primary repository, or the path with its role. */
678
+ const repoEntrySchema = z.union([z.string(), z.object({
679
+ path: z.string(),
680
+ role: z.enum(["primary", "reference"])
681
+ })]);
682
+ /** The path of a listed repository, relative to the hub root. */
683
+ const repoPath = (entry) => typeof entry === "string" ? entry : entry.path;
684
+ /** The role of a listed repository; the plain path form is a primary repository. */
685
+ const repoRole = (entry) => typeof entry === "string" ? "primary" : entry.role;
686
+ /** Every repository config.yaml lists, by name, with its path relative to the hub root and its role. */
687
+ const listedRepos = (repos) => Object.entries(repos).map(([name, entry]) => ({
688
+ name,
689
+ path: repoPath(entry),
690
+ role: repoRole(entry)
691
+ }));
692
+ /** Every listed repository's path relative to the hub root, by name, whatever its role. */
693
+ const repoPaths = (repos) => Object.fromEntries(Object.entries(repos).map(([name, entry]) => [name, repoPath(entry)]));
669
694
  const configSchema = z.object({
670
695
  /** Also the first label of the hub's host name, {hub}.mainflow.localhost, so kebab-case */
671
696
  hub: z.string().regex(HUB_NAME, "hub name must be kebab-case, e.g. carlos-space"),
672
697
  formatVersion: z.number().int().positive(),
673
- repos: z.record(z.string(), z.string()),
698
+ /** Repositories tasks and grants may name: name to a path relative to the hub root, or to { path, role } */
699
+ repos: z.record(z.string(), repoEntrySchema),
674
700
  /** Other hubs this hub's references point into: hub name to its root, relative to this hub's root */
675
701
  hubs: z.record(z.string().regex(HUB_NAME, "a hub name must be kebab-case").refine((n) => !RESERVED_HUB_NAMES.includes(n), `${RESERVED_HUB_NAMES.join(" and ")} are reserved`), z.string()).optional(),
676
702
  /** Categories this hub uses on top of DEFAULT_CATEGORIES */
677
703
  categories: z.array(z.string().regex(SLUG, "a category must be kebab-case")).optional(),
678
704
  /** The port `mainflow ui` tries first for this hub; when it is taken, the next free one is used and the host name follows */
679
- ui: z.object({ port: z.number().int().min(1024).max(65535) }).optional()
705
+ ui: z.object({ port: z.number().int().min(1024).max(65535) }).optional(),
706
+ /**
707
+ * The mainflow versions that may write to this hub through a sync server, as a semver range like ">=0.43.0", compared as
708
+ * MAINFLOW_RANGE says: the server refuses every write from a CLI outside it, and from one that does not say its version
709
+ */
710
+ mainflow: z.string().refine(isMainflowRange, "mainflow is a semver range of the mainflow versions that may write to this hub, like \">=0.43.0\"").optional()
711
+ });
712
+ /**
713
+ * One hub as this machine registered it, every path absolute and resolved by the file system (realpath), so a path
714
+ * is compared as the file system spells it. Derived from the hub's config.yaml by `mainflow hub register`.
715
+ */
716
+ const registeredHubSchema = z.object({
717
+ /** The hub repository: the directory config.yaml's repository paths are relative to */
718
+ root: z.string(),
719
+ /** The directory holding config.yaml and the hub's documents */
720
+ data: z.string(),
721
+ /** Content hash of config.yaml when this entry was derived from it, so a changed config.yaml is noticed */
722
+ config: z.string(),
723
+ /** The repositories this hub is the primary hub of, by the name config.yaml gives them; the hub repository is one of them whether listed or not */
724
+ repos: z.record(z.string(), z.string()),
725
+ /** The repositories this hub lists as references, by name */
726
+ references: z.record(z.string(), z.string())
727
+ });
728
+ /** The user-level hub registry, $MAINFLOW_HOME/hubs.json: which hub a directory on this machine belongs to. */
729
+ const hubRegistrySchema = z.object({
730
+ version: z.literal(1),
731
+ hubs: z.record(z.string().regex(HUB_NAME), registeredHubSchema)
680
732
  });
681
733
  const refSchema = z.object({
682
734
  title: text,
@@ -889,11 +941,6 @@ function parseActionRef(input) {
889
941
  const waitingForAction = (waitingFor) => waitingFor && parseActionRef(waitingFor) ? waitingFor : void 0;
890
942
  /** The condition forms, one example each; facts and error messages read them from here. */
891
943
  const UNTIL_FORMS = [
892
- {
893
- kind: "commit",
894
- format: "commit:{branch}:{text}",
895
- example: "commit:main:feat(core): 行動阻塞"
896
- },
897
944
  {
898
945
  kind: "action-done",
899
946
  format: "action-done:{sessionId}#a{NNN}",
@@ -911,8 +958,14 @@ const UNTIL_FORMS = [
911
958
  }
912
959
  ];
913
960
  const UNTIL_EXAMPLES = UNTIL_FORMS.map((f) => f.format).join(", ");
914
- /** A branch name git log can take as a revision; a leading hyphen would read as an option. */
915
- const BRANCH = /^[^\s:~^?*[\\-][^\s:~^?*[\\]*$/;
961
+ /** What to tell the writer of an `input` that was a wait condition once and is removed now, or null when it is not one. */
962
+ function removedUntil(input) {
963
+ return input.startsWith("commit:") ? `the commit: condition was removed; use one of ${UNTIL_EXAMPLES}, for example file:{relative path} for a file the commit produces` : null;
964
+ }
965
+ /** Why `input` is not a wait condition, for the messages that reject it; a form that was removed says so and names what to use. */
966
+ function untilProblem(input) {
967
+ return removedUntil(input) ?? `expected a condition like ${UNTIL_EXAMPLES}`;
968
+ }
916
969
  /** Parse a wait condition, or return null when it matches none of the forms. */
917
970
  function parseUntil(input) {
918
971
  const colon = input.indexOf(":");
@@ -920,16 +973,6 @@ function parseUntil(input) {
920
973
  const kind = input.slice(0, colon);
921
974
  const argument = input.slice(colon + 1);
922
975
  switch (kind) {
923
- case "commit": {
924
- const split = argument.indexOf(":");
925
- const branch = argument.slice(0, split);
926
- const text = argument.slice(split + 1);
927
- return split > 0 && BRANCH.test(branch) && !branch.includes("..") && text.length > 0 ? {
928
- kind,
929
- branch,
930
- text
931
- } : null;
932
- }
933
976
  case "action-done": {
934
977
  const ref = parseActionRef(argument);
935
978
  return ref ? {
@@ -956,7 +999,12 @@ function parseUntil(input) {
956
999
  const actionBlockSchema = z.object({
957
1000
  reason: text,
958
1001
  waitingFor: text.optional(),
959
- until: z.string().refine((s) => parseUntil(s) !== null, `expected a condition like ${UNTIL_EXAMPLES}`).optional()
1002
+ until: z.string().superRefine((s, ctx) => {
1003
+ if (parseUntil(s) === null) ctx.addIssue({
1004
+ code: "custom",
1005
+ message: untilProblem(s)
1006
+ });
1007
+ }).optional()
960
1008
  });
961
1009
  const actionSchema = z.object({
962
1010
  id: id("action"),
@@ -1110,4 +1158,4 @@ function parseOpsLine(line) {
1110
1158
  return parsed.success ? parsed.data : null;
1111
1159
  }
1112
1160
  //#endregion
1113
- export { stampMs as $, TASK_REQUIRED_SECTIONS as A, mainlineSchema as B, RESERVED_HUB_NAMES as C, SLUG as D, SESSION_STATUS as E, decisionSchema as F, parseUntil as G, parseActionRef as H, findingSchema as I, sessionSchema as J, refExample as K, formatOpsEntry as L, UNTIL_FORMS as M, WORK_STATUS as N, SURVEY_LINES as O, configSchema as P, OFFSET as Q, groupSchema as R, REF_EXAMPLES as S, SESSION_ID as T, parseOpsLine as U, opsEntrySchema as V, parseRef as W, waitingForAction as X, taskSchema as Y, waitingForRef as Z, LINK_RELS as _, DECISION_SECTIONS as a, grantForm as at, MERGE_STATES as b, FINDING_SECTIONS as c, GROUP_SECTIONS as d, GRANT_ACTIONS as et, GROUP_STATUS as f, ISSUE_STATUS as g, ISSUE_SECTIONS as h, DECISION_BASES as i, grantCovers as it, TASK_SECTIONS as j, SURVEY_SECTION as k, FINDING_STATUS as l, ID_PATTERNS as m, CLOSED_STATUS as n, GRANT_TARGET_FORMS as nt, DECISION_STATUS as o, grantRepo as ot, GROUP_SUMMARY_SECTION as p, refMainline as q, DECIDERS as r, describeRequest as rt, DEFAULT_CATEGORIES as s, parseGrantTarget as st, CHECKLIST_REF as t, GRANT_FORMS as tt, FORMAT_VERSION as u, MAINLINE_SECTIONS as v, SERVES_REF as w, PRIORITIES as x, MAINLINE_STATUS as y, issueSchema as z };
1161
+ export { removedUntil as $, SURVEY_LINES as A, groupSchema as B, PRIORITIES as C, SESSION_ID as D, SERVES_REF as E, WORK_STATUS as F, mainlineSchema as G, isMainflowRange as H, configSchema as I, parseOpsLine as J, opsEntrySchema as K, decisionSchema as L, TASK_REQUIRED_SECTIONS as M, TASK_SECTIONS as N, SESSION_STATUS as O, UNTIL_FORMS as P, refMainline as Q, findingSchema as R, MERGE_STATES as S, RESERVED_HUB_NAMES as T, issueSchema as U, hubRegistrySchema as V, listedRepos as W, parseUntil as X, parseRef as Y, refExample as Z, ISSUE_STATUS as _, DECISION_SECTIONS as a, waitingForAction as at, MAINLINE_SECTIONS as b, FINDING_SECTIONS as c, stampMs as ct, GROUP_SECTIONS as d, GRANT_TARGET_FORMS as dt, repoPath as et, GROUP_STATUS as f, describeRequest as ft, ISSUE_SECTIONS as g, parseGrantTarget as gt, ID_PATTERNS as h, grantRepo as ht, DECISION_BASES as i, untilProblem as it, SURVEY_SECTION as j, SLUG as k, FINDING_STATUS as l, GRANT_ACTIONS as lt, HUB_NAME as m, grantForm as mt, CLOSED_STATUS as n, sessionSchema as nt, DECISION_STATUS as o, waitingForRef as ot, GROUP_SUMMARY_SECTION as p, grantCovers as pt, parseActionRef as q, DECIDERS as r, taskSchema as rt, DEFAULT_CATEGORIES as s, OFFSET as st, CHECKLIST_REF as t, repoPaths as tt, FORMAT_VERSION as u, GRANT_FORMS as ut, LINK_RELS as v, REF_EXAMPLES as w, MAINLINE_STATUS as x, MAINFLOW_RANGE as y, formatOpsEntry as z };
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-Dk4fCI7q.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-BqPQp6TF.js";
2
2
  //#region src/facts.d.ts
3
3
  export interface CommandSpec {
4
4
  usage: string;
@@ -14,7 +14,31 @@ export interface CommandSpec {
14
14
  export declare const COMMAND_SPECS: {
15
15
  readonly init: {
16
16
  readonly usage: 'init <hub-name>';
17
- readonly summary: 'Create .mainflow/ in the current directory';
17
+ readonly summary: 'Create the data directory of a new hub in $MAINFLOW_HOME/<hub-name> (~/.mainflow/<hub-name> by default) and register the hub on this machine with the current directory as its repository; nothing is written into the repository';
18
+ readonly args: 1;
19
+ readonly needsHub: false;
20
+ };
21
+ readonly 'hub register': {
22
+ readonly usage: 'hub register <name> <path>';
23
+ readonly summary: 'Register on this machine the hub whose repository is <path>, so mainflow run in its primary repositories and their worktrees works on it; run it again after config.yaml changes to update it at once';
24
+ readonly args: 2;
25
+ readonly needsHub: false;
26
+ };
27
+ readonly 'hub migrate-data': {
28
+ readonly usage: 'hub migrate-data <path> [--dry-run]';
29
+ readonly summary: 'Move the data of the hub whose repository is <path> from <path>/.mainflow/ to $MAINFLOW_HOME/<name>, and register the hub with its data there. The data is copied and compared before the registry switches to it; the original is then renamed to <path>/.mainflow.migrated, not deleted. A .git file of a git worktree is not copied and git is not run: the command prints the git worktree remove to run. Refused while mainflow sync or the hub\'s interface runs, or when $MAINFLOW_HOME/<name> is taken; --dry-run says what it would do';
30
+ readonly args: 1;
31
+ readonly needsHub: false;
32
+ };
33
+ readonly 'hub list': {
34
+ readonly usage: 'hub list';
35
+ readonly summary: 'List the hubs registered on this machine with their primary and referenced repositories';
36
+ readonly args: 0;
37
+ readonly needsHub: false;
38
+ };
39
+ readonly 'hub unregister': {
40
+ readonly usage: 'hub unregister <name>';
41
+ readonly summary: 'Remove a hub from this machine\'s registry; its files stay';
18
42
  readonly args: 1;
19
43
  readonly needsHub: false;
20
44
  };
@@ -26,13 +50,13 @@ export declare const COMMAND_SPECS: {
26
50
  };
27
51
  readonly check: {
28
52
  readonly usage: 'check';
29
- readonly summary: 'Validate every file under .mainflow/';
53
+ readonly summary: 'Validate every file in the hub\'s data directory';
30
54
  readonly args: 0;
31
55
  readonly needsHub: true;
32
56
  };
33
57
  readonly migrate: {
34
58
  readonly usage: 'migrate [--dry-run]';
35
- readonly summary: 'Convert the hub to the data format this CLI reads; --dry-run lists the changes without writing';
59
+ readonly summary: 'Convert the hub to the data format this CLI reads, on the sync server when it keeps the hub; --dry-run lists the changes without writing';
36
60
  readonly args: 0;
37
61
  readonly needsHub: true;
38
62
  };
@@ -44,7 +68,7 @@ export declare const COMMAND_SPECS: {
44
68
  };
45
69
  readonly 'repo add': {
46
70
  readonly usage: 'repo add <name> <path>';
47
- readonly summary: 'Register a repository that tasks can point at (path relative to the hub root)';
71
+ readonly summary: 'Register a repository that tasks can point at as a primary repository of the hub (path relative to the hub root)';
48
72
  readonly args: 2;
49
73
  readonly needsHub: true;
50
74
  };
@@ -366,25 +390,25 @@ export declare const COMMAND_SPECS: {
366
390
  };
367
391
  readonly retro: {
368
392
  readonly usage: 'retro <m> [--json]';
369
- readonly summary: 'Read the Claude Code transcripts of the sessions that served a mainline, and their subagents\', into estimated list-price cost (per model, opening context, cache hit rate, hook injections) and candidates for where work went wrong (tool errors, rerun Bash commands, hook injections followed by no tool call, blocks, pauses over 5 minutes, failed completion checks); read-only. With MAINFLOW_TELEMETRY_URL, MAINFLOW_TELEMETRY_ACCESS_CLIENT_ID and MAINFLOW_TELEMETRY_ACCESS_CLIENT_SECRET (a Cloudflare Access service token) set it also reads the measurement receiver\'s export: billed cost per request, and the run time of every hook, the silent ones too; sessions whose transcript is not on this machine are downloaded from the receiver into .mainflow/local/transcripts/ first';
393
+ readonly summary: 'Read the Claude Code transcripts of the sessions that served a mainline, and their subagents\', into estimated list-price cost (per model, opening context, cache hit rate, hook injections) and candidates for where work went wrong (tool errors, rerun Bash commands, hook injections followed by no tool call, blocks, pauses over 5 minutes, failed completion checks); read-only. With the measurement upload\'s settings (the receiver\'s origin from TRANSCRIPT_UPLOAD_URL, in the environment or the env of the Claude Code user settings; the Cloudflare Access service token from TELEMETRY_ACCESS_CLIENT_ID and TELEMETRY_ACCESS_CLIENT_SECRET, else the macOS keychain items telemetry-access-client-id and telemetry-access-client-secret) found it also reads the measurement receiver\'s export: billed cost per request, and the run time of every hook, the silent ones too; sessions whose transcript is not on this machine are downloaded from the receiver into .mainflow/local/transcripts/ first';
370
394
  readonly args: 1;
371
395
  readonly needsHub: true;
372
396
  };
373
397
  readonly sync: {
374
398
  readonly usage: 'sync';
375
- readonly summary: `Keep .mainflow/ in step with the hub on the sync server until stopped: pull its changes, send every local change. The server is https://mainflow-sync.carlos209310.space unless MAINFLOW_SYNC_URL, or url in .mainflow/local/sync.json, names another; its Cloudflare Access service token comes from MAINFLOW_SYNC_CLIENT_ID and MAINFLOW_SYNC_CLIENT_SECRET, or clientId and clientSecret in the same file. On connecting it downloads only the files that differ from the versions it last synced; with MAINFLOW_SYNC_ON_DEMAND=1, or onDemand: true in sync.json, it leaves the artifacts of done and dropped mainlines on the server until they are used (sync fetch). While it runs, or with a service token set, every mainflow write is applied here and put in the outbox, which it sends in order; a writing command waits up to 15 s for the server's answer`;
399
+ readonly summary: `Keep .mainflow/ in step with the hub on the sync server until stopped: pull its changes, send every local change. The server is https://mainflow-sync.carlos209310.space unless MAINFLOW_SYNC_URL, or url in .mainflow/local/sync.json, names another; its Cloudflare Access service token comes from MAINFLOW_SYNC_CLIENT_ID and MAINFLOW_SYNC_CLIENT_SECRET, or clientId and clientSecret in the same file. On connecting it downloads only the files that differ from the versions it last synced; unless MAINFLOW_SYNC_ON_DEMAND=0, or onDemand: false in sync.json, it leaves the artifacts of done and dropped mainlines on the server until they are used (sync fetch). In a new environment whose data directory has no config.yaml yet (after mainflow hub register), the hub's name comes from the registry and config.yaml and the other files are downloaded. While it runs, or with a service token set, every mainflow write is applied here and put in the outbox, which it sends in order; a writing command waits up to 15 s for the server's answer`;
376
400
  readonly args: 0;
377
401
  readonly needsHub: true;
378
402
  };
379
403
  readonly 'sync flush': {
380
404
  readonly usage: 'sync flush [--hook]';
381
- readonly summary: 'Send every change to .mainflow/ now and print what did not reach the hub as written, such as a refused edit and where it was saved; --hook reads a PostToolUse payload from stdin and answers as the hook';
405
+ readonly summary: 'Send every change to .mainflow/ now and print what did not reach the hub as written, such as a refused edit and where it was saved; --hook reads a PostToolUse payload from stdin and answers as the hook, for the hub whose data directory holds the file the tool wrote, else the hub of the payload\'s cwd, and nothing outside a registered hub';
382
406
  readonly args: 0;
383
407
  readonly needsHub: true;
384
408
  };
385
409
  readonly 'sync fetch': {
386
410
  readonly usage: 'sync fetch [<path|m|m/t>] [--hook]';
387
- readonly summary: 'Download files only on the sync server: with MAINFLOW_SYNC_ON_DEMAND, or onDemand in .mainflow/local/sync.json, the artifacts of done and dropped mainlines stay there until used. Give a file or directory (from here or from .mainflow/), or a mainline or task ID for all of its artifacts; mainflow sync must be running. Grep and Glob do not see a file that is not downloaded, so fetch the mainline before searching it. --hook reads a PreToolUse payload from stdin: a Read or Write of such a file downloads it first (Claude Code 2.1.285 answers an Edit of a missing file before any hook runs, so Read it or fetch it before editing); a Write, a failed download, or a mainflow sync that does not run or does not answer within 30 s answers deny with the reason';
411
+ readonly summary: 'Download files only on the sync server: unless MAINFLOW_SYNC_ON_DEMAND=0, or onDemand: false in .mainflow/local/sync.json, the artifacts of done and dropped mainlines stay there until used. Give a file or directory (from here or from .mainflow/), or a mainline or task ID for all of its artifacts; mainflow sync must be running. Grep and Glob do not see a file that is not downloaded, so fetch the mainline before searching it. --hook reads a PreToolUse payload from stdin and acts only when its file_path is in a registered hub\'s data directory, from whatever directory it runs: a Read or Write of such a file downloads it first (Claude Code 2.1.285 answers an Edit of a missing file before any hook runs, so Read it or fetch it before editing); a Write, a failed download, or a mainflow sync that does not run or does not answer within 30 s answers deny with the reason';
388
412
  readonly args: 0;
389
413
  readonly optionalArgs: 1;
390
414
  readonly needsHub: true;
@@ -395,6 +419,20 @@ export declare const COMMAND_SPECS: {
395
419
  readonly args: 0;
396
420
  readonly needsHub: true;
397
421
  };
422
+ readonly 'sync log': {
423
+ readonly usage: 'sync log [<path|m|m/t>] [--limit <n>] [--json]';
424
+ readonly summary: 'List the changes the sync server logged, newest first (50 unless --limit says otherwise): of a file or directory (from here or from .mainflow/), of a mainline or task by its ID, or of the whole hub. Each line is the seq, when the server made the change, who sent it (the first 8 characters of the Cloudflare Access Client ID of its environment, (here) for this one), the request, what it did, the path, and what the path held before and after (a hash\'s first 7 digits, dir, or -). The server keeps a change a year, 1 GiB of texts and 2 GiB of binary files a hub; it lists from what seq the hub can be put back';
425
+ readonly args: 0;
426
+ readonly optionalArgs: 1;
427
+ readonly needsHub: true;
428
+ };
429
+ readonly 'sync restore': {
430
+ readonly usage: 'sync restore [<path|m|m/t>] (--seq <n> | --at <time>) [--dry-run] [--json]';
431
+ readonly summary: 'Put the whole hub, or a file, directory, mainline or task, back to what it held after a seq of sync log (--seq), or at a time (--at, local time unless it names its offset, like 2026-10-19T15:30), as a new change on the sync server, which every sync client takes in; nothing of the log is undone, so it prints the command that undoes it. --dry-run lists what it would change. A path whose content the server no longer keeps, or a directory it could not remove, is left as it is and the command fails, after the rest is put back';
432
+ readonly args: 0;
433
+ readonly optionalArgs: 1;
434
+ readonly needsHub: true;
435
+ };
398
436
  };
399
437
  export type CommandName = keyof typeof COMMAND_SPECS;
400
438
  export declare const OPTIONS: {
@@ -522,6 +560,15 @@ export declare const OPTIONS: {
522
560
  readonly month: {
523
561
  readonly type: 'string';
524
562
  };
563
+ readonly limit: {
564
+ readonly type: 'string';
565
+ };
566
+ readonly seq: {
567
+ readonly type: 'string';
568
+ };
569
+ readonly at: {
570
+ readonly type: 'string';
571
+ };
525
572
  readonly all: {
526
573
  readonly type: 'boolean';
527
574
  readonly default: false;
@@ -626,8 +673,7 @@ export declare const RETRO_FORMATS: {
626
673
  };
627
674
  };
628
675
  /**
629
- * Conditions a blocked action's `until` takes and `wait` checks, one example each: a commit on a branch whose
630
- * 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.
676
+ * Conditions a blocked action's `until` takes and `wait` checks, one example each: 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.
631
677
  */
632
678
  export declare const UNTIL: readonly {
633
679
  kind: Until['kind'];
@@ -658,7 +704,10 @@ export declare const GRANTS: {
658
704
  readonly release: '單獨執行 `gh release create <tag> -R <owner/repo>`(可加 --title、--notes、--generate-notes、--draft、--prerelease、--verify-tag)';
659
705
  };
660
706
  };
661
- /** Where each kind of entry lives under the hub root. */
707
+ /**
708
+ * Where each kind of entry lives in the hub's data directory, written as messages name a file of the hub: `.mainflow/`
709
+ * stands for the data directory, $MAINFLOW_HOME/<hub> (~/.mainflow/<hub>) on a machine that registered the hub.
710
+ */
662
711
  export declare const LAYOUT: {
663
712
  readonly config: '.mainflow/config.yaml';
664
713
  readonly mainline: '.mainflow/mainlines/m{NNN}-{slug}/mainline.md';
package/dist/facts.js CHANGED
@@ -1,10 +1,34 @@
1
- import { A as TASK_REQUIRED_SECTIONS, D as SLUG, E as SESSION_STATUS, M as UNTIL_FORMS, N as WORK_STATUS, O as SURVEY_LINES, _ as LINK_RELS, a as DECISION_SECTIONS, b as MERGE_STATES, c as FINDING_SECTIONS, d as GROUP_SECTIONS, et as GRANT_ACTIONS, f as GROUP_STATUS, g as ISSUE_STATUS, h as ISSUE_SECTIONS, i as DECISION_BASES, j as TASK_SECTIONS, k as SURVEY_SECTION, l as FINDING_STATUS, m as ID_PATTERNS, n as CLOSED_STATUS, nt as GRANT_TARGET_FORMS, o as DECISION_STATUS, r as DECIDERS, s as DEFAULT_CATEGORIES, t as CHECKLIST_REF, tt as GRANT_FORMS, u as FORMAT_VERSION, v as MAINLINE_SECTIONS, w as SERVES_REF, x as PRIORITIES, y as MAINLINE_STATUS } from "./dist-zSabglPq.js";
1
+ import { A as SURVEY_LINES, C as PRIORITIES, E as SERVES_REF, F as WORK_STATUS, M as TASK_REQUIRED_SECTIONS, N as TASK_SECTIONS, O as SESSION_STATUS, P as UNTIL_FORMS, S as MERGE_STATES, _ as ISSUE_STATUS, a as DECISION_SECTIONS, b as MAINLINE_SECTIONS, c as FINDING_SECTIONS, d as GROUP_SECTIONS, dt as GRANT_TARGET_FORMS, f as GROUP_STATUS, g as ISSUE_SECTIONS, h as ID_PATTERNS, i as DECISION_BASES, j as SURVEY_SECTION, k as SLUG, l as FINDING_STATUS, lt as GRANT_ACTIONS, n as CLOSED_STATUS, o as DECISION_STATUS, r as DECIDERS, s as DEFAULT_CATEGORIES, t as CHECKLIST_REF, u as FORMAT_VERSION, ut as GRANT_FORMS, v as LINK_RELS, x as MAINLINE_STATUS } from "./dist-D5rZ0MQH.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 = {
5
5
  init: {
6
6
  usage: "init <hub-name>",
7
- summary: "Create .mainflow/ in the current directory",
7
+ summary: "Create the data directory of a new hub in $MAINFLOW_HOME/<hub-name> (~/.mainflow/<hub-name> by default) and register the hub on this machine with the current directory as its repository; nothing is written into the repository",
8
+ args: 1,
9
+ needsHub: false
10
+ },
11
+ "hub register": {
12
+ usage: "hub register <name> <path>",
13
+ summary: "Register on this machine the hub whose repository is <path>, so mainflow run in its primary repositories and their worktrees works on it; run it again after config.yaml changes to update it at once",
14
+ args: 2,
15
+ needsHub: false
16
+ },
17
+ "hub migrate-data": {
18
+ usage: "hub migrate-data <path> [--dry-run]",
19
+ summary: "Move the data of the hub whose repository is <path> from <path>/.mainflow/ to $MAINFLOW_HOME/<name>, and register the hub with its data there. The data is copied and compared before the registry switches to it; the original is then renamed to <path>/.mainflow.migrated, not deleted. A .git file of a git worktree is not copied and git is not run: the command prints the git worktree remove to run. Refused while mainflow sync or the hub's interface runs, or when $MAINFLOW_HOME/<name> is taken; --dry-run says what it would do",
20
+ args: 1,
21
+ needsHub: false
22
+ },
23
+ "hub list": {
24
+ usage: "hub list",
25
+ summary: "List the hubs registered on this machine with their primary and referenced repositories",
26
+ args: 0,
27
+ needsHub: false
28
+ },
29
+ "hub unregister": {
30
+ usage: "hub unregister <name>",
31
+ summary: "Remove a hub from this machine's registry; its files stay",
8
32
  args: 1,
9
33
  needsHub: false
10
34
  },
@@ -16,13 +40,13 @@ const COMMAND_SPECS = {
16
40
  },
17
41
  check: {
18
42
  usage: "check",
19
- summary: "Validate every file under .mainflow/",
43
+ summary: "Validate every file in the hub's data directory",
20
44
  args: 0,
21
45
  needsHub: true
22
46
  },
23
47
  migrate: {
24
48
  usage: "migrate [--dry-run]",
25
- summary: "Convert the hub to the data format this CLI reads; --dry-run lists the changes without writing",
49
+ summary: "Convert the hub to the data format this CLI reads, on the sync server when it keeps the hub; --dry-run lists the changes without writing",
26
50
  args: 0,
27
51
  needsHub: true
28
52
  },
@@ -34,7 +58,7 @@ const COMMAND_SPECS = {
34
58
  },
35
59
  "repo add": {
36
60
  usage: "repo add <name> <path>",
37
- summary: "Register a repository that tasks can point at (path relative to the hub root)",
61
+ summary: "Register a repository that tasks can point at as a primary repository of the hub (path relative to the hub root)",
38
62
  args: 2,
39
63
  needsHub: true
40
64
  },
@@ -356,25 +380,25 @@ const COMMAND_SPECS = {
356
380
  },
357
381
  retro: {
358
382
  usage: "retro <m> [--json]",
359
- summary: "Read the Claude Code transcripts of the sessions that served a mainline, and their subagents', into estimated list-price cost (per model, opening context, cache hit rate, hook injections) and candidates for where work went wrong (tool errors, rerun Bash commands, hook injections followed by no tool call, blocks, pauses over 5 minutes, failed completion checks); read-only. With MAINFLOW_TELEMETRY_URL, MAINFLOW_TELEMETRY_ACCESS_CLIENT_ID and MAINFLOW_TELEMETRY_ACCESS_CLIENT_SECRET (a Cloudflare Access service token) set it also reads the measurement receiver's export: billed cost per request, and the run time of every hook, the silent ones too; sessions whose transcript is not on this machine are downloaded from the receiver into .mainflow/local/transcripts/ first",
383
+ summary: "Read the Claude Code transcripts of the sessions that served a mainline, and their subagents', into estimated list-price cost (per model, opening context, cache hit rate, hook injections) and candidates for where work went wrong (tool errors, rerun Bash commands, hook injections followed by no tool call, blocks, pauses over 5 minutes, failed completion checks); read-only. With the measurement upload's settings (the receiver's origin from TRANSCRIPT_UPLOAD_URL, in the environment or the env of the Claude Code user settings; the Cloudflare Access service token from TELEMETRY_ACCESS_CLIENT_ID and TELEMETRY_ACCESS_CLIENT_SECRET, else the macOS keychain items telemetry-access-client-id and telemetry-access-client-secret) found it also reads the measurement receiver's export: billed cost per request, and the run time of every hook, the silent ones too; sessions whose transcript is not on this machine are downloaded from the receiver into .mainflow/local/transcripts/ first",
360
384
  args: 1,
361
385
  needsHub: true
362
386
  },
363
387
  sync: {
364
388
  usage: "sync",
365
- summary: `Keep .mainflow/ in step with the hub on the sync server until stopped: pull its changes, send every local change. The server is https://mainflow-sync.carlos209310.space unless MAINFLOW_SYNC_URL, or url in .mainflow/local/sync.json, names another; its Cloudflare Access service token comes from MAINFLOW_SYNC_CLIENT_ID and MAINFLOW_SYNC_CLIENT_SECRET, or clientId and clientSecret in the same file. On connecting it downloads only the files that differ from the versions it last synced; with MAINFLOW_SYNC_ON_DEMAND=1, or onDemand: true in sync.json, it leaves the artifacts of done and dropped mainlines on the server until they are used (sync fetch). While it runs, or with a service token set, every mainflow write is applied here and put in the outbox, which it sends in order; a writing command waits up to 15 s for the server's answer`,
389
+ summary: `Keep .mainflow/ in step with the hub on the sync server until stopped: pull its changes, send every local change. The server is https://mainflow-sync.carlos209310.space unless MAINFLOW_SYNC_URL, or url in .mainflow/local/sync.json, names another; its Cloudflare Access service token comes from MAINFLOW_SYNC_CLIENT_ID and MAINFLOW_SYNC_CLIENT_SECRET, or clientId and clientSecret in the same file. On connecting it downloads only the files that differ from the versions it last synced; unless MAINFLOW_SYNC_ON_DEMAND=0, or onDemand: false in sync.json, it leaves the artifacts of done and dropped mainlines on the server until they are used (sync fetch). In a new environment whose data directory has no config.yaml yet (after mainflow hub register), the hub's name comes from the registry and config.yaml and the other files are downloaded. While it runs, or with a service token set, every mainflow write is applied here and put in the outbox, which it sends in order; a writing command waits up to 15 s for the server's answer`,
366
390
  args: 0,
367
391
  needsHub: true
368
392
  },
369
393
  "sync flush": {
370
394
  usage: "sync flush [--hook]",
371
- summary: "Send every change to .mainflow/ now and print what did not reach the hub as written, such as a refused edit and where it was saved; --hook reads a PostToolUse payload from stdin and answers as the hook",
395
+ summary: "Send every change to .mainflow/ now and print what did not reach the hub as written, such as a refused edit and where it was saved; --hook reads a PostToolUse payload from stdin and answers as the hook, for the hub whose data directory holds the file the tool wrote, else the hub of the payload's cwd, and nothing outside a registered hub",
372
396
  args: 0,
373
397
  needsHub: true
374
398
  },
375
399
  "sync fetch": {
376
400
  usage: "sync fetch [<path|m|m/t>] [--hook]",
377
- summary: "Download files only on the sync server: with MAINFLOW_SYNC_ON_DEMAND, or onDemand in .mainflow/local/sync.json, the artifacts of done and dropped mainlines stay there until used. Give a file or directory (from here or from .mainflow/), or a mainline or task ID for all of its artifacts; mainflow sync must be running. Grep and Glob do not see a file that is not downloaded, so fetch the mainline before searching it. --hook reads a PreToolUse payload from stdin: a Read or Write of such a file downloads it first (Claude Code 2.1.285 answers an Edit of a missing file before any hook runs, so Read it or fetch it before editing); a Write, a failed download, or a mainflow sync that does not run or does not answer within 30 s answers deny with the reason",
401
+ summary: "Download files only on the sync server: unless MAINFLOW_SYNC_ON_DEMAND=0, or onDemand: false in .mainflow/local/sync.json, the artifacts of done and dropped mainlines stay there until used. Give a file or directory (from here or from .mainflow/), or a mainline or task ID for all of its artifacts; mainflow sync must be running. Grep and Glob do not see a file that is not downloaded, so fetch the mainline before searching it. --hook reads a PreToolUse payload from stdin and acts only when its file_path is in a registered hub's data directory, from whatever directory it runs: a Read or Write of such a file downloads it first (Claude Code 2.1.285 answers an Edit of a missing file before any hook runs, so Read it or fetch it before editing); a Write, a failed download, or a mainflow sync that does not run or does not answer within 30 s answers deny with the reason",
378
402
  args: 0,
379
403
  optionalArgs: 1,
380
404
  needsHub: true
@@ -384,6 +408,20 @@ const COMMAND_SPECS = {
384
408
  summary: "Say whether mainflow sync runs for this hub, whether it is online and caught up with the server, and how many writes wait to be sent",
385
409
  args: 0,
386
410
  needsHub: true
411
+ },
412
+ "sync log": {
413
+ usage: "sync log [<path|m|m/t>] [--limit <n>] [--json]",
414
+ summary: "List the changes the sync server logged, newest first (50 unless --limit says otherwise): of a file or directory (from here or from .mainflow/), of a mainline or task by its ID, or of the whole hub. Each line is the seq, when the server made the change, who sent it (the first 8 characters of the Cloudflare Access Client ID of its environment, (here) for this one), the request, what it did, the path, and what the path held before and after (a hash's first 7 digits, dir, or -). The server keeps a change a year, 1 GiB of texts and 2 GiB of binary files a hub; it lists from what seq the hub can be put back",
415
+ args: 0,
416
+ optionalArgs: 1,
417
+ needsHub: true
418
+ },
419
+ "sync restore": {
420
+ usage: "sync restore [<path|m|m/t>] (--seq <n> | --at <time>) [--dry-run] [--json]",
421
+ summary: "Put the whole hub, or a file, directory, mainline or task, back to what it held after a seq of sync log (--seq), or at a time (--at, local time unless it names its offset, like 2026-10-19T15:30), as a new change on the sync server, which every sync client takes in; nothing of the log is undone, so it prints the command that undoes it. --dry-run lists what it would change. A path whose content the server no longer keeps, or a directory it could not remove, is left as it is and the command fails, after the rest is put back",
422
+ args: 0,
423
+ optionalArgs: 1,
424
+ needsHub: true
387
425
  }
388
426
  };
389
427
  const OPTIONS = {
@@ -431,6 +469,9 @@ const OPTIONS = {
431
469
  result: { type: "string" },
432
470
  ref: { type: "string" },
433
471
  month: { type: "string" },
472
+ limit: { type: "string" },
473
+ seq: { type: "string" },
474
+ at: { type: "string" },
434
475
  all: {
435
476
  type: "boolean",
436
477
  default: false
@@ -547,8 +588,7 @@ const RETRO_FORMATS = {
547
588
  }
548
589
  };
549
590
  /**
550
- * Conditions a blocked action's `until` takes and `wait` checks, one example each: a commit on a branch whose
551
- * 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.
591
+ * Conditions a blocked action's `until` takes and `wait` checks, one example each: 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.
552
592
  */
553
593
  const UNTIL = UNTIL_FORMS;
554
594
  /** Relation types a document's links take; readers derive the reverse direction. */
@@ -567,7 +607,10 @@ const GRANTS = {
567
607
  enforcedByHook: ["publish", "release"],
568
608
  forms: GRANT_FORMS
569
609
  };
570
- /** Where each kind of entry lives under the hub root. */
610
+ /**
611
+ * Where each kind of entry lives in the hub's data directory, written as messages name a file of the hub: `.mainflow/`
612
+ * stands for the data directory, $MAINFLOW_HOME/<hub> (~/.mainflow/<hub>) on a machine that registered the hub.
613
+ */
571
614
  const LAYOUT = {
572
615
  config: ".mainflow/config.yaml",
573
616
  mainline: ".mainflow/mainlines/m{NNN}-{slug}/mainline.md",
@@ -672,7 +715,7 @@ const FIELDS = {
672
715
  field("order", false, "在主脈絡內的順序,小的在前"),
673
716
  field("dependsOn", false, "開始前必須先完成的任務"),
674
717
  field("repo", false, "主要動到的倉庫,名稱需在 config.yaml 登記"),
675
- field("touches", false, "預計會改到的路徑或 glob 清單,相對於 repo 的根目錄;兩條進行中的主脈絡各有未結束的任務在同一個 repo 碰到重疊的路徑、彼此又沒有 depends-on 時,check 列出警告;有 touches 卻沒填 repo 時路徑不會被比對,check 也列警告"),
718
+ field("touches", false, "預計會改到的路徑或 glob 清單,相對於 repo 的根目錄;只供開場簡報列出兩條進行中主脈絡的未結束任務在同一個 repo 重疊的路徑,check 不警告,重疊由提交時兩邊的會話協調,不必記 depends-on;有 touches 卻沒填 repo 時路徑不會被比對,check 也列警告"),
676
719
  field("startedAt", false, "第一次轉為 doing 時由 CLI 寫入"),
677
720
  field("checklist", true, "checklist 項目清單,每項的欄位見 checklistItem"),
678
721
  field("blockedBy", false, "卡住時的原因、等待對象與繼續的訊號,blocked 時必填;等待對象整串是參照時(例如 m031/t002)會檢查它存在"),
@@ -1,4 +1,4 @@
1
- import "./index-Dk4fCI7q.js";
1
+ import "./index-BqPQp6TF.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 { tt as GRANT_FORMS } from "./dist-zSabglPq.js";
1
+ import { ut as GRANT_FORMS } from "./dist-D5rZ0MQH.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";