mainflow 0.44.0-alpha.2 → 0.44.0-alpha.4

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,18 +1,46 @@
1
1
  #!/usr/bin/env node
2
- import { P as UNTIL_FORMS, X as parseUntil, Y as parseRef, Z as refExample, ct as stampMs, it as untilProblem, n as CLOSED_STATUS } from "./dist-CXrUfRQp.js";
3
- import { $t as realPath, Bt as notRegisteredMessage, Ft as requireSlug, Gt as resolveHub, Ht as registerHub, It as hubAtRoot, Jt as nodeStorage, Kt as unregisterHub, Lt as hubHoldingFile, Mt as MAINLINE_FILE, O as loadSessions, Pt as TASK_FILE, Q as HubError, S as sessionBriefing, Ut as registeredName, Vt as readRegistry, Wt as registryFile, X as applyOperation, Xt as dataFile, Z as checkHubName, b as untilState, d as migrateHubData, f as migrateHub, ft as nowMinute, g as RETRY_RULES, gt as BLOB_HASH, h as receiverFromEnv, k as readSession, lt as remoteMembers, m as retroReportFromReceiver, nn as DATA_DIR, p as retroReport, qt as userDataDir, rn as WriteError, rt as sectionText, st as memberOfGroup, tt as loadHub, u as groupPlan, ut as unavailableHubs, vt as MAX_CHANGE_PAGE, x as duplicateClaimLines, y as initHub, yt as MAX_FILE_BYTES, zt as listHubs } from "./host-C74j2Qox.js";
4
- import { t as listProgress } from "./progress-1BfPg2zN.js";
5
- import { A as hubName, C as readRemote, D as Unreachable, E as ServerError, F as syncPaths, M as SETTINGS_FILE, O as ENV, T as ServerApi, _ as DaemonUnavailable, a as versionGate, b as readDaemon, c as oversizedFiles, d as planAnnounce, f as planFetch, g as DaemonRunning, h as startDaemon, i as requireWritableVersion, j as syncSettings, k as earlierToken, l as fetchDenial, m as toolCallOf, n as hubWriter, o as queuedCount, p as planFlush, r as writtenLines, s as remoteMode, t as CONFIRM_MS, u as hookAnswer, v as callDaemon, w as HubRewritingError, x as silentHint, y as probeDaemon } from "./operation-CUeOFEaf.js";
2
+ import { D as SESSION_ID, P as UNTIL_FORMS, X as parseUntil, Y as parseRef, Z as refExample, ct as stampMs, it as untilProblem, n as CLOSED_STATUS } from "./dist-CXrUfRQp.js";
3
+ import { $t as unregisterHub, At as requestTime, Bt as endedSessionFileIn, C as untilState, Ct as MAX_FILE_BYTES, Ht as sessionFileIn, Jt as readRegistry, Kt as listHubs, Lt as MAINLINE_FILE, M as loadSessions, N as readSession, Nt as hubFormatVersion, Qt as resolveHub, S as initHub, St as MAX_CHANGE_PAGE, T as sessionBriefing, Ut as hubAtRoot, Vt as requireSlug, Wt as hubHoldingFile, Xt as registeredName, Yt as registerHub, Zt as registryFile, _ as retroReportFromReceiver, at as loadHub, bt as BLOB_HASH, d as groupPlan, dt as memberOfGroup, en as userDataDir, et as applyOperation, f as listProgress, g as retroReport, gt as nowMinute, h as relocateSessions, i as hostName, j as unhandledNotices, ln as DATA_DIR, m as migrateHub, mt as unavailableHubs, nt as HubError, on as realPath, p as migrateHubData, pt as remoteMembers, qt as notRegisteredMessage, rn as dataFile, st as sectionText, tn as nodeStorage, tt as checkHubName, u as sessionView, un as WriteError, v as receiverFromEnv, w as duplicateClaimLines, y as RETRY_RULES, zt as TASK_FILE } from "./host-CfBcUcg2.js";
4
+ import { A as hubName, C as readRemote, D as Unreachable, E as ServerError, F as syncPaths, M as SETTINGS_FILE, O as ENV, T as ServerApi, _ as DaemonUnavailable, a as versionGate, b as readDaemon, c as oversizedFiles, d as planAnnounce, f as planFetch, g as DaemonRunning, h as startDaemon, i as requireWritableVersion, j as syncSettings, k as earlierToken, l as fetchDenial, m as toolCallOf, n as hubWriter, o as queuedCount, p as planFlush, r as writtenLines, s as remoteMode, t as CONFIRM_MS, u as hookAnswer, v as callDaemon, w as HubRewritingError, x as silentHint, y as probeDaemon } from "./operation-BmNzQC-L.js";
6
5
  import { COMMAND_SPECS, OPTIONS } from "./facts.js";
