mainflow 0.40.0 → 0.41.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +3 -3
- package/dist/facts.d.ts +15 -0
- package/dist/facts.js +21 -1
- package/dist/hooks.d.ts +0 -15
- package/dist/hooks.js +4 -23
- package/dist/{host-erQ_Sr8U.js → host-BAMZmRy8.js} +168 -1
- package/dist/host.js +1 -1
- package/dist/{launch-I_JDTAtU.js → launch-CMeKJsNd.js} +2 -2
- package/dist/launch.js +1 -1
- package/dist/native/mainflow-swap-darwin-arm64 +0 -0
- package/dist/native/mainflow-swap-darwin-x64 +0 -0
- package/dist/{operation-Bf6DtnfD.js → operation-DlR1P_Pt.js} +1 -1
- package/dist/{ui-CFtibt8x.js → ui-9uuI4Jif.js} +32 -37
- package/dist/web/assets/index-D3P-pLas.css +1 -0
- package/dist/web/assets/{index-BrA-csjk.js → index-DvYj_6IC.js} +28 -28
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-CuHEjqof.css +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { $ as stampMs, G as parseUntil, K as refExample, M as UNTIL_FORMS, W as parseRef, n as CLOSED_STATUS } from "./dist-zSabglPq.js";
|
|
3
|
-
import {
|
|
3
|
+
import { At as WriteError, Ct as TASK_FILE, D as loadSessions, Et as findHubRoot, G as applyOperation, K as checkHubName, O as readSession, St as MAINLINE_FILE, X as sectionText, Y as loadHub, at as nowMinute, b as duplicateClaimLines, ct as nodeStorage, d as migrateHub, et as memberOfGroup, f as retroReport, ft as MAX_FILE_BYTES, h as RETRY_RULES, kt as DATA_DIR, m as receiverFromEnv, nt as remoteMembers, p as retroReportFromReceiver, q as HubError, rt as unavailableHubs, u as groupPlan, v as initHub, wt as requireSlug, x as sessionBriefing, y as untilState } from "./host-BAMZmRy8.js";
|
|
4
4
|
import { t as listProgress } from "./progress-1BfPg2zN.js";
|
|
5
|
-
import { C as ServerError, D as syncSettings, E as hubName, O as SETTINGS_FILE, S as ServerApi, T as earlierToken, _ as readDaemon, a as remoteMode, b as readRemote, c as planAnnounce, d as toolCallOf, f as startDaemon, g as probeDaemon, h as callDaemon, i as queuedCount, j as syncPaths, l as planFetch, m as DaemonUnavailable, n as hubWriter, o as fetchDenial, p as DaemonRunning, r as writtenLines, s as hookAnswer, t as CONFIRM_MS, u as planFlush, v as silentHint, w as ENV, x as oversizedFiles } from "./operation-
|
|
5
|
+
import { C as ServerError, D as syncSettings, E as hubName, O as SETTINGS_FILE, S as ServerApi, T as earlierToken, _ as readDaemon, a as remoteMode, b as readRemote, c as planAnnounce, d as toolCallOf, f as startDaemon, g as probeDaemon, h as callDaemon, i as queuedCount, j as syncPaths, l as planFetch, m as DaemonUnavailable, n as hubWriter, o as fetchDenial, p as DaemonRunning, r as writtenLines, s as hookAnswer, t as CONFIRM_MS, u as planFlush, v as silentHint, w as ENV, x as oversizedFiles } from "./operation-DlR1P_Pt.js";
|
|
6
6
|
import { COMMAND_SPECS, OPTIONS } from "./facts.js";
|
|
7
7
|
import { hookFallback, parsePayload, runHook } from "./hooks.js";
|
|
8
|
-
import { f as CLI_VERSION, i as launchUi, l as startUiInBackground, r as exitCodeFor, u as watchHubGone } from "./launch-
|
|
8
|
+
import { f as CLI_VERSION, i as launchUi, l as startUiInBackground, r as exitCodeFor, u as watchHubGone } from "./launch-CMeKJsNd.js";
|
|
9
9
|
import { randomBytes } from "node:crypto";
|
|
10
10
|
import { existsSync, readFileSync, readdirSync, realpathSync, rmSync, statSync } from "node:fs";
|
|
11
11
|
import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";
|
package/dist/facts.d.ts
CHANGED
|
@@ -724,5 +724,20 @@ export declare const FIELDS: {
|
|
|
724
724
|
readonly action: readonly [FieldSpec, FieldSpec, FieldSpec, FieldSpec, FieldSpec, FieldSpec, FieldSpec, FieldSpec];
|
|
725
725
|
readonly link: readonly [FieldSpec, FieldSpec, FieldSpec];
|
|
726
726
|
};
|
|
727
|
+
/** The first day the start gate applies; a task created earlier began before the lines below existed. */
|
|
728
|
+
export declare const START_GATE_FROM = "2026-10-19";
|
|
729
|
+
/** The kinds of change that need no acceptance scenarios, as a skip line names them. */
|
|
730
|
+
export declare const SCENARIO_SKIP_KINDS: readonly ['措辭或格式', '照抄或同步版號', '單一設定值', '一句話說得完'];
|
|
731
|
+
/** The two lines under a task's 紀錄 that let writing start: the scenarios were confirmed, or the change needs none. */
|
|
732
|
+
export declare const SCENARIO_LINES: {
|
|
733
|
+
readonly confirmed: {
|
|
734
|
+
readonly form: '- 驗收情境確認:{使用者怎麼回覆}';
|
|
735
|
+
readonly pattern: RegExp;
|
|
736
|
+
};
|
|
737
|
+
readonly skipped: {
|
|
738
|
+
readonly form: `- 驗收情境略過({${string} 之一}):{理由}`;
|
|
739
|
+
readonly pattern: RegExp;
|
|
740
|
+
};
|
|
741
|
+
};
|
|
727
742
|
//#endregion
|
|
728
743
|
export { CHECKLIST_REF, FORMAT_VERSION, ID_PATTERNS, SERVES_REF, SLUG };
|
package/dist/facts.js
CHANGED
|
@@ -734,5 +734,25 @@ const FIELDS = {
|
|
|
734
734
|
field("note", false, "一句話說明這個關係")
|
|
735
735
|
]
|
|
736
736
|
};
|
|
737
|
+
/** The first day the start gate applies; a task created earlier began before the lines below existed. */
|
|
738
|
+
const START_GATE_FROM = "2026-10-19";
|
|
739
|
+
/** The kinds of change that need no acceptance scenarios, as a skip line names them. */
|
|
740
|
+
const SCENARIO_SKIP_KINDS = [
|
|
741
|
+
"措辭或格式",
|
|
742
|
+
"照抄或同步版號",
|
|
743
|
+
"單一設定值",
|
|
744
|
+
"一句話說得完"
|
|
745
|
+
];
|
|
746
|
+
/** The two lines under a task's 紀錄 that let writing start: the scenarios were confirmed, or the change needs none. */
|
|
747
|
+
const SCENARIO_LINES = {
|
|
748
|
+
confirmed: {
|
|
749
|
+
form: "- 驗收情境確認:{使用者怎麼回覆}",
|
|
750
|
+
pattern: /^- 驗收情境確認:\S/
|
|
751
|
+
},
|
|
752
|
+
skipped: {
|
|
753
|
+
form: `- 驗收情境略過({${SCENARIO_SKIP_KINDS.join("、")} 之一}):{理由}`,
|
|
754
|
+
pattern: new RegExp(`^- 驗收情境略過((?:${SCENARIO_SKIP_KINDS.join("|")})):\\S`)
|
|
755
|
+
}
|
|
756
|
+
};
|
|
737
757
|
//#endregion
|
|
738
|
-
export { CHECKLIST_REF, CLASSIFICATION, CLOSED_STATUSES, COMMAND_SPECS, FIELDS, FORMAT_VERSION, GRANTS, ID_PATTERNS, LAYOUT, LINKS, OPTIONS, REFERENCES, RETRO_FORMATS, SECTIONS, SERVES_REF, SLUG, STATUSES, SURVEY, UNTIL };
|
|
758
|
+
export { CHECKLIST_REF, CLASSIFICATION, CLOSED_STATUSES, COMMAND_SPECS, FIELDS, FORMAT_VERSION, GRANTS, ID_PATTERNS, LAYOUT, LINKS, OPTIONS, REFERENCES, RETRO_FORMATS, SCENARIO_LINES, SCENARIO_SKIP_KINDS, SECTIONS, SERVES_REF, SLUG, START_GATE_FROM, STATUSES, SURVEY, UNTIL };
|
package/dist/hooks.d.ts
CHANGED
|
@@ -618,21 +618,6 @@ export type HookFailure = {
|
|
|
618
618
|
} | {
|
|
619
619
|
kind: 'unreadable-input';
|
|
620
620
|
};
|
|
621
|
-
/** The first day the start gate applies; a task created earlier began before the lines below existed. */
|
|
622
|
-
export declare const START_GATE_FROM = "2026-10-19";
|
|
623
|
-
/** The kinds of change that need no acceptance scenarios, as a skip line names them. */
|
|
624
|
-
export declare const SCENARIO_SKIP_KINDS: readonly ['措辭或格式', '照抄或同步版號', '單一設定值', '一句話說得完'];
|
|
625
|
-
/** The two lines under a task's 紀錄 that let writing start: the scenarios were confirmed, or the change needs none. */
|
|
626
|
-
export declare const SCENARIO_LINES: {
|
|
627
|
-
readonly confirmed: {
|
|
628
|
-
readonly form: '- 驗收情境確認:{使用者怎麼回覆}';
|
|
629
|
-
readonly pattern: RegExp;
|
|
630
|
-
};
|
|
631
|
-
readonly skipped: {
|
|
632
|
-
readonly form: `- 驗收情境略過({${string} 之一}):{理由}`;
|
|
633
|
-
readonly pattern: RegExp;
|
|
634
|
-
};
|
|
635
|
-
};
|
|
636
621
|
/** Whether a task's 紀錄 holds a confirmation or a skip line as a top-level item; its lines are read untrimmed, so an indented one does not count. */
|
|
637
622
|
export declare function hasScenarioLine(body: string): boolean;
|
|
638
623
|
export declare const HOOK_PLATFORMS: Record<string, Record<string, Handler>>;
|
package/dist/hooks.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { P as configSchema, W as parseRef, Y as taskSchema, rt as describeRequest } from "./dist-zSabglPq.js";
|
|
2
|
-
import { A as
|
|
3
|
-
import { n as hubWriter, r as writtenLines } from "./operation-
|
|
2
|
+
import { A as grantScope, D as loadSessions, Dt as worktreesOf, Et as findHubRoot, F as mayDecorate, M as decorateReply, N as endsWithMarker, O as readSession, P as headerLine, S as turnReminder, Tt as currentBranch, Y as loadHub, bt as formatMismatch, ct as nodeStorage, i as hostName, it as taskFile, j as matchGrants, kt as DATA_DIR, ot as readFrontMatter, q as HubError, st as readYaml, x as sessionBriefing, xt as hubFormatVersion } from "./host-BAMZmRy8.js";
|
|
3
|
+
import { n as hubWriter, r as writtenLines } from "./operation-DlR1P_Pt.js";
|
|
4
|
+
import { SCENARIO_LINES, SCENARIO_SKIP_KINDS } from "./facts.js";
|
|
4
5
|
import { resolveFixed, scanCommand, scopedCwd } from "./grant-commands.js";
|
|
5
6
|
import { appendFileSync, existsSync, readFileSync } from "node:fs";
|
|
6
7
|
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
@@ -355,26 +356,6 @@ async function decideByGrants(root, { payload, warn, write }) {
|
|
|
355
356
|
await logGranted(write, scoped.scope, scan.call, resolved.request, grant, warn);
|
|
356
357
|
return permission("allow", `授權 ${grant.ref} 涵蓋 ${describeRequest(resolved.request)}`);
|
|
357
358
|
}
|
|
358
|
-
/** The first day the start gate applies; a task created earlier began before the lines below existed. */
|
|
359
|
-
const START_GATE_FROM = "2026-10-19";
|
|
360
|
-
/** The kinds of change that need no acceptance scenarios, as a skip line names them. */
|
|
361
|
-
const SCENARIO_SKIP_KINDS = [
|
|
362
|
-
"措辭或格式",
|
|
363
|
-
"照抄或同步版號",
|
|
364
|
-
"單一設定值",
|
|
365
|
-
"一句話說得完"
|
|
366
|
-
];
|
|
367
|
-
/** The two lines under a task's 紀錄 that let writing start: the scenarios were confirmed, or the change needs none. */
|
|
368
|
-
const SCENARIO_LINES = {
|
|
369
|
-
confirmed: {
|
|
370
|
-
form: "- 驗收情境確認:{使用者怎麼回覆}",
|
|
371
|
-
pattern: /^- 驗收情境確認:\S/
|
|
372
|
-
},
|
|
373
|
-
skipped: {
|
|
374
|
-
form: `- 驗收情境略過({${SCENARIO_SKIP_KINDS.join("、")} 之一}):{理由}`,
|
|
375
|
-
pattern: new RegExp(`^- 驗收情境略過((?:${SCENARIO_SKIP_KINDS.join("|")})):\\S`)
|
|
376
|
-
}
|
|
377
|
-
};
|
|
378
359
|
/** Whether a task's 紀錄 holds a confirmation or a skip line as a top-level item; its lines are read untrimmed, so an indented one does not count. */
|
|
379
360
|
function hasScenarioLine(body) {
|
|
380
361
|
const lines = body.split("\n");
|
|
@@ -582,4 +563,4 @@ async function runHook(platform, event, ctx) {
|
|
|
582
563
|
}
|
|
583
564
|
}
|
|
584
565
|
//#endregion
|
|
585
|
-
export { HOOK_PLATFORMS,
|
|
566
|
+
export { HOOK_PLATFORMS, claudeCodeSessionId, hasScenarioLine, hookFallback, openLink, parsePayload, runHook, subagentTranscript };
|
|
@@ -3919,6 +3919,157 @@ function setReminderDigest(root, id, digest) {
|
|
|
3919
3919
|
}));
|
|
3920
3920
|
}
|
|
3921
3921
|
//#endregion
|
|
3922
|
+
//#region ../core/dist/sync-notices.js
|
|
3923
|
+
const SYNC_DIR = "local/sync";
|
|
3924
|
+
const NOTICES_FILE = `${SYNC_DIR}/notices.jsonl`;
|
|
3925
|
+
const KNOWN_FILE = `${SYNC_DIR}/known.json`;
|
|
3926
|
+
const QUEUE_FILE = `${SYNC_DIR}/queue.json`;
|
|
3927
|
+
/** Where the notices marked handled by hand are kept, relative to .mainflow/ */
|
|
3928
|
+
const HANDLED_FILE = `${SYNC_DIR}/handled.json`;
|
|
3929
|
+
/** What sync's known.json writes before a binary file's sha256 (BLOB_PREFIX in packages/sync/src/state.ts) */
|
|
3930
|
+
const BLOB_PREFIX = "blob:";
|
|
3931
|
+
/** The key a notice is marked handled by. */
|
|
3932
|
+
const noticeKey = (n) => `${n.at}|${n.kind}|${n.path}`;
|
|
3933
|
+
/** One line of notices.jsonl as a notice; null for a line that is not a warn-level notice. */
|
|
3934
|
+
function parseWarn(line) {
|
|
3935
|
+
if (!line.trim()) return null;
|
|
3936
|
+
try {
|
|
3937
|
+
const { at, level, kind, path, message, saved, renumbered } = JSON.parse(line);
|
|
3938
|
+
if (level !== "warn" || typeof at !== "string" || typeof kind !== "string" || typeof message !== "string") return null;
|
|
3939
|
+
const ids = Array.isArray(renumbered) ? renumbered.filter((r) => typeof r?.from === "string" && typeof r?.to === "string") : [];
|
|
3940
|
+
return {
|
|
3941
|
+
at,
|
|
3942
|
+
kind,
|
|
3943
|
+
path: typeof path === "string" ? path : "",
|
|
3944
|
+
message,
|
|
3945
|
+
...typeof saved === "string" ? { saved } : {},
|
|
3946
|
+
...ids.length ? { renumbered: ids.map(({ from, to }) => ({
|
|
3947
|
+
from,
|
|
3948
|
+
to
|
|
3949
|
+
})) } : {}
|
|
3950
|
+
};
|
|
3951
|
+
} catch {
|
|
3952
|
+
return null;
|
|
3953
|
+
}
|
|
3954
|
+
}
|
|
3955
|
+
function readJson$1(file, fallback) {
|
|
3956
|
+
try {
|
|
3957
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
3958
|
+
} catch {
|
|
3959
|
+
return fallback;
|
|
3960
|
+
}
|
|
3961
|
+
}
|
|
3962
|
+
/** A path relative to .mainflow/ that stays inside it. */
|
|
3963
|
+
const inside = (path) => path !== "" && !isAbsolute(path) && !path.split("/").some((part) => part === "" || part === "." || part === "..");
|
|
3964
|
+
/** The hash known.json would name for the file at `file`, null when it cannot be read. */
|
|
3965
|
+
function localHash(file) {
|
|
3966
|
+
let bytes;
|
|
3967
|
+
try {
|
|
3968
|
+
bytes = readFileSync(file);
|
|
3969
|
+
} catch {
|
|
3970
|
+
return null;
|
|
3971
|
+
}
|
|
3972
|
+
try {
|
|
3973
|
+
return contentHash(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
3974
|
+
} catch {
|
|
3975
|
+
return `${BLOB_PREFIX}${createHash("sha256").update(bytes).digest("hex")}`;
|
|
3976
|
+
}
|
|
3977
|
+
}
|
|
3978
|
+
function readSyncState(data) {
|
|
3979
|
+
const known = readJson$1(join(data, KNOWN_FILE), {}).files ?? {};
|
|
3980
|
+
const entries = readJson$1(join(data, QUEUE_FILE), {}).entries ?? [];
|
|
3981
|
+
const queued = /* @__PURE__ */ new Set();
|
|
3982
|
+
for (const entry of entries) {
|
|
3983
|
+
if (entry.refused !== void 0) continue;
|
|
3984
|
+
if (entry.kind === "op") for (const path of Object.keys(entry.written ?? {})) queued.add(path);
|
|
3985
|
+
else if (typeof entry.path === "string") queued.add(entry.path);
|
|
3986
|
+
}
|
|
3987
|
+
return {
|
|
3988
|
+
known,
|
|
3989
|
+
queued,
|
|
3990
|
+
hashes: /* @__PURE__ */ new Map()
|
|
3991
|
+
};
|
|
3992
|
+
}
|
|
3993
|
+
/**
|
|
3994
|
+
* Kinds telling that a file did not reach the server. Once the file is the confirmed version it did, whenever it last
|
|
3995
|
+
* changed: a binary file an older server skipped is uploaded later as it is, without being written again.
|
|
3996
|
+
*/
|
|
3997
|
+
const UNSENT_KINDS = /* @__PURE__ */ new Set([
|
|
3998
|
+
"skipped-binary",
|
|
3999
|
+
"skipped-large",
|
|
4000
|
+
"binary-text-file",
|
|
4001
|
+
"upload-failing",
|
|
4002
|
+
"download-failed"
|
|
4003
|
+
]);
|
|
4004
|
+
/**
|
|
4005
|
+
* Whether the files show a notice settled. With a saved text, once the saved text is removed. Without one, when the
|
|
4006
|
+
* file is the confirmed version (its hash is known.json's) and no write of it waits in the outbox; for the other kinds,
|
|
4007
|
+
* whose file is already the confirmed version when the notice is written, it must also have changed after the notice.
|
|
4008
|
+
* A file that neither this machine nor known.json has needs nothing either. A notice naming no file is settled by hand
|
|
4009
|
+
* only.
|
|
4010
|
+
*/
|
|
4011
|
+
function settled(data, notice, state) {
|
|
4012
|
+
if (notice.saved !== void 0) return inside(notice.saved) && !existsSync(join(data, notice.saved));
|
|
4013
|
+
if (!inside(notice.path)) return false;
|
|
4014
|
+
const file = join(data, notice.path);
|
|
4015
|
+
const known = state.known[notice.path];
|
|
4016
|
+
if (!existsSync(file)) return known === void 0;
|
|
4017
|
+
if (known === void 0 || state.queued.has(notice.path)) return false;
|
|
4018
|
+
if (!state.hashes.has(file)) state.hashes.set(file, localHash(file));
|
|
4019
|
+
if (state.hashes.get(file) !== known) return false;
|
|
4020
|
+
if (UNSENT_KINDS.has(notice.kind)) return true;
|
|
4021
|
+
try {
|
|
4022
|
+
return statSync(file).mtimeMs > Date.parse(notice.at);
|
|
4023
|
+
} catch {
|
|
4024
|
+
return false;
|
|
4025
|
+
}
|
|
4026
|
+
}
|
|
4027
|
+
const handledFile = (root) => join(root, DATA_DIR, HANDLED_FILE);
|
|
4028
|
+
/** The keys marked handled by hand. */
|
|
4029
|
+
function readHandledKeys(root) {
|
|
4030
|
+
const keys = readJson$1(handledFile(root), {}).keys;
|
|
4031
|
+
return keys && typeof keys === "object" && !Array.isArray(keys) ? keys : {};
|
|
4032
|
+
}
|
|
4033
|
+
/** Every warn-level line of notices.jsonl, newest first, each with how it was handled if it was; unreadable lines are skipped. */
|
|
4034
|
+
function readNotices(root) {
|
|
4035
|
+
const data = join(root, DATA_DIR);
|
|
4036
|
+
const file = join(data, NOTICES_FILE);
|
|
4037
|
+
if (!existsSync(file)) return [];
|
|
4038
|
+
const manual = readHandledKeys(root);
|
|
4039
|
+
const state = readSyncState(data);
|
|
4040
|
+
const notices = [];
|
|
4041
|
+
for (const line of readFileSync(file, "utf8").split("\n")) {
|
|
4042
|
+
const notice = parseWarn(line);
|
|
4043
|
+
if (!notice) continue;
|
|
4044
|
+
const handled = noticeKey(notice) in manual ? "manual" : settled(data, notice, state) ? "auto" : void 0;
|
|
4045
|
+
notices.push(handled ? {
|
|
4046
|
+
...notice,
|
|
4047
|
+
handled
|
|
4048
|
+
} : notice);
|
|
4049
|
+
}
|
|
4050
|
+
return notices.reverse();
|
|
4051
|
+
}
|
|
4052
|
+
/** The notices nobody has handled yet, newest first. */
|
|
4053
|
+
const unhandledNotices = (root) => readNotices(root).filter((n) => n.handled === void 0);
|
|
4054
|
+
/**
|
|
4055
|
+
* Mark notices handled by hand, writing handled.json by a rename so a reader never sees half of it. `keys` names the
|
|
4056
|
+
* notices (noticeKey); without it every notice unhandled now is marked. Returns the number marked.
|
|
4057
|
+
*/
|
|
4058
|
+
function markNoticesHandled(root, keys) {
|
|
4059
|
+
const unhandled = new Set(unhandledNotices(root).map(noticeKey));
|
|
4060
|
+
const wanted = (keys ?? [...unhandled]).filter((key) => unhandled.has(key));
|
|
4061
|
+
if (wanted.length === 0) return 0;
|
|
4062
|
+
const marked = readHandledKeys(root);
|
|
4063
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4064
|
+
for (const key of wanted) marked[key] = now;
|
|
4065
|
+
const file = handledFile(root);
|
|
4066
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
4067
|
+
const temp = `${file}.${process.pid}.tmp`;
|
|
4068
|
+
writeFileSync(temp, JSON.stringify({ keys: marked }, null, 1));
|
|
4069
|
+
renameSync(temp, file);
|
|
4070
|
+
return wanted.length;
|
|
4071
|
+
}
|
|
4072
|
+
//#endregion
|
|
3922
4073
|
//#region ../core/dist/briefing.js
|
|
3923
4074
|
function describe(root, ref) {
|
|
3924
4075
|
const parsed = parseRef(ref);
|
|
@@ -4118,6 +4269,21 @@ function overlapLines(root, session) {
|
|
|
4118
4269
|
}
|
|
4119
4270
|
return lines;
|
|
4120
4271
|
}
|
|
4272
|
+
/** How many unhandled sync notices the briefing lists; the page lists the rest. */
|
|
4273
|
+
const BRIEFING_NOTICES = 5;
|
|
4274
|
+
/** Longest message summary of a notice in the briefing, in characters. */
|
|
4275
|
+
const NOTICE_SUMMARY = 80;
|
|
4276
|
+
/** The unhandled sync notices for the briefing: up to BRIEFING_NOTICES, the total, and where the page lists them; empty when none. */
|
|
4277
|
+
function syncNoticeLines(root) {
|
|
4278
|
+
const notices = unhandledNotices(root);
|
|
4279
|
+
if (notices.length === 0) return [];
|
|
4280
|
+
const summary = (message) => message.length > NOTICE_SUMMARY ? `${message.slice(0, NOTICE_SUMMARY)}…` : message;
|
|
4281
|
+
return [
|
|
4282
|
+
`有 ${notices.length} 條同步提醒還沒處理,寫入者的內容可能沒有照原樣進入 hub。先列最新的 ${Math.min(notices.length, BRIEFING_NOTICES)} 條:`,
|
|
4283
|
+
...notices.slice(0, BRIEFING_NOTICES).map((n) => `- ${n.kind}${n.path ? ` ${n.path}` : ""}:${summary(n.message)}`),
|
|
4284
|
+
`全部 ${notices.length} 條與標記已處理的操作在介面的同步提醒頁(/sync)。`
|
|
4285
|
+
];
|
|
4286
|
+
}
|
|
4121
4287
|
/** Full briefing for the start of a session, after resuming, clearing or compacting. */
|
|
4122
4288
|
function sessionBriefing(root, session, options) {
|
|
4123
4289
|
const { cli } = options;
|
|
@@ -4147,6 +4313,7 @@ function sessionBriefing(root, session, options) {
|
|
|
4147
4313
|
if (!session.actions.some(isOpen)) lines.push(...handoffLines(root, session, cli));
|
|
4148
4314
|
const warnings = queueWarnings(root, session, cli);
|
|
4149
4315
|
if (warnings.length > 0) lines.push("需要處理:", ...warnings.map((w) => `- ${w}`));
|
|
4316
|
+
lines.push(...syncNoticeLines(root));
|
|
4150
4317
|
lines.push("工作方式:", `- 開始一件新工作前,先列進佇列:${cli} action add "<做法>" --serves <m/t#c>。主脈絡還沒拆任務時(查核與規劃階段),--serves 指向解決條件 <m#r>。`, `- 開始做標 doing,做完標 done:${cli} action set <a編號> --status doing。`, `- 勾選 checklist 要附證據:${cli} item check <m/t#c> --evidence <commit、檔案:行號或指令輸出>。`, `- 內容直接寫進檔案:意圖或需求寫進 mainline.md 的內文,任務的目標與完成定義寫進 task.md,選擇寫成決策(${cli} new decision <m> <slug> --title <標題> 建檔後再寫內文)。`, `- 改完 .mainflow/ 的檔案跑 ${cli} check,把它列出的問題修到沒有。`, "- 用完其他 skill 或子流程後,回到上面的目前行動再繼續。", "- 要開主脈絡、拆任務、記決策、延後或收尾時,用 mainflow skill,它依序帶過並告訴你資訊該寫在哪裡。");
|
|
4151
4318
|
return lines.join("\n");
|
|
4152
4319
|
}
|
|
@@ -8430,4 +8597,4 @@ function openInApp(hub, target, closeAfterMs = 150) {
|
|
|
8430
8597
|
return true;
|
|
8431
8598
|
}
|
|
8432
8599
|
//#endregion
|
|
8433
|
-
export {
|
|
8600
|
+
export { linksInto as $, grantScope as A, WriteError as At, renumberRequest as B, HANDLED_FILE as C, TASK_FILE as Ct, loadSessions as D, worktreesOf as Dt, readNotices as E, findHubRoot as Et, mayDecorate as F, applyOperation as G, renumberPath as H, attributeTrace as I, loadConfig as J, checkHubName as K, traceTranscript as L, decorateReply as M, splitFrontMatter as Mt, endsWithMarker as N, readSession as O, ConflictError as Ot, headerLine as P, mainlineEdges as Q, transcriptTitle as R, turnReminder as S, MAINLINE_FILE as St, parseWarn as T, currentBranch as Tt, renumberText as U, renumberFiles as V, scopeOfPath as W, sectionText as X, loadHub as Y, dependencyGraph as Z, setArchivedMany as _, readsAsText as _t, openInApp as a, nowMinute as at, duplicateClaimLines as b, formatMismatch as bt, siteBlock as c, nodeStorage as ct, migrateHub as d, MAX_BLOB_BYTES as dt, memberOfGroup as et, retroReport as f, MAX_FILE_BYTES as ft, ARCHIVE_KINDS as g, exceedsFileLimit as gt, RETRY_RULES as h, comparePaths as ht, hostName as i, taskFile as it, matchGrants as j, contentHash as jt, transcriptPath as k, DATA_DIR as kt, syncHost as l, ACCESS_HEADERS as lt, receiverFromEnv as m, SOCKET_MAX_AGE_CLOSE as mt, DEFAULT_UI_PORT as n, remoteMembers as nt, removeDanglingLinks as o, readFrontMatter as ot, retroReportFromReceiver as p, ROUTES as pt, HubError as q, caddySites as r, unavailableHubs as rt, removeHostLink as s, readYaml as st, APP_BUNDLE_ID as t, refIndex as tt, groupPlan as u, BLOB_HASH as ut, initHub as v, requestTime as vt, markNoticesHandled as w, requireSlug as wt, sessionBriefing as x, hubFormatVersion as xt, untilState as y, textBytes as yt, applyOffline as z };
|
package/dist/host.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as openInApp, c as siteBlock, i as hostName, l as syncHost, n as DEFAULT_UI_PORT, o as removeDanglingLinks, r as caddySites, s as removeHostLink, t as APP_BUNDLE_ID } from "./host-
|
|
1
|
+
import { a as openInApp, c as siteBlock, i as hostName, l as syncHost, n as DEFAULT_UI_PORT, o as removeDanglingLinks, r as caddySites, s as removeHostLink, t as APP_BUNDLE_ID } from "./host-BAMZmRy8.js";
|
|
2
2
|
export { APP_BUNDLE_ID, DEFAULT_UI_PORT, caddySites, hostName, openInApp, removeDanglingLinks, removeHostLink, siteBlock, syncHost };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { At as WriteError, i as hostName, kt as DATA_DIR, l as syncHost, n as DEFAULT_UI_PORT, s as removeHostLink } from "./host-BAMZmRy8.js";
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
4
|
import { spawn, spawnSync } from "node:child_process";
|
|
@@ -276,7 +276,7 @@ async function launchUi(root, hub, configured, options) {
|
|
|
276
276
|
}
|
|
277
277
|
let server;
|
|
278
278
|
if (!reuse) {
|
|
279
|
-
const { serveUi } = await import("./ui-
|
|
279
|
+
const { serveUi } = await import("./ui-9uuI4Jif.js");
|
|
280
280
|
server = await serveUi(root, choice.port);
|
|
281
281
|
writeUiRecord(root, {
|
|
282
282
|
port: choice.port,
|
package/dist/launch.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as networkProbe, c as realRoot, d as withUiLock, i as launchUi, l as startUiInBackground, n as choosePort, o as portCandidates, r as exitCodeFor, s as readUiRecord, t as PORT_SPAN, u as watchHubGone } from "./launch-
|
|
1
|
+
import { a as networkProbe, c as realRoot, d as withUiLock, i as launchUi, l as startUiInBackground, n as choosePort, o as portCandidates, r as exitCodeFor, s as readUiRecord, t as PORT_SPAN, u as watchHubGone } from "./launch-CMeKJsNd.js";
|
|
2
2
|
export { PORT_SPAN, choosePort, exitCodeFor, launchUi, networkProbe, portCandidates, readUiRecord, realRoot, startUiInBackground, watchHubGone, withUiLock };
|
|
Binary file
|
|
Binary file
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { At as WriteError, B as renumberRequest, G as applyOperation, H as renumberPath, Mt as splitFrontMatter, Ot as ConflictError, U as renumberText, V as renumberFiles, W as scopeOfPath, _t as readsAsText, at as nowMinute, ct as nodeStorage, dt as MAX_BLOB_BYTES, ft as MAX_FILE_BYTES, gt as exceedsFileLimit, ht as comparePaths, jt as contentHash, kt as DATA_DIR, lt as ACCESS_HEADERS, mt as SOCKET_MAX_AGE_CLOSE, pt as ROUTES, ut as BLOB_HASH, vt as requestTime, yt as textBytes, z as applyOffline } from "./host-BAMZmRy8.js";
|
|
2
2
|
import { createHash, randomBytes } from "node:crypto";
|
|
3
3
|
import { accessSync, appendFileSync, chmodSync, closeSync, constants, copyFileSync, createReadStream, createWriteStream, existsSync, linkSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, renameSync, rmSync, statSync, watch, writeFileSync } from "node:fs";
|
|
4
4
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { P as configSchema } from "./dist-zSabglPq.js";
|
|
2
|
-
import {
|
|
2
|
+
import { $ as linksInto, At as WriteError, C as HANDLED_FILE, D as loadSessions, E as readNotices$1, I as attributeTrace, J as loadConfig, L as traceTranscript, O as readSession, Ot as ConflictError, Q as mainlineEdges, R as transcriptTitle, T as parseWarn, X as sectionText, Y as loadHub, Z as dependencyGraph, _ as setArchivedMany, a as openInApp, g as ARCHIVE_KINDS, k as transcriptPath, kt as DATA_DIR, nt as remoteMembers, q as HubError, st as readYaml, tt as refIndex, u as groupPlan, w as markNoticesHandled } from "./host-BAMZmRy8.js";
|
|
3
3
|
import { t as listProgress } from "./progress-1BfPg2zN.js";
|
|
4
|
-
import { A as isSynced, _ as readDaemon, a as remoteMode, b as readRemote, h as callDaemon, j as syncPaths, k as SYNC_DIR, m as DaemonUnavailable, n as hubWriter, r as writtenLines, y as watchTree } from "./operation-
|
|
5
|
-
import { c as realRoot, f as CLI_VERSION, s as readUiRecord } from "./launch-
|
|
4
|
+
import { A as isSynced, _ as readDaemon, a as remoteMode, b as readRemote, h as callDaemon, j as syncPaths, k as SYNC_DIR, m as DaemonUnavailable, n as hubWriter, r as writtenLines, y as watchTree } from "./operation-DlR1P_Pt.js";
|
|
5
|
+
import { c as realRoot, f as CLI_VERSION, s as readUiRecord } from "./launch-CMeKJsNd.js";
|
|
6
6
|
import { createRequire } from "node:module";
|
|
7
7
|
import { closeSync, existsSync, lstatSync, openSync, readFileSync, readSync, realpathSync, statSync } from "node:fs";
|
|
8
8
|
import { basename, dirname, extname, join, normalize, relative, resolve, sep } from "node:path";
|
|
@@ -414,6 +414,11 @@ function createApp(root, options = {}) {
|
|
|
414
414
|
running: readDaemon(root) !== null,
|
|
415
415
|
notices: readNotices(root)
|
|
416
416
|
}));
|
|
417
|
+
app.post("/api/sync/handled", async (c) => {
|
|
418
|
+
const body = await c.req.json().catch(() => ({}));
|
|
419
|
+
if (body.key !== void 0 && typeof body.key !== "string") return c.json({ error: "key must be a string" }, 400);
|
|
420
|
+
return c.json({ marked: markNoticesHandled(root, body.key === void 0 ? void 0 : [body.key]) });
|
|
421
|
+
});
|
|
417
422
|
app.get("/api/sync/conflicts/:name", async (c) => {
|
|
418
423
|
const name = c.req.param("name");
|
|
419
424
|
const saved = `${SYNC_DIR}/conflicts/${name}`;
|
|
@@ -448,38 +453,8 @@ function runsElsewhere(root, session) {
|
|
|
448
453
|
if (existsSync(session.cwd)) return false;
|
|
449
454
|
return !(session.transcript && existsSync(transcriptPath(root, session.transcript)));
|
|
450
455
|
}
|
|
451
|
-
/**
|
|
452
|
-
|
|
453
|
-
const file = syncPaths(root).notices;
|
|
454
|
-
if (!existsSync(file)) return [];
|
|
455
|
-
const notices = [];
|
|
456
|
-
for (const line of readFileSync(file, "utf8").split("\n")) {
|
|
457
|
-
const notice = parseWarn(line);
|
|
458
|
-
if (notice) notices.push(notice);
|
|
459
|
-
}
|
|
460
|
-
return notices.reverse();
|
|
461
|
-
}
|
|
462
|
-
function parseWarn(line) {
|
|
463
|
-
if (!line.trim()) return null;
|
|
464
|
-
try {
|
|
465
|
-
const { at, level, kind, path, message, saved, renumbered } = JSON.parse(line);
|
|
466
|
-
if (level !== "warn" || typeof at !== "string" || typeof kind !== "string" || typeof message !== "string") return null;
|
|
467
|
-
const ids = Array.isArray(renumbered) ? renumbered.filter((r) => typeof r?.from === "string" && typeof r?.to === "string") : [];
|
|
468
|
-
return {
|
|
469
|
-
at,
|
|
470
|
-
kind,
|
|
471
|
-
path: typeof path === "string" ? path : "",
|
|
472
|
-
message,
|
|
473
|
-
...typeof saved === "string" ? { saved } : {},
|
|
474
|
-
...ids.length ? { renumbered: ids.map(({ from, to }) => ({
|
|
475
|
-
from,
|
|
476
|
-
to
|
|
477
|
-
})) } : {}
|
|
478
|
-
};
|
|
479
|
-
} catch {
|
|
480
|
-
return null;
|
|
481
|
-
}
|
|
482
|
-
}
|
|
456
|
+
/** The notices of the sync client as the interface lists them (core reads the files and tells which are handled). */
|
|
457
|
+
const readNotices = (root) => readNotices$1(root);
|
|
483
458
|
/**
|
|
484
459
|
* Whether `file` really is inside the hub's .mainflow/ once every symbolic link on the way is followed. A file that
|
|
485
460
|
* does not exist yet is judged by the deepest directory above it that does.
|
|
@@ -573,6 +548,7 @@ async function fetchFromServer(root, path) {
|
|
|
573
548
|
function eventOf(name) {
|
|
574
549
|
const path = name.split(sep).join("/");
|
|
575
550
|
if (path === `local/sync/notices.jsonl`) return "notices";
|
|
551
|
+
if (path === HANDLED_FILE) return "handled";
|
|
576
552
|
if (path === `local/sync/remote.json`) return "change";
|
|
577
553
|
if (path === "local/sync" || path.startsWith(`local/sync/`)) return null;
|
|
578
554
|
if (path.endsWith("write.lock") || path.endsWith(".tmp")) return null;
|
|
@@ -605,14 +581,17 @@ var HubWatch = class {
|
|
|
605
581
|
watcher;
|
|
606
582
|
/** How far notices.jsonl has been read */
|
|
607
583
|
offset;
|
|
584
|
+
/** handled.json as last told of, so the several events of one write tell the interface once */
|
|
585
|
+
handledSeen;
|
|
608
586
|
constructor(root) {
|
|
609
587
|
this.root = root;
|
|
610
588
|
this.offset = sizeOf(syncPaths(root).notices);
|
|
589
|
+
this.handledSeen = this.handledSignature();
|
|
611
590
|
this.watcher = watchDataDir(join(root, DATA_DIR), (name) => {
|
|
612
591
|
const kind = eventOf(name);
|
|
613
|
-
if (kind === null || kind === "notices" && !this.newWarnings()) return;
|
|
592
|
+
if (kind === null || kind === "notices" && !this.newWarnings() || kind === "handled" && !this.handledChanged()) return;
|
|
614
593
|
const event = {
|
|
615
|
-
kind: kind === "notices" ? "notice" : kind,
|
|
594
|
+
kind: kind === "notices" || kind === "handled" ? "notice" : kind,
|
|
616
595
|
name
|
|
617
596
|
};
|
|
618
597
|
for (const notify of this.listeners) notify(event);
|
|
@@ -645,6 +624,22 @@ var HubWatch = class {
|
|
|
645
624
|
this.offset += complete;
|
|
646
625
|
return buffer.subarray(0, complete).toString("utf8").split("\n").some((line) => parseWarn(line) !== null);
|
|
647
626
|
}
|
|
627
|
+
/** handled.json's time and size, empty while there is none. */
|
|
628
|
+
handledSignature() {
|
|
629
|
+
try {
|
|
630
|
+
const info = statSync(join(this.root, DATA_DIR, HANDLED_FILE));
|
|
631
|
+
return `${info.mtimeMs}:${info.size}`;
|
|
632
|
+
} catch {
|
|
633
|
+
return "";
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
/** Whether handled.json changed since the interface was last told of it. */
|
|
637
|
+
handledChanged() {
|
|
638
|
+
const signature = this.handledSignature();
|
|
639
|
+
if (signature === this.handledSeen) return false;
|
|
640
|
+
this.handledSeen = signature;
|
|
641
|
+
return true;
|
|
642
|
+
}
|
|
648
643
|
};
|
|
649
644
|
const watches = /* @__PURE__ */ new Map();
|
|
650
645
|
/** The size of a file, 0 when it does not exist; it may be removed between any two calls, so it is read in one. */
|