mainflow 0.24.0 → 0.26.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,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { $ as reopenIssue, A as addAction, C as setCriterionText, D as updateTask, E as supersedeDecision, F as setSessionArchived, J as appendOps, L as setSessionStatus, M as handoffSession, O as sessionBriefing, P as readSession, Q as planIssue, S as setCriterion, V as updateAction, X as createFinding, Y as closeIssue, Z as createIssue, _ as createTask, b as setArchived, et as setFindingArchived, f as acceptDecision, ft as WriteError, g as createMainline, gt as findHubRoot, h as createDecision, it as loadHub, l as migrateHub, lt as MAINLINE_FILE, m as addRepo, nt as setIssueArchived, p as addHub, rt as HubError, tt as setFindingStatus, ut as TASK_FILE, v as dropChecklistItem, w as setMainlineStatus, x as setChecklistItem, y as initHub, z as startSession } from "./host-2a_9-kKv.js";
3
- import { I as parseRef, L as refExample } from "./dist-Dy45myuN.js";
2
+ import { $ as createIssue, A as sessionBriefing, C as setCriterionText, D as updateTask, E as supersedeDecision, F as loadSessions, I as readSession, L as setSessionArchived, M as addAction, O as untilState, P as handoffSession, Q as createFinding, S as setCriterion, U as updateAction, V as startSession, X as appendOps, Z as closeIssue, _ as createTask, at as HubError, b as setArchived, dt as MAINLINE_FILE, et as planIssue, f as acceptDecision, ft as TASK_FILE, g as createMainline, h as createDecision, it as setIssueArchived, k as duplicateClaimLines, l as migrateHub, m as addRepo, mt as WriteError, nt as setFindingArchived, ot as loadHub, p as addHub, rt as setFindingStatus, tt as reopenIssue, v as dropChecklistItem, vt as findHubRoot, w as setMainlineStatus, x as setChecklistItem, y as initHub, z as setSessionStatus } from "./host-Dr0bCHd8.js";
3
+ import { B as parseUntil, O as UNTIL_FORMS, V as refExample, z as parseRef } from "./dist-DlmecRLf.js";
4
4
  import { t as listProgress } from "./progress-1BfPg2zN.js";
5
5
  import { COMMAND_SPECS, OPTIONS } from "./facts.js";
6
6
  import { hookFallback, parsePayload, runHook } from "./hooks.js";
7
- import { l as CLI_VERSION, r as launchUi, s as startUiInBackground } from "./launch-Bk3UgS-o.js";
7
+ import { l as CLI_VERSION, r as launchUi, s as startUiInBackground } from "./launch-lnD-Z3hb.js";
8
8
  import { readFileSync, realpathSync } from "node:fs";
9
9
  import { join, relative } from "node:path";
10
10
  import { fileURLToPath } from "node:url";
