mainflow 0.10.0 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import { $ as MAINLINE_FILE, A as setSessionStatus, B as appendOps, C as updateTask, D as readSession, F as sessionBriefing, G as reopenIssue, H as createFinding, J as setIssueArchived, K as setFindingArchived, M as startSession, O as setSessionArchived, P as updateAction, S as supersedeDecision, U as createIssue, V as closeIssue, W as planIssue, X as loadHub, Y as HubError, _ as setArchived, at as findHubRoot, b as setMainlineStatus, c as migrateHub, d as addRepo, et as TASK_FILE, f as createDecision, g as initHub, h as dropChecklistItem, l as acceptDecision, m as createTask, nt as WriteError, p as createMainline, q as setFindingStatus, u as addHub, v as setChecklistItem, w as addAction, y as setCriterion } from "./host-BFGfpTDq.js";
2
+ import { A as setSessionStatus, C as updateTask, D as readSession, F as sessionBriefing, G as planIssue, H as closeIssue, J as setFindingStatus, K as reopenIssue, M as startSession, O as setSessionArchived, P as updateAction, S as supersedeDecision, U as createFinding, V as appendOps, W as createIssue, X as HubError, Y as setIssueArchived, Z as loadHub, _ as setArchived, b as setMainlineStatus, c as migrateHub, d as addRepo, et as MAINLINE_FILE, f as createDecision, g as initHub, h as dropChecklistItem, l as acceptDecision, m as createTask, ot as findHubRoot, p as createMainline, q as setFindingArchived, rt as WriteError, tt as TASK_FILE, u as addHub, v as setChecklistItem, w as addAction, y as setCriterion } from "./host-ByGN9mGS.js";
3
3
  import { t as listProgress } from "./progress-1BfPg2zN.js";
4
4
  import { COMMAND_SPECS, OPTIONS } from "./facts.js";
5
5
  import { runHook } from "./hooks.js";
6
- import { c as CLI_VERSION, r as launchUi, s as startUiInBackground } from "./launch-D5uHpqkh.js";
6
+ import { c as CLI_VERSION, r as launchUi, s as startUiInBackground } from "./launch-B9QrvwZQ.js";
7
7
  import { readFileSync, realpathSync } from "node:fs";
8
8
  import { join, relative } from "node:path";
9
9
  import { fileURLToPath } from "node:url";
package/dist/hooks.js CHANGED
@@ -1,4 +1,4 @@
1
- import { A as setSessionStatus, D as readSession, F as sessionBriefing, I as turnReminder, L as decorateReply, R as endsWithMarker, T as ensureSession, X as loadHub, Y as HubError, at as findHubRoot, i as hostName, it as currentBranch, j as settleSession, k as setSessionInfo, ot as worktreesOf, rt as DATA_DIR, z as mayDecorate } from "./host-BFGfpTDq.js";
1
+ import { A as setSessionStatus, B as mayDecorate, D as readSession, F as sessionBriefing, I as turnReminder, L as decorateReply, R as endsWithMarker, T as ensureSession, X as HubError, Z as loadHub, at as currentBranch, i as hostName, it as DATA_DIR, j as settleSession, k as setSessionInfo, ot as findHubRoot, st as worktreesOf, z as headerLine } from "./host-ByGN9mGS.js";
2
2
  import { appendFileSync, existsSync, readFileSync } from "node:fs";
3
3
  import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
4
4
  //#region src/hooks.ts
@@ -55,8 +55,9 @@ const settleOnEnd = (root, { payload }) => {
55
55
  /** How a hub's interface is linked from a reply: through its /open route, which shows the address in the app. */
56
56
  const openLink = (hub) => (path) => `https://${hostName(hub)}/open${path}`;
57
57
  /**
58
- * MessageDisplay: link the references in the lines about to be shown and expand a trailing end marker into the
59
- * current action line. Display only, so the model keeps reading what it wrote. Text with nothing to change
58
+ * MessageDisplay: link the references in the lines about to be shown and expand the trailing header line into
59
+ * the mainline › task › action its ids name, titles included, whatever state that action is in now. Display
60
+ * only, so the model keeps reading what it wrote. Text with nothing to change
60
61
  * returns before the hub is loaded, because Claude Code holds every batch until the hook answers.
61
62
  */
62
63
  const displayReply = (root, { payload }) => {
@@ -75,18 +76,19 @@ const displayReply = (root, { payload }) => {
75
76
  } });
