mainflow 0.28.0 → 0.29.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.d.ts +3 -1
- package/dist/cli.js +817 -213
- package/dist/facts.d.ts +64 -2
- package/dist/facts.js +64 -3
- package/dist/grant-commands.d.ts +1 -1
- package/dist/hooks.d.ts +514 -3
- package/dist/hooks.js +66 -35
- package/dist/host-BJnrv8xF.js +7300 -0
- package/dist/host.js +1 -1
- package/dist/index-DV6Gqu8K.d.ts +423 -0
- package/dist/{launch-BKKoCyTj.js → launch-DefacJEL.js} +2 -2
- package/dist/launch.js +1 -1
- package/dist/native/mainflow-swap-linux-x64 +0 -0
- package/dist/native/mainflow-swap.c +67 -0
- package/dist/operation-Bp4U2bx3.js +2862 -0
- package/dist/retro-formats-ECy0gGOL.js +30 -0
- package/dist/ui-CiapFoik.js +658 -0
- package/dist/web/assets/index-CyQML0b0.css +1 -0
- package/dist/web/assets/index-tAb99n75.js +82 -0
- package/dist/web/index.html +2 -2
- package/package.json +3 -1
- package/dist/host-CeN3dvXg.js +0 -3268
- package/dist/index-bIFT1ORw.d.ts +0 -46
- package/dist/ui-D_9x6XNs.js +0 -501
- package/dist/web/assets/index-BUxmYhLT.css +0 -1
- package/dist/web/assets/index-ypKt0hMg.js +0 -80
package/dist/cli.js
CHANGED
|
@@ -1,65 +1,813 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { $ as
|
|
2
|
+
import { $ as nowMinute, G as loadHub, H as applyOperation, Q as requireSlug, S as readSession, U as checkHubName, W as HubError, X as MAINLINE_FILE, Z as TASK_FILE, _ as untilState, d as retroReportFromReceiver, f as receiverFromEnv, g as initHub, l as migrateHub, ot as DATA_DIR, p as RETRY_RULES, rt as findHubRoot, st as WriteError, tt as nodeStorage, u as retroReport, v as duplicateClaimLines, x as loadSessions, y as sessionBriefing } from "./host-BJnrv8xF.js";
|
|
3
3
|
import { B as parseRef, H as refExample, V as parseUntil, k as UNTIL_FORMS } from "./dist-BbamrIPW.js";
|
|
4
4
|
import { t as listProgress } from "./progress-1BfPg2zN.js";
|
|
5
|
+
import { C as ServerError, S as ServerApi, _ as silentHint, a as remoteMode, c as syncSettings, d as startDaemon, f as DaemonRunning, g as readDaemon, h as probeDaemon, i as queuedCount, l as hookAnswer, m as callDaemon, n as hubWriter, o as ENV, p as DaemonUnavailable, r as writtenLines, s as hubName, t as CONFIRM_MS, u as planFlush, v as SETTINGS_FILE, x as syncPaths } from "./operation-Bp4U2bx3.js";
|
|
5
6
|
import { COMMAND_SPECS, OPTIONS } from "./facts.js";
|
|
6
7
|
import { hookFallback, parsePayload, runHook } from "./hooks.js";
|
|
7
|
-
import { l as CLI_VERSION, r as launchUi, s as startUiInBackground } from "./launch-
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
8
|
+
import { c as watchHubGone, l as CLI_VERSION, r as launchUi, s as startUiInBackground } from "./launch-DefacJEL.js";
|
|
9
|
+
import { randomBytes } from "node:crypto";
|
|
10
|
+
import { existsSync, readFileSync, realpathSync, rmSync, statSync } from "node:fs";
|
|
11
|
+
import { basename, join, relative, resolve } from "node:path";
|
|
10
12
|
import { fileURLToPath } from "node:url";
|
|
11
13
|
import { parseArgs } from "node:util";
|
|
14
|
+
//#region src/remote/sync.ts
|
|
15
|
+
/**
|
|
16
|
+
* Run the sync client for the hub at `root` until the process is told to stop, or the hub's directory goes. A write
|
|
17
|
+
* that found no sync client may have started this one in the background (src/remote/start.ts); a clean stop clears
|
|
18
|
+
* that record, so the next write does not take the stop for a failed start.
|
|
19
|
+
*/
|
|
20
|
+
async function syncRun(root) {
|
|
21
|
+
const settings = syncSettings(root);
|
|
22
|
+
if (!settings) {
|
|
23
|
+
console.error(`✗ no sync server: set ${ENV.url} and ${ENV.token}, or url and token in .mainflow/${SETTINGS_FILE}`);
|
|
24
|
+
return 1;
|
|
25
|
+
}
|
|
26
|
+
const running = await probeDaemon(root);
|
|
27
|
+
if (running && running.state !== "gone") {
|
|
28
|
+
console.error(`✗ mainflow sync already runs for this hub (pid ${running.daemon.pid})${running.state === "silent" ? `; ${silentHint(running.daemon)}` : ""}`);
|
|
29
|
+
return 1;
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
await new ServerApi(settings).snapshot();
|
|
33
|
+
} catch (error) {
|
|
34
|
+
if (error instanceof ServerError) {
|
|
35
|
+
console.error(`✗ ${error.message} (hub ${settings.hub} at ${settings.url})`);
|
|
36
|
+
return 1;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
let daemon;
|
|
40
|
+
try {
|
|
41
|
+
daemon = await startDaemon({
|
|
42
|
+
root,
|
|
43
|
+
settings,
|
|
44
|
+
log: (line) => console.log(line)
|
|
45
|
+
});
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (!(error instanceof DaemonRunning)) throw error;
|
|
48
|
+
console.error(`✗ ${error.message}`);
|
|
49
|
+
return 1;
|
|
50
|
+
}
|
|
51
|
+
console.log(`syncing hub ${settings.hub} with ${settings.url} (${daemon.client.online ? "online" : "offline, retrying"})`);
|
|
52
|
+
await new Promise((resolve) => {
|
|
53
|
+
process.once("SIGINT", resolve);
|
|
54
|
+
process.once("SIGTERM", resolve);
|
|
55
|
+
watchHubGone(root, resolve);
|
|
56
|
+
});
|
|
57
|
+
await daemon.stop();
|
|
58
|
+
forgetStarted(root);
|
|
59
|
+
return 0;
|
|
60
|
+
}
|
|
61
|
+
/** Clear the record of a background start when it names this process. */
|
|
62
|
+
function forgetStarted(root) {
|
|
63
|
+
const file = syncPaths(root).started;
|
|
64
|
+
try {
|
|
65
|
+
if (JSON.parse(readFileSync(file, "utf8")).pid === process.pid) rmSync(file, { force: true });
|
|
66
|
+
} catch {}
|
|
67
|
+
}
|
|
68
|
+
function readPayload() {
|
|
69
|
+
try {
|
|
70
|
+
const parsed = JSON.parse(readFileSync(0, "utf8"));
|
|
71
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
72
|
+
} catch {
|
|
73
|
+
return {};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Send every change now and report what did not reach the hub as written. With `hook`, read the PostToolUse
|
|
78
|
+
* payload from stdin, skip tools that wrote nothing synced, and answer as the hook: a block with the problems as
|
|
79
|
+
* the reason, or nothing. A hook never fails the tool, so without a running sync client it answers nothing.
|
|
80
|
+
*/
|
|
81
|
+
async function syncFlush(root, hook) {
|
|
82
|
+
const payload = hook ? readPayload() : {};
|
|
83
|
+
const daemon = readDaemon(root);
|
|
84
|
+
if (!daemon) {
|
|
85
|
+
if (hook) return 0;
|
|
86
|
+
console.error("✗ mainflow sync is not running for this hub");
|
|
87
|
+
return 1;
|
|
88
|
+
}
|
|
89
|
+
const plan = hook ? planFlush(root, payload) : { skip: false };
|
|
90
|
+
if (plan.skip) return 0;
|
|
91
|
+
let result;
|
|
92
|
+
try {
|
|
93
|
+
result = await callDaemon(daemon, "/flush", plan.edit ? { edit: plan.edit } : {}, 3e4);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (hook && error instanceof DaemonUnavailable) return 0;
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
if (hook) {
|
|
99
|
+
const answer = hookAnswer(result);
|
|
100
|
+
if (answer) console.log(answer);
|
|
101
|
+
return 0;
|
|
102
|
+
}
|
|
103
|
+
for (const problem of result.problems) console.log(`! ${problem.message}`);
|
|
104
|
+
console.log(`${result.problems.length} problems, ${result.queued} queued, ${result.online ? "online" : "offline"}`);
|
|
105
|
+
return result.problems.length > 0 ? 1 : 0;
|
|
106
|
+
}
|
|
107
|
+
async function syncStatus(root) {
|
|
108
|
+
const probe = await probeDaemon(root);
|
|
109
|
+
if (probe?.state === "silent") {
|
|
110
|
+
console.log(`mainflow sync runs for this hub (pid ${probe.daemon.pid}), but ${silentHint(probe.daemon)}`);
|
|
111
|
+
return 0;
|
|
112
|
+
}
|
|
113
|
+
if (probe?.state === "running") {
|
|
114
|
+
const { status, daemon } = probe;
|
|
115
|
+
if (!status) console.log(`mainflow sync runs for this hub (pid ${daemon.pid}); it did not give its status`);
|
|
116
|
+
else console.log(`mainflow sync runs (pid ${daemon.pid}): ${status.online ? status.caughtUp ? "online, caught up" : "online, catching up" : "offline"}, ${status.queued} queued, atomic exchange ${status.swapping ? "on" : "off"}, ${status.problems} problems so far`);
|
|
117
|
+
return 0;
|
|
118
|
+
}
|
|
119
|
+
const queue = queuedCount(root);
|
|
120
|
+
console.log(`mainflow sync is not running; ${remoteMode(root) ? `${queue} write(s) wait in the outbox until it runs` : "writes go to the local files"}`);
|
|
121
|
+
return 0;
|
|
122
|
+
}
|
|
123
|
+
//#endregion
|
|
12
124
|
//#region src/retro.ts
|
|
13
|
-
const int = (n) => n.toLocaleString("en-US");
|
|
125
|
+
const int = (n) => Math.round(n).toLocaleString("en-US");
|
|
14
126
|
const usd = (n) => `$${n.toFixed(2)}`;
|
|
15
127
|
const pct = (n) => n === null ? "–" : `${(n * 100).toFixed(1)}%`;
|
|
128
|
+
const sec = (ms) => `${(ms / 1e3).toFixed(1)} 秒`;
|
|
129
|
+
/** A local stamp of the report (2026-10-05T10:01:12) as the text shows it. */
|
|
130
|
+
const stampText = (s) => s.replace("T", " ");
|
|
16
131
|
/** A transcript timestamp in local time, to the minute. */
|
|
17
132
|
const local = (iso) => {
|
|
18
133
|
const d = new Date(iso);
|
|
19
|
-
return Number.isNaN(d.getTime()) ? iso : `${d.toLocaleDateString("sv-SE")} ${d.toTimeString().slice(0, 5)}`;
|
|
134
|
+
return Number.isNaN(d.getTime()) ? String(iso) : `${d.toLocaleDateString("sv-SE")} ${d.toTimeString().slice(0, 5)}`;
|
|
20
135
|
};
|
|
21
136
|
const tokens = (u) => `輸入 ${int(u.input)}、快取寫 5m ${int(u.cacheWrite5m)}、快取寫 1h ${int(u.cacheWrite1h)}、快取讀 ${int(u.cacheRead)}、輸出 ${int(u.output)}`;
|
|
22
137
|
const costOf = (t) => `${usd(t.cost)}${t.unknownPriceModels.length ? `(另有未知單價:${t.unknownPriceModels.join("、")})` : ""}`;
|
|
23
138
|
const byCount = (r) => Object.entries(r).sort((a, b) => b[1] - a[1]).map(([k, v]) => `${k} ${v}`).join("、");
|
|
139
|
+
const joined = (parts) => parts.filter(Boolean).join(",");
|
|
24
140
|
const COMMAND_SHOWN = 100;
|
|
25
|
-
|
|
26
|
-
|
|
141
|
+
const LISTED = 15;
|
|
142
|
+
function calibration(s, undercount) {
|
|
143
|
+
const c = s.calibration;
|
|
144
|
+
if (!c) return "";
|
|
145
|
+
switch (c.method) {
|
|
146
|
+
case "otel": return `依 OTel api_request ${usd(c.calibrated)}`;
|
|
147
|
+
case "cost-state": {
|
|
148
|
+
const { total, after } = c.costState;
|
|
149
|
+
return after.calls > 0 ? `cost-state 合計 ${usd(total)},加合計沒有的呼叫 ${usd(after.calibrated)}(${int(after.calls)} 次呼叫,對話紀錄 ${usd(after.transcript)}),共 ${usd(c.calibrated)}` : `和 cost-state 一致 ${usd(c.calibrated)}`;
|
|
150
|
+
}
|
|
151
|
+
case "cost-state-split": {
|
|
152
|
+
const { total, covered, outside, after } = c.costState;
|
|
153
|
+
const later = after.calls > 0 ? `;合計沒有的呼叫 ${usd(after.calibrated)}(${int(after.calls)} 次呼叫,對話紀錄 ${usd(after.transcript)})` : "";
|
|
154
|
+
return `依 cost-state 分項校正 ${usd(c.calibrated)}(估算:cost-state ${usd(total)},涵蓋的對話紀錄 ${usd(covered)};範圍外 ${usd(outside)}${later})`;
|
|
155
|
+
}
|
|
156
|
+
case "none": return `沒有校正,${undercount ?? ""}`;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function stretches(s) {
|
|
160
|
+
if (!s.window.length) return "沒有計入的時段";
|
|
161
|
+
if (s.window.length === 1 && !s.window[0].from && !s.window[0].to) return "整份對話紀錄";
|
|
162
|
+
const spans = s.window.map((w) => `${w.from ? stampText(w.from) : "開頭"} 到 ${w.to ? stampText(w.to) : "結尾"}`);
|
|
163
|
+
return spans.length > 3 ? `${spans.length} 段:${spans.slice(0, 3).join("、")} 等` : spans.join("、");
|
|
164
|
+
}
|
|
165
|
+
/** Which part of the transcript counted, and by which rule. */
|
|
166
|
+
function window(s) {
|
|
167
|
+
const scope = s.scope;
|
|
168
|
+
if (!scope || scope.rule === "given") return stretches(s);
|
|
169
|
+
if (scope.rule === "time") return `${stretches(s)}(對話紀錄沒有 mainflow 的輪次標頭,依行動時段)`;
|
|
170
|
+
const notes = joined([
|
|
171
|
+
scope.inferred > 0 && `${scope.inferred} 輪開始時沒有進行中的行動,依這輪開始或接手的行動${scope.inferredCost !== void 0 ? `(對話紀錄 ${usd(scope.inferredCost)})` : ""}`,
|
|
172
|
+
scope.inherited > 0 && `${scope.inherited} 輪沒有標頭,沿用前一輪或派出子代理的那一輪`,
|
|
173
|
+
scope.shared > 0 && `${scope.shared} 輪的行動也服務別條主脈絡,只算給第一個參照`,
|
|
174
|
+
scope.switched > 0 && `${scope.switched} 輪中途開始了別條主脈絡的行動,整輪仍算在這裡`,
|
|
175
|
+
scope.beforeHeads > 0 && `標頭出現前的 ${scope.beforeHeads} 輪依行動時段`
|
|
176
|
+
]);
|
|
177
|
+
return `${int(scope.counted)} / ${int(scope.turns)} 輪(依每輪的 mainflow 標頭${notes ? `;${notes}` : ""})`;
|
|
178
|
+
}
|
|
179
|
+
function nodeLine(n) {
|
|
180
|
+
const cost = joined([
|
|
181
|
+
n.subagents > 0 && `子代理 ${usd(n.subagentCost)} ×${n.subagents}`,
|
|
182
|
+
n.occupancy.cost > 0 && `主會話佔用 ${usd(n.occupancy.cost)}${n.repeatOccupancy.cost > 0 ? `(其中重複注入 ${usd(n.repeatOccupancy.cost)})` : ""}`,
|
|
183
|
+
n.skillTurns > 0 && `主會話 skill 輪次 ${usd(n.skillTurnCost)} ×${int(n.skillTurns)}`,
|
|
184
|
+
n.subagentOccupancy.cost > 0 && `子代理內佔用 ${usd(n.subagentOccupancy.cost)}(已含在子代理費用)`
|
|
185
|
+
]);
|
|
186
|
+
const work = joined([
|
|
187
|
+
n.hookRuns > 0 && `hook 執行 ${int(n.hookRuns)} 次 ${sec(n.hookMs)}${n.cancelled ? `,取消 ${n.cancelled}` : ""}${n.blocked ? `,擋下 ${n.blocked}` : ""}`,
|
|
188
|
+
n.injections > 0 && `注入 ${int(n.injections)} 次 ${int(n.chars)} 字${n.repeats ? `(同內容重複 ${n.repeats})` : ""}`,
|
|
189
|
+
n.loads > 0 && `載入 ${int(n.loads)} 次`,
|
|
190
|
+
n.scriptRuns > 0 && `腳本執行 ${int(n.scriptRuns)} 次 ${sec(n.scriptMs)}`
|
|
191
|
+
]);
|
|
192
|
+
const timing = n.measured && joined([n.measured.unfinished > 0 && `未結束 ${n.measured.unfinished}`, n.measured.killed > 0 && `被訊號中止 ${n.measured.killed}`]);
|
|
193
|
+
return `- ${n.node}(${n.kind}):歸因 ${usd(n.attributed)}${cost ? `;${cost}` : ""}${work ? `;${work}` : ""}${timing ? `(計時紀錄:${timing})` : ""}`;
|
|
194
|
+
}
|
|
195
|
+
function sourceLine(s, by) {
|
|
196
|
+
const head = `- [${s.source}] ${s.key}:${s.anomalies} 則`;
|
|
197
|
+
const reasons = byCount(s.reasons);
|
|
198
|
+
if (by === "ratio") return `${head},最大 ${s.maxRatio.toFixed(1)} 倍(${reasons})`;
|
|
199
|
+
return `${head},超出 ${sec(s.excessMs)},預期 ${s.expectedMs === null ? "–" : sec(s.expectedMs)},中位數 ${s.medianMs === null ? "–" : sec(s.medianMs)}(${reasons})`;
|
|
200
|
+
}
|
|
201
|
+
function costSection(report) {
|
|
202
|
+
const { cost } = report;
|
|
27
203
|
return [
|
|
28
|
-
`retro ${mainline.id} ${mainline.title}(${mainline.status})`,
|
|
29
|
-
`Claude Code 版本:${report.versions.join("、") || "(對話紀錄沒有版本欄位)"}`,
|
|
30
|
-
`費用為依牌價的估算:${report.pricing.url}(${report.pricing.date},${report.pricing.unit})`,
|
|
31
|
-
`無法解析的列:${int(report.unreadable)}`,
|
|
32
|
-
`對話紀錄:${cost.sessions.length} 場會話、${cost.subagents.length} 個子代理`,
|
|
33
|
-
"",
|
|
34
204
|
"== 費用 ==",
|
|
35
|
-
`估算合計:${usd(cost.total)}${cost.unknownPriceModels.length ? `,不含未知單價的 ${cost.unknownPriceModels.join("、")}` : ""}`,
|
|
205
|
+
`估算合計:${usd(cost.total)}(校正後,對話紀錄本身 ${usd(cost.transcriptTotal)})${cost.unknownPriceModels.length ? `,不含未知單價的 ${cost.unknownPriceModels.join("、")}` : ""}`,
|
|
206
|
+
...cost.undercount ? [`誤差:有會話沒有 OTel 也沒有 cost-state,${cost.undercount}`] : [],
|
|
36
207
|
`快取命中率:${pct(cost.cacheHitRate)}(快取讀 ÷ 全部上下文)`,
|
|
37
208
|
"各模型:",
|
|
38
|
-
...cost.models.map((m) => `- ${m.model}:呼叫 ${int(m.calls)},${tokens(m.usage)},${m.cost === null ? "未知單價" : usd(m.cost)}`),
|
|
209
|
+
...cost.models.map((m) => `- ${m.model}:呼叫 ${int(m.calls)},${tokens(m.usage)},${m.cost === null ? "未知單價" : `對話紀錄 ${usd(m.cost)},校正後 ${usd(m.calibrated ?? m.cost)}`}`),
|
|
39
210
|
"會話:",
|
|
40
|
-
...cost.sessions.map((s) => s.missing ? `- ${s.session}:沒有讀到對話紀錄(${s.missing})` : `- ${s.session}
|
|
211
|
+
...cost.sessions.map((s) => s.missing ? `- ${s.session}:沒有讀到對話紀錄(${s.missing})` : `- ${s.session}:計入 ${window(s)},開場上下文 ${s.openingContext === null ? "–" : int(s.openingContext)} tokens,呼叫 ${int(s.calls)},快取命中率 ${pct(s.cacheHitRate)},${costOf(s)};連同子代理:${calibration(s, cost.undercount)}${s.calibration?.otelSkipped ? `(沒用 OTel:${s.calibration.otelSkipped})` : ""}`),
|
|
41
212
|
"子代理:",
|
|
42
213
|
...cost.subagents.length === 0 ? ["- 無"] : cost.subagents.map((a) => {
|
|
43
214
|
const who = [a.agentType, a.description].filter(Boolean).join(":");
|
|
44
215
|
return `- ${basename(a.file, ".jsonl")}${who ? `(${who})` : ""},委派自 ${a.parent}:${a.models.join("、") || "沒有回覆"},呼叫 ${int(a.calls)},${tokens(a.usage)},開場上下文 ${a.openingContext === null ? "–" : int(a.openingContext)},${costOf(a)}`;
|
|
45
216
|
}),
|
|
46
|
-
"hook
|
|
47
|
-
...cost.hooks.length === 0 ? ["- 無"] : cost.hooks.map((h) => `- ${h.event}:${
|
|
48
|
-
|
|
217
|
+
cost.hooksNote ? `hook 事件(${cost.hooksNote}):` : "hook 事件:",
|
|
218
|
+
...cost.hooks.length === 0 ? ["- 無"] : cost.hooks.map((h) => `- ${h.event}:${joined([
|
|
219
|
+
h.runs > 0 && `執行 ${int(h.runs)} 次 ${sec(h.durationMs)}${h.cancelled ? `(取消 ${h.cancelled})` : ""}`,
|
|
220
|
+
`注入 ${int(h.injections)} 次,${int(h.chars)} 字`,
|
|
221
|
+
h.blocked > 0 && `擋下 ${h.blocked} 次`
|
|
222
|
+
])}`)
|
|
223
|
+
];
|
|
224
|
+
}
|
|
225
|
+
function attributionSection(report) {
|
|
226
|
+
const a = report.attribution;
|
|
227
|
+
return [
|
|
228
|
+
"== 來源歸因 ==",
|
|
229
|
+
`promptex 紀錄:${a.roots.filter((r) => r.units.length).map((r) => `${r.root}(${r.units.join("、")})`).join("、") || "沒有找到,節點 id 依安裝路徑推得、未核對"}`,
|
|
230
|
+
`能對回節點:${usd(a.attributed)},占已歸到來源的費用 ${pct(a.coverage)}(分母不含主會話自己的對話與輸出 ${usd(a.conversation)}),占總費用 ${pct(a.coverageOfTotal)}。以對話紀錄的費用 ${usd(a.total)} 計,未校正`,
|
|
231
|
+
`節點(依歸因金額,前 ${LISTED}):`,
|
|
232
|
+
...a.nodes.length === 0 ? ["- 無"] : a.nodes.slice(0, LISTED).map(nodeLine),
|
|
233
|
+
"對不回節點:",
|
|
234
|
+
...a.unmapped.length === 0 ? ["- 無"] : a.unmapped.slice(0, 8).map((u) => `- ${u.reason}:${usd(u.cost)}(${int(u.count)} 筆)`),
|
|
235
|
+
...measuredLines(a.measured)
|
|
236
|
+
];
|
|
237
|
+
}
|
|
238
|
+
/** The hook timing log's part of the attribution, when the measurement receiver was read. */
|
|
239
|
+
function measuredLines(m) {
|
|
240
|
+
if (!m) return [];
|
|
241
|
+
const states = joined([
|
|
242
|
+
m.unfinished > 0 && `未結束 ${int(m.unfinished)}`,
|
|
243
|
+
m.killed > 0 && `被訊號中止 ${int(m.killed)}`,
|
|
244
|
+
m.endOnly > 0 && `只收到結束列 ${int(m.endOnly)}`
|
|
245
|
+
]);
|
|
246
|
+
return [
|
|
247
|
+
`hook 計時紀錄:執行 ${int(m.runs)} 次 ${sec(m.ms)},對回節點 ${int(m.mapped)} 次${states ? `(${states})` : ""}。節點的 hook 執行次數與耗時取自它,再加上對話紀錄有、計時紀錄沒有對應的 ${int(m.transcriptOnly.runs)} 次 ${sec(m.transcriptOnly.ms)}`,
|
|
248
|
+
"計時紀錄對不回節點:",
|
|
249
|
+
...m.unmapped.length === 0 ? ["- 無"] : m.unmapped.map((u) => `- ${u.reason}:${int(u.runs)} 次 ${sec(u.ms)}${u.scripts.length ? `(${u.scripts.join("、")})` : ""}`)
|
|
250
|
+
];
|
|
251
|
+
}
|
|
252
|
+
/** What the measurement receiver gave and what it could not. */
|
|
253
|
+
function measurementSection(report) {
|
|
254
|
+
const m = report.measurements;
|
|
255
|
+
if (!m) return [];
|
|
256
|
+
const rows = Object.entries(m.rows).sort((a, b) => b[1] - a[1]).map(([k, v]) => `${k} ${int(v)}`).join("、");
|
|
257
|
+
return [
|
|
258
|
+
"== 量測接收端 ==",
|
|
259
|
+
`來源:${m.origin || "(沒有讀取)"},${m.sessions} 場會話,列出物件 ${int(m.objects)} 個,下載 ${int(m.downloaded)} 個,缺 ${int(m.missing)} 個,請求 ${int(m.requests)} 次${report.timings.receiver !== void 0 ? `,耗時 ${int(report.timings.receiver)} ms` : ""}`,
|
|
260
|
+
`範圍內的列:${rows || "無"}`,
|
|
261
|
+
`api_request:範圍內 ${int(m.apiRequests.rows)} 列${m.apiRequests.withoutCost ? `,缺模型或費用 ${int(m.apiRequests.withoutCost)} 列` : ""}。涵蓋整個範圍的會話依它校正,其餘會話的校正方式寫在各會話那一行`,
|
|
262
|
+
"hook 批次(平台只記事件,不記腳本,所以對不回節點):",
|
|
263
|
+
...m.hookBatches.length === 0 ? ["- 無"] : m.hookBatches.map((b) => `- ${b.event}:${joined([
|
|
264
|
+
`批次 ${int(b.batches)}`,
|
|
265
|
+
`hook ${int(b.hooks)} 支`,
|
|
266
|
+
sec(b.ms),
|
|
267
|
+
b.blocking > 0 && `擋下 ${int(b.blocking)}`,
|
|
268
|
+
b.cancelled > 0 && `取消 ${int(b.cancelled)}`,
|
|
269
|
+
b.nonBlockingErrors > 0 && `非阻擋錯誤 ${int(b.nonBlockingErrors)}`
|
|
270
|
+
])}`),
|
|
271
|
+
"缺的部分:",
|
|
272
|
+
...m.problems.length === 0 ? ["- 無"] : m.problems.map((p) => `- ${p}`)
|
|
273
|
+
];
|
|
274
|
+
}
|
|
275
|
+
function anomalySection(report) {
|
|
276
|
+
const x = report.anomalies;
|
|
277
|
+
const { TIMEOUT_SHARE, ROBUST_Z, MIN_EXCESS_MS, MIN_BASELINE, RERUN_WINDOW } = x.rules;
|
|
278
|
+
const types = [
|
|
279
|
+
"hook",
|
|
280
|
+
"script",
|
|
281
|
+
"subagent",
|
|
282
|
+
"bash"
|
|
283
|
+
].map((t) => `${t} ${x.anomalies.filter((a) => a.source === t).length}`).join("、");
|
|
284
|
+
const top = (list, by) => [
|
|
285
|
+
"hook",
|
|
286
|
+
"script",
|
|
287
|
+
"subagent",
|
|
288
|
+
"bash"
|
|
289
|
+
].flatMap((t) => list.filter((s) => s.source === t).slice(0, 3)).map((s) => sourceLine(s, by));
|
|
290
|
+
return [
|
|
291
|
+
"== 異常 ==",
|
|
292
|
+
`規則:每個來源以自己的基線判斷(至少 ${MIN_BASELINE} 筆熱執行),robust z > ${ROBUST_Z} 且超出 ${sec(MIN_EXCESS_MS)},同組與整個來源都要超過;沒有基線的 hook 以宣告逾時的 ${TIMEOUT_SHARE * 100}% 當預期;逾時、取消、結束碼非 0、前 ${RERUN_WINDOW} 次內重跑同一指令、使用者中止一律列出`,
|
|
293
|
+
`共 ${x.anomalies.length} 則(${types}),計時的執行 ${int(x.runs.hook + x.runs.script + x.runs.subagent)} 筆,冷啟動 ${x.runs.cold} 筆不進基線`,
|
|
294
|
+
`擋下(把關的決定,不算異常):${int(blockTotal(x.blocks))} 次,${blockMatch(blockTotal(x.blocks), report.precision.blocked.total)}。有紀錄的依節點,PreToolUse 只留訊息,依訊息開頭`,
|
|
295
|
+
...Object.entries(x.blocks).sort((a, b) => b[1] - a[1]).slice(0, 8).map(([k, v]) => `- ${k}:${v} 次`),
|
|
296
|
+
"超出時間 × 次數(各類前 3):",
|
|
297
|
+
...x.byExcess.length ? top(x.byExcess, "excess") : ["- 無"],
|
|
298
|
+
"實際 ÷ 預期(各類前 3):",
|
|
299
|
+
...x.byRatio.length ? top(x.byRatio, "ratio") : ["- 無"]
|
|
300
|
+
];
|
|
301
|
+
}
|
|
302
|
+
const blockTotal = (blocks) => Object.values(blocks).reduce((a, b) => a + b, 0);
|
|
303
|
+
/** The gate decisions against the precision section's blocks, which count the same runs. */
|
|
304
|
+
const blockMatch = (gates, blocked) => gates === blocked ? "和精準度候選的「被 hook 擋下」相同" : `比精準度候選的「被 hook 擋下」(${int(blocked)} 次)${gates > blocked ? "多" : "少"} ${int(Math.abs(gates - blocked))} 次`;
|
|
305
|
+
function reviewSection(report) {
|
|
306
|
+
const r = report.reviews;
|
|
307
|
+
const tally = (t) => t.total ? `發現 ${t.total} 則(嚴重度 ${byCount(t.bySev)};成因 ${byCount(t.byClass)})` : "發現 0 則";
|
|
308
|
+
return [
|
|
309
|
+
"== 委派與審查 ==",
|
|
310
|
+
`委派 ${r.delegations} 個,帶標籤 ${r.labeled} 個${r.otherMainline ? `,其中 ${r.otherMainline} 個屬於其他主脈絡` : ""},沒有標籤 ${r.unlabeled.length} 個`,
|
|
311
|
+
"各項目的輪數:",
|
|
312
|
+
...r.items.length === 0 ? ["- 無"] : r.items.map((i) => {
|
|
313
|
+
const roles = Object.entries(i.roles).map(([role, v]) => `${role} ${v.delegations}(到 r${v.maxRound})`).join("、");
|
|
314
|
+
return `- ${i.item}:${roles}${i.roles.review ? `;${tally(i.findings)}` : ""}`;
|
|
315
|
+
}),
|
|
316
|
+
`審查發現合計:${tally(r.findings)}`,
|
|
317
|
+
`缺 review-findings 區塊的審查回覆:${r.missingBlock.length}`,
|
|
318
|
+
...r.missingBlock.map((m) => `- ${m.agentId}(${m.item} review r${m.round})`),
|
|
319
|
+
`review-findings 區塊讀不懂的行:${r.badLines.length}`,
|
|
320
|
+
...r.badLines.slice(0, 10).map((b) => `- ${b.agentId}(${b.item}):${b.why}:${b.line.slice(0, 80)}`),
|
|
321
|
+
`區塊之後還有文字的審查回覆:${r.trailingText.length}`,
|
|
322
|
+
...r.trailingText.slice(0, 10).map((t) => `- ${t.agentId}(${t.item}):${t.text.replace(/\s+/g, " ").slice(0, 80)}`),
|
|
323
|
+
`無標籤的審查回覆(審查代理的委派沒寫標籤,發現不計入合計):${r.unlabeledReviews.length}`,
|
|
324
|
+
...r.unlabeledReviews.map((u) => `- ${u.agentId}(${u.agentType}${u.description ? `:${u.description}` : ""}):${u.found ? tally(u.findings) : "缺 review-findings 區塊"}${u.badLines ? `,讀不懂的行 ${u.badLines}` : ""}`)
|
|
325
|
+
];
|
|
326
|
+
}
|
|
327
|
+
const UNMATCHED = {
|
|
328
|
+
"hand-written": "手寫段",
|
|
329
|
+
"no-node": "標題對不到節點"
|
|
330
|
+
};
|
|
331
|
+
const STRENGTH = {
|
|
332
|
+
strong: "強",
|
|
333
|
+
weak: "弱",
|
|
334
|
+
stop: "Stop"
|
|
335
|
+
};
|
|
336
|
+
function retryLine(b) {
|
|
337
|
+
const between = b.between?.length ? `,中間呼叫 ${b.between.join("、")}` : "";
|
|
338
|
+
return `- [${STRENGTH[b.strength]}] ${b.blockedAt === void 0 ? "–" : local(b.blockedAt)} ${b.event} ${b.tool},${b.hook}:擋下 ${b.attempts} 次後通過${between}(${b.where}):${b.input}`;
|
|
339
|
+
}
|
|
340
|
+
function offlineSection(report) {
|
|
341
|
+
const { sections, sectionWarning, blockRetries } = report.offline;
|
|
342
|
+
const t = report.timings;
|
|
343
|
+
const strength = (k) => blockRetries.filter((b) => b.strength === k).length;
|
|
344
|
+
return [
|
|
345
|
+
"== 離線分析 ==",
|
|
346
|
+
`常駐指示與 rules 逐區段佔用(依會話載入的內容切 # 與 ## 標題,依佔用費用前 ${LISTED},共 ${sections.length} 段):`,
|
|
347
|
+
...sectionWarning ? [`警告:${sectionWarning}`] : [],
|
|
348
|
+
...sections.length === 0 ? ["- 無"] : sections.slice(0, LISTED).map((s) => `- ${s.file} ${s.heading}(${s.node ?? UNMATCHED[s.unmatched ?? "no-node"]}):${int(s.chars)} 字 ${int(s.bytes)} B,載入 ${s.loads} 次${s.carried ? `、範圍前帶入 ${s.carried} 次` : ""}${s.versions > 1 ? `(${s.versions} 種版本)` : ""},佔用 ${usd(s.occupancy.cost)}、${int(s.occupancy.tokenCalls)} token 次`),
|
|
349
|
+
`擋下誤判候選:強 ${strength("strong")}、弱 ${strength("weak")}、Stop ${strength("stop")}(強:${RETRY_RULES.strong};弱:${RETRY_RULES.weak};Stop:${RETRY_RULES.stop})`,
|
|
350
|
+
...blockRetries.slice(0, 20).map(retryLine),
|
|
351
|
+
`耗時:讀取 ${int(t.read)} ms,${t.receiver !== void 0 ? `接收端 ${int(t.receiver)} ms,` : ""}來源歸因 ${int(t.attribution)} ms,異常 ${int(t.anomalies)} ms,委派與審查 ${int(t.reviews)} ms,逐區段佔用 ${int(t.sections)} ms,擋下誤判 ${int(t.blockRetries)} ms,合計 ${int(t.total)} ms`
|
|
352
|
+
];
|
|
353
|
+
}
|
|
354
|
+
function precisionSection(report) {
|
|
355
|
+
const { mainline, precision } = report;
|
|
356
|
+
return [
|
|
49
357
|
"== 精準度候選 ==",
|
|
50
358
|
`工具錯誤:${precision.toolErrors.total}${precision.toolErrors.total ? `(${byCount(precision.toolErrors.byTool)})` : ""}`,
|
|
51
359
|
`同一個 Bash 指令重跑:${precision.bashReruns.total} 次`,
|
|
52
360
|
...precision.bashReruns.commands.slice(0, 10).map((c) => `- ${c.runs} 次(${c.session}):${c.command.length > COMMAND_SHOWN ? `${c.command.slice(0, 99)}…` : c.command}`.replace(/\n/g, " ")),
|
|
53
|
-
`誤觸候選(hook 注入後,下一則回覆沒有任何工具呼叫):${precision.misfireCandidates.total}${precision.misfireCandidates.total ? `(${byCount(precision.misfireCandidates.byEvent)})` : ""}`,
|
|
54
361
|
`被 hook 擋下:${precision.blocked.total}${precision.blocked.total ? `(${byCount(precision.blocked.byEvent)})` : ""}`,
|
|
55
362
|
`超過 5 分鐘的間隔:${precision.gaps.length}`,
|
|
56
363
|
...precision.gaps.map((g) => `- ${local(g.from)} → ${local(g.to)},${g.minutes} 分鐘,${g.waitingForUser ? "等使用者" : "工具或子代理執行中"}(${g.session})`),
|
|
57
364
|
`主脈絡:解決條件 ${mainline.criteria} 條,任務 ${mainline.tasks} 個,完成檢查未過 ${precision.failedChecks.length} 行`,
|
|
58
365
|
...precision.failedChecks.map((f) => `- ${f.task}:${f.line}`)
|
|
366
|
+
];
|
|
367
|
+
}
|
|
368
|
+
function retroText(report) {
|
|
369
|
+
const { mainline, cost } = report;
|
|
370
|
+
return [
|
|
371
|
+
`retro ${mainline.id} ${mainline.title}(${mainline.status})`,
|
|
372
|
+
`Claude Code 版本:${report.versions.join("、") || "(對話紀錄沒有版本欄位)"}`,
|
|
373
|
+
`費用為依牌價的估算:${report.pricing.url}(${report.pricing.date},${report.pricing.unit})`,
|
|
374
|
+
`無法解析的列:${int(report.unreadable)},重複 uuid 略過 ${int(report.records.duplicates)} 列,範圍外未計入 ${int(report.records.outsideWindow)} 列`,
|
|
375
|
+
`對話紀錄:${cost.sessions.length} 場會話、${cost.subagents.length} 個子代理`,
|
|
376
|
+
"",
|
|
377
|
+
...report.measurements ? [...measurementSection(report), ""] : [],
|
|
378
|
+
...costSection(report),
|
|
379
|
+
"",
|
|
380
|
+
...attributionSection(report),
|
|
381
|
+
"",
|
|
382
|
+
...anomalySection(report),
|
|
383
|
+
"",
|
|
384
|
+
...reviewSection(report),
|
|
385
|
+
"",
|
|
386
|
+
...offlineSection(report),
|
|
387
|
+
"",
|
|
388
|
+
...precisionSection(report)
|
|
59
389
|
].join("\n");
|
|
60
390
|
}
|
|
61
391
|
//#endregion
|
|
62
392
|
//#region src/cli.ts
|
|
393
|
+
const write = (spec) => spec;
|
|
394
|
+
/** Where a document an operation created or changed is, as the commands print it: .mainflow/... */
|
|
395
|
+
const inData = (...parts) => join(DATA_DIR, ...parts);
|
|
396
|
+
const WRITES = {
|
|
397
|
+
"repo add": write({
|
|
398
|
+
op: "repo.add",
|
|
399
|
+
check: ({ root, args }) => {
|
|
400
|
+
requireSlug(args[0]);
|
|
401
|
+
const target = resolve(root, args[1]);
|
|
402
|
+
if (!existsSync(target) || !statSync(target).isDirectory()) throw new WriteError(`repo path ${args[1]} does not exist from the hub root (${target})`);
|
|
403
|
+
},
|
|
404
|
+
input: ({ args }) => ({
|
|
405
|
+
name: args[0],
|
|
406
|
+
path: args[1]
|
|
407
|
+
}),
|
|
408
|
+
print: ({ args }, name) => done(`registered ${name} ${args[1]}`)
|
|
409
|
+
}),
|
|
410
|
+
"hub add": write({
|
|
411
|
+
op: "hub.add",
|
|
412
|
+
check: ({ root, args }) => {
|
|
413
|
+
const [name, path] = [args[0], args[1]];
|
|
414
|
+
checkHubName(nodeStorage(root), name);
|
|
415
|
+
const target = resolve(root, path);
|
|
416
|
+
if (!existsSync(join(target, ".mainflow", "config.yaml"))) throw new WriteError(`no hub at ${path} from the hub root (${target})`);
|
|
417
|
+
const other = hubName(target);
|
|
418
|
+
if (other !== name) throw new WriteError(`${path} holds hub "${other}"; register it as "${other}"`);
|
|
419
|
+
},
|
|
420
|
+
input: ({ args }) => ({
|
|
421
|
+
name: args[0],
|
|
422
|
+
path: args[1]
|
|
423
|
+
}),
|
|
424
|
+
print: ({ args }, name) => done(`registered hub ${name} ${args[1]}`)
|
|
425
|
+
}),
|
|
426
|
+
"session start": write({
|
|
427
|
+
op: "session.start",
|
|
428
|
+
input: ({ opts }) => {
|
|
429
|
+
const agent = opts.agent ?? "manual";
|
|
430
|
+
return {
|
|
431
|
+
id: opts.id ?? `${agent}-${randomBytes(4).toString("hex")}`,
|
|
432
|
+
agent,
|
|
433
|
+
cwd: process.cwd(),
|
|
434
|
+
delegatedFrom: opts["delegated-from"],
|
|
435
|
+
handoffFrom: opts["handoff-from"]
|
|
436
|
+
};
|
|
437
|
+
},
|
|
438
|
+
print: (_, session) => done(`started ${session.id}${session.actions.length ? ` with ${session.actions.length} carried actions` : ""}`)
|
|
439
|
+
}),
|
|
440
|
+
"session handoff": write({
|
|
441
|
+
op: "session.handoff",
|
|
442
|
+
input: ({ args, opts }) => ({
|
|
443
|
+
id: sessionOf(opts),
|
|
444
|
+
from: args[0]
|
|
445
|
+
}),
|
|
446
|
+
print: ({ args }, session) => done(`${session.id} took over ${session.actions.filter((a) => a.status === "todo" || a.status === "doing").length} actions from ${args[0]}`)
|
|
447
|
+
}),
|
|
448
|
+
"session set": write({
|
|
449
|
+
op: "session.set",
|
|
450
|
+
input: ({ args, opts }) => ({
|
|
451
|
+
id: args[0],
|
|
452
|
+
status: required(opts, "status", "--status paused")
|
|
453
|
+
}),
|
|
454
|
+
print: ({ args }, session) => done(`updated ${args[0]} (${session.status})`)
|
|
455
|
+
}),
|
|
456
|
+
"session end": write({
|
|
457
|
+
op: "session.set",
|
|
458
|
+
input: ({ args, opts }) => ({
|
|
459
|
+
id: sessionOf(opts, args[0]),
|
|
460
|
+
status: "ended"
|
|
461
|
+
}),
|
|
462
|
+
print: (_, session) => done(`ended ${session.id}`)
|
|
463
|
+
}),
|
|
464
|
+
"session archive": write({
|
|
465
|
+
op: "session.archive",
|
|
466
|
+
input: ({ args }) => ({
|
|
467
|
+
id: args[0],
|
|
468
|
+
archived: true
|
|
469
|
+
}),
|
|
470
|
+
print: (_, session) => done(`archived ${session.id}`)
|
|
471
|
+
}),
|
|
472
|
+
"session unarchive": write({
|
|
473
|
+
op: "session.archive",
|
|
474
|
+
input: ({ args }) => ({
|
|
475
|
+
id: args[0],
|
|
476
|
+
archived: false
|
|
477
|
+
}),
|
|
478
|
+
print: (_, session) => done(`unarchived ${session.id}`)
|
|
479
|
+
}),
|
|
480
|
+
"action add": write({
|
|
481
|
+
op: "action.add",
|
|
482
|
+
input: ({ args, opts }) => ({
|
|
483
|
+
session: sessionOf(opts),
|
|
484
|
+
do: args[0],
|
|
485
|
+
serves: list(opts.serves),
|
|
486
|
+
before: opts.before
|
|
487
|
+
}),
|
|
488
|
+
print: ({ root }, ref) => {
|
|
489
|
+
done(`created ${ref}`);
|
|
490
|
+
return remindDuplicates(root, ref);
|
|
491
|
+
}
|
|
492
|
+
}),
|
|
493
|
+
"action set": write({
|
|
494
|
+
op: "action.set",
|
|
495
|
+
input: ({ args, opts }) => {
|
|
496
|
+
const blockedBy = opts.reason ? {
|
|
497
|
+
reason: opts.reason,
|
|
498
|
+
...opts["waiting-for"] ? { waitingFor: opts["waiting-for"] } : {},
|
|
499
|
+
...opts.until ? { until: opts.until } : {}
|
|
500
|
+
} : void 0;
|
|
501
|
+
if (!blockedBy && (opts["waiting-for"] || opts.until)) throw new WriteError("--waiting-for and --until need --reason, e.g. --status blocked --reason \"等 schema 定案\"");
|
|
502
|
+
return {
|
|
503
|
+
ref: actionRef(opts, args[0]),
|
|
504
|
+
patch: {
|
|
505
|
+
status: opts.status,
|
|
506
|
+
do: opts.text,
|
|
507
|
+
serves: opts.serves === void 0 ? void 0 : list(opts.serves),
|
|
508
|
+
before: opts.before,
|
|
509
|
+
note: opts.note,
|
|
510
|
+
blockedBy
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
},
|
|
514
|
+
print: ({ root, args, opts }, action) => {
|
|
515
|
+
const ref = actionRef(opts, args[0]);
|
|
516
|
+
done(`updated ${ref} (${action.status})`);
|
|
517
|
+
return opts.status === "doing" ? remindDuplicates(root, ref) : 0;
|
|
518
|
+
}
|
|
519
|
+
}),
|
|
520
|
+
"new mainline": write({
|
|
521
|
+
op: "mainline.new",
|
|
522
|
+
input: ({ args, opts }) => ({
|
|
523
|
+
slug: args[0],
|
|
524
|
+
title: required(opts, "title", "--title \"重建 mainline 任務管理\"")
|
|
525
|
+
}),
|
|
526
|
+
print: (_, { id, dir }) => done(`created ${id} ${inData(dir, MAINLINE_FILE)}`)
|
|
527
|
+
}),
|
|
528
|
+
"new task": write({
|
|
529
|
+
op: "task.new",
|
|
530
|
+
input: ({ args, opts }) => ({
|
|
531
|
+
mainline: args[0],
|
|
532
|
+
slug: args[1],
|
|
533
|
+
title: required(opts, "title", "--title \"定義資料格式 schema\""),
|
|
534
|
+
kind: opts.kind,
|
|
535
|
+
parent: opts.parent,
|
|
536
|
+
repo: opts.repo
|
|
537
|
+
}),
|
|
538
|
+
print: (_, { ref, dir }) => done(`created ${ref} ${inData(dir, TASK_FILE)}`)
|
|
539
|
+
}),
|
|
540
|
+
"new decision": write({
|
|
541
|
+
op: "decision.new",
|
|
542
|
+
input: ({ args, opts }) => ({
|
|
543
|
+
mainline: args[0],
|
|
544
|
+
slug: args[1],
|
|
545
|
+
title: required(opts, "title", "--title \"以文字檔為準\""),
|
|
546
|
+
status: opts.status,
|
|
547
|
+
by: opts.by,
|
|
548
|
+
basis: opts.basis,
|
|
549
|
+
affects: opts.affects ? list(opts.affects) : void 0
|
|
550
|
+
}),
|
|
551
|
+
print: (_, { ref, file }) => done(`created ${ref} ${inData(file)}`)
|
|
552
|
+
}),
|
|
553
|
+
"new issue": write({
|
|
554
|
+
op: "issue.new",
|
|
555
|
+
input: ({ args, opts }) => ({
|
|
556
|
+
slug: args[0],
|
|
557
|
+
title: required(opts, "title", "--title \"重試不穩定的 VM\""),
|
|
558
|
+
origin: opts.origin,
|
|
559
|
+
labels: opts.labels ? list(opts.labels) : void 0
|
|
560
|
+
}),
|
|
561
|
+
print: (_, { id, file }) => done(`created ${id} ${inData(file)}`)
|
|
562
|
+
}),
|
|
563
|
+
"new finding": write({
|
|
564
|
+
op: "finding.new",
|
|
565
|
+
input: ({ args, opts }) => ({
|
|
566
|
+
slug: args[0],
|
|
567
|
+
title: required(opts, "title", "--title \"先 await 再寫入 state\""),
|
|
568
|
+
origin: opts.origin,
|
|
569
|
+
labels: opts.labels ? list(opts.labels) : void 0
|
|
570
|
+
}),
|
|
571
|
+
print: (_, { id, file }) => done(`created ${id} ${inData(file)}`)
|
|
572
|
+
}),
|
|
573
|
+
"mainline set": write({
|
|
574
|
+
op: "mainline.set",
|
|
575
|
+
input: ({ args, opts }) => ({
|
|
576
|
+
id: args[0],
|
|
577
|
+
status: required(opts, "status", "--status active")
|
|
578
|
+
}),
|
|
579
|
+
print: (_, m) => done(`updated ${m.id} (${m.status})`)
|
|
580
|
+
}),
|
|
581
|
+
"mainline archive": write({
|
|
582
|
+
op: "mainline.archive",
|
|
583
|
+
input: ({ args }) => ({
|
|
584
|
+
id: args[0],
|
|
585
|
+
archived: true
|
|
586
|
+
}),
|
|
587
|
+
print: (_, m) => done(`archived ${m.id}`)
|
|
588
|
+
}),
|
|
589
|
+
"mainline unarchive": write({
|
|
590
|
+
op: "mainline.archive",
|
|
591
|
+
input: ({ args }) => ({
|
|
592
|
+
id: args[0],
|
|
593
|
+
archived: false
|
|
594
|
+
}),
|
|
595
|
+
print: (_, m) => done(`unarchived ${m.id}`)
|
|
596
|
+
}),
|
|
597
|
+
"criterion add": write({
|
|
598
|
+
op: "criterion.add",
|
|
599
|
+
input: ({ args, opts }) => ({
|
|
600
|
+
mainline: args[0],
|
|
601
|
+
text: args[1],
|
|
602
|
+
doneWhen: opts["done-when"] ?? ""
|
|
603
|
+
}),
|
|
604
|
+
print: (_, { ref }) => done(`created ${ref}`)
|
|
605
|
+
}),
|
|
606
|
+
"criterion check": write({
|
|
607
|
+
op: "criterion.check",
|
|
608
|
+
input: ({ args, opts }) => ({
|
|
609
|
+
ref: args[0],
|
|
610
|
+
done: true,
|
|
611
|
+
evidence: opts.evidence
|
|
612
|
+
}),
|
|
613
|
+
print: ({ args }) => done(`checked ${args[0]}`)
|
|
614
|
+
}),
|
|
615
|
+
"criterion uncheck": write({
|
|
616
|
+
op: "criterion.check",
|
|
617
|
+
input: ({ args }) => ({
|
|
618
|
+
ref: args[0],
|
|
619
|
+
done: false
|
|
620
|
+
}),
|
|
621
|
+
print: ({ args }) => done(`unchecked ${args[0]}`)
|
|
622
|
+
}),
|
|
623
|
+
"criterion set": write({
|
|
624
|
+
op: "criterion.set",
|
|
625
|
+
input: ({ args, opts }) => ({
|
|
626
|
+
ref: args[0],
|
|
627
|
+
patch: {
|
|
628
|
+
text: opts.text,
|
|
629
|
+
doneWhen: opts["done-when"]
|
|
630
|
+
}
|
|
631
|
+
}),
|
|
632
|
+
print: ({ args }) => done(`updated ${args[0]}`)
|
|
633
|
+
}),
|
|
634
|
+
"task set": write({
|
|
635
|
+
op: "task.set",
|
|
636
|
+
input: ({ args, opts }) => {
|
|
637
|
+
const blockedBy = opts.reason ? {
|
|
638
|
+
reason: opts.reason,
|
|
639
|
+
...opts["waiting-for"] ? { waitingFor: opts["waiting-for"] } : {},
|
|
640
|
+
...opts.signal ? { signal: opts.signal } : {}
|
|
641
|
+
} : void 0;
|
|
642
|
+
return {
|
|
643
|
+
ref: args[0],
|
|
644
|
+
patch: {
|
|
645
|
+
status: opts.status,
|
|
646
|
+
repo: opts.repo,
|
|
647
|
+
blockedBy,
|
|
648
|
+
branch: opts.branch,
|
|
649
|
+
worktree: opts.worktree,
|
|
650
|
+
base: opts.base,
|
|
651
|
+
merge: opts.merge
|
|
652
|
+
}
|
|
653
|
+
};
|
|
654
|
+
},
|
|
655
|
+
print: ({ args }, t) => done(`updated ${args[0]} (${t.status})`)
|
|
656
|
+
}),
|
|
657
|
+
"item add": write({
|
|
658
|
+
op: "item.add",
|
|
659
|
+
input: ({ args, opts }) => ({
|
|
660
|
+
task: args[0],
|
|
661
|
+
text: args[1],
|
|
662
|
+
doneWhen: opts["done-when"] ?? "",
|
|
663
|
+
serves: opts.serves === void 0 ? void 0 : list(opts.serves)
|
|
664
|
+
}),
|
|
665
|
+
print: (_, { ref }) => done(`created ${ref}`)
|
|
666
|
+
}),
|
|
667
|
+
"item check": write({
|
|
668
|
+
op: "item.check",
|
|
669
|
+
input: ({ args, opts }) => ({
|
|
670
|
+
ref: args[0],
|
|
671
|
+
done: true,
|
|
672
|
+
evidence: opts.evidence
|
|
673
|
+
}),
|
|
674
|
+
print: ({ args }) => done(`checked ${args[0]}`)
|
|
675
|
+
}),
|
|
676
|
+
"item uncheck": write({
|
|
677
|
+
op: "item.check",
|
|
678
|
+
input: ({ args }) => ({
|
|
679
|
+
ref: args[0],
|
|
680
|
+
done: false
|
|
681
|
+
}),
|
|
682
|
+
print: ({ args }) => done(`unchecked ${args[0]}`)
|
|
683
|
+
}),
|
|
684
|
+
"decision accept": write({
|
|
685
|
+
op: "decision.accept",
|
|
686
|
+
input: ({ args, opts }) => ({
|
|
687
|
+
ref: args[0],
|
|
688
|
+
by: opts.by,
|
|
689
|
+
basis: opts.basis
|
|
690
|
+
}),
|
|
691
|
+
print: ({ args }, d) => done(`accepted ${args[0]} (${d.decidedAt})`)
|
|
692
|
+
}),
|
|
693
|
+
"item drop": write({
|
|
694
|
+
op: "item.drop",
|
|
695
|
+
input: ({ args, opts }) => ({
|
|
696
|
+
ref: args[0],
|
|
697
|
+
supersededBy: opts.by,
|
|
698
|
+
note: opts.note
|
|
699
|
+
}),
|
|
700
|
+
print: ({ args }) => done(`dropped ${args[0]}`)
|
|
701
|
+
}),
|
|
702
|
+
"item undrop": write({
|
|
703
|
+
op: "item.undrop",
|
|
704
|
+
input: ({ args }) => ({ ref: args[0] }),
|
|
705
|
+
print: ({ args }) => done(`undropped ${args[0]}`)
|
|
706
|
+
}),
|
|
707
|
+
"issue plan": write({
|
|
708
|
+
op: "issue.plan",
|
|
709
|
+
input: ({ args }) => ({
|
|
710
|
+
id: args[0],
|
|
711
|
+
task: args[1]
|
|
712
|
+
}),
|
|
713
|
+
print: ({ args }, i) => done(`planned ${i.id} in ${args[1]}`)
|
|
714
|
+
}),
|
|
715
|
+
"issue close": write({
|
|
716
|
+
op: "issue.close",
|
|
717
|
+
input: ({ args, opts }) => ({
|
|
718
|
+
id: args[0],
|
|
719
|
+
resolution: opts.by ?? ""
|
|
720
|
+
}),
|
|
721
|
+
print: (_, i) => done(`closed ${i.id}`)
|
|
722
|
+
}),
|
|
723
|
+
"issue reopen": write({
|
|
724
|
+
op: "issue.reopen",
|
|
725
|
+
input: ({ args }) => ({ id: args[0] }),
|
|
726
|
+
print: (_, i) => done(`reopened ${i.id}`)
|
|
727
|
+
}),
|
|
728
|
+
"issue archive": write({
|
|
729
|
+
op: "issue.archive",
|
|
730
|
+
input: ({ args }) => ({
|
|
731
|
+
id: args[0],
|
|
732
|
+
archived: true
|
|
733
|
+
}),
|
|
734
|
+
print: (_, i) => done(`archived ${i.id}`)
|
|
735
|
+
}),
|
|
736
|
+
"issue unarchive": write({
|
|
737
|
+
op: "issue.archive",
|
|
738
|
+
input: ({ args }) => ({
|
|
739
|
+
id: args[0],
|
|
740
|
+
archived: false
|
|
741
|
+
}),
|
|
742
|
+
print: (_, i) => done(`unarchived ${i.id}`)
|
|
743
|
+
}),
|
|
744
|
+
"finding set": write({
|
|
745
|
+
op: "finding.set",
|
|
746
|
+
input: ({ args, opts }) => ({
|
|
747
|
+
id: args[0],
|
|
748
|
+
status: required(opts, "status", "--status obsolete")
|
|
749
|
+
}),
|
|
750
|
+
print: (_, f) => done(`updated ${f.id}`)
|
|
751
|
+
}),
|
|
752
|
+
"finding archive": write({
|
|
753
|
+
op: "finding.archive",
|
|
754
|
+
input: ({ args }) => ({
|
|
755
|
+
id: args[0],
|
|
756
|
+
archived: true
|
|
757
|
+
}),
|
|
758
|
+
print: (_, f) => done(`archived ${f.id}`)
|
|
759
|
+
}),
|
|
760
|
+
"finding unarchive": write({
|
|
761
|
+
op: "finding.archive",
|
|
762
|
+
input: ({ args }) => ({
|
|
763
|
+
id: args[0],
|
|
764
|
+
archived: false
|
|
765
|
+
}),
|
|
766
|
+
print: (_, f) => done(`unarchived ${f.id}`)
|
|
767
|
+
}),
|
|
768
|
+
"ops add": write({
|
|
769
|
+
op: "ops.add",
|
|
770
|
+
input: ({ opts }) => ({
|
|
771
|
+
target: required(opts, "target", "--target github.com/owner/repo"),
|
|
772
|
+
operation: required(opts, "op", "--op \"push tag v1.2.0\""),
|
|
773
|
+
result: required(opts, "result", "--result success"),
|
|
774
|
+
...opts.ref ? { ref: opts.ref } : {}
|
|
775
|
+
}),
|
|
776
|
+
print: (_, entry) => done(`logged ${entry.at} ${entry.operation}`)
|
|
777
|
+
}),
|
|
778
|
+
"section append": write({
|
|
779
|
+
op: "section.append",
|
|
780
|
+
input: ({ args }) => ({
|
|
781
|
+
ref: args[0],
|
|
782
|
+
section: args[1],
|
|
783
|
+
text: args[2]
|
|
784
|
+
}),
|
|
785
|
+
print: ({ args }, { file }) => done(`appended to ${args[0]} ## ${args[1].trim()} ${inData(file)}`)
|
|
786
|
+
}),
|
|
787
|
+
"decision supersede": write({
|
|
788
|
+
op: "decision.supersede",
|
|
789
|
+
input: ({ args, opts }) => ({
|
|
790
|
+
ref: args[0],
|
|
791
|
+
by: required(opts, "by", "--by d002")
|
|
792
|
+
}),
|
|
793
|
+
print: ({ args }, d) => done(`superseded ${args[0]} by ${d.supersededBy}`)
|
|
794
|
+
})
|
|
795
|
+
};
|
|
796
|
+
/**
|
|
797
|
+
* Run a writing command. Locally, the operation runs on the hub's files under the write lock. In remote mode it is
|
|
798
|
+
* applied here and put in the hub's outbox, and the command waits for the sync server's answer, which may give a
|
|
799
|
+
* new entry another id; without an answer in time it ends with the write still in the outbox.
|
|
800
|
+
*/
|
|
801
|
+
function runWrite(ctx, spec) {
|
|
802
|
+
spec.check?.(ctx);
|
|
803
|
+
const input = spec.input(ctx);
|
|
804
|
+
if (!remoteMode(ctx.root)) return spec.print(ctx, applyOperation(nodeStorage(ctx.root), spec.op, input, { now: nowMinute() }).value);
|
|
805
|
+
return hubWriter(ctx.root, process.env, { confirmMs: CONFIRM_MS })(spec.op, input).then((written) => {
|
|
806
|
+
const code = spec.print(ctx, written.value);
|
|
807
|
+
for (const line of writtenLines(written)) (line.startsWith("!") ? console.error : console.log)(line);
|
|
808
|
+
return code;
|
|
809
|
+
});
|
|
810
|
+
}
|
|
63
811
|
const RUNS = {
|
|
64
812
|
init: ({ root, args }) => done(`created ${relative(root, initHub(root, args[0]))}`),
|
|
65
813
|
status: ({ root, opts }) => printStatus(loadHub(root), opts.all ?? false),
|
|
@@ -94,59 +842,12 @@ const RUNS = {
|
|
|
94
842
|
});
|
|
95
843
|
return 0;
|
|
96
844
|
},
|
|
97
|
-
"repo add": ({ root, args }) => done(`registered ${addRepo(root, args[0], args[1])} ${args[1]}`),
|
|
98
|
-
"hub add": ({ root, args }) => done(`registered hub ${addHub(root, args[0], args[1])} ${args[1]}`),
|
|
99
|
-
"session start": ({ root, opts }) => {
|
|
100
|
-
const session = startSession(root, {
|
|
101
|
-
id: opts.id,
|
|
102
|
-
agent: opts.agent ?? "manual",
|
|
103
|
-
cwd: process.cwd(),
|
|
104
|
-
delegatedFrom: opts["delegated-from"],
|
|
105
|
-
handoffFrom: opts["handoff-from"]
|
|
106
|
-
});
|
|
107
|
-
return done(`started ${session.id}${session.actions.length ? ` with ${session.actions.length} carried actions` : ""}`);
|
|
108
|
-
},
|
|
109
|
-
"session handoff": ({ root, args, opts }) => {
|
|
110
|
-
const session = handoffSession(root, sessionOf(opts), args[0]);
|
|
111
|
-
return done(`${session.id} took over ${session.actions.filter((a) => a.status === "todo" || a.status === "doing").length} actions from ${args[0]}`);
|
|
112
|
-
},
|
|
113
845
|
"session show": ({ root, args, opts }) => {
|
|
114
846
|
const id = sessionOf(opts, args[0]);
|
|
115
847
|
const session = readSession(root, id);
|
|
116
848
|
if (!session) throw new WriteError(`session ${id} not found; start one with "mainflow session start"`);
|
|
117
849
|
return done(sessionBriefing(root, session, { cli: CLI }));
|
|
118
850
|
},
|
|
119
|
-
"session set": ({ root, args, opts }) => done(`updated ${args[0]} (${setSessionStatus(root, args[0], required(opts, "status", "--status paused")).status})`),
|
|
120
|
-
"session end": ({ root, args, opts }) => done(`ended ${setSessionStatus(root, sessionOf(opts, args[0]), "ended").id}`),
|
|
121
|
-
"session archive": ({ root, args }) => done(`archived ${setSessionArchived(root, args[0], true).id}`),
|
|
122
|
-
"session unarchive": ({ root, args }) => done(`unarchived ${setSessionArchived(root, args[0], false).id}`),
|
|
123
|
-
"action add": ({ root, args, opts }) => {
|
|
124
|
-
const ref = addAction(root, sessionOf(opts), {
|
|
125
|
-
do: args[0],
|
|
126
|
-
serves: list(opts.serves),
|
|
127
|
-
before: opts.before
|
|
128
|
-
});
|
|
129
|
-
done(`created ${ref}`);
|
|
130
|
-
return remindDuplicates(root, ref);
|
|
131
|
-
},
|
|
132
|
-
"action set": ({ root, args, opts }) => {
|
|
133
|
-
const ref = actionRef(opts, args[0]);
|
|
134
|
-
const blockedBy = opts.reason ? {
|
|
135
|
-
reason: opts.reason,
|
|
136
|
-
...opts["waiting-for"] ? { waitingFor: opts["waiting-for"] } : {},
|
|
137
|
-
...opts.until ? { until: opts.until } : {}
|
|
138
|
-
} : void 0;
|
|
139
|
-
if (!blockedBy && (opts["waiting-for"] || opts.until)) throw new WriteError("--waiting-for and --until need --reason, e.g. --status blocked --reason \"等 schema 定案\"");
|
|
140
|
-
done(`updated ${ref} (${updateAction(root, ref, {
|
|
141
|
-
status: opts.status,
|
|
142
|
-
do: opts.text,
|
|
143
|
-
serves: opts.serves === void 0 ? void 0 : list(opts.serves),
|
|
144
|
-
before: opts.before,
|
|
145
|
-
note: opts.note,
|
|
146
|
-
blockedBy
|
|
147
|
-
}).status})`);
|
|
148
|
-
return opts.status === "doing" ? remindDuplicates(root, ref) : 0;
|
|
149
|
-
},
|
|
150
851
|
wait: ({ root, args, opts }) => waitUntil(root, args[0], opts),
|
|
151
852
|
hook: ({ args }) => {
|
|
152
853
|
let input = "";
|
|
@@ -163,100 +864,17 @@ const RUNS = {
|
|
|
163
864
|
if (fallback) console.log(fallback);
|
|
164
865
|
return 0;
|
|
165
866
|
}
|
|
166
|
-
|
|
867
|
+
return runHook(args[0], args[1], {
|
|
167
868
|
payload,
|
|
168
869
|
envFile: process.env.CLAUDE_ENV_FILE,
|
|
169
870
|
projectDir: process.env.CLAUDE_PROJECT_DIR,
|
|
170
871
|
cli: CLI,
|
|
171
872
|
startUi: startUiInBackground
|
|
873
|
+
}).then(({ stdout, error }) => {
|
|
874
|
+
if (error) console.error(error);
|
|
875
|
+
if (stdout) console.log(stdout);
|
|
876
|
+
return 0;
|
|
172
877
|
});
|
|
173
|
-
if (error) console.error(error);
|
|
174
|
-
if (stdout) console.log(stdout);
|
|
175
|
-
return 0;
|
|
176
|
-
},
|
|
177
|
-
"new mainline": ({ root, args, opts }) => {
|
|
178
|
-
const { id, dir } = createMainline(root, {
|
|
179
|
-
slug: args[0],
|
|
180
|
-
title: required(opts, "title", "--title \"重建 mainline 任務管理\"")
|
|
181
|
-
});
|
|
182
|
-
return done(`created ${id} ${relative(root, join(dir, MAINLINE_FILE))}`);
|
|
183
|
-
},
|
|
184
|
-
"new task": ({ root, args, opts }) => {
|
|
185
|
-
const { ref, dir } = createTask(root, args[0], {
|
|
186
|
-
slug: args[1],
|
|
187
|
-
title: required(opts, "title", "--title \"定義資料格式 schema\""),
|
|
188
|
-
kind: opts.kind,
|
|
189
|
-
parent: opts.parent,
|
|
190
|
-
repo: opts.repo
|
|
191
|
-
});
|
|
192
|
-
return done(`created ${ref} ${relative(root, join(dir, TASK_FILE))}`);
|
|
193
|
-
},
|
|
194
|
-
"new decision": ({ root, args, opts }) => {
|
|
195
|
-
const { ref, file } = createDecision(root, args[0], {
|
|
196
|
-
slug: args[1],
|
|
197
|
-
title: required(opts, "title", "--title \"以文字檔為準\""),
|
|
198
|
-
status: opts.status,
|
|
199
|
-
by: opts.by,
|
|
200
|
-
basis: opts.basis,
|
|
201
|
-
affects: opts.affects ? list(opts.affects) : void 0
|
|
202
|
-
});
|
|
203
|
-
return done(`created ${ref} ${relative(root, file)}`);
|
|
204
|
-
},
|
|
205
|
-
"new issue": ({ root, args, opts }) => {
|
|
206
|
-
const { id, file } = createIssue(root, {
|
|
207
|
-
slug: args[0],
|
|
208
|
-
title: required(opts, "title", "--title \"重試不穩定的 VM\""),
|
|
209
|
-
origin: opts.origin,
|
|
210
|
-
labels: opts.labels ? list(opts.labels) : void 0
|
|
211
|
-
});
|
|
212
|
-
return done(`created ${id} ${relative(root, file)}`);
|
|
213
|
-
},
|
|
214
|
-
"new finding": ({ root, args, opts }) => {
|
|
215
|
-
const { id, file } = createFinding(root, {
|
|
216
|
-
slug: args[0],
|
|
217
|
-
title: required(opts, "title", "--title \"先 await 再寫入 state\""),
|
|
218
|
-
origin: opts.origin,
|
|
219
|
-
labels: opts.labels ? list(opts.labels) : void 0
|
|
220
|
-
});
|
|
221
|
-
return done(`created ${id} ${relative(root, file)}`);
|
|
222
|
-
},
|
|
223
|
-
"mainline set": ({ root, args, opts }) => {
|
|
224
|
-
const m = setMainlineStatus(root, args[0], required(opts, "status", "--status active"));
|
|
225
|
-
return done(`updated ${m.id} (${m.status})`);
|
|
226
|
-
},
|
|
227
|
-
"mainline archive": ({ root, args }) => done(`archived ${setArchived(root, args[0], true).id}`),
|
|
228
|
-
"mainline unarchive": ({ root, args }) => done(`unarchived ${setArchived(root, args[0], false).id}`),
|
|
229
|
-
"criterion check": ({ root, args, opts }) => {
|
|
230
|
-
setCriterion(root, args[0], true, opts.evidence);
|
|
231
|
-
return done(`checked ${args[0]}`);
|
|
232
|
-
},
|
|
233
|
-
"criterion uncheck": ({ root, args }) => {
|
|
234
|
-
setCriterion(root, args[0], false);
|
|
235
|
-
return done(`unchecked ${args[0]}`);
|
|
236
|
-
},
|
|
237
|
-
"criterion set": ({ root, args, opts }) => {
|
|
238
|
-
setCriterionText(root, args[0], {
|
|
239
|
-
text: opts.text,
|
|
240
|
-
doneWhen: opts["done-when"]
|
|
241
|
-
});
|
|
242
|
-
return done(`updated ${args[0]}`);
|
|
243
|
-
},
|
|
244
|
-
"task set": ({ root, args, opts }) => {
|
|
245
|
-
const blockedBy = opts.reason ? {
|
|
246
|
-
reason: opts.reason,
|
|
247
|
-
...opts["waiting-for"] ? { waitingFor: opts["waiting-for"] } : {},
|
|
248
|
-
...opts.signal ? { signal: opts.signal } : {}
|
|
249
|
-
} : void 0;
|
|
250
|
-
const t = updateTask(root, args[0], {
|
|
251
|
-
status: opts.status,
|
|
252
|
-
repo: opts.repo,
|
|
253
|
-
blockedBy,
|
|
254
|
-
branch: opts.branch,
|
|
255
|
-
worktree: opts.worktree,
|
|
256
|
-
base: opts.base,
|
|
257
|
-
merge: opts.merge
|
|
258
|
-
});
|
|
259
|
-
return done(`updated ${args[0]} (${t.status})`);
|
|
260
878
|
},
|
|
261
879
|
"task show": ({ root, args }) => {
|
|
262
880
|
const { mainline, task } = findTask(loadHub(root), args[0]);
|
|
@@ -270,79 +888,39 @@ const RUNS = {
|
|
|
270
888
|
for (const link of task.links ?? []) console.log(` ${link.rel} → ${link.to}${link.note ? ` ${link.note}` : ""}`);
|
|
271
889
|
return done(`${task.checklist.length} checklist items`);
|
|
272
890
|
},
|
|
273
|
-
"item check": ({ root, args, opts }) => {
|
|
274
|
-
setChecklistItem(root, args[0], true, opts.evidence);
|
|
275
|
-
return done(`checked ${args[0]}`);
|
|
276
|
-
},
|
|
277
|
-
"item uncheck": ({ root, args }) => {
|
|
278
|
-
setChecklistItem(root, args[0], false);
|
|
279
|
-
return done(`unchecked ${args[0]}`);
|
|
280
|
-
},
|
|
281
|
-
"decision accept": ({ root, args, opts }) => done(`accepted ${args[0]} (${acceptDecision(root, args[0], {
|
|
282
|
-
by: opts.by,
|
|
283
|
-
basis: opts.basis
|
|
284
|
-
}).decidedAt})`),
|
|
285
|
-
"item drop": ({ root, args, opts }) => {
|
|
286
|
-
dropChecklistItem(root, args[0], {
|
|
287
|
-
supersededBy: opts.by,
|
|
288
|
-
note: opts.note
|
|
289
|
-
});
|
|
290
|
-
return done(`dropped ${args[0]}`);
|
|
291
|
-
},
|
|
292
|
-
"item undrop": ({ root, args }) => {
|
|
293
|
-
undropChecklistItem(root, args[0]);
|
|
294
|
-
return done(`undropped ${args[0]}`);
|
|
295
|
-
},
|
|
296
|
-
"issue plan": ({ root, args }) => done(`planned ${planIssue(root, args[0], args[1]).id} in ${args[1]}`),
|
|
297
|
-
"issue close": ({ root, args, opts }) => done(`closed ${closeIssue(root, args[0], opts.by ?? "").id}`),
|
|
298
|
-
"issue reopen": ({ root, args }) => done(`reopened ${reopenIssue(root, args[0]).id}`),
|
|
299
|
-
"issue archive": ({ root, args }) => done(`archived ${setIssueArchived(root, args[0], true).id}`),
|
|
300
|
-
"issue unarchive": ({ root, args }) => done(`unarchived ${setIssueArchived(root, args[0], false).id}`),
|
|
301
891
|
"issue list": ({ root, opts }) => {
|
|
302
892
|
const shown = loadHub(root).issues.filter((i) => opts.all || i.status !== "closed");
|
|
303
893
|
for (const i of shown) console.log(` ${i.id} ${i.status.padEnd(7)} ${i.title}${i.plannedIn ? ` → ${i.plannedIn}` : ""}${i.origin ? ` (from ${i.origin})` : ""}`);
|
|
304
894
|
return done(`${shown.length} issues`);
|
|
305
895
|
},
|
|
306
|
-
"finding set": ({ root, args, opts }) => done(`updated ${setFindingStatus(root, args[0], required(opts, "status", "--status obsolete")).id}`),
|
|
307
|
-
"finding archive": ({ root, args }) => done(`archived ${setFindingArchived(root, args[0], true).id}`),
|
|
308
|
-
"finding unarchive": ({ root, args }) => done(`unarchived ${setFindingArchived(root, args[0], false).id}`),
|
|
309
896
|
"finding list": ({ root, opts }) => {
|
|
310
897
|
const shown = loadHub(root).findings.filter((f) => opts.all || f.status === "valid");
|
|
311
898
|
for (const f of shown) console.log(` ${f.id} ${f.created} ${f.title}${f.labels?.length ? ` [${f.labels.join(", ")}]` : ""}`);
|
|
312
899
|
return done(`${shown.length} findings`);
|
|
313
900
|
},
|
|
314
|
-
"ops add": ({ root, opts }) => {
|
|
315
|
-
const entry = appendOps(root, {
|
|
316
|
-
target: required(opts, "target", "--target github.com/owner/repo"),
|
|
317
|
-
operation: required(opts, "op", "--op \"push tag v1.2.0\""),
|
|
318
|
-
result: required(opts, "result", "--result success"),
|
|
319
|
-
...opts.ref ? { ref: opts.ref } : {}
|
|
320
|
-
});
|
|
321
|
-
return done(`logged ${entry.at} ${entry.operation}`);
|
|
322
|
-
},
|
|
323
901
|
"ops list": ({ root, opts }) => {
|
|
324
902
|
const covers = (ref) => !opts.ref || ref !== void 0 && (ref === opts.ref || ref.startsWith(`${opts.ref}/`) || ref.startsWith(`${opts.ref}#`));
|
|
325
903
|
const shown = loadHub(root).ops.filter((o) => (!opts.month || o.at.startsWith(opts.month)) && covers(o.ref));
|
|
326
904
|
for (const o of shown) console.log(` ${o.at} ${o.target} ${o.operation} ${o.result}${o.ref ? ` ${o.ref}` : ""}`);
|
|
327
905
|
return done(`${shown.length} operations`);
|
|
328
906
|
},
|
|
329
|
-
"decision supersede": ({ root, args, opts }) => {
|
|
330
|
-
const d = supersedeDecision(root, args[0], required(opts, "by", "--by d002"));
|
|
331
|
-
return done(`superseded ${args[0]} by ${d.supersededBy}`);
|
|
332
|
-
},
|
|
333
907
|
retro: ({ root, args, opts }) => {
|
|
334
|
-
const
|
|
335
|
-
|
|
908
|
+
const print = (report) => done(opts.json ? JSON.stringify(report, null, 2) : retroText(report));
|
|
909
|
+
const receiver = receiverFromEnv(process.env);
|
|
910
|
+
return receiver ? retroReportFromReceiver(root, args[0], receiver).then(print) : print(retroReport(root, args[0]));
|
|
336
911
|
},
|
|
337
912
|
"decision list": ({ root, args }) => {
|
|
338
913
|
const mainline = findMainline(loadHub(root), args[0]);
|
|
339
914
|
for (const d of mainline.decisions) console.log(` ${mainline.id}/${d.id} ${d.status.padEnd(10)} ${d.title}`);
|
|
340
915
|
return done(`${mainline.decisions.length} decisions`);
|
|
341
|
-
}
|
|
916
|
+
},
|
|
917
|
+
sync: ({ root }) => syncRun(root),
|
|
918
|
+
"sync flush": ({ root, opts }) => syncFlush(root, opts.hook ?? false),
|
|
919
|
+
"sync status": ({ root }) => syncStatus(root)
|
|
342
920
|
};
|
|
343
921
|
const COMMANDS = Object.fromEntries(Object.keys(COMMAND_SPECS).map((name) => [name, {
|
|
344
922
|
...COMMAND_SPECS[name],
|
|
345
|
-
run: RUNS[name]
|
|
923
|
+
run: name in WRITES ? (ctx) => runWrite(ctx, WRITES[name]) : RUNS[name]
|
|
346
924
|
}]));
|
|
347
925
|
const USAGE = [
|
|
348
926
|
"Usage: mainflow <command>",
|
|
@@ -354,11 +932,28 @@ const USAGE = [
|
|
|
354
932
|
" --version Print the version",
|
|
355
933
|
" --help Print this message"
|
|
356
934
|
].join("\n");
|
|
935
|
+
/**
|
|
936
|
+
* `argv` with `--` put before the last argument of a command whose last argument is literal text, unless the
|
|
937
|
+
* caller already put it there, so the parser takes text such as "- 完成" as an argument and not as options.
|
|
938
|
+
*/
|
|
939
|
+
function literalLast(argv) {
|
|
940
|
+
for (const [name, spec] of Object.entries(COMMAND_SPECS)) {
|
|
941
|
+
if (!spec.literalLast) continue;
|
|
942
|
+
const words = name.split(" ");
|
|
943
|
+
const at = words.length + spec.args - 1;
|
|
944
|
+
if (words.every((word, i) => argv[i] === word) && argv.length > at && !argv.slice(0, at + 1).includes("--")) return [
|
|
945
|
+
...argv.slice(0, at),
|
|
946
|
+
"--",
|
|
947
|
+
...argv.slice(at)
|
|
948
|
+
];
|
|
949
|
+
}
|
|
950
|
+
return argv;
|
|
951
|
+
}
|
|
357
952
|
function run(argv, cwd) {
|
|
358
953
|
let parsed;
|
|
359
954
|
try {
|
|
360
955
|
parsed = parseArgs({
|
|
361
|
-
args: argv,
|
|
956
|
+
args: literalLast(argv),
|
|
362
957
|
allowPositionals: true,
|
|
363
958
|
options: OPTIONS
|
|
364
959
|
});
|
|
@@ -384,20 +979,25 @@ function run(argv, cwd) {
|
|
|
384
979
|
root = found;
|
|
385
980
|
}
|
|
386
981
|
try {
|
|
387
|
-
|
|
982
|
+
const exit = command.run({
|
|
388
983
|
root,
|
|
389
984
|
args,
|
|
390
985
|
opts
|
|
391
986
|
});
|
|
987
|
+
return typeof exit === "number" ? exit : exit.catch(reportError);
|
|
392
988
|
} catch (error) {
|
|
393
|
-
|
|
394
|
-
printProblems(error.problems);
|
|
395
|
-
return fail(error.message);
|
|
396
|
-
}
|
|
397
|
-
if (error instanceof WriteError) return fail(`✗ ${error.message}`);
|
|
398
|
-
throw error;
|
|
989
|
+
return reportError(error);
|
|
399
990
|
}
|
|
400
991
|
}
|
|
992
|
+
/** A refused write or an unreadable hub ends the command with its message; anything else is a bug and is thrown. */
|
|
993
|
+
function reportError(error) {
|
|
994
|
+
if (error instanceof HubError) {
|
|
995
|
+
printProblems(error.problems);
|
|
996
|
+
return fail(error.message);
|
|
997
|
+
}
|
|
998
|
+
if (error instanceof WriteError) return fail(`✗ ${error.message}`);
|
|
999
|
+
throw error;
|
|
1000
|
+
}
|
|
401
1001
|
function done(line) {
|
|
402
1002
|
console.log(line);
|
|
403
1003
|
return 0;
|
|
@@ -546,6 +1146,10 @@ function printProblems(problems, mark = "✗") {
|
|
|
546
1146
|
for (const problem of problems) console.error(`${mark} ${problem.file}${problem.field ? ` › ${problem.field}` : ""}: ${problem.message}`);
|
|
547
1147
|
}
|
|
548
1148
|
const entry = process.argv[1];
|
|
549
|
-
if (entry && realpathSync(entry) === realpathSync(fileURLToPath(import.meta.url)))
|
|
1149
|
+
if (entry && realpathSync(entry) === realpathSync(fileURLToPath(import.meta.url))) {
|
|
1150
|
+
const exit = run(process.argv.slice(2), process.cwd());
|
|
1151
|
+
if (typeof exit === "number") process.exitCode = exit;
|
|
1152
|
+
else exit.then((code) => process.exitCode = code);
|
|
1153
|
+
}
|
|
550
1154
|
//#endregion
|
|
551
1155
|
export { run };
|