mainflow 0.42.0 → 0.43.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,37 @@ 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 which': {
40
+ readonly usage: 'hub which [--json]';
41
+ readonly summary: 'Print the name, repository root and data directory of the hub the current directory belongs to, as hub list finds it. --json prints one object {"name","root","data"}. Outside any registered hub it prints nothing to stdout, one line to stderr, and exits 1. Writes nothing';
42
+ readonly args: 0;
43
+ readonly needsHub: false;
44
+ };
45
+ readonly 'hub unregister': {
46
+ readonly usage: 'hub unregister <name>';
47
+ readonly summary: 'Remove a hub from this machine\'s registry; its files stay';
18
48
  readonly args: 1;
19
49
  readonly needsHub: false;
20
50
  };
@@ -26,13 +56,13 @@ export declare const COMMAND_SPECS: {
26
56
  };
27
57
  readonly check: {
28
58
  readonly usage: 'check';
29
- readonly summary: 'Validate every file under .mainflow/';
59
+ readonly summary: 'Validate every file in the hub\'s data directory';
30
60
  readonly args: 0;
31
61
  readonly needsHub: true;
32
62
  };
33
63
  readonly migrate: {
34
64
  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';
65
+ 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
66
  readonly args: 0;
37
67
  readonly needsHub: true;
38
68
  };
@@ -44,7 +74,7 @@ export declare const COMMAND_SPECS: {
44
74
  };
45
75
  readonly 'repo add': {
46
76
  readonly usage: 'repo add <name> <path>';
47
- readonly summary: 'Register a repository that tasks can point at (path relative to the hub root)';
77
+ readonly summary: 'Register a repository that tasks can point at as a primary repository of the hub (path relative to the hub root)';
48
78
  readonly args: 2;
49
79
  readonly needsHub: true;
50
80
  };
@@ -366,25 +396,25 @@ export declare const COMMAND_SPECS: {
366
396
  };
367
397
  readonly retro: {
368
398
  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';
399
+ 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
400
  readonly args: 1;
371
401
  readonly needsHub: true;
372
402
  };
373
403
  readonly sync: {
374
404
  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`;
405
+ 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
406
  readonly args: 0;
377
407
  readonly needsHub: true;
378
408
  };
379
409
  readonly 'sync flush': {
380
410
  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';
411
+ 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
412
  readonly args: 0;
383
413
  readonly needsHub: true;
384
414
  };
385
415
  readonly 'sync fetch': {
386
416
  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';
417
+ 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
418
  readonly args: 0;
389
419
  readonly optionalArgs: 1;
390
420
  readonly needsHub: true;
@@ -395,6 +425,20 @@ export declare const COMMAND_SPECS: {
395
425
  readonly args: 0;
396
426
  readonly needsHub: true;
397
427
  };
428
+ readonly 'sync log': {
429
+ readonly usage: 'sync log [<path|m|m/t>] [--limit <n>] [--json]';
430
+ 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';
431
+ readonly args: 0;
432
+ readonly optionalArgs: 1;
433
+ readonly needsHub: true;
434
+ };
435
+ readonly 'sync restore': {
436
+ readonly usage: 'sync restore [<path|m|m/t>] (--seq <n> | --at <time>) [--dry-run] [--json]';
437
+ 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';
438
+ readonly args: 0;
439
+ readonly optionalArgs: 1;
440
+ readonly needsHub: true;
441
+ };
398
442
  };
399
443
  export type CommandName = keyof typeof COMMAND_SPECS;
400
444
  export declare const OPTIONS: {
@@ -522,6 +566,15 @@ export declare const OPTIONS: {
522
566
  readonly month: {
523
567
  readonly type: 'string';
524
568
  };
569
+ readonly limit: {
570
+ readonly type: 'string';
571
+ };
572
+ readonly seq: {
573
+ readonly type: 'string';
574
+ };
575
+ readonly at: {
576
+ readonly type: 'string';
577
+ };
525
578
  readonly all: {
526
579
  readonly type: 'boolean';
527
580
  readonly default: false;
@@ -626,8 +679,7 @@ export declare const RETRO_FORMATS: {
626
679
  };
627
680
  };
628
681
  /**
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.
682
+ * 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
683
  */
632
684
  export declare const UNTIL: readonly {
633
685
  kind: Until['kind'];
@@ -658,7 +710,10 @@ export declare const GRANTS: {
658
710
  readonly release: '單獨執行 `gh release create <tag> -R <owner/repo>`(可加 --title、--notes、--generate-notes、--draft、--prerelease、--verify-tag)';
659
711
  };
660
712
  };
661
- /** Where each kind of entry lives under the hub root. */
713
+ /**
714
+ * Where each kind of entry lives in the hub's data directory, written as messages name a file of the hub: `.mainflow/`
715
+ * stands for the data directory, $MAINFLOW_HOME/<hub> (~/.mainflow/<hub>) on a machine that registered the hub.
716
+ */
662
717
  export declare const LAYOUT: {
663
718
  readonly config: '.mainflow/config.yaml';
664
719
  readonly mainline: '.mainflow/mainlines/m{NNN}-{slug}/mainline.md';
package/dist/facts.js CHANGED
@@ -1,10 +1,40 @@
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 which": {
30
+ usage: "hub which [--json]",
31
+ summary: "Print the name, repository root and data directory of the hub the current directory belongs to, as hub list finds it. --json prints one object {\"name\",\"root\",\"data\"}. Outside any registered hub it prints nothing to stdout, one line to stderr, and exits 1. Writes nothing",
32
+ args: 0,
33
+ needsHub: false
34
+ },
35
+ "hub unregister": {
36
+ usage: "hub unregister <name>",
37
+ summary: "Remove a hub from this machine's registry; its files stay",
8
38
  args: 1,
9
39
  needsHub: false
10
40
  },
@@ -16,13 +46,13 @@ const COMMAND_SPECS = {
16
46
  },
17
47
  check: {
18
48
  usage: "check",
19
- summary: "Validate every file under .mainflow/",
49
+ summary: "Validate every file in the hub's data directory",
20
50
  args: 0,
21
51
  needsHub: true
22
52
  },
23
53
  migrate: {
24
54
  usage: "migrate [--dry-run]",
25
- summary: "Convert the hub to the data format this CLI reads; --dry-run lists the changes without writing",
55
+ 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
56
  args: 0,
27
57
  needsHub: true
28
58
  },
@@ -34,7 +64,7 @@ const COMMAND_SPECS = {
34
64
  },
35
65
  "repo add": {
36
66
  usage: "repo add <name> <path>",
37
- summary: "Register a repository that tasks can point at (path relative to the hub root)",
67
+ summary: "Register a repository that tasks can point at as a primary repository of the hub (path relative to the hub root)",
38
68
  args: 2,
39
69
  needsHub: true
40
70
  },
@@ -356,25 +386,25 @@ const COMMAND_SPECS = {
356
386
  },
357
387
  retro: {
358
388
  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",
389
+ 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
390
  args: 1,
361
391
  needsHub: true
362
392
  },
363
393
  sync: {
364
394
  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`,
395
+ 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
396
  args: 0,
367
397
  needsHub: true
368
398
  },