76
77
  };
77
78
  /**
78
- * Stop: settle the session, then ask for the end marker when a session with an action in progress ended its
79
- * turn without one. Only the model knows which message closes a turn, so the marker is its to write; this makes
79
+ * Stop: settle the session, then ask for the header line when a session with an action in progress ended its
80
+ * turn without one. Only the model knows which message closes a turn, so the header is its to write; this makes
80
81
  * a forgotten one cost a single extra message. stop_hook_active stops it from asking twice in a row.
81
82
  */
82
83
  const settleAndCheckMarker = (root, context) => {
83
84
  settleOnEnd(root, context);
84
85
  const { payload } = context;
85
86
  if (!payload.session_id || payload.stop_hook_active || endsWithMarker(payload.last_assistant_message ?? "")) return null;
86
- if (!readSession(root, claudeCodeSessionId(payload.session_id))?.actions.some((a) => a.status === "doing")) return null;
87
+ const header = headerLine(readSession(root, claudeCodeSessionId(payload.session_id)));
88
+ if (!header) return null;
87
89
  return JSON.stringify({ hookSpecificOutput: {
88
90
  hookEventName: "Stop",
89
- additionalContext: `[mainflow] 這一輪最後一則回覆沒有以單獨一行的 ▸ 結尾。請只回一則內容為 ▸ 的訊息,不加其他文字。`
91
+ additionalContext: `[mainflow] 這一輪最後一則回覆的最後一行不是標頭。請只回一則內容為「${header}」的訊息,不加其他文字。`
90
92
  } });
91
93
  };
92
94
  const CLAUDE_CODE_EVENTS = {
@@ -936,38 +936,76 @@ function linkMentions(text, context) {
936
936
  return path ? `[${ref}](${context.link(path)})` : all;
937
937
  })).join("");
938
938
  }