7
6
  import { t as CLI_VERSION } from "./version-Dvv4jwF1.js";
8
- import { hookFallback, parsePayload, runHook } from "./hooks.js";
7
+ import { claudeCodeSessionId, hookFallback, openLink, parsePayload, runHook } from "./hooks.js";
9
8
  import { runGrantCheck } from "./grant-check.js";
10
9
  import { exitCodeFor, launchUi, networkProbe, readUiRecord, startUiInBackground, watchHubGone } from "./launch.js";
11
- import { randomBytes } from "node:crypto";
10
+ import { randomBytes, randomUUID } from "node:crypto";
12
11
  import { existsSync, readFileSync, readdirSync, realpathSync, rmSync, statSync } from "node:fs";
13
12
  import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";
14
13
  import { fileURLToPath } from "node:url";
15
14
  import { parseArgs } from "node:util";
15
+ //#region src/session-json.ts
16
+ /** The mainflow session a host's session id stands for: the id itself when such a session exists, else Claude Code's. */
17
+ function sessionIdOf(dirs, raw) {
18
+ return SESSION_ID.test(raw) && readSession(dirs, raw) ? raw : claudeCodeSessionId(raw);
19
+ }
20
+ /** The sync client's state; null when the hub is not synced, that is no client runs and writes go to the files. */
21
+ async function syncState(dirs) {
22
+ const probe = await probeDaemon(dirs);
23
+ if (!probe && !remoteMode(dirs)) return null;
24
+ const running = probe?.state === "running";
25
+ return {
26
+ running,
27
+ caughtUp: running && probe.status?.caughtUp === true,
28
+ queued: queuedCount(dirs),
29
+ notices: unhandledNotices(dirs).length
30
+ };
31
+ }
32
+ /** What `session show --json` prints for a mainflow or host session id; a missing session gives `session: null`. */
33
+ async function sessionJson(dirs, raw) {
34
+ const hub = loadHub(dirs, { remotes: false });
35
+ return sessionView({
36
+ hub,
37
+ session: readSession(dirs, sessionIdOf(dirs, raw)),
38
+ url: `https://${hostName(hub.config.hub)}`,
39
+ link: openLink(hub.config.hub),
40
+ sync: await syncState(dirs)
41
+ });
42
+ }
43
+ //#endregion
16
44
  //#region src/migration.ts
