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 CHANGED
@@ -1,14 +1,64 @@
1
1
  #!/usr/bin/env node
2
- import { $ as createIssue, A as sessionBriefing, C as setCriterionText, D as updateTask, E as supersedeDecision, F as loadSessions, I as readSession, L as setSessionArchived, M as addAction, O as untilState, P as handoffSession, Q as createFinding, S as setCriterion, U as updateAction, V as startSession, X as appendOps, Z as closeIssue, _ as createTask, at as HubError, b as setArchived, dt as MAINLINE_FILE, et as planIssue, f as acceptDecision, ft as TASK_FILE, g as createMainline, h as createDecision, it as setIssueArchived, k as duplicateClaimLines, l as migrateHub, m as addRepo, mt as WriteError, nt as setFindingArchived, ot as loadHub, p as addHub, rt as setFindingStatus, tt as reopenIssue, v as dropChecklistItem, vt as findHubRoot, w as setMainlineStatus, x as setChecklistItem, y as initHub, z as setSessionStatus } from "./host-Dr0bCHd8.js";
3
- import { B as parseUntil, O as UNTIL_FORMS, V as refExample, z as parseRef } from "./dist-DlmecRLf.js";
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-lnD-Z3hb.js";
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
- serves: z.array(z.string().regex(CHECKLIST_REF)),
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 { grantRepo as $, configSchema as A, parseUntil as B, SLUG as C, TASK_SECTIONS as D, TASK_REQUIRED_SECTIONS as E, mainlineSchema as F, waitingForAction as G, refMainline as H, opsEntrySchema as I, GRANT_FORMS as J, waitingForRef as K, parseActionRef as L, findingSchema as M, formatOpsEntry as N, UNTIL_FORMS as O, issueSchema as P, grantForm as Q, parseOpsLine as R, SESSION_STATUS as S, SURVEY_SECTION as T, sessionSchema as U, refExample as V, taskSchema as W, describeRequest as X, GRANT_TARGET_FORMS as Y, grantCovers as Z, MERGE_STATES as _, DECISION_SECTIONS as a, RESERVED_HUB_NAMES as b, FINDING_SECTIONS as c, ID_PATTERNS as d, parseGrantTarget as et, ISSUE_SECTIONS as f, MAINLINE_STATUS as g, MAINLINE_SECTIONS as h, DECISION_BASES as i, decisionSchema as j, WORK_STATUS as k, FINDING_STATUS as l, LINK_RELS as m, CLOSED_STATUS as n, DECISION_STATUS as o, ISSUE_STATUS as p, GRANT_ACTIONS as q, DECIDERS as r, DEFAULT_CATEGORIES as s, CHECKLIST_REF as t, FORMAT_VERSION as u, PRIORITIES as v, SURVEY_LINES as w, SESSION_ID as x, REF_EXAMPLES as y, parseRef as z };
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 SurveyLine, i as SLUG, n as FORMAT_VERSION, o as Until, r as ID_PATTERNS, t as CHECKLIST_REF } from "./index-D4IIwDGE.js";
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>[,<m/t#c>] [--before <a>] [--session <session>]';
101
- readonly summary: 'Add an action to the session queue; prints a reminder when another session active in the last 15 minutes is doing an item it serves';
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 SLUG, D as TASK_SECTIONS, E as TASK_REQUIRED_SECTIONS, J as GRANT_FORMS, O as UNTIL_FORMS, S as SESSION_STATUS, T as SURVEY_SECTION, Y as GRANT_TARGET_FORMS, _ as MERGE_STATES, a as DECISION_SECTIONS, c as FINDING_SECTIONS, d as ID_PATTERNS, f as ISSUE_SECTIONS, g as MAINLINE_STATUS, h as MAINLINE_SECTIONS, i as DECISION_BASES, k as WORK_STATUS, l as FINDING_STATUS, m as LINK_RELS, n as CLOSED_STATUS, o as DECISION_STATUS, p as ISSUE_STATUS, q as GRANT_ACTIONS, r as DECIDERS, s as DEFAULT_CATEGORIES, t as CHECKLIST_REF, u as FORMAT_VERSION, v as PRIORITIES, w as SURVEY_LINES } from "./dist-DlmecRLf.js";
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>[,<m/t#c>] [--before <a>] [--session <session>]",
92
- summary: "Add an action to the session queue; prints a reminder when another session active in the last 15 minutes is doing an item it serves",
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 };
@@ -1,4 +1,4 @@
1
- import "./index-D4IIwDGE.js";
1
+ import "./index-bIFT1ORw.js";
2
2
  //#region ../schema/dist/grants.d.ts
3
3
  /** An operation about to run, resolved to what a grant is written against. */
4
4
  type GrantRequest = {
@@ -1,4 +1,4 @@
1
- import { J as GRANT_FORMS } from "./dist-DlmecRLf.js";
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 { A as sessionBriefing, B as settleSession, F as loadSessions, G as matchGrants, I as readSession, J as headerLine, K as decorateReply, N as ensureSession, R as setSessionInfo, W as grantScope, X as appendOps, Y as mayDecorate, _t as currentBranch, at as HubError, gt as DATA_DIR, ht as readYaml, i as hostName, j as turnReminder, ot as loadHub, q as endsWithMarker, vt as findHubRoot, yt as worktreesOf, z as setSessionStatus } from "./host-Dr0bCHd8.js";
2
- import { A as configSchema, X as describeRequest } from "./dist-DlmecRLf.js";
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 but the reply talks about an open mainline, since the header only names
131
- * the session's own action in progress. Null when there is nothing to ask for.
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("、")},但這場會話沒有行動,所以回覆結尾沒有標頭。這一輪若在推進它,先用 ${cli} action add "<做法>" --serves <m/t#c> 列出行動並用 ${cli} action set <a編號> --status doing 標為進行中,或用 ${cli} session handoff <會話> 接手上一場沒做完的行動;再只回一則內容為「▸ <m/t> · <a編號>」的訊息,不加其他文字。只是提到、沒有推進時,不必處理。`;
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, {