mainflow 0.26.0 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +64 -5
- package/dist/{dist-DlmecRLf.js → dist-BbamrIPW.js} +12 -2
- package/dist/facts.d.ts +22 -6
- package/dist/facts.js +24 -7
- package/dist/grant-commands.d.ts +1 -1
- package/dist/grant-commands.js +1 -1
- package/dist/hooks.js +52 -6
- package/dist/{host-Dr0bCHd8.js → host-CeN3dvXg.js} +712 -44
- package/dist/host.js +1 -1
- package/dist/{index-D4IIwDGE.d.ts → index-bIFT1ORw.d.ts} +3 -1
- package/dist/{launch-lnD-Z3hb.js → launch-BKKoCyTj.js} +2 -2
- package/dist/launch.js +1 -1
- package/dist/{ui-Cz8suWal.js → ui-D_9x6XNs.js} +2 -2
- package/dist/web/assets/{index-xrrh30zr.js → index-ypKt0hMg.js} +2 -2
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,14 +1,64 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { $ as
|
|
3
|
-
import { B as
|
|
2
|
+
import { $ as appendOps, A as untilState, B as setSessionArchived, C as setCriterion, D as supersedeDecision, H as setSessionStatus, I as handoffSession, K as updateAction, L as loadSessions, M as sessionBriefing, O as undropChecklistItem, P as addAction, R as readSession, S as setChecklistItem, T as setMainlineStatus, W as startSession, _ as createMainline, _t as WriteError, at as setFindingArchived, b as initHub, ct as HubError, et as closeIssue, g as createDecision, h as addRepo, ht as TASK_FILE, it as reopenIssue, j as duplicateClaimLines, k as updateTask, l as migrateHub, lt as loadHub, m as addHub, mt as MAINLINE_FILE, nt as createIssue, ot as setFindingStatus, p as acceptDecision, rt as planIssue, st as setIssueArchived, tt as createFinding, u as retroReport, v as createTask, w as setCriterionText, x as setArchived, xt as findHubRoot, y as dropChecklistItem } from "./host-CeN3dvXg.js";
|
|
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
5
|
import { COMMAND_SPECS, OPTIONS } from "./facts.js";
|
|
6
6
|
import { hookFallback, parsePayload, runHook } from "./hooks.js";
|
|
7
|
-
import { l as CLI_VERSION, r as launchUi, s as startUiInBackground } from "./launch-
|
|
7
|
+
import { l as CLI_VERSION, r as launchUi, s as startUiInBackground } from "./launch-BKKoCyTj.js";
|
|
8
8
|
import { readFileSync, realpathSync } from "node:fs";
|
|
9
|
-
import { join, relative } from "node:path";
|
|
9
|
+
import { basename, join, relative } from "node:path";
|
|
10
10
|
import { fileURLToPath } from "node:url";
|
|
11
11
|
import { parseArgs } from "node:util";
|
|
12
|
+
//#region src/retro.ts
|
|
13
|
+
const int = (n) => n.toLocaleString("en-US");
|
|
14
|
+
const usd = (n) => `$${n.toFixed(2)}`;
|
|
15
|
+
const pct = (n) => n === null ? "–" : `${(n * 100).toFixed(1)}%`;
|
|
16
|
+
/** A transcript timestamp in local time, to the minute. */
|
|
17
|
+
const local = (iso) => {
|
|
18
|
+
const d = new Date(iso);
|
|
19
|
+
return Number.isNaN(d.getTime()) ? iso : `${d.toLocaleDateString("sv-SE")} ${d.toTimeString().slice(0, 5)}`;
|
|
20
|
+
};
|
|
21
|
+
const tokens = (u) => `輸入 ${int(u.input)}、快取寫 5m ${int(u.cacheWrite5m)}、快取寫 1h ${int(u.cacheWrite1h)}、快取讀 ${int(u.cacheRead)}、輸出 ${int(u.output)}`;
|
|
22
|
+
const costOf = (t) => `${usd(t.cost)}${t.unknownPriceModels.length ? `(另有未知單價:${t.unknownPriceModels.join("、")})` : ""}`;
|
|
23
|
+
const byCount = (r) => Object.entries(r).sort((a, b) => b[1] - a[1]).map(([k, v]) => `${k} ${v}`).join("、");
|
|
24
|
+
const COMMAND_SHOWN = 100;
|
|
25
|
+
function retroText(report) {
|
|
26
|
+
const { mainline, cost, precision } = report;
|
|
27
|
+
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
|
+
"== 費用 ==",
|
|
35
|
+
`估算合計:${usd(cost.total)}${cost.unknownPriceModels.length ? `,不含未知單價的 ${cost.unknownPriceModels.join("、")}` : ""}`,
|
|
36
|
+
`快取命中率:${pct(cost.cacheHitRate)}(快取讀 ÷ 全部上下文)`,
|
|
37
|
+
"各模型:",
|
|
38
|
+
...cost.models.map((m) => `- ${m.model}:呼叫 ${int(m.calls)},${tokens(m.usage)},${m.cost === null ? "未知單價" : usd(m.cost)}`),
|
|
39
|
+
"會話:",
|
|
40
|
+
...cost.sessions.map((s) => s.missing ? `- ${s.session}:沒有讀到對話紀錄(${s.missing})` : `- ${s.session}:開場上下文 ${s.openingContext === null ? "–" : int(s.openingContext)} tokens,呼叫 ${int(s.calls)},快取命中率 ${pct(s.cacheHitRate)},${costOf(s)}`),
|
|
41
|
+
"子代理:",
|
|
42
|
+
...cost.subagents.length === 0 ? ["- 無"] : cost.subagents.map((a) => {
|
|
43
|
+
const who = [a.agentType, a.description].filter(Boolean).join(":");
|
|
44
|
+
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
|
+
}),
|
|
46
|
+
"hook 注入:",
|
|
47
|
+
...cost.hooks.length === 0 ? ["- 無"] : cost.hooks.map((h) => `- ${h.event}:${int(h.injections)} 次,${int(h.chars)} 字${h.blocked ? `,擋下 ${h.blocked} 次` : ""}`),
|
|
48
|
+
"",
|
|
49
|
+
"== 精準度候選 ==",
|
|
50
|
+
`工具錯誤:${precision.toolErrors.total}${precision.toolErrors.total ? `(${byCount(precision.toolErrors.byTool)})` : ""}`,
|
|
51
|
+
`同一個 Bash 指令重跑:${precision.bashReruns.total} 次`,
|
|
52
|
+
...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
|
+
`被 hook 擋下:${precision.blocked.total}${precision.blocked.total ? `(${byCount(precision.blocked.byEvent)})` : ""}`,
|
|
55
|
+
`超過 5 分鐘的間隔:${precision.gaps.length}`,
|
|
56
|
+
...precision.gaps.map((g) => `- ${local(g.from)} → ${local(g.to)},${g.minutes} 分鐘,${g.waitingForUser ? "等使用者" : "工具或子代理執行中"}(${g.session})`),
|
|
57
|
+
`主脈絡:解決條件 ${mainline.criteria} 條,任務 ${mainline.tasks} 個,完成檢查未過 ${precision.failedChecks.length} 行`,
|
|
58
|
+
...precision.failedChecks.map((f) => `- ${f.task}:${f.line}`)
|
|
59
|
+
].join("\n");
|
|
60
|
+
}
|
|
61
|
+
//#endregion
|
|
12
62
|
//#region src/cli.ts
|
|
13
63
|
const RUNS = {
|
|
14
64
|
init: ({ root, args }) => done(`created ${relative(root, initHub(root, args[0]))}`),
|
|
@@ -156,7 +206,8 @@ const RUNS = {
|
|
|
156
206
|
const { id, file } = createIssue(root, {
|
|
157
207
|
slug: args[0],
|
|
158
208
|
title: required(opts, "title", "--title \"重試不穩定的 VM\""),
|
|
159
|
-
origin: opts.origin
|
|
209
|
+
origin: opts.origin,
|
|
210
|
+
labels: opts.labels ? list(opts.labels) : void 0
|
|
160
211
|
});
|
|
161
212
|
return done(`created ${id} ${relative(root, file)}`);
|
|
162
213
|
},
|
|
@@ -238,6 +289,10 @@ const RUNS = {
|
|
|
238
289
|
});
|
|
239
290
|
return done(`dropped ${args[0]}`);
|
|
240
291
|
},
|
|
292
|
+
"item undrop": ({ root, args }) => {
|
|
293
|
+
undropChecklistItem(root, args[0]);
|
|
294
|
+
return done(`undropped ${args[0]}`);
|
|
295
|
+
},
|
|
241
296
|
"issue plan": ({ root, args }) => done(`planned ${planIssue(root, args[0], args[1]).id} in ${args[1]}`),
|
|
242
297
|
"issue close": ({ root, args, opts }) => done(`closed ${closeIssue(root, args[0], opts.by ?? "").id}`),
|
|
243
298
|
"issue reopen": ({ root, args }) => done(`reopened ${reopenIssue(root, args[0]).id}`),
|
|
@@ -275,6 +330,10 @@ const RUNS = {
|
|
|
275
330
|
const d = supersedeDecision(root, args[0], required(opts, "by", "--by d002"));
|
|
276
331
|
return done(`superseded ${args[0]} by ${d.supersededBy}`);
|
|
277
332
|
},
|
|
333
|
+
retro: ({ root, args, opts }) => {
|
|
334
|
+
const report = retroReport(root, args[0]);
|
|
335
|
+
return done(opts.json ? JSON.stringify(report, null, 2) : retroText(report));
|
|
336
|
+
},
|
|
278
337
|
"decision list": ({ root, args }) => {
|
|
279
338
|
const mainline = findMainline(loadHub(root), args[0]);
|
|
280
339
|
for (const d of mainline.decisions) console.log(` ${mainline.id}/${d.id} ${d.status.padEnd(10)} ${d.title}`);
|
|
@@ -358,6 +358,8 @@ const ID_PATTERNS = {
|
|
|
358
358
|
grant: /^g\d{3}$/
|
|
359
359
|
};
|
|
360
360
|
const CHECKLIST_REF = /^m\d{3}\/t\d{3}#c\d{3}$/;
|
|
361
|
+
/** What an action may serve: a checklist item, or a mainline's criterion while the mainline has no task for it yet. */
|
|
362
|
+
const SERVES_REF = /^m\d{3}(?:\/t\d{3}#c\d{3}|#r\d{3})$/;
|
|
361
363
|
const SLUG = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
362
364
|
const DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
363
365
|
const DATETIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/;
|
|
@@ -773,6 +775,11 @@ const taskSchema = z.object({
|
|
|
773
775
|
/** Tasks that must be done before this one starts */
|
|
774
776
|
dependsOn: z.array(id("task")).optional(),
|
|
775
777
|
repo: z.string().min(1).nullable().optional(),
|
|
778
|
+
/**
|
|
779
|
+
* Paths or globs, relative to the root of the task's repo, that the task expects to change. Check warns when
|
|
780
|
+
* open tasks of two open mainlines touch the same paths in one repo with no depends-on between them.
|
|
781
|
+
*/
|
|
782
|
+
touches: z.array(text.refine((p) => !p.startsWith("/"), "a touches entry is relative to the root of the task's repo")).optional(),
|
|
776
783
|
startedAt: date.optional(),
|
|
777
784
|
checklist: z.array(checklistItemSchema),
|
|
778
785
|
/**
|
|
@@ -885,7 +892,8 @@ const actionSchema = z.object({
|
|
|
885
892
|
id: id("action"),
|
|
886
893
|
do: z.string().min(1),
|
|
887
894
|
status: z.enum(WORK_STATUS),
|
|
888
|
-
|
|
895
|
+
/** Checklist items (m001/t002#c003) or criteria (m001#r001) the action pushes forward */
|
|
896
|
+
serves: z.array(z.string().regex(SERVES_REF)),
|
|
889
897
|
note: text.optional(),
|
|
890
898
|
/** Required while blocked, absent otherwise */
|
|
891
899
|
blockedBy: actionBlockSchema.optional(),
|
|
@@ -940,6 +948,8 @@ const sessionSchema = z.object({
|
|
|
940
948
|
description: text.optional(),
|
|
941
949
|
depth: z.number().int().nonnegative().optional(),
|
|
942
950
|
summary: text.optional(),
|
|
951
|
+
/** Digest of the grants, waits and queue warnings the last turn reminder listed in full, so an unchanged set is not listed again */
|
|
952
|
+
reminderDigest: text.optional(),
|
|
943
953
|
/** Hidden from the default views; independent of status, an ended session stays visible until archived */
|
|
944
954
|
archivedAt: date.optional()
|
|
945
955
|
});
|
|
@@ -1010,4 +1020,4 @@ function parseOpsLine(line) {
|
|
|
1010
1020
|
return parsed.success ? parsed.data : null;
|
|
1011
1021
|
}
|
|
1012
1022
|
//#endregion
|
|
1013
|
-
export {
|
|
1023
|
+
export { grantForm as $, WORK_STATUS as A, parseRef as B, SESSION_STATUS as C, TASK_REQUIRED_SECTIONS as D, SURVEY_SECTION as E, issueSchema as F, taskSchema as G, refExample as H, mainlineSchema as I, GRANT_ACTIONS as J, waitingForAction as K, opsEntrySchema as L, decisionSchema as M, findingSchema as N, TASK_SECTIONS as O, formatOpsEntry as P, grantCovers as Q, parseActionRef as R, SESSION_ID as S, SURVEY_LINES as T, refMainline as U, parseUntil as V, sessionSchema as W, GRANT_TARGET_FORMS as X, GRANT_FORMS as Y, describeRequest as Z, MERGE_STATES as _, DECISION_SECTIONS as a, RESERVED_HUB_NAMES as b, FINDING_SECTIONS as c, ID_PATTERNS as d, grantRepo as et, ISSUE_SECTIONS as f, MAINLINE_STATUS as g, MAINLINE_SECTIONS as h, DECISION_BASES as i, configSchema as j, UNTIL_FORMS as k, FINDING_STATUS as l, LINK_RELS as m, CLOSED_STATUS as n, DECISION_STATUS as o, ISSUE_STATUS as p, waitingForRef as q, DECIDERS as r, DEFAULT_CATEGORIES as s, CHECKLIST_REF as t, parseGrantTarget as tt, FORMAT_VERSION as u, PRIORITIES as v, SLUG as w, SERVES_REF as x, REF_EXAMPLES as y, parseOpsLine as z };
|
package/dist/facts.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as SLUG, i as SERVES_REF, n as FORMAT_VERSION, o as SurveyLine, r as ID_PATTERNS, s as Until, t as CHECKLIST_REF } from "./index-bIFT1ORw.js";
|
|
2
2
|
//#region src/facts.d.ts
|
|
3
3
|
export interface CommandSpec {
|
|
4
4
|
usage: string;
|
|
@@ -97,8 +97,8 @@ export declare const COMMAND_SPECS: {
|
|
|
97
97
|
readonly needsHub: true;
|
|
98
98
|
};
|
|
99
99
|
readonly 'action add': {
|
|
100
|
-
readonly usage: 'action add <text> --serves <m/t#c
|
|
101
|
-
readonly summary: 'Add an action to the session queue;
|
|
100
|
+
readonly usage: 'action add <text> --serves <m/t#c|m#r>[,...] [--before <a>] [--session <session>]';
|
|
101
|
+
readonly summary: 'Add an action to the session queue; --serves names checklist items, or criteria while the mainline has no task for them. Prints a reminder when another session active in the last 15 minutes is doing what it serves';
|
|
102
102
|
readonly args: 1;
|
|
103
103
|
readonly needsHub: true;
|
|
104
104
|
};
|
|
@@ -140,7 +140,7 @@ export declare const COMMAND_SPECS: {
|
|
|
140
140
|
readonly needsHub: true;
|
|
141
141
|
};
|
|
142
142
|
readonly 'new issue': {
|
|
143
|
-
readonly usage: 'new issue <slug> --title <title> [--origin <ref>]';
|
|
143
|
+
readonly usage: 'new issue <slug> --title <title> [--origin <ref>] [--labels a,b]';
|
|
144
144
|
readonly summary: 'Create an issue file for a follow-up that must outlive its mainline';
|
|
145
145
|
readonly args: 1;
|
|
146
146
|
readonly needsHub: true;
|
|
@@ -223,6 +223,12 @@ export declare const COMMAND_SPECS: {
|
|
|
223
223
|
readonly args: 1;
|
|
224
224
|
readonly needsHub: true;
|
|
225
225
|
};
|
|
226
|
+
readonly 'item undrop': {
|
|
227
|
+
readonly usage: 'item undrop <m/t#c>';
|
|
228
|
+
readonly summary: 'Bring a dropped item back as an unchecked item, clearing dropped and supersededBy';
|
|
229
|
+
readonly args: 1;
|
|
230
|
+
readonly needsHub: true;
|
|
231
|
+
};
|
|
226
232
|
readonly 'issue plan': {
|
|
227
233
|
readonly usage: 'issue plan <i> <m/t>';
|
|
228
234
|
readonly summary: 'Schedule an issue into the task that will carry it out';
|
|
@@ -307,6 +313,12 @@ export declare const COMMAND_SPECS: {
|
|
|
307
313
|
readonly args: 1;
|
|
308
314
|
readonly needsHub: true;
|
|
309
315
|
};
|
|
316
|
+
readonly retro: {
|
|
317
|
+
readonly usage: 'retro <m> [--json]';
|
|
318
|
+
readonly summary: 'Read the Claude Code transcripts of the sessions that served a mainline, and their subagents\', into estimated list-price cost (per model, opening context, cache hit rate, hook injections) and candidates for where work went wrong (tool errors, rerun Bash commands, hook injections followed by no tool call, blocks, pauses over 5 minutes, failed completion checks); read-only';
|
|
319
|
+
readonly args: 1;
|
|
320
|
+
readonly needsHub: true;
|
|
321
|
+
};
|
|
310
322
|
};
|
|
311
323
|
export type CommandName = keyof typeof COMMAND_SPECS;
|
|
312
324
|
export declare const OPTIONS: {
|
|
@@ -429,6 +441,10 @@ export declare const OPTIONS: {
|
|
|
429
441
|
readonly type: 'boolean';
|
|
430
442
|
readonly default: false;
|
|
431
443
|
};
|
|
444
|
+
readonly json: {
|
|
445
|
+
readonly type: 'boolean';
|
|
446
|
+
readonly default: false;
|
|
447
|
+
};
|
|
432
448
|
readonly 'dry-run': {
|
|
433
449
|
readonly type: 'boolean';
|
|
434
450
|
readonly default: false;
|
|
@@ -575,7 +591,7 @@ export interface FieldSpec {
|
|
|
575
591
|
/** Every front matter field of each document and list entry; facts.test keeps it equal to the schema. */
|
|
576
592
|
export declare const FIELDS: {
|
|
577
593
|
readonly mainline: readonly [...FieldSpec[], FieldSpec, FieldSpec, FieldSpec, FieldSpec, FieldSpec, FieldSpec];
|
|
578
|
-
readonly task: readonly [...FieldSpec[], FieldSpec, FieldSpec, FieldSpec, FieldSpec, FieldSpec, FieldSpec, FieldSpec, FieldSpec, FieldSpec, FieldSpec, FieldSpec, FieldSpec];
|
|
594
|
+
readonly task: readonly [...FieldSpec[], FieldSpec, FieldSpec, FieldSpec, FieldSpec, FieldSpec, FieldSpec, FieldSpec, FieldSpec, FieldSpec, FieldSpec, FieldSpec, FieldSpec, FieldSpec];
|
|
579
595
|
readonly decision: readonly [...FieldSpec[], FieldSpec, FieldSpec, FieldSpec, FieldSpec, FieldSpec, FieldSpec];
|
|
580
596
|
readonly issue: readonly [...FieldSpec[], FieldSpec, FieldSpec, FieldSpec, FieldSpec];
|
|
581
597
|
readonly finding: readonly [...FieldSpec[], FieldSpec, FieldSpec, FieldSpec];
|
|
@@ -586,4 +602,4 @@ export declare const FIELDS: {
|
|
|
586
602
|
readonly link: readonly [FieldSpec, FieldSpec, FieldSpec];
|
|
587
603
|
};
|
|
588
604
|
//#endregion
|
|
589
|
-
export { CHECKLIST_REF, FORMAT_VERSION, ID_PATTERNS, SLUG };
|
|
605
|
+
export { CHECKLIST_REF, FORMAT_VERSION, ID_PATTERNS, SERVES_REF, SLUG };
|
package/dist/facts.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as
|
|
1
|
+
import { A as WORK_STATUS, C as SESSION_STATUS, D as TASK_REQUIRED_SECTIONS, E as SURVEY_SECTION, J as GRANT_ACTIONS, O as TASK_SECTIONS, T as SURVEY_LINES, X as GRANT_TARGET_FORMS, Y as GRANT_FORMS, _ as MERGE_STATES, a as DECISION_SECTIONS, c as FINDING_SECTIONS, d as ID_PATTERNS, f as ISSUE_SECTIONS, g as MAINLINE_STATUS, h as MAINLINE_SECTIONS, i as DECISION_BASES, k as UNTIL_FORMS, l as FINDING_STATUS, m as LINK_RELS, n as CLOSED_STATUS, o as DECISION_STATUS, p as ISSUE_STATUS, r as DECIDERS, s as DEFAULT_CATEGORIES, t as CHECKLIST_REF, u as FORMAT_VERSION, v as PRIORITIES, w as SLUG, x as SERVES_REF } from "./dist-BbamrIPW.js";
|
|
2
2
|
//#region src/facts.ts
|
|
3
3
|
const COMMAND_SPECS = {
|
|
4
4
|
init: {
|
|
@@ -88,8 +88,8 @@ const COMMAND_SPECS = {
|
|
|
88
88
|
needsHub: true
|
|
89
89
|
},
|
|
90
90
|
"action add": {
|
|
91
|
-
usage: "action add <text> --serves <m/t#c
|
|
92
|
-
summary: "Add an action to the session queue;
|
|
91
|
+
usage: "action add <text> --serves <m/t#c|m#r>[,...] [--before <a>] [--session <session>]",
|
|
92
|
+
summary: "Add an action to the session queue; --serves names checklist items, or criteria while the mainline has no task for them. Prints a reminder when another session active in the last 15 minutes is doing what it serves",
|
|
93
93
|
args: 1,
|
|
94
94
|
needsHub: true
|
|
95
95
|
},
|
|
@@ -131,7 +131,7 @@ const COMMAND_SPECS = {
|
|
|
131
131
|
needsHub: true
|
|
132
132
|
},
|
|
133
133
|
"new issue": {
|
|
134
|
-
usage: "new issue <slug> --title <title> [--origin <ref>]",
|
|
134
|
+
usage: "new issue <slug> --title <title> [--origin <ref>] [--labels a,b]",
|
|
135
135
|
summary: "Create an issue file for a follow-up that must outlive its mainline",
|
|
136
136
|
args: 1,
|
|
137
137
|
needsHub: true
|
|
@@ -214,6 +214,12 @@ const COMMAND_SPECS = {
|
|
|
214
214
|
args: 1,
|
|
215
215
|
needsHub: true
|
|
216
216
|
},
|
|
217
|
+
"item undrop": {
|
|
218
|
+
usage: "item undrop <m/t#c>",
|
|
219
|
+
summary: "Bring a dropped item back as an unchecked item, clearing dropped and supersededBy",
|
|
220
|
+
args: 1,
|
|
221
|
+
needsHub: true
|
|
222
|
+
},
|
|
217
223
|
"issue plan": {
|
|
218
224
|
usage: "issue plan <i> <m/t>",
|
|
219
225
|
summary: "Schedule an issue into the task that will carry it out",
|
|
@@ -297,6 +303,12 @@ const COMMAND_SPECS = {
|
|
|
297
303
|
summary: "List a mainline's decisions",
|
|
298
304
|
args: 1,
|
|
299
305
|
needsHub: true
|
|
306
|
+
},
|
|
307
|
+
retro: {
|
|
308
|
+
usage: "retro <m> [--json]",
|
|
309
|
+
summary: "Read the Claude Code transcripts of the sessions that served a mainline, and their subagents', into estimated list-price cost (per model, opening context, cache hit rate, hook injections) and candidates for where work went wrong (tool errors, rerun Bash commands, hook injections followed by no tool call, blocks, pauses over 5 minutes, failed completion checks); read-only",
|
|
310
|
+
args: 1,
|
|
311
|
+
needsHub: true
|
|
300
312
|
}
|
|
301
313
|
};
|
|
302
314
|
const OPTIONS = {
|
|
@@ -345,6 +357,10 @@ const OPTIONS = {
|
|
|
345
357
|
type: "boolean",
|
|
346
358
|
default: false
|
|
347
359
|
},
|
|
360
|
+
json: {
|
|
361
|
+
type: "boolean",
|
|
362
|
+
default: false
|
|
363
|
+
},
|
|
348
364
|
"dry-run": {
|
|
349
365
|
type: "boolean",
|
|
350
366
|
default: false
|
|
@@ -517,7 +533,7 @@ const LIST_ITEM = [
|
|
|
517
533
|
field("done", true, "是否已完成,用 check 指令改"),
|
|
518
534
|
field("evidence", false, "完成的證據,done 為 true 時必填"),
|
|
519
535
|
field("doneAt", false, "完成的日期,由 CLI 寫入"),
|
|
520
|
-
field("dropped", false, "不再需要時為 true,用 item drop
|
|
536
|
+
field("dropped", false, "不再需要時為 true,用 item drop 標記,item undrop 撤銷;已放棄的條目不能 check"),
|
|
521
537
|
field("supersededBy", false, "被哪個參照取代"),
|
|
522
538
|
field("note", false, "一句備註,例如放棄的原因")
|
|
523
539
|
];
|
|
@@ -539,6 +555,7 @@ const FIELDS = {
|
|
|
539
555
|
field("order", false, "在主脈絡內的順序,小的在前"),
|
|
540
556
|
field("dependsOn", false, "開始前必須先完成的任務"),
|
|
541
557
|
field("repo", false, "主要動到的倉庫,名稱需在 config.yaml 登記"),
|
|
558
|
+
field("touches", false, "預計會改到的路徑或 glob 清單,相對於 repo 的根目錄;兩條進行中的主脈絡各有未結束的任務在同一個 repo 碰到重疊的路徑、彼此又沒有 depends-on 時,check 列出警告;有 touches 卻沒填 repo 時路徑不會被比對,check 也列警告"),
|
|
542
559
|
field("startedAt", false, "第一次轉為 doing 時由 CLI 寫入"),
|
|
543
560
|
field("checklist", true, "checklist 項目清單,每項的欄位見 checklistItem"),
|
|
544
561
|
field("blockedBy", false, "卡住時的原因、等待對象與繼續的訊號,blocked 時必填;等待對象整串是參照時(例如 m031/t002)會檢查它存在"),
|
|
@@ -581,7 +598,7 @@ const FIELDS = {
|
|
|
581
598
|
field("id", true, "會話內不重複的編號 a{NNN},由 action add 配發"),
|
|
582
599
|
field("do", true, "要做的事,一句話"),
|
|
583
600
|
field("status", true, "狀態值見 STATUSES,用 action set 改"),
|
|
584
|
-
field("serves", true, "推進的 checklist
|
|
601
|
+
field("serves", true, "推進的 checklist 項目(m{NNN}/t{NNN}#c{NNN})或解決條件(m{NNN}#r{NNN})參照清單,可以是空的;主脈絡還沒拆任務時指向解決條件"),
|
|
585
602
|
field("note", false, "一句備註"),
|
|
586
603
|
field("blockedBy", false, "卡住時的原因 reason、等待對象 waitingFor 與程式可判斷的條件 until,blocked 時必填,其他狀態不得有;等待對象整串是其他會話的行動(例如 claude-code-1a2b3c4d#a003)時,對方的簡報會列出有人在等它;條件格式見 UNTIL"),
|
|
587
604
|
field("startedAt", false, "轉為 doing 時由 CLI 寫入"),
|
|
@@ -594,4 +611,4 @@ const FIELDS = {
|
|
|
594
611
|
]
|
|
595
612
|
};
|
|
596
613
|
//#endregion
|
|
597
|
-
export { CHECKLIST_REF, CLASSIFICATION, CLOSED_STATUSES, COMMAND_SPECS, FIELDS, FORMAT_VERSION, GRANTS, ID_PATTERNS, LAYOUT, LINKS, OPTIONS, REFERENCES, SECTIONS, SLUG, STATUSES, SURVEY, UNTIL };
|
|
614
|
+
export { CHECKLIST_REF, CLASSIFICATION, CLOSED_STATUSES, COMMAND_SPECS, FIELDS, FORMAT_VERSION, GRANTS, ID_PATTERNS, LAYOUT, LINKS, OPTIONS, REFERENCES, SECTIONS, SERVES_REF, SLUG, STATUSES, SURVEY, UNTIL };
|
package/dist/grant-commands.d.ts
CHANGED
package/dist/grant-commands.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Y as GRANT_FORMS } from "./dist-BbamrIPW.js";
|
|
2
2
|
import { closeSync, constants, fstatSync, openSync, readFileSync, realpathSync } from "node:fs";
|
|
3
3
|
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
package/dist/hooks.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { $ as appendOps, F as ensureSession, H as setSessionStatus, J as matchGrants, L as loadSessions, M as sessionBriefing, N as turnReminder, Q as mayDecorate, R as readSession, St as worktreesOf, U as settleSession, V as setSessionInfo, X as endsWithMarker, Y as decorateReply, Z as headerLine, bt as currentBranch, ct as HubError, i as hostName, lt as loadHub, q as grantScope, vt as readYaml, xt as findHubRoot, yt as DATA_DIR, z as setReminderDigest } from "./host-CeN3dvXg.js";
|
|
2
|
+
import { Z as describeRequest, j as configSchema } from "./dist-BbamrIPW.js";
|
|
3
3
|
import { resolveFixed, scanCommand, scopedCwd } from "./grant-commands.js";
|
|
4
4
|
import { appendFileSync, existsSync, readFileSync } from "node:fs";
|
|
5
5
|
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
@@ -125,10 +125,55 @@ function openMainlinesIn(root, text) {
|
|
|
125
125
|
if (mentioned.size === 0) return [];
|
|
126
126
|
return loadHub(root, { remotes: false }).mainlines.filter((m) => mentioned.has(m.id) && !m.archivedAt && (m.status === "active" || m.status === "planning")).map((m) => m.id);
|
|
127
127
|
}
|
|
128
|
+
/** Tools whose `file_path` input writes a file. */
|
|
129
|
+
const WRITE_TOOLS = /* @__PURE__ */ new Set([
|
|
130
|
+
"Edit",
|
|
131
|
+
"Write",
|
|
132
|
+
"MultiEdit",
|
|
133
|
+
"NotebookEdit"
|
|
134
|
+
]);
|
|
135
|
+
/** A Bash command that runs the mainflow CLI: by the form the hook texts give, or as a `mainflow` command word that starts a command, not one that is only an argument. */
|
|
136
|
+
const isMainflowCommand = (command, cli) => command.includes(cli) || /(?:^|[;&|(])\s*mainflow\s+\S|(?:\.bin|mainflow\/dist)\/(?:cli\.js|mainflow)\b/.test(command);
|
|
137
|
+
/** Whether a path lies under a `.mainflow/` directory. */
|
|
138
|
+
const inMainflowData = (path) => /(?:^|\/)\.mainflow\//.test(path);
|
|
139
|
+
/** A user record that holds what the user typed; tool results come back as user records too, but carry only tool_result blocks. */
|
|
140
|
+
const isUserPrompt = (r) => r.type === "user" && !r.isSidechain && (typeof r.message?.content === "string" || Array.isArray(r.message?.content) && r.message.content.some((b) => b.type === "text"));
|
|
141
|
+
/**
|
|
142
|
+
* Whether the turn that is ending, everything after the last user message in the transcript, ran a mainflow
|
|
143
|
+
* command or wrote a file under `.mainflow/`. False when the transcript is missing or unreadable, so a hook that
|
|
144
|
+
* cannot tell never asks.
|
|
145
|
+
*/
|
|
146
|
+
function turnTouchedMainflow(transcript, cli) {
|
|
147
|
+
if (!transcript || !existsSync(transcript)) return false;
|
|
148
|
+
let lines;
|
|
149
|
+
try {
|
|
150
|
+
lines = readFileSync(transcript, "utf8").split("\n");
|
|
151
|
+
} catch {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
155
|
+
if (!lines[i].trim()) continue;
|
|
156
|
+
let record;
|
|
157
|
+
try {
|
|
158
|
+
record = JSON.parse(lines[i]);
|
|
159
|
+
} catch {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (isUserPrompt(record)) return false;
|
|
163
|
+
const content = Array.isArray(record.message?.content) ? record.message.content : [];
|
|
164
|
+
for (const block of content) {
|
|
165
|
+
if (block.type !== "tool_use") continue;
|
|
166
|
+
if (block.name === "Bash" && typeof block.input?.command === "string" && isMainflowCommand(block.input.command, cli)) return true;
|
|
167
|
+
if (block.name && WRITE_TOOLS.has(block.name) && typeof block.input?.file_path === "string" && inMainflowData(block.input.file_path)) return true;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
128
172
|
/**
|
|
129
173
|
* What to ask for when a turn ends without its header line: the header itself when an action is in progress, or
|
|
130
|
-
* an action first when the queue is empty
|
|
131
|
-
* the session's own action in progress.
|
|
174
|
+
* an action first when the queue is empty, the reply talks about an open mainline and the turn ran a mainflow
|
|
175
|
+
* command or wrote under `.mainflow/`, since the header only names the session's own action in progress. A reply
|
|
176
|
+
* that only mentions a mainline, in a draft or a question, asks for nothing. Null when there is nothing to ask for.
|
|
132
177
|
*/
|
|
133
178
|
function missingHeaderRequest(root, { payload, cli }) {
|
|
134
179
|
if (!payload.session_id || payload.stop_hook_active) return null;
|
|
@@ -139,8 +184,8 @@ function missingHeaderRequest(root, { payload, cli }) {
|
|
|
139
184
|
if (header) return `[mainflow] 這一輪最後一則回覆的最後一行不是標頭。請只回一則內容為「${header}」的訊息,不加其他文字。`;
|
|
140
185
|
if (session && session.actions.length > 0) return null;
|
|
141
186
|
const mainlines = openMainlinesIn(root, message);
|
|
142
|
-
if (mainlines.length === 0) return null;
|
|
143
|
-
return `[mainflow] 這一輪的回覆提到 ${mainlines.join("、")}
|
|
187
|
+
if (mainlines.length === 0 || !turnTouchedMainflow(payload.transcript_path, cli)) return null;
|
|
188
|
+
return `[mainflow] 這一輪的回覆提到 ${mainlines.join("、")},也執行了 mainflow 指令或寫入 .mainflow/,但這場會話沒有行動,所以回覆結尾沒有標頭。這一輪若在推進它,先用 ${cli} action add "<做法>" --serves <m/t#c>(還沒拆任務時用解決條件 <m#r>)列出行動並用 ${cli} action set <a編號> --status doing 標為進行中,或用 ${cli} session handoff <會話> 接手上一場沒做完的行動;再只回一則內容為「▸ <m/t> · <a編號>」的訊息,不加其他文字。只是提到、沒有推進時,不必處理。`;
|
|
144
189
|
}
|
|
145
190
|
/**
|
|
146
191
|
* Stop: ask for what the turn's header needs, then settle the session. Only the model knows which message closes a
|
|
@@ -162,6 +207,7 @@ const CLAUDE_CODE_EVENTS = {
|
|
|
162
207
|
if (!payload.session_id) return null;
|
|
163
208
|
const id = claudeCodeSessionId(payload.session_id);
|
|
164
209
|
const session = recordSession(root, payload, id, true);
|
|
210
|
+
setReminderDigest(root, id, null);
|
|
165
211
|
if (envFile) appendFileSync(envFile, `export MAINFLOW_SESSION=${id}\n`);
|
|
166
212
|
startUi?.(root);
|
|
167
213
|
return sessionBriefing(root, session, {
|