17
45
  /**
18
46
  * What `mainflow migrate` prints of a migration's report, the same whether the hub's files are here or on the sync
@@ -26,6 +54,19 @@ function migrationLines(report, dryRun) {
26
54
  ...report.missingDefinitions.length ? [`${report.missingDefinitions.length} open entries still need a definition of done; fill them in the files:`, ...report.missingDefinitions.map((entry) => ` ${entry}`)] : []
27
55
  ];
28
56
  }
57
+ /**
58
+ * What `mainflow migrate` prints of moving the session files: each session moved to local/sessions/ended/, as one that
59
+ * ended or was handed off, and each moved back, as one under way; one line when every file is where it belongs.
60
+ */
61
+ function relocationLines(moved, dryRun) {
62
+ if (moved.ended.length === 0 && moved.resumed.length === 0) return [`sessions: every session file is in place already${dryRun ? " (dry run)" : ""}`];
63
+ const line = (from, to) => ` ${dryRun ? "would move" : "moved"} ${DATA_DIR}/${from} → ${DATA_DIR}/${to}`;
64
+ return [
65
+ `sessions: ${moved.ended.length} to ${DATA_DIR}/local/sessions/ended/, ${moved.resumed.length} back to ${DATA_DIR}/local/sessions/${dryRun ? " (dry run, nothing moved)" : ""}`,
66
+ ...moved.ended.map((id) => line(sessionFileIn(id), endedSessionFileIn(id))),
67
+ ...moved.resumed.map((id) => line(endedSessionFileIn(id), sessionFileIn(id)))
68
+ ];
69
+ }
29
70
  //#endregion
30
71
  //#region src/remote/sync.ts
31
72
  /**
@@ -699,6 +740,50 @@ async function syncMigrate(dirs, opts) {
699
740
  if (!readDaemon(dirs)) console.log("! mainflow sync is not running for this hub; this copy takes the migration in once it runs");
700
741
  return 0;
701
742
  }
743
+ /**
744
+ * Move the session files of a hub the sync server keeps to the directory each belongs in (session.relocate), a batch a
745
+ * request until none is left, straight on the server as `mainflow migrate` converts the data format there. The sync
746
+ * client takes the moves in as it takes any write; one that does not run takes them once it runs. A dry run lists the
747
+ * moves of this machine's copy, which the sync client keeps as the server has it, since the server refuses any
748
+ * operation on a hub whose data format a dry run of the conversion left as it was.
749
+ */
750
+ async function syncRelocate(dirs, opts) {
751
+ if (opts.dryRun) {
752
+ for (const line of relocationLines(relocateSessions(dirs, { dryRun: true }), true)) console.log(line);
753
+ console.log(" (listed from this machine's copy of the hub)");
754
+ return 0;
755
+ }
756
+ const settings = serverOf(dirs);
757
+ if (!settings) return 1;
758
+ const outcome = await onServer(settings, async (api) => {
759
+ const moved = {
760
+ ended: [],
761
+ resumed: []
762
+ };
763
+ for (;;) {
764
+ const request = {
765
+ protocol: 3,
766
+ op: "session.relocate",
767
+ input: { limit: 500 },
768
+ now: requestTime(),
769
+ requestId: randomUUID()
770
+ };
771
+ const answer = await api.send(request);
772
+ if (!answer.ok) return { refused: answer.reason === "unknown-operation" ? `${answer.message}; deploy the sync server of this mainflow version (apps/sync-server)` : answer.message };
773
+ moved.ended.push(...answer.value.ended);
774
+ moved.resumed.push(...answer.value.resumed);
775
+ if (!answer.value.more) return moved;
776
+ }
777
+ }, "operation route");
778
+ if (!outcome) return 1;
779
+ if ("refused" in outcome) {
780
+ console.error(`✗ the sync server refused moving the session files: ${outcome.refused}`);
781
+ return 1;
782
+ }
783
+ for (const line of relocationLines(outcome, false)) console.log(line);
784
+ if (outcome.ended.length + outcome.resumed.length > 0 && !readDaemon(dirs)) console.log("! mainflow sync is not running for this hub; this copy takes the moves in once it runs");
785
+ return 0;
786
+ }
702
787
  //#endregion
703
788
  //#region src/retro.ts
704
789
  const int = (n) => Math.round(n).toLocaleString("en-US");
@@ -1587,8 +1672,10 @@ const RUNS = {
1587
1672
  },