369
399
  "sync flush": {
370
400
  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",
401
+ 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
402
  args: 0,
373
403
  needsHub: true
374
404
  },
375
405
  "sync fetch": {
376
406
  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",
407
+ 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
408
  args: 0,
379
409
  optionalArgs: 1,
380
410
  needsHub: true
@@ -384,6 +414,20 @@ const COMMAND_SPECS = {
384
414
  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
415
  args: 0,
386
416
  needsHub: true
417
+ },
418
+ "sync log": {
419
+ usage: "sync log [<path|m|m/t>] [--limit <n>] [--json]",
420
+ 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",
421
+ args: 0,
422
+ optionalArgs: 1,
423
+ needsHub: true
424
+ },
425
+ "sync restore": {
426
+ usage: "sync restore [<path|m|m/t>] (--seq <n> | --at <time>) [--dry-run] [--json]",
427
+ 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",
428
+ args: 0,
429
+ optionalArgs: 1,
430
+ needsHub: true
387
431
  }
388
432
  };
389
433
  const OPTIONS = {
@@ -431,6 +475,9 @@ const OPTIONS = {
431
475
  result: { type: "string" },
432
476
  ref: { type: "string" },
433
477
  month: { type: "string" },
478
+ limit: { type: "string" },
479
+ seq: { type: "string" },
480
+ at: { type: "string" },
434
481
  all: {
435
482
  type: "boolean",
436
483
  default: false
@@ -547,8 +594,7 @@ const RETRO_FORMATS = {
547
594
  }
548
595
  };
549
596
  /**
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.
597
+ * 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
598
  */
553
599
  const UNTIL = UNTIL_FORMS;
554
600
  /** Relation types a document's links take; readers derive the reverse direction. */
@@ -567,7 +613,10 @@ const GRANTS = {
567
613
  enforcedByHook: ["publish", "release"],
568
614
  forms: GRANT_FORMS
569
615
  };
570
- /** Where each kind of entry lives under the hub root. */
616
+ /**
617
+ * Where each kind of entry lives in the hub's data directory, written as messages name a file of the hub: `.mainflow/`
618
+ * stands for the data directory, $MAINFLOW_HOME/<hub> (~/.mainflow/<hub>) on a machine that registered the hub.
619
+ */
571
620
  const LAYOUT = {
572
621
  config: ".mainflow/config.yaml",
573
622
  mainline: ".mainflow/mainlines/m{NNN}-{slug}/mainline.md",
@@ -672,7 +721,7 @@ const FIELDS = {
672
721
  field("order", false, "在主脈絡內的順序,小的在前"),
673
722
  field("dependsOn", false, "開始前必須先完成的任務"),
674
723
  field("repo", false, "主要動到的倉庫,名稱需在 config.yaml 登記"),
675
- field("touches", false, "預計會改到的路徑或 glob 清單,相對於 repo 的根目錄;兩條進行中的主脈絡各有未結束的任務在同一個 repo 碰到重疊的路徑、彼此又沒有 depends-on 時,check 列出警告;有 touches 卻沒填 repo 時路徑不會被比對,check 也列警告"),
724
+ field("touches", false, "預計會改到的路徑或 glob 清單,相對於 repo 的根目錄;只供開場簡報列出兩條進行中主脈絡的未結束任務在同一個 repo 重疊的路徑,check 不警告,重疊由提交時兩邊的會話協調,不必記 depends-on;有 touches 卻沒填 repo 時路徑不會被比對,check 也列警告"),
676
725
  field("startedAt", false, "第一次轉為 doing 時由 CLI 寫入"),
677
726
  field("checklist", true, "checklist 項目清單,每項的欄位見 checklistItem"),
678
727
  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";