939
- /** `▸ m001 title › t001 title › a001 do`, each id linked; null when the session has no action in progress. */
940
- function currentActionLine(context) {
941
- const action = context.session?.actions.find((a) => a.status === "doing");
942
- if (!context.session || !action) return null;
939
+ /** `▸ m001 title › t001 title › a001 do` for the given ids, each linked; titles come from the hub and the session. */
940
+ function headerLineFor(ids, context) {
943
941
  const index = refIndex(context.hub);
944
942
  const parts = [];
945
- const served = action.serves.map((s) => parseRef(s)).find((r) => r !== null);
946
- if (served && "mainline" in served) {
947
- const mainline = index.get(served.mainline);
948
- parts.push(`[${served.mainline}](${context.link(`/mainlines/${served.mainline}`)}) ${mainline?.title ?? ""}`.trimEnd());
949
- if ("task" in served) {
950
- const ref = `${served.mainline}/${served.task}`;
951
- parts.push(`[${served.task}](${context.link(`/mainlines/${served.mainline}/tasks/${served.task}`)}) ${index.get(ref)?.title ?? ""}`.trimEnd());
943
+ if (ids.mainline) {
944
+ parts.push(`[${ids.mainline}](${context.link(`/mainlines/${ids.mainline}`)}) ${index.get(ids.mainline)?.title ?? ""}`.trimEnd());
945
+ if (ids.task) {
946
+ const ref = `${ids.mainline}/${ids.task}`;
947
+ parts.push(`[${ids.task}](${context.link(`/mainlines/${ids.mainline}/tasks/${ids.task}`)}) ${index.get(ref)?.title ?? ""}`.trimEnd());
952
948
  }
953
949
  }
954
- parts.push(`[${action.id}](${context.link(sessionPath(context.session.id))}) ${action.do}`);
950
+ const action = context.session?.actions.find((a) => a.id === ids.action);
951
+ parts.push(context.session ? `[${ids.action}](${context.link(sessionPath(context.session.id))}) ${action?.do ?? ""}`.trimEnd() : ids.action);
955
952
  return `▸ ${parts.join(" › ")}`;
956
953
  }
957
- /** Whether a message ends with the marker line, as the turn's last message should. */
958
- const endsWithMarker = (text) => text.trimEnd().split("\n").at(-1)?.trim() === "▸";
954
+ /** The ids of the session's action in progress; null when there is none. */
955
+ function currentIds(session) {
956
+ const action = session?.actions.find((a) => a.status === "doing");
957
+ if (!action) return null;
958
+ const served = action.serves.map((s) => parseRef(s)).find((r) => r !== null);
959
+ if (served && "mainline" in served) return {
960
+ mainline: served.mainline,
961
+ task: "task" in served ? served.task : void 0,
962
+ action: action.id
963
+ };
964
+ return { action: action.id };
965
+ }
966
+ /** The current action line for a bare marker; null when the session has no action in progress. */
967
+ function currentActionLine(context) {
968
+ const ids = currentIds(context.session);
969
+ return ids ? headerLineFor(ids, context) : null;
970
+ }
971
+ /** The header line the model writes at the end of a turn, e.g. `▸ m001/t001 · a001`; null without an action in progress. */
972
+ function headerLine(session) {
973
+ const ids = currentIds(session);
974
+ if (!ids) return null;
975
+ const where = ids.mainline ? ids.task ? `${ids.mainline}/${ids.task}` : ids.mainline : null;
976
+ return `▸ ${where ? `${where} · ` : ""}${ids.action}`;
977
+ }
978
+ /** The ids in a header line the model wrote, or null for a bare marker or a line that does not parse. */
979
+ const WRITTEN = new RegExp(`^\\s*▸\\s+(?:(m\\d{3})(?:\\/(t\\d{3}))?\\s*·\\s*)?(a\\d{3})\\s*$`);
980
+ function parseHeader(line) {
981
+ const m = WRITTEN.exec(line);
982
+ return m ? {
983
+ mainline: m[1],
984
+ task: m[2],
985
+ action: m[3]
986
+ } : null;
987
+ }
988
+ /** A line that is the header: the marker alone, or the marker followed by the ids the model copied. */
989
+ const HEADER = new RegExp(`^[ \\t]*▸(?:[ \\t].*)?$`);
990
+ /** The last non-empty line, if it is the header line. */
991
+ const lastHeader = (text) => {
992
+ const last = text.trimEnd().split("\n").at(-1) ?? "";
993
+ return HEADER.test(last) ? last : null;
994
+ };
995
+ /** Whether a message ends with the header line, as the turn's last message should. */
996
+ const endsWithMarker = (text) => lastHeader(text) !== null;
959
997
  /**
960
- * The text to display for a piece of a reply: mentions linked, and a trailing marker line replaced with the
961
- * current action line (or removed when there is none). Returns the input unchanged when there is nothing to do.
998
+ * The text to display for a piece of a reply: mentions linked, and a trailing header line replaced with the
999
+ * mainline › task › action it names, titles and links included. A bare marker names the session's action in
1000
+ * progress and is dropped without one. Returns the input unchanged when there is nothing to do.
962
1001
  */
963
1002
  function decorateReply(text, context) {
964
- let shown = linkMentions(text, context);
965
- if (endsWithMarker(shown)) {
966
- const body = shown.trimEnd().slice(0, -1).trimEnd();
967
- const line = currentActionLine(context);
968
- shown = line ? `${body}\n\n${line}\n` : `${body}\n`;
969
- }
970
- return shown;
1003
+ const header = lastHeader(text);
1004
+ if (header === null) return linkMentions(text, context);
1005
+ const body = linkMentions(text.trimEnd().slice(0, -header.length).trimEnd(), context);
1006
+ const written = parseHeader(header);
1007
+ const line = written ? headerLineFor(written, context) : header.trim() === "▸" ? currentActionLine(context) : linkMentions(header.trim(), context);
1008
+ return line ? `${body}\n\n${line}\n` : `${body}\n`;
971
1009
  }
972
1010
  /** Cheap test for text that decorateReply could change, so a display hook can skip loading the hub. */
973
1011
  const mayDecorate = (text) => /m\d{3}|[if]\d{3}|\.mainflow\/mainlines/.test(text) || endsWithMarker(text);
@@ -1039,9 +1077,11 @@ function turnReminder(root, session, options) {
1039
1077
  if (session.actions.length === 0) return null;
1040
1078
  const doing = current(session);
1041
1079
  const open = session.actions.filter((a) => a.status === "todo" || a.status === "doing");
1080
+ const head = doing ? `[mainflow] 目前行動 ${doing.id} ${doing.do},服務 ${doing.serves.join("、") || "(未指定)"}。這一輪的工作若不屬於它,先調整佇列再做。` : open.length === 0 ? `[mainflow] ${session.id} 的行動都已結束。繼續工作前先用 ${options.cli} action add 列出下一個行動。` : `[mainflow] ${session.id} 目前沒有進行中的行動。`;
1081
+ const header = headerLine(session);
1042
1082
  return [
1043
- doing ? `[mainflow] 目前行動 ${doing.id} ${doing.do},服務 ${doing.serves.join("、") || "(未指定)"}。這一輪的工作若不屬於它,先調整佇列再做。` : open.length === 0 ? `[mainflow] ${session.id} 的行動都已結束。繼續工作前先用 ${options.cli} action add 列出下一個行動。` : `[mainflow] ${session.id} 目前沒有進行中的行動。`,
1044
- ...doing ? [`- 這一輪最後一則回覆的最後一行只寫 ▸,回覆範例等其他內容都放在它前面。畫面上它會換成目前行動與連結。`] : [],
1083
+ head,
1084
+ ...header ? [`- 這一輪最後一則回覆的最後一行寫「${header}」,回覆範例等其他內容都放在它前面。有顯示 hook 時,它會換成標題與連結。`] : [],
1045
1085
  ...queueWarnings(root, session, options.cli).map((w) => `- ${w}`)
1046
1086
  ].join("\n");
1047
1087
  }
@@ -1834,4 +1874,4 @@ function openInApp(hub, target, closeAfterMs = 150) {
1834
1874
  return true;
1835
1875
  }
1836
1876
  //#endregion
1837
- export { MAINLINE_FILE as $, setSessionStatus as A, appendOps as B, updateTask as C, readSession as D, loadSessions as E, sessionBriefing as F, reopenIssue as G, createFinding as H, turnReminder as I, setIssueArchived as J, setFindingArchived as K, decorateReply as L, startSession as M, transcriptPath as N, setSessionArchived as O, updateAction as P, refIndex as Q, endsWithMarker as R, supersedeDecision as S, ensureSession as T, createIssue as U, closeIssue as V, planIssue as W, loadHub as X, HubError as Y, linksInto as Z, setArchived as _, openInApp as a, findHubRoot as at, setMainlineStatus as b, migrateHub as c, addRepo as d, TASK_FILE as et, createDecision as f, initHub as g, dropChecklistItem as h, hostName as i, currentBranch as it, settleSession as j, setSessionInfo as k, acceptDecision as l, createTask as m, DEFAULT_UI_PORT as n, WriteError as nt, siteBlock as o, worktreesOf as ot, createMainline as p, setFindingStatus as q, caddySites as r, DATA_DIR as rt, syncHost as s, APP_BUNDLE_ID as t, ConflictError as tt, addHub as u, setChecklistItem as v, addAction as w, setMainlineTitle as x, setCriterion as y, mayDecorate as z };
1877
+ export { refIndex as $, setSessionStatus as A, mayDecorate as B, updateTask as C, readSession as D, loadSessions as E, sessionBriefing as F, planIssue as G, closeIssue as H, turnReminder as I, setFindingStatus as J, reopenIssue as K, decorateReply as L, startSession as M, transcriptPath as N, setSessionArchived as O, updateAction as P, linksInto as Q, endsWithMarker as R, supersedeDecision as S, ensureSession as T, createFinding as U, appendOps as V, createIssue as W, HubError as X, setIssueArchived as Y, loadHub as Z, setArchived as _, openInApp as a, currentBranch as at, setMainlineStatus as b, migrateHub as c, addRepo as d, MAINLINE_FILE as et, createDecision as f, initHub as g, dropChecklistItem as h, hostName as i, DATA_DIR as it, settleSession as j, setSessionInfo as k, acceptDecision as l, createTask as m, DEFAULT_UI_PORT as n, ConflictError as nt, siteBlock as o, findHubRoot as ot, createMainline as p, setFindingArchived as q, caddySites as r, WriteError as rt, syncHost as s, worktreesOf as st, APP_BUNDLE_ID as t, TASK_FILE as tt, addHub as u, setChecklistItem as v, addAction as w, setMainlineTitle as x, setCriterion as y, headerLine as z };
package/dist/host.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as openInApp, i as hostName, n as DEFAULT_UI_PORT, o as siteBlock, r as caddySites, s as syncHost, t as APP_BUNDLE_ID } from "./host-BFGfpTDq.js";
1
+ import { a as openInApp, i as hostName, n as DEFAULT_UI_PORT, o as siteBlock, r as caddySites, s as syncHost, t as APP_BUNDLE_ID } from "./host-ByGN9mGS.js";
2
2
  export { APP_BUNDLE_ID, DEFAULT_UI_PORT, caddySites, hostName, openInApp, siteBlock, syncHost };
@@ -1,4 +1,4 @@
1
- import { n as DEFAULT_UI_PORT, nt as WriteError, rt as DATA_DIR, s as syncHost } from "./host-BFGfpTDq.js";
1
+ import { it as DATA_DIR, n as DEFAULT_UI_PORT, rt as WriteError, s as syncHost } from "./host-ByGN9mGS.js";
2
2
  import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { spawn, spawnSync } from "node:child_process";
@@ -162,7 +162,7 @@ async function launchUi(root, hub, configured, options) {
162
162
  }
163
163
  let server;
164
164
  if (!reuse) {
165
- const { serveUi } = await import("./ui-DbEmJiwO.js");
165
+ const { serveUi } = await import("./ui-BY65lR7x.js");
166
166
  server = await serveUi(root, choice.port);
167
167
  writeUiRecord(root, {
168
168
  port: choice.port,
package/dist/launch.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as portCandidates, i as networkProbe, n as choosePort, o as readUiRecord, r as launchUi, s as startUiInBackground, t as PORT_SPAN } from "./launch-D5uHpqkh.js";
1
+ import { a as portCandidates, i as networkProbe, n as choosePort, o as readUiRecord, r as launchUi, s as startUiInBackground, t as PORT_SPAN } from "./launch-B9QrvwZQ.js";
2
2
  export { PORT_SPAN, choosePort, launchUi, networkProbe, portCandidates, readUiRecord, startUiInBackground };
@@ -1,6 +1,6 @@
1
- import { C as updateTask, D as readSession, E as loadSessions, J as setIssueArchived, K as setFindingArchived, N as transcriptPath, O as setSessionArchived, Q as refIndex, X as loadHub, Z as linksInto, _ as setArchived, a as openInApp, b as setMainlineStatus, nt as WriteError, rt as DATA_DIR, tt as ConflictError, v as setChecklistItem, x as setMainlineTitle, y as setCriterion } from "./host-BFGfpTDq.js";
1
+ import { $ as refIndex, C as updateTask, D as readSession, E as loadSessions, N as transcriptPath, O as setSessionArchived, Q as linksInto, Y as setIssueArchived, Z as loadHub, _ as setArchived, a as openInApp, b as setMainlineStatus, it as DATA_DIR, nt as ConflictError, q as setFindingArchived, rt as WriteError, v as setChecklistItem, x as setMainlineTitle, y as setCriterion } from "./host-ByGN9mGS.js";
2
2
  import { t as listProgress } from "./progress-1BfPg2zN.js";
3
- import { c as CLI_VERSION } from "./launch-D5uHpqkh.js";
3
+ import { c as CLI_VERSION } from "./launch-B9QrvwZQ.js";
4
4
  import { createRequire } from "node:module";
5
5
  import { closeSync, existsSync, openSync, readFileSync, readSync, statSync, watch } from "node:fs";
6
6
  import { basename, dirname, extname, join, normalize, relative, sep } from "node:path";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mainflow",
3
- "version": "0.10.0",
3
+ "version": "0.11.1",
4
4
  "description": "本機多 AI 協作的主脈絡任務管理:CLI 與介面",
5
5
  "license": "MIT",
6
6
  "type": "module",