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