@@ -15,9 +15,10 @@ const RUNS = {
15
15
  status: ({ root, opts }) => printStatus(loadHub(root), opts.all ?? false),
16
16
  check: ({ root }) => {
17
17
  const hub = loadHub(root);
18
+ const problems = [...hub.problems, ...loadSessions(root).problems];
18
19
  printProblems(hub.warnings, "!");
19
- if (hub.problems.length === 0) return done(`✓ ${hub.mainlines.length} mainlines, no problems${hub.warnings.length ? `, ${hub.warnings.length} ${hub.warnings.length === 1 ? "warning" : "warnings"}` : ""}`);
20
- printProblems(hub.problems);
20
+ if (problems.length === 0) return done(`✓ ${hub.mainlines.length} mainlines, no problems${hub.warnings.length ? `, ${hub.warnings.length} ${hub.warnings.length === 1 ? "warning" : "warnings"}` : ""}`);
21
+ printProblems(problems);
21
22
  return 1;
22
23
  },
23
24
  migrate: ({ root, opts }) => {
@@ -69,21 +70,34 @@ const RUNS = {
69
70
  "session end": ({ root, args, opts }) => done(`ended ${setSessionStatus(root, sessionOf(opts, args[0]), "ended").id}`),
70
71
  "session archive": ({ root, args }) => done(`archived ${setSessionArchived(root, args[0], true).id}`),
71
72
  "session unarchive": ({ root, args }) => done(`unarchived ${setSessionArchived(root, args[0], false).id}`),
72
- "action add": ({ root, args, opts }) => done(`created ${addAction(root, sessionOf(opts), {
73
- do: args[0],
74
- serves: list(opts.serves),
75
- before: opts.before
76
- })}`),
73
+ "action add": ({ root, args, opts }) => {
74
+ const ref = addAction(root, sessionOf(opts), {
75
+ do: args[0],
76
+ serves: list(opts.serves),
77
+ before: opts.before
78
+ });
79
+ done(`created ${ref}`);
80
+ return remindDuplicates(root, ref);
81
+ },
77
82
  "action set": ({ root, args, opts }) => {
78
- const ref = /^a\d{3}$/.test(args[0]) ? `${sessionOf(opts)}#${args[0]}` : args[0];
79
- return done(`updated ${ref} (${updateAction(root, ref, {
83
+ const ref = actionRef(opts, args[0]);
84
+ const blockedBy = opts.reason ? {
85
+ reason: opts.reason,
86
+ ...opts["waiting-for"] ? { waitingFor: opts["waiting-for"] } : {},
87
+ ...opts.until ? { until: opts.until } : {}
88
+ } : void 0;
89
+ if (!blockedBy && (opts["waiting-for"] || opts.until)) throw new WriteError("--waiting-for and --until need --reason, e.g. --status blocked --reason \"等 schema 定案\"");
90
+ done(`updated ${ref} (${updateAction(root, ref, {
80
91
  status: opts.status,
81
92
  do: opts.text,
82
93
  serves: opts.serves === void 0 ? void 0 : list(opts.serves),
83
94
  before: opts.before,
84
- note: opts.note
95
+ note: opts.note,
96
+ blockedBy
85
97
  }).status})`);
98
+ return opts.status === "doing" ? remindDuplicates(root, ref) : 0;
86
99
  },
100
+ wait: ({ root, args, opts }) => waitUntil(root, args[0], opts),
87
101
  hook: ({ args }) => {
88
102
  let input = "";
89
103
  try {
@@ -341,6 +355,98 @@ function sessionOf(opts, arg) {
341
355
  if (!id) throw new WriteError("no session given; pass --session <id> or set MAINFLOW_SESSION (the SessionStart hook sets it)");
342
356
  return id;
343
357
  }
358
+ /** An action named as a{NNN} in the current session, or as {session}#a{NNN}. */
359
+ const actionRef = (opts, arg) => /^a\d{3}$/.test(arg) ? `${sessionOf(opts)}#${arg}` : arg;
360
+ /** Print a line for each running session already doing what this action serves; a reminder, so the exit code stays 0. */
361
+ function remindDuplicates(root, ref) {
362
+ const hash = ref.lastIndexOf("#");
363
+ for (const line of duplicateClaimLines(root, ref.slice(0, hash), ref.slice(hash + 1))) console.log(line);
364
+ return 0;
365
+ }
366
+ /** Exit codes of `wait`: the condition holds, the time ran out, or there is no condition to wait for. */
367
+ const WAIT_EXIT = {
368
+ met: 0,
369
+ timedOut: 1,
370
+ unusable: 2
371
+ };
372
+ const WAIT_DEFAULTS = {
373
+ timeout: 1800,
374
+ interval: 10
375
+ };
376
+ const UNTIL_HINT = UNTIL_FORMS.map((f) => f.format).join(", ");
377
+ /** Block the process for a while; wait runs synchronously like every other command. */
378
+ const sleep = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
379
+ function seconds(value, fallback, name, zero) {
380
+ if (value === void 0) return fallback;
381
+ const n = Number(value);
382
+ if (value.trim() === "" || !Number.isFinite(n) || n < 0 || !zero && n === 0) throw new WriteError(`--${name} takes ${zero ? "zero or more" : "more than zero"} seconds, got "${value}"`);
383
+ return n;
384
+ }
385
+ /**
386
+ * Poll a condition until it holds or the time runs out. Given an action, its `until` is read again on each
387
+ * round, so a condition changed while waiting is the one checked; once the action leaves blocked or loses its
388
+ * `until`, there is nothing left to wait for. A condition that can no longer come true ends the wait at once.
389
+ */
390
+ function waitUntil(root, arg, opts) {
391
+ let timeout;
392
+ let interval;
393
+ let ref;
394
+ let text;
395
+ let condition;
396
+ const unusable = (message) => {
397
+ console.error(`✗ ${message}`);
398
+ return WAIT_EXIT.unusable;
399
+ };
400
+ /** The action's current `until`, or the reason it has none. */
401
+ const untilOf = (actionRef) => {
402
+ const hash = actionRef.lastIndexOf("#");
403
+ let session;
404
+ try {
405
+ session = readSession(root, actionRef.slice(0, hash));
406
+ } catch (error) {
407
+ return { error: error.message };
408
+ }
409
+ const action = session?.actions.find((a) => a.id === actionRef.slice(hash + 1));
410
+ if (!action) return { error: `action ${actionRef} not found` };
411
+ return action.blockedBy?.until ?? { error: `${actionRef} has no until condition (it is ${action.status}); give it one with action set ${arg} --status blocked --reason <reason> --until <condition>, where the condition is one of ${UNTIL_HINT}` };
412
+ };
413
+ try {
414
+ timeout = seconds(opts.timeout, WAIT_DEFAULTS.timeout, "timeout", true);
415
+ interval = seconds(opts.interval, WAIT_DEFAULTS.interval, "interval", false);
416
+ if (arg && opts.until) return unusable("give an action or --until, not both");
417
+ if (!arg && !opts.until) return unusable(`name the action to wait for, e.g. wait a002, or pass --until <condition>, one of ${UNTIL_HINT}`);
418
+ if (arg) {
419
+ ref = actionRef(opts, arg);
420
+ if (!/^[^#]+#a\d{3}$/.test(ref)) return unusable(`expected an action like a002 or claude-code-1a2b3c4d#a002, got "${arg}"`);
421
+ const until = untilOf(ref);
422
+ if (typeof until !== "string") return unusable(until.error);
423
+ text = until;
424
+ } else text = opts.until;
425
+ const parsed = parseUntil(text);
426
+ if (!parsed) return unusable(`"${text}" is not a condition; use one of ${UNTIL_HINT}`);
427
+ condition = parsed;
428
+ } catch (error) {
429
+ if (error instanceof WriteError) return unusable(error.message);
430
+ throw error;
431
+ }
432
+ const deadline = Date.now() + timeout * 1e3;
433
+ for (;;) {
434
+ const state = untilState(root, condition);
435
+ if (state.met) return done(`✓ ${text}: ${state.detail}`);
436
+ if (state.unreachable) return unusable(`${text} can no longer hold: ${state.detail}`);
437
+ const left = deadline - Date.now();
438
+ if (left <= 0) {
439
+ console.error(`✗ still waiting after ${timeout}s for ${text}: ${state.detail}`);
440
+ return WAIT_EXIT.timedOut;
441
+ }
442
+ sleep(Math.min(interval * 1e3, left));
443
+ if (!ref) continue;
444
+ const next = untilOf(ref);
445
+ if (typeof next !== "string") return unusable(`stopped waiting: ${next.error}`);
446
+ text = next;
447
+ condition = parseUntil(next);
448
+ }
449
+ }
344
450
  const list = (value) => (value ?? "").split(",").map((v) => v.trim()).filter(Boolean);
345
451
  /** The mainline a reference names; throws with the same message resolve.ts gives when it is missing. */
346
452
  function findMainline(hub, id) {
@@ -43,7 +43,7 @@ const GRANT_TARGET_FORMS = {
43
43
  };
44
44
  const KEBAB = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
45
45
  /** A git branch name, `*` standing for any run of characters. */
46
- const BRANCH = /^(?!\/)(?!.*\/\/)(?!.*\.\.)[A-Za-z0-9._/*-]+(?<!\/)$/;
46
+ const BRANCH$1 = /^(?!\/)(?!.*\/\/)(?!.*\.\.)[A-Za-z0-9._/*-]+(?<!\/)$/;
47
47
  /** npm package names: lowercase, optionally scoped. */
48
48
  const PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._~-]*\/)?[a-z0-9][a-z0-9._~-]*$/;
49
49
  /** A tag pattern: no whitespace, `*` and `?` as in a shell glob. */
@@ -62,7 +62,7 @@ function parseGrantTarget(action, target) {
62
62
  const repo = colon < 0 ? target : target.slice(0, colon);
63
63
  const branch = colon < 0 ? void 0 : target.slice(colon + 1);
64
64
  if (!repo) return fail(action, "the repo name is missing");
65
- if (branch !== void 0 && !BRANCH.test(branch)) return fail(action, `"${branch}" is not a branch name`);
65
+ if (branch !== void 0 && !BRANCH$1.test(branch)) return fail(action, `"${branch}" is not a branch name`);
66
66
  return {
67
67
  ok: true,
68
68
  target: {
@@ -805,16 +805,122 @@ const taskSchema = z.object({
805
805
  message: "a task cannot depend on itself"
806
806
  });
807
807
  });
808
+ /** A session id: lowercase letters, digits and hyphens, e.g. claude-code-1a2b3c4d. */
809
+ const SESSION_ID = /^[a-z0-9][a-z0-9-]*$/;
810
+ /** An action in some session, `{sessionId}#a{NNN}`. */
811
+ const ACTION_REF = /^([a-z0-9][a-z0-9-]*)#(a\d{3})$/;
812
+ /** Split an action reference into its session and action ids, or return null when the text is not one. */
813
+ function parseActionRef(input) {
814
+ const m = ACTION_REF.exec(input);
815
+ return m ? {
816
+ session: m[1],
817
+ action: m[2]
818
+ } : null;
819
+ }
820
+ /** The action a blocked action waits for, when its whole `waitingFor` text is an action reference; otherwise undefined. */
821
+ const waitingForAction = (waitingFor) => waitingFor && parseActionRef(waitingFor) ? waitingFor : void 0;
822
+ /** The condition forms, one example each; facts and error messages read them from here. */
823
+ const UNTIL_FORMS = [
824
+ {
825
+ kind: "commit",
826
+ format: "commit:{branch}:{text}",
827
+ example: "commit:main:feat(core): 行動阻塞"
828
+ },
829
+ {
830
+ kind: "action-done",
831
+ format: "action-done:{sessionId}#a{NNN}",
832
+ example: "action-done:claude-code-1a2b3c4d#a003"
833
+ },
834
+ {
835
+ kind: "file",
836
+ format: "file:{relative path}",
837
+ example: "file:packages/cli/dist/cli.js"
838
+ }
839
+ ];
840
+ const UNTIL_EXAMPLES = UNTIL_FORMS.map((f) => f.format).join(", ");
841
+ /** A branch name git log can take as a revision; a leading hyphen would read as an option. */
842
+ const BRANCH = /^[^\s:~^?*[\\-][^\s:~^?*[\\]*$/;
843
+ /** Parse a wait condition, or return null when it matches none of the forms. */
844
+ function parseUntil(input) {
845
+ const colon = input.indexOf(":");
846
+ if (colon < 0) return null;
847
+ const kind = input.slice(0, colon);
848
+ const argument = input.slice(colon + 1);
849
+ switch (kind) {
850
+ case "commit": {
851
+ const split = argument.indexOf(":");
852
+ const branch = argument.slice(0, split);
853
+ const text = argument.slice(split + 1);
854
+ return split > 0 && BRANCH.test(branch) && !branch.includes("..") && text.length > 0 ? {
855
+ kind,
856
+ branch,
857
+ text
858
+ } : null;
859
+ }
860
+ case "action-done": {
861
+ const ref = parseActionRef(argument);
862
+ return ref ? {
863
+ kind,
864
+ ...ref
865
+ } : null;
866
+ }
867
+ case "file": return argument.length > 0 && !argument.startsWith("/") ? {
868
+ kind,
869
+ path: argument
870
+ } : null;
871
+ default: return null;
872
+ }
873
+ }
874
+ /**
875
+ * Why a blocked action cannot move, what it waits for, and the condition a program checks to end the wait.
876
+ * `waitingFor` is free text, another session's action such as claude-code-1a2b3c4d#a003, or a hub reference
877
+ * such as m031/t002 when the whole text is one.
878
+ */
879
+ const actionBlockSchema = z.object({
880
+ reason: text,
881
+ waitingFor: text.optional(),
882
+ until: z.string().refine((s) => parseUntil(s) !== null, `expected a condition like ${UNTIL_EXAMPLES}`).optional()
883
+ });
808
884
  const actionSchema = z.object({
809
885
  id: id("action"),
810
886
  do: z.string().min(1),
811
887
  status: z.enum(WORK_STATUS),
812
888
  serves: z.array(z.string().regex(CHECKLIST_REF)),
813
889
  note: text.optional(),
890
+ /** Required while blocked, absent otherwise */
891
+ blockedBy: actionBlockSchema.optional(),
814
892
  /** Set when the action turns doing and done; the transcript trace attributes tool use by these */
815
893
  startedAt: datetime.optional(),
816
894
  doneAt: datetime.optional()
895
+ }).superRefine((a, ctx) => {
896
+ if (a.status === "blocked" && !a.blockedBy) ctx.addIssue({
897
+ code: "custom",
898
+ path: ["blockedBy"],
899
+ message: "a blocked action must say what blocks it"
900
+ });
901
+ if (a.status !== "blocked" && a.blockedBy) ctx.addIssue({
902
+ code: "custom",
903
+ path: ["blockedBy"],
904
+ message: `only a blocked action carries blockedBy; this one is ${a.status}`
905
+ });
817
906
  });
907
+ /** The reason a blocked action written before `blockedBy` existed is given when it carries no note either. */
908
+ const LEGACY_BLOCK_REASON = "(舊資料未寫原因)";
909
+ /**
910
+ * Session files written before `blockedBy` existed hold blocked actions without one. Reading fills it from the
911
+ * action's note, so those files stay readable and the next write stores the filled form. `actionSchema` itself
912
+ * stays strict, so a new block still has to say what blocks it.
913
+ */
914
+ function withLegacyBlock(input) {
915
+ if (typeof input !== "object" || input === null) return input;
916
+ const action = input;
917
+ if (action.status !== "blocked" || action.blockedBy !== void 0) return input;
918
+ const reason = typeof action.note === "string" && action.note.length > 0 ? action.note : LEGACY_BLOCK_REASON;
919
+ return {
920
+ ...action,
921
+ blockedBy: { reason }
922
+ };
923
+ }
818
924
  const sessionSchema = z.object({
819
925
  id: z.string().min(1),
820
926
  agent: z.string().min(1),
@@ -823,7 +929,7 @@ const sessionSchema = z.object({
823
929
  delegatedFrom: z.string().min(1).nullable(),
824
930
  handoffFrom: z.string().min(1).nullable(),
825
931
  cwd: z.string().min(1),
826
- actions: z.array(actionSchema),
932
+ actions: z.array(z.preprocess(withLegacyBlock, actionSchema)),
827
933
  /** Where the platform keeps the conversation, for tracing and for opening it */
828
934
  transcript: text.optional(),
829
935
  worktree: text.optional(),
@@ -904,4 +1010,4 @@ function parseOpsLine(line) {
904
1010
  return parsed.success ? parsed.data : null;
905
1011
  }
906
1012
  //#endregion
907
- export { findingSchema as A, taskSchema as B, SURVEY_LINES as C, WORK_STATUS as D, TASK_SECTIONS as E, parseOpsLine as F, describeRequest as G, GRANT_ACTIONS as H, parseRef as I, grantRepo as J, grantCovers as K, refExample as L, issueSchema as M, mainlineSchema as N, configSchema as O, opsEntrySchema as P, refMainline as R, SLUG as S, TASK_REQUIRED_SECTIONS as T, GRANT_FORMS as U, waitingForRef as V, GRANT_TARGET_FORMS as W, parseGrantTarget as Y, MERGE_STATES as _, DECISION_SECTIONS as a, RESERVED_HUB_NAMES as b, FINDING_SECTIONS as c, ID_PATTERNS as d, ISSUE_SECTIONS as f, MAINLINE_STATUS as g, MAINLINE_SECTIONS as h, DECISION_BASES as i, formatOpsEntry as j, decisionSchema as k, FINDING_STATUS as l, LINK_RELS as m, CLOSED_STATUS as n, DECISION_STATUS as o, ISSUE_STATUS as p, grantForm as q, DECIDERS as r, DEFAULT_CATEGORIES as s, CHECKLIST_REF as t, FORMAT_VERSION as u, PRIORITIES as v, SURVEY_SECTION as w, SESSION_STATUS as x, REF_EXAMPLES as y, sessionSchema as z };
1013
+ export { grantRepo as $, configSchema as A, parseUntil as B, SLUG as C, TASK_SECTIONS as D, TASK_REQUIRED_SECTIONS as E, mainlineSchema as F, waitingForAction as G, refMainline as H, opsEntrySchema as I, GRANT_FORMS as J, waitingForRef as K, parseActionRef as L, findingSchema as M, formatOpsEntry as N, UNTIL_FORMS as O, issueSchema as P, grantForm as Q, parseOpsLine as R, SESSION_STATUS as S, SURVEY_SECTION as T, sessionSchema as U, refExample as V, taskSchema as W, describeRequest as X, GRANT_TARGET_FORMS as Y, grantCovers as Z, MERGE_STATES as _, DECISION_SECTIONS as a, RESERVED_HUB_NAMES as b, FINDING_SECTIONS as c, ID_PATTERNS as d, parseGrantTarget as et, ISSUE_SECTIONS as f, MAINLINE_STATUS as g, MAINLINE_SECTIONS as h, DECISION_BASES as i, decisionSchema as j, WORK_STATUS as k, FINDING_STATUS as l, LINK_RELS as m, CLOSED_STATUS as n, DECISION_STATUS as o, ISSUE_STATUS as p, GRANT_ACTIONS as q, DECIDERS as r, DEFAULT_CATEGORIES as s, CHECKLIST_REF as t, FORMAT_VERSION as u, PRIORITIES as v, SURVEY_LINES as w, SESSION_ID as x, REF_EXAMPLES as y, parseRef as z };
package/dist/facts.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as SurveyLine, i as SLUG, n as FORMAT_VERSION, r as ID_PATTERNS, t as CHECKLIST_REF } from "./index-CV30LkNZ.js";
1
+ import { a as SurveyLine, i as SLUG, n as FORMAT_VERSION, o as Until, r as ID_PATTERNS, t as CHECKLIST_REF } from "./index-D4IIwDGE.js";
2
2
  //#region src/facts.d.ts
3
3
  export interface CommandSpec {
4
4
  usage: string;
@@ -98,16 +98,23 @@ export declare const COMMAND_SPECS: {
98
98
  };
99
99
  readonly 'action add': {
100
100
  readonly usage: 'action add <text> --serves <m/t#c>[,<m/t#c>] [--before <a>] [--session <session>]';
101
- readonly summary: 'Add an action to the session queue';
101
+ readonly summary: 'Add an action to the session queue; prints a reminder when another session active in the last 15 minutes is doing an item it serves';
102
102
  readonly args: 1;
103
103
  readonly needsHub: true;
104
104
  };
105
105
  readonly 'action set': {
106
- readonly usage: 'action set <a|session#a> [--status] [--text] [--serves] [--before <a>] [--note <text>] [--session <session>]';
107
- readonly summary: 'Change, reorder or finish an action';
106
+ readonly usage: 'action set <a|session#a> [--status] [--reason --waiting-for --until] [--text] [--serves] [--before <a>] [--note <text>] [--session <session>]';
107
+ readonly summary: 'Change, reorder or finish an action; --status blocked needs --reason, and leaving blocked clears it. --status doing reminds like action add. A handed-off action keeps its note';
108
108
  readonly args: 1;
109
109
  readonly needsHub: true;
110
110
  };
111
+ readonly wait: {
112
+ readonly usage: 'wait [a|session#a] [--until <condition>] [--timeout <seconds>] [--interval <seconds>] [--session <session>]';
113
+ readonly summary: 'Wait until an action\'s until condition, or the --until one, holds: exits 0 when it does, 1 when the timeout (1800 s) runs out, 2 when there is no usable condition, the action leaves blocked, or the condition can no longer hold; checks every 10 s';
114
+ readonly args: 0;
115
+ readonly optionalArgs: 1;
116
+ readonly needsHub: true;
117
+ };
111
118
  readonly hook: {
112
119
  readonly usage: 'hook <platform> <event>';
113
120
  readonly summary: 'Handle a host hook event read from stdin, e.g. hook claude-code session-start';
@@ -373,6 +380,15 @@ export declare const OPTIONS: {
373
380
  readonly signal: {
374
381
  readonly type: 'string';
375
382
  };
383
+ readonly until: {
384
+ readonly type: 'string';
385
+ };
386
+ readonly timeout: {
387
+ readonly type: 'string';
388
+ };
389
+ readonly interval: {
390
+ readonly type: 'string';
391
+ };
376
392
  readonly branch: {
377
393
  readonly type: 'string';
378
394
  };
@@ -476,6 +492,15 @@ export declare const REFERENCES: readonly [{
476
492
  readonly format: '{sessionId}#a{NNN}';
477
493
  readonly example: 'claude-code-1a2b3c4d#a003';
478
494
  }];
495
+ /**
496
+ * Conditions a blocked action's `until` takes and `wait` checks, one example each: a commit on a branch whose
497
+ * 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).
498
+ */
499
+ export declare const UNTIL: readonly {
500
+ kind: Until['kind'];
501
+ format: string;
502
+ example: string;
503
+ }[];
479
504
  /** Relation types a document's links take; readers derive the reverse direction. */
480
505
  export declare const LINKS: {
481
506
  readonly rels: readonly ["depends-on", "relates", "split-from"];
@@ -557,6 +582,7 @@ export declare const FIELDS: {
557
582
  readonly criterion: FieldSpec[];
558
583
  readonly checklistItem: readonly [...FieldSpec[], FieldSpec];
559
584
  readonly grant: readonly [FieldSpec, FieldSpec, FieldSpec, FieldSpec];
585
+ readonly action: readonly [FieldSpec, FieldSpec, FieldSpec, FieldSpec, FieldSpec, FieldSpec, FieldSpec, FieldSpec];
560
586
  readonly link: readonly [FieldSpec, FieldSpec, FieldSpec];
561
587
  };
562
588
  //#endregion
package/dist/facts.js CHANGED
@@ -1,4 +1,4 @@
1
- import { C as SURVEY_LINES, D as WORK_STATUS, E as TASK_SECTIONS, H as GRANT_ACTIONS, S as SLUG, T as TASK_REQUIRED_SECTIONS, U as GRANT_FORMS, W as GRANT_TARGET_FORMS, _ as MERGE_STATES, a as DECISION_SECTIONS, c as FINDING_SECTIONS, d as ID_PATTERNS, f as ISSUE_SECTIONS, g as MAINLINE_STATUS, h as MAINLINE_SECTIONS, i as DECISION_BASES, l as FINDING_STATUS, m as LINK_RELS, n as CLOSED_STATUS, o as DECISION_STATUS, p as ISSUE_STATUS, r as DECIDERS, s as DEFAULT_CATEGORIES, t as CHECKLIST_REF, u as FORMAT_VERSION, v as PRIORITIES, w as SURVEY_SECTION, x as SESSION_STATUS } from "./dist-Dy45myuN.js";
1
+ import { C as SLUG, D as TASK_SECTIONS, E as TASK_REQUIRED_SECTIONS, J as GRANT_FORMS, O as UNTIL_FORMS, S as SESSION_STATUS, T as SURVEY_SECTION, Y as GRANT_TARGET_FORMS, _ as MERGE_STATES, a as DECISION_SECTIONS, c as FINDING_SECTIONS, d as ID_PATTERNS, f as ISSUE_SECTIONS, g as MAINLINE_STATUS, h as MAINLINE_SECTIONS, i as DECISION_BASES, k as WORK_STATUS, l as FINDING_STATUS, m as LINK_RELS, n as CLOSED_STATUS, o as DECISION_STATUS, p as ISSUE_STATUS, q as GRANT_ACTIONS, r as DECIDERS, s as DEFAULT_CATEGORIES, t as CHECKLIST_REF, u as FORMAT_VERSION, v as PRIORITIES, w as SURVEY_LINES } from "./dist-DlmecRLf.js";
2
2
  //#region src/facts.ts
3
3
  const COMMAND_SPECS = {
4
4
  init: {
@@ -89,16 +89,23 @@ const COMMAND_SPECS = {
89
89
  },
90
90
  "action add": {
91
91
  usage: "action add <text> --serves <m/t#c>[,<m/t#c>] [--before <a>] [--session <session>]",
92
- summary: "Add an action to the session queue",
92
+ summary: "Add an action to the session queue; prints a reminder when another session active in the last 15 minutes is doing an item it serves",
93
93
  args: 1,
94
94
  needsHub: true
95
95
  },
96
96
  "action set": {
97
- usage: "action set <a|session#a> [--status] [--text] [--serves] [--before <a>] [--note <text>] [--session <session>]",
98
- summary: "Change, reorder or finish an action",
97
+ usage: "action set <a|session#a> [--status] [--reason --waiting-for --until] [--text] [--serves] [--before <a>] [--note <text>] [--session <session>]",
98
+ summary: "Change, reorder or finish an action; --status blocked needs --reason, and leaving blocked clears it. --status doing reminds like action add. A handed-off action keeps its note",
99
99
  args: 1,
100
100
  needsHub: true
101
101
  },
102
+ wait: {
103
+ usage: "wait [a|session#a] [--until <condition>] [--timeout <seconds>] [--interval <seconds>] [--session <session>]",
104
+ summary: "Wait until an action's until condition, or the --until one, holds: exits 0 when it does, 1 when the timeout (1800 s) runs out, 2 when there is no usable condition, the action leaves blocked, or the condition can no longer hold; checks every 10 s",
105
+ args: 0,
106
+ optionalArgs: 1,
107
+ needsHub: true
108
+ },
102
109
  hook: {
103
110
  usage: "hook <platform> <event>",
104
111
  summary: "Handle a host hook event read from stdin, e.g. hook claude-code session-start",
@@ -319,6 +326,9 @@ const OPTIONS = {
319
326
  reason: { type: "string" },
320
327
  "waiting-for": { type: "string" },
321
328
  signal: { type: "string" },
329
+ until: { type: "string" },
330
+ timeout: { type: "string" },
331
+ interval: { type: "string" },
322
332
  branch: { type: "string" },
323
333
  worktree: { type: "string" },
324
334
  base: { type: "string" },
@@ -408,6 +418,11 @@ const REFERENCES = [
408
418
  example: "claude-code-1a2b3c4d#a003"
409
419
  }
410
420
  ];
421
+ /**
422
+ * Conditions a blocked action's `until` takes and `wait` checks, one example each: a commit on a branch whose
423
+ * 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).
424
+ */
425
+ const UNTIL = UNTIL_FORMS;
411
426
  /** Relation types a document's links take; readers derive the reverse direction. */
412
427
  const LINKS = { rels: LINK_RELS };
413
428
  /**
@@ -562,6 +577,16 @@ const FIELDS = {
562
577
  field("target", true, "授權的目標,格式依動作而定,見 GRANTS.targets;倉庫名稱需在 config.yaml 登記"),
563
578
  field("note", false, "一句話說明")
564
579
  ],
580
+ action: [
581
+ field("id", true, "會話內不重複的編號 a{NNN},由 action add 配發"),
582
+ field("do", true, "要做的事,一句話"),
583
+ field("status", true, "狀態值見 STATUSES,用 action set 改"),
584
+ field("serves", true, "推進的 checklist 項目參照清單,可以是空的"),
585
+ field("note", false, "一句備註"),
586
+ field("blockedBy", false, "卡住時的原因 reason、等待對象 waitingFor 與程式可判斷的條件 until,blocked 時必填,其他狀態不得有;等待對象整串是其他會話的行動(例如 claude-code-1a2b3c4d#a003)時,對方的簡報會列出有人在等它;條件格式見 UNTIL"),
587
+ field("startedAt", false, "轉為 doing 時由 CLI 寫入"),
588
+ field("doneAt", false, "轉為 done 或 dropped 時由 CLI 寫入")
589
+ ],
565
590
  link: [
566
591
  field("rel", true, "關係類型:depends-on、relates、split-from,反向由讀者推出"),
567
592
  field("to", true, "對方的參照,不能在同一條主脈絡內;其他 hub 的加 {hub}: 前綴"),
@@ -569,4 +594,4 @@ const FIELDS = {
569
594
  ]
570
595
  };
571
596
  //#endregion
572
- export { CHECKLIST_REF, CLASSIFICATION, CLOSED_STATUSES, COMMAND_SPECS, FIELDS, FORMAT_VERSION, GRANTS, ID_PATTERNS, LAYOUT, LINKS, OPTIONS, REFERENCES, SECTIONS, SLUG, STATUSES, SURVEY };
597
+ export { CHECKLIST_REF, CLASSIFICATION, CLOSED_STATUSES, COMMAND_SPECS, FIELDS, FORMAT_VERSION, GRANTS, ID_PATTERNS, LAYOUT, LINKS, OPTIONS, REFERENCES, SECTIONS, SLUG, STATUSES, SURVEY, UNTIL };
@@ -1,4 +1,4 @@
1
- import "./index-CV30LkNZ.js";
1
+ import "./index-D4IIwDGE.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 { U as GRANT_FORMS } from "./dist-Dy45myuN.js";
1
+ import { J as GRANT_FORMS } from "./dist-DlmecRLf.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.js CHANGED
@@ -1,5 +1,5 @@
1
- import { G as endsWithMarker, H as grantScope, I as setSessionInfo, J as appendOps, K as headerLine, L as setSessionStatus, O as sessionBriefing, P as readSession, R as settleSession, U as matchGrants, W as decorateReply, _t as worktreesOf, gt as findHubRoot, ht as currentBranch, i as hostName, it as loadHub, j as ensureSession, k as turnReminder, mt as DATA_DIR, pt as readYaml, q as mayDecorate, rt as HubError } from "./host-2a_9-kKv.js";
2
- import { G as describeRequest, O as configSchema } from "./dist-Dy45myuN.js";
1
+ import { A as sessionBriefing, B as settleSession, F as loadSessions, G as matchGrants, I as readSession, J as headerLine, K as decorateReply, N as ensureSession, R as setSessionInfo, W as grantScope, X as appendOps, Y as mayDecorate, _t as currentBranch, at as HubError, gt as DATA_DIR, ht as readYaml, i as hostName, j as turnReminder, ot as loadHub, q as endsWithMarker, vt as findHubRoot, yt as worktreesOf, z as setSessionStatus } from "./host-Dr0bCHd8.js";
2
+ import { A as configSchema, X as describeRequest } from "./dist-DlmecRLf.js";
3
3
  import { resolveFixed, scanCommand, scopedCwd } from "./grant-commands.js";
4
4
  import { appendFileSync, existsSync, readFileSync } from "node:fs";
5
5
  import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
@@ -320,7 +320,7 @@ function checkAfterWrite(root, { payload, cli }) {
320
320
  }
321
321
  let problems;
322
322
  try {
323
- problems = loadHub(hub).problems;
323
+ problems = [...loadHub(hub).problems, ...loadSessions(hub).problems];
324
324
  } catch (error) {
325
325
  problems = error instanceof HubError ? [...error.problems, {
326
326
  file: `${DATA_DIR}/config.yaml`,