1588
1673
  migrate: ({ dirs, opts }) => {
1589
1674
  const dryRun = opts["dry-run"] ?? false;
1590
- if (remoteMode(dirs)) return syncMigrate(dirs, { dryRun });
1591
- for (const line of migrationLines(migrateHub(dirs, { dryRun }), dryRun)) console.log(line);
1675
+ if (remoteMode(dirs)) return syncMigrate(dirs, { dryRun }).then((code) => code === 0 ? syncRelocate(dirs, { dryRun }) : code);
1676
+ if (hubFormatVersion(nodeStorage(dirs)) === 3) console.log(`format 3: the data format is current already`);
1677
+ else for (const line of migrationLines(migrateHub(dirs, { dryRun }), dryRun)) console.log(line);
1678
+ for (const line of relocationLines(relocateSessions(dirs, { dryRun }), dryRun)) console.log(line);
1592
1679
  return 0;
1593
1680
  },
1594
1681
  ui: ({ dirs, opts }) => {
@@ -1604,8 +1691,9 @@ const RUNS = {
1604
1691
  });
1605
1692
  return 0;
1606
1693
  },
1607
- "session show": ({ dirs, args, opts }) => {
1694
+ "session show": async ({ dirs, args, opts }) => {
1608
1695
  const id = sessionOf(opts, args[0]);
1696
+ if (opts.json) return done(JSON.stringify(await sessionJson(dirs, id)));
1609
1697
  const session = readSession(dirs, id);
1610
1698
  if (!session) throw new WriteError(`session ${id} not found; start one with "mainflow session start"`);
1611
1699
  return done(sessionBriefing(dirs, session, { cli: CLI }));
package/dist/facts.d.ts CHANGED
@@ -97,8 +97,8 @@ export declare const COMMAND_SPECS: {
97
97
  readonly needsHub: true;
98
98
  };
99
99
  readonly 'session show': {
100
- readonly usage: 'session show [session]';
101
- readonly summary: 'Print the briefing a hook would give this session';
100
+ readonly usage: 'session show [session] [--json]';
101
+ readonly summary: 'Print the briefing a hook would give this session. --json prints one object for a host to read: {"hub","session","focus","tasks","mainlines","sync"}; session is null, and the exit code 0, when the session does not exist. The session is a mainflow session id or the host\'s own (a Claude Code UUID)';
102
102
  readonly args: 0;
103
103
  readonly optionalArgs: 1;
104
104
  readonly needsHub: true;
@@ -741,6 +741,7 @@ export declare const LAYOUT: {
741
741
  readonly group: '.mainflow/groups/w{NNN}-{slug}.md';
742
742
  readonly ops: '.mainflow/ops/{YYYY-MM}.log';
743
743
  readonly sessions: '.mainflow/local/sessions/{sessionId}.yaml';
744
+ readonly endedSessions: '.mainflow/local/sessions/ended/{sessionId}.yaml';
744
745
  };
745
746
  /** Statuses that close each kind of document; the CLI stamps closedAt when a document enters one. */
746
747
  export declare const CLOSED_STATUSES: {
package/dist/facts.js CHANGED
@@ -87,8 +87,8 @@ const COMMAND_SPECS = {
87
87
  needsHub: true
88
88
  },
89
89
  "session show": {
90
- usage: "session show [session]",
91
- summary: "Print the briefing a hook would give this session",
90
+ usage: "session show [session] [--json]",
91
+ summary: "Print the briefing a hook would give this session. --json prints one object for a host to read: {\"hub\",\"session\",\"focus\",\"tasks\",\"mainlines\",\"sync\"}; session is null, and the exit code 0, when the session does not exist. The session is a mainflow session id or the host's own (a Claude Code UUID)",
92
92
  args: 0,
93
93
  optionalArgs: 1,
94
94
  needsHub: true
@@ -646,7 +646,8 @@ const LAYOUT = {
646
646
  finding: ".mainflow/findings/f{NNN}-{slug}.md",
647
647
  group: ".mainflow/groups/w{NNN}-{slug}.md",
648
648
  ops: ".mainflow/ops/{YYYY-MM}.log",
649
- sessions: ".mainflow/local/sessions/{sessionId}.yaml"
649
+ sessions: ".mainflow/local/sessions/{sessionId}.yaml",
650
+ endedSessions: ".mainflow/local/sessions/ended/{sessionId}.yaml"
650
651
  };
651
652
  /** Statuses that close each kind of document; the CLI stamps closedAt when a document enters one. */
652
653
  const CLOSED_STATUSES = CLOSED_STATUS;
@@ -1,5 +1,5 @@
1
- import { o as HubDirs } from "./index-jq7lN8V8.js";
2
- import { t as HubWrite } from "./operation-BroV98VB.js";
1
+ import { o as HubDirs } from "./index-lsW80LDU.js";
2
+ import { t as HubWrite } from "./operation-BTQm13OQ.js";
3
3
  //#region src/grant-check.d.ts
4
4
  /** What grant check prints. `grants` lists every grant used when more than one was, as for an install of several units. */
5
5
  export interface GrantVerdict {
@@ -1,6 +1,6 @@
1
1
  import { D as SESSION_ID, I as configSchema, ft as describeRequest, gt as hasControl, tt as repoPaths } from "./dist-CXrUfRQp.js";
2
- import { $t as realPath, Gt as resolveHub, Jt as nodeStorage, M as matchGrants, Ot as formatMismatch, Yt as currentBranch, Zt as gitCheckout, j as grantScope, k as readSession, kt as hubFormatVersion, mt as readYaml } from "./host-C74j2Qox.js";
3
- import { n as hubWriter, r as writtenLines } from "./operation-CUeOFEaf.js";
2
+ import { F as grantScope, I as matchGrants, Mt as formatMismatch, N as readSession, Nt as hubFormatVersion, Qt as resolveHub, in as gitCheckout, nn as currentBranch, on as realPath, tn as nodeStorage, vt as readYaml } from "./host-CfBcUcg2.js";
3
+ import { n as hubWriter, r as writtenLines } from "./operation-BmNzQC-L.js";
4
4
  import { OWNER_REPO, githubOrigin, quote, readManifest, resolveFixed, scanCommand, scopedCwd } from "./grant-commands.js";
5
5
  import { claudeCodeSessionId, parsePayload } from "./hooks.js";
6
6
  import { readFileSync } from "node:fs";
package/dist/hooks.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { o as HubDirs } from "./index-jq7lN8V8.js";
2
- import { t as HubWrite } from "./operation-BroV98VB.js";
1
+ import { o as HubDirs } from "./index-lsW80LDU.js";
2
+ import { t as HubWrite } from "./operation-BTQm13OQ.js";
3
3
  //#region src/hooks.d.ts
4
4
  /** Fields Claude Code sends on stdin to every command hook, plus the ones specific to the events handled here. */
5
5
  export interface ClaudeCodePayload {
package/dist/hooks.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { I as configSchema, Y as parseRef, ft as describeRequest, rt as taskSchema, tt as repoPaths } from "./dist-CXrUfRQp.js";
2
- import { $ as isOpenMainline, $t as realPath, Bt as notRegisteredMessage, C as turnReminder, F as headerLine, Gt as resolveHub, I as mayDecorate, Jt as nodeStorage, Lt as hubHoldingFile, M as matchGrants, N as decorateReply, Nt as SESSIONS_DIR, O as loadSessions, Ot as formatMismatch, P as endsWithMarker, Q as HubError, Qt as hubPath, Rt as inRepositoryHubAt, S as sessionBriefing, Xt as dataFile, Yt as currentBranch, dt as taskFile, en as worktreesOf, i as hostName, j as grantScope, k as readSession, kt as hubFormatVersion, mt as readYaml, nn as DATA_DIR, nt as loadMainlines, pt as readFrontMatter, tt as loadHub } from "./host-C74j2Qox.js";
3
- import { n as hubWriter, r as writtenLines } from "./operation-CUeOFEaf.js";
2
+ import { B as mayDecorate, E as turnReminder, F as grantScope, Gt as inRepositoryHubAt, I as matchGrants, It as ENDED_SESSIONS_DIR, L as decorateReply, M as loadSessions, Mt as formatMismatch, N as readSession, Nt as hubFormatVersion, Qt as resolveHub, R as endsWithMarker, Rt as SESSIONS_DIR, T as sessionBriefing, Wt as hubHoldingFile, _t as readFrontMatter, an as hubPath, at as loadHub, ht as taskFile, i as hostName, ln as DATA_DIR, nn as currentBranch, nt as HubError, on as realPath, ot as loadMainlines, qt as notRegisteredMessage, rn as dataFile, rt as isOpenMainline, sn as worktreesOf, tn as nodeStorage, vt as readYaml, z as headerLine } from "./host-CfBcUcg2.js";
3
+ import { n as hubWriter, r as writtenLines } from "./operation-BmNzQC-L.js";
4
4
  import { SCENARIO_LINES, SCENARIO_SKIP_KINDS } from "./facts.js";
5
5
  import { resolveFixed, scanCommand, scopedCwd } from "./grant-commands.js";
6
6
  import { createHash } from "node:crypto";
@@ -102,13 +102,17 @@ const settleOnEnd = async (_root, { payload, write }) => {
102
102
  };
103
103
  /** How a hub's interface is linked from a reply: through its /open route, which shows the address in the app. */
104
104
  const openLink = (hub) => (path) => `https://${hostName(hub)}/open${path}`;
105
+ /** Whether a host's display mod is loaded, which it says by setting MAINFLOW_DISPLAY_MOD; it then shows the current action itself. */
106
+ const displayModOn = (env = process.env) => !!env.MAINFLOW_DISPLAY_MOD;
105
107
  /**
106
108
  * MessageDisplay: link the references in the lines about to be shown and expand the trailing header line into
107
109
  * the mainline › task › action its ids name, titles included, whatever state that action is in now. Display
108
110
  * only, so the model keeps reading what it wrote. Text with nothing to change
109
- * returns before the hub is loaded, because Claude Code holds every batch until the hook answers.
111
+ * returns before the hub is loaded, because Claude Code holds every batch until the hook answers. With a display
112
+ * mod loaded it returns null at once.
110
113
  */
111
- const displayReply = (dirs, { payload }) => {
114
+ const displayReply = (dirs, { payload, env }) => {
115
+ if (displayModOn(env)) return null;
112
116
  const text = payload.delta;
113
117
  if (!text || !mayDecorate(text)) return null;
114
118
  const hub = loadHub(dirs, { remotes: false });
@@ -183,18 +187,22 @@ function turnTouchedMainflow(transcript, cli, env) {
183
187
  * an action first when the queue is empty, the reply talks about an open mainline and the turn ran a mainflow
184
188
  * command or wrote a file of the hub, since the header only names the session's own action in progress. A reply
185
189
  * that only mentions a mainline, in a draft or a question, asks for nothing. Null when there is nothing to ask for.
190
+ * With a display mod loaded the mod shows the current action, so no header is asked for, but an empty queue
191
+ * still is: the action is what grants and the hub's tracking hang on.
186
192
  */
187
193
  function missingHeaderRequest(dirs, { payload, cli, env }) {
188
194
  if (!payload.session_id || payload.stop_hook_active) return null;
195
+ const displayMod = displayModOn(env);
189
196
  const message = payload.last_assistant_message ?? "";
190
- if (endsWithMarker(message)) return null;
197
+ if (!displayMod && endsWithMarker(message)) return null;
191
198
  const session = readSession(dirs, claudeCodeSessionId(payload.session_id));
192
199
  const header = headerLine(session);
193
- if (header) return `[mainflow] 這一輪最後一則回覆的最後一行不是標頭。請只回一則內容為「${header}」的訊息,不加其他文字。`;
200
+ if (header) return displayMod ? null : `[mainflow] 這一輪最後一則回覆的最後一行不是標頭。請只回一則內容為「${header}」的訊息,不加其他文字。`;
194
201
  if (session && session.actions.length > 0) return null;
195
202
  const mainlines = openMainlinesIn(dirs, message);
196
203
  if (mainlines.length === 0 || !turnTouchedMainflow(payload.transcript_path, cli, env)) return null;
197
- return `[mainflow] 這一輪的回覆提到 ${mainlines.join("、")},也執行了 mainflow 指令或寫入 hub 的資料目錄,但這場會話沒有行動,所以回覆結尾沒有標頭。這一輪若在推進它,先用 ${cli} action add "<做法>" --serves <m/t#c>(還沒拆任務時用解決條件 <m#r>)列出行動並用 ${cli} action set <a編號> --status doing 標為進行中,或用 ${cli} session handoff <會話> 接手上一場沒做完的行動;再只回一則內容為「▸ <m/t> · <a編號>」的訊息,不加其他文字。只是提到、沒有推進時,不必處理。`;
204
+ const list = `先用 ${cli} action add "<做法>" --serves <m/t#c>(還沒拆任務時用解決條件 <m#r>)列出行動並用 ${cli} action set <a編號> --status doing 標為進行中,或用 ${cli} session handoff <會話> 接手上一場沒做完的行動`;
205
+ return displayMod ? `[mainflow] 這一輪的回覆提到 ${mainlines.join("、")},也執行了 mainflow 指令或寫入 hub 的資料目錄,但這場會話沒有行動。這一輪若在推進它,${list}。只是提到、沒有推進時,不必處理。` : `[mainflow] 這一輪的回覆提到 ${mainlines.join("、")},也執行了 mainflow 指令或寫入 hub 的資料目錄,但這場會話沒有行動,所以回覆結尾沒有標頭。這一輪若在推進它,${list};再只回一則內容為「▸ <m/t> · <a編號>」的訊息,不加其他文字。只是提到、沒有推進時,不必處理。`;
198
206
  }
199
207
  /**
200
208
  * Stop: ask for what the turn's header needs, then settle the session. Only the model knows which message closes a
@@ -231,12 +239,13 @@ const CLAUDE_CODE_EVENTS = {
231
239
  envNote: envFile ? "已設定環境變數 MAINFLOW_SESSION,指令不必再帶 --session" : `執行指令時帶 --session ${id}`
232
240
  });
233
241
  },
234
- "user-prompt-submit": async (dirs, { payload, cli, write }) => {
242
+ "user-prompt-submit": async (dirs, { payload, cli, write, env }) => {
235
243
  if (!payload.session_id) return null;
236
244
  const session = await recordSession(write, dirs, payload, claudeCodeSessionId(payload.session_id), false);
237
245
  const saved = [];
238
246
  const text = turnReminder(dirs, session, {
239
247
  cli,
248
+ displayMod: displayModOn(env),
240
249
  saveDigest: (digest) => saved.push(write("session.reminder", {
241
250
  id: session.id,
242
251
  digest
@@ -454,7 +463,7 @@ function inSessionScope(file, session) {
454
463
  const mainlines = `${DATA_DIR}/mainlines/`;
455
464
  const sessions = `${DATA_DIR}/${SESSIONS_DIR}/`;
456
465
  if (file.startsWith(mainlines)) return [...servedMainlines(session)].some((m) => file.startsWith(`${mainlines}${m}-`));
457
- if (file.startsWith(sessions)) return file === `${sessions}${session.id}.yaml`;
466
+ if (file.startsWith(sessions)) return file === `${sessions}${session.id}.yaml` || file === `.mainflow/${ENDED_SESSIONS_DIR}/${session.id}.yaml`;
458
467
  return true;
459
468
  }
460
469
  /**