mainflow 0.27.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,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { $ as createFinding, A as duplicateClaimLines, B as setSessionStatus, C as setCriterion, D as supersedeDecision, F as handoffSession, H as startSession, I as loadSessions, L as readSession, N as addAction, O as updateTask, Q as closeIssue, R as setSessionArchived, S as setChecklistItem, T as setMainlineStatus, W as updateAction, Z as appendOps, _ as createMainline, at as setIssueArchived, b as initHub, et as createIssue, ft as MAINLINE_FILE, g as createDecision, h as addRepo, ht as WriteError, it as setFindingStatus, j as sessionBriefing, k as untilState, l as migrateHub, m as addHub, nt as reopenIssue, ot as HubError, p as acceptDecision, pt as TASK_FILE, rt as setFindingArchived, st as loadHub, tt as planIssue, u as retroReport, v as createTask, w as setCriterionText, x as setArchived, y as dropChecklistItem, yt as findHubRoot } from "./host-sSWrUJeC.js";
3
- import { B as parseRef, H as refExample, V as parseUntil, k as UNTIL_FORMS } from "./dist-CBGKXsoi.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-1i4OAsD6.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
9
  import { basename, join, relative } from "node:path";
10
10
  import { fileURLToPath } from "node:url";
@@ -206,7 +206,8 @@ const RUNS = {
206
206
  const { id, file } = createIssue(root, {
207
207
  slug: args[0],
208
208
  title: required(opts, "title", "--title \"重試不穩定的 VM\""),
209
- origin: opts.origin
209
+ origin: opts.origin,
210
+ labels: opts.labels ? list(opts.labels) : void 0
210
211
  });
211
212
  return done(`created ${id} ${relative(root, file)}`);
212
213
  },
@@ -288,6 +289,10 @@ const RUNS = {
288
289
  });
289
290
  return done(`dropped ${args[0]}`);
290
291
  },
292
+ "item undrop": ({ root, args }) => {
293
+ undropChecklistItem(root, args[0]);
294
+ return done(`undropped ${args[0]}`);
295
+ },
291
296
  "issue plan": ({ root, args }) => done(`planned ${planIssue(root, args[0], args[1]).id} in ${args[1]}`),
292
297
  "issue close": ({ root, args, opts }) => done(`closed ${closeIssue(root, args[0], opts.by ?? "").id}`),
293
298
  "issue reopen": ({ root, args }) => done(`reopened ${reopenIssue(root, args[0]).id}`),
@@ -948,6 +948,8 @@ const sessionSchema = z.object({
948
948
  description: text.optional(),
949
949
  depth: z.number().int().nonnegative().optional(),
950
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(),
951
953
  /** Hidden from the default views; independent of status, an ended session stays visible until archived */
952
954
  archivedAt: date.optional()
953
955
  });
package/dist/facts.d.ts CHANGED
@@ -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';
package/dist/facts.js CHANGED
@@ -1,4 +1,4 @@
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-CBGKXsoi.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: {
@@ -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",
@@ -527,7 +533,7 @@ const LIST_ITEM = [
527
533
  field("done", true, "是否已完成,用 check 指令改"),
528
534
  field("evidence", false, "完成的證據,done 為 true 時必填"),
529
535
  field("doneAt", false, "完成的日期,由 CLI 寫入"),
530
- field("dropped", false, "不再需要時為 true,用 item drop 標記"),
536
+ field("dropped", false, "不再需要時為 true,用 item drop 標記,item undrop 撤銷;已放棄的條目不能 check"),
531
537
  field("supersededBy", false, "被哪個參照取代"),
532
538
  field("note", false, "一句備註,例如放棄的原因")
533
539
  ];
@@ -549,7 +555,7 @@ const FIELDS = {
549
555
  field("order", false, "在主脈絡內的順序,小的在前"),
550
556
  field("dependsOn", false, "開始前必須先完成的任務"),
551
557
  field("repo", false, "主要動到的倉庫,名稱需在 config.yaml 登記"),
552
- field("touches", false, "預計會改到的路徑或 glob 清單,相對於 repo 的根目錄;兩條進行中的主脈絡各有未結束的任務在同一個 repo 碰到重疊的路徑、彼此又沒有 depends-on 時,check 列出警告"),
558
+ field("touches", false, "預計會改到的路徑或 glob 清單,相對於 repo 的根目錄;兩條進行中的主脈絡各有未結束的任務在同一個 repo 碰到重疊的路徑、彼此又沒有 depends-on 時,check 列出警告;有 touches 卻沒填 repo 時路徑不會被比對,check 也列警告"),
553
559
  field("startedAt", false, "第一次轉為 doing 時由 CLI 寫入"),
554
560
  field("checklist", true, "checklist 項目清單,每項的欄位見 checklistItem"),
555
561
  field("blockedBy", false, "卡住時的原因、等待對象與繼續的訊號,blocked 時必填;等待對象整串是參照時(例如 m031/t002)會檢查它存在"),
@@ -1,4 +1,4 @@
1
- import { Y as GRANT_FORMS } from "./dist-CBGKXsoi.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 { B as setSessionStatus, G as grantScope, I as loadSessions, J as endsWithMarker, K as matchGrants, L as readSession, M as turnReminder, P as ensureSession, V as settleSession, X as mayDecorate, Y as headerLine, Z as appendOps, _t as DATA_DIR, bt as worktreesOf, gt as readYaml, i as hostName, j as sessionBriefing, ot as HubError, q as decorateReply, st as loadHub, vt as currentBranch, yt as findHubRoot, z as setSessionInfo } from "./host-sSWrUJeC.js";
2
- import { Z as describeRequest, j as configSchema } from "./dist-CBGKXsoi.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>(還沒拆任務時用解決條件 <m#r>)列出行動並用 ${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, {
@@ -1,4 +1,4 @@
1
- import { $ as grantForm, A as WORK_STATUS, B as parseRef, C as SESSION_STATUS, D as TASK_REQUIRED_SECTIONS, E as SURVEY_SECTION, F as issueSchema, G as taskSchema, H as refExample, I as mainlineSchema, K as waitingForAction, L as opsEntrySchema, M as decisionSchema, N as findingSchema, O as TASK_SECTIONS, P as formatOpsEntry, Q as grantCovers, R as parseActionRef, S as SESSION_ID, T as SURVEY_LINES, U as refMainline, V as parseUntil, W as sessionSchema, Z as describeRequest, _ as MERGE_STATES, a as DECISION_SECTIONS, b as RESERVED_HUB_NAMES, c as FINDING_SECTIONS, et as grantRepo, f as ISSUE_SECTIONS, g as MAINLINE_STATUS, h as MAINLINE_SECTIONS, i as DECISION_BASES, j as configSchema, l as FINDING_STATUS, n as CLOSED_STATUS, q as waitingForRef, r as DECIDERS, s as DEFAULT_CATEGORIES, tt as parseGrantTarget, w as SLUG, x as SERVES_REF, y as REF_EXAMPLES, z as parseOpsLine } from "./dist-CBGKXsoi.js";
1
+ import { $ as grantForm, A as WORK_STATUS, B as parseRef, C as SESSION_STATUS, D as TASK_REQUIRED_SECTIONS, E as SURVEY_SECTION, F as issueSchema, G as taskSchema, H as refExample, I as mainlineSchema, K as waitingForAction, L as opsEntrySchema, M as decisionSchema, N as findingSchema, O as TASK_SECTIONS, P as formatOpsEntry, Q as grantCovers, R as parseActionRef, S as SESSION_ID, T as SURVEY_LINES, U as refMainline, V as parseUntil, W as sessionSchema, Z as describeRequest, _ as MERGE_STATES, a as DECISION_SECTIONS, b as RESERVED_HUB_NAMES, c as FINDING_SECTIONS, et as grantRepo, f as ISSUE_SECTIONS, g as MAINLINE_STATUS, h as MAINLINE_SECTIONS, i as DECISION_BASES, j as configSchema, l as FINDING_STATUS, n as CLOSED_STATUS, q as waitingForRef, r as DECIDERS, s as DEFAULT_CATEGORIES, tt as parseGrantTarget, w as SLUG, x as SERVES_REF, y as REF_EXAMPLES, z as parseOpsLine } from "./dist-BbamrIPW.js";
2
2
  import { appendFileSync, closeSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, symlinkSync, writeFileSync, writeSync } from "node:fs";
3
3
  import { basename, dirname, isAbsolute, join, matchesGlob, parse, posix, relative, resolve, sep } from "node:path";
4
4
  import { execFileSync, spawn, spawnSync } from "node:child_process";
@@ -451,16 +451,27 @@ function dependsOn(edges, from, to) {
451
451
  const lands = (ref) => ref === to.mainline || ref.startsWith(`${to.mainline}#`) || ref === `${to.mainline}/${to.task.id}` || ref.startsWith(`${to.mainline}/${to.task.id}#`);
452
452
  return edges.some((e) => fromRefs.includes(e.from) && lands(e.to));
453
453
  }
454
+ /** Unfinished tasks of planning or active mainlines that list `touches`, each with the mainline it sits in. */
455
+ function openTouching(mainlines) {
456
+ return mainlines.filter((m) => COMPARED_MAINLINE.includes(m.status)).flatMap((m) => m.tasks.filter((t) => !CLOSED_STATUS.task.includes(t.status) && t.touches?.length).map((task) => ({
457
+ mainline: m.id,
458
+ task
459
+ })));
460
+ }
461
+ /** Unfinished tasks that list `touches` but no `repo`: their paths have no root, so touchOverlaps skips them. */
462
+ function touchesWithoutRepo(mainlines) {
463
+ return openTouching(mainlines).filter((p) => !p.task.repo).map((p) => ({
464
+ task: `${p.mainline}/${p.task.id}`,
465
+ file: p.task.file
466
+ }));
467
+ }
454
468
  /**
455
469
  * Open tasks of different open mainlines whose `touches` overlap in the same repo and which have no depends-on
456
470
  * between them in either direction, written on the mainline or on the task. Tasks without a repo are not compared,
457
- * since their paths have no root to share.
471
+ * since their paths have no root to share; touchesWithoutRepo names them.
458
472
  */
459
473
  function touchOverlaps(mainlines, edges) {
460
- const open = mainlines.filter((m) => COMPARED_MAINLINE.includes(m.status)).flatMap((m) => m.tasks.filter((t) => !CLOSED_STATUS.task.includes(t.status) && t.repo && t.touches?.length).map((task) => ({
461
- mainline: m.id,
462
- task
463
- })));
474
+ const open = openTouching(mainlines).filter((p) => p.task.repo);
464
475
  const overlaps = [];
465
476
  open.forEach((a, i) => {
466
477
  for (const b of open.slice(i + 1)) {
@@ -715,11 +726,18 @@ function loadHub(root, options = {}) {
715
726
  });
716
727
  const hubIssues = hubDocs(join(dataDir, "issues"), issueSchema);
717
728
  const taskRefs = new Set(mainlines.flatMap((m) => m.tasks.map((t) => `${m.id}/${t.id}`)));
718
- for (const issue of hubIssues) if (issue.plannedIn && !taskRefs.has(issue.plannedIn)) problems.push({
719
- file: issue.file,
720
- field: "plannedIn",
721
- message: `task ${issue.plannedIn} does not exist`
722
- });
729
+ for (const issue of hubIssues) {
730
+ if (issue.plannedIn && !taskRefs.has(issue.plannedIn)) problems.push({
731
+ file: issue.file,
732
+ field: "plannedIn",
733
+ message: `task ${issue.plannedIn} does not exist`
734
+ });
735
+ if (issue.status !== "closed" && !issue.archivedAt && !issue.labels?.length) warnings.push({
736
+ file: issue.file,
737
+ field: "labels",
738
+ message: `issue ${issue.id} is ${issue.status} but has no labels; add labels so it can be found by topic`
739
+ });
740
+ }
723
741
  const findings = hubDocs(join(dataDir, "findings"), findingSchema);
724
742
  const ops = [];
725
743
  const opsPath = join(dataDir, "ops");
@@ -772,6 +790,11 @@ function loadHub(root, options = {}) {
772
790
  message: `${o.tasks[0]} and ${o.tasks[1]} both touch ${pairs} in repo ${o.repo}, and neither depends on the other; coordinate before both run, or add a depends-on link`
773
791
  });
774
792
  }
793
+ for (const t of touchesWithoutRepo(mainlines)) warnings.push({
794
+ file: t.file,
795
+ field: "repo",
796
+ message: `${t.task} lists touches but no repo, so its paths are not compared with other tasks; add the repo they are relative to`
797
+ });
775
798
  return {
776
799
  root,
777
800
  config,
@@ -1093,7 +1116,8 @@ function createIssue(root, input) {
1093
1116
  title: input.title,
1094
1117
  status: "open",
1095
1118
  created: today(),
1096
- ...input.origin ? { origin: input.origin } : {}
1119
+ ...input.origin ? { origin: input.origin } : {},
1120
+ ...input.labels?.length ? { labels: input.labels } : {}
1097
1121
  }, "issue");
1098
1122
  const file = join(dir, `${id}-${input.slug}.md`);
1099
1123
  writeAtomic(file, frontMatterText(issue, ISSUE_BODY));
@@ -1519,9 +1543,11 @@ function requireServes(root, serves) {
1519
1543
  }
1520
1544
  }
1521
1545
  const isOpen$1 = (a) => a.status === "todo" || a.status === "doing";
1546
+ const isCarried$1 = (a) => isOpen$1(a) || a.status === "blocked";
1522
1547
  /**
1523
1548
  * The previous session's unfinished actions, numbered after `existing`, with the previous session marked
1524
- * handed-off. Each carried action is dropped in the previous session with a note naming its new reference,
1549
+ * handed-off. Blocked actions go along with their `blockedBy`, since the wait is still on and whoever is
1550
+ * waited for keeps seeing it. Each carried action is dropped in the previous session with a note naming its new reference,
1525
1551
  * so the work stays open in one place only and the old session still shows where it went. Runs inside the
1526
1552
  * caller's lock.
1527
1553
  */
@@ -1532,7 +1558,7 @@ function takeUnfinished(root, from, to, existing) {
1532
1558
  const carried = [];
1533
1559
  const at = nowMinute();
1534
1560
  const left = previous.actions.map((a) => {
1535
- if (!isOpen$1(a)) return a;
1561
+ if (!isCarried$1(a)) return a;
1536
1562
  const id = nextId("a", ids);
1537
1563
  ids.push(id);
1538
1564
  carried.push({
@@ -1540,8 +1566,9 @@ function takeUnfinished(root, from, to, existing) {
1540
1566
  id
1541
1567
  });
1542
1568
  const handedOff = handedOffNote(`${to}#${id}`);
1569
+ const { blockedBy: _moved, ...rest } = a;
1543
1570
  return {
1544
- ...a,
1571
+ ...rest,
1545
1572
  status: "dropped",
1546
1573
  note: a.note ? `${a.note}; ${handedOff}` : handedOff,
1547
1574
  doneAt: at
@@ -1617,7 +1644,7 @@ function handoffSession(root, id, from) {
1617
1644
  if (session.handoffFrom && session.handoffFrom !== from) throw new WriteError(`session ${id} already took over from ${session.handoffFrom}`);
1618
1645
  const open = session.actions.filter(isOpen$1);
1619
1646
  if (open.length > 0) throw new WriteError(`session ${id} has unfinished actions (${open.map((a) => a.id).join(", ")}); finish or drop them before taking over from ${from}`);
1620
- if (!readYaml(requireSession(root, from), sessionSchema).data.actions.some(isOpen$1)) throw new WriteError(`session ${from} has no unfinished actions to take over`);
1647
+ if (!readYaml(requireSession(root, from), sessionSchema).data.actions.some(isCarried$1)) throw new WriteError(`session ${from} has no unfinished actions to take over`);
1621
1648
  const carried = takeUnfinished(root, from, id, session.actions.map((a) => a.id));
1622
1649
  const next = validate(sessionSchema, {
1623
1650
  ...session,
@@ -1638,6 +1665,13 @@ function setSessionInfo(root, id, info) {
1638
1665
  for (const [key, value] of Object.entries(info)) if (value !== void 0 && value !== null && value !== "") s[key] = value;
1639
1666
  });
1640
1667
  }
1668
+ /** Remember the digest of what the last turn reminder listed in full; null forgets it, so the next reminder lists everything again. */
1669
+ function setReminderDigest(root, id, digest) {
1670
+ mutateYaml(root, requireSession(root, id), sessionSchema, (s) => {
1671
+ if (digest === null) delete s.reminderDigest;
1672
+ else s.reminderDigest = digest;
1673
+ });
1674
+ }
1641
1675
  function setSessionStatus(root, id, status) {
1642
1676
  const value = requireOneOf(status, SESSION_STATUS, "session status");
1643
1677
  return mutateYaml(root, requireSession(root, id), sessionSchema, (s) => {
@@ -1734,6 +1768,8 @@ function updateAction(root, ref, patch) {
1734
1768
  const index = s.actions.findIndex((a) => a.id === action);
1735
1769
  if (index < 0) throw new WriteError(`action ${ref} not found`);
1736
1770
  const [current] = s.actions.splice(index, 1);
1771
+ const movedTo = HANDED_OFF.exec(current.note ?? "")?.[1];
1772
+ if (movedTo && current.status === "dropped" && status !== void 0 && status !== "dropped") throw new WriteError(`${ref} was handed off to ${movedTo}, and references to ${ref} follow its note only while it stays dropped; change ${movedTo} instead`);
1737
1773
  const handedTo = patch.note ? HANDED_OFF.exec(current.note ?? "")?.[1] : void 0;
1738
1774
  if (handedTo) throw new WriteError(`${ref} was handed off to ${handedTo}, and references to ${ref} follow its note there, so the note stays as it is; add the note to ${handedTo} instead`);
1739
1775
  const next = {
@@ -1834,6 +1870,8 @@ function queueWarnings(root, session, cli) {
1834
1870
  }
1835
1871
  const current = (session) => session.actions.find((a) => a.status === "doing");
1836
1872
  const isOpen = (a) => a.status === "todo" || a.status === "doing";
1873
+ /** What a handoff takes over: open actions and blocked ones, which are still unfinished. */
1874
+ const isCarried = (a) => isOpen(a) || a.status === "blocked";
1837
1875
  /** A blocked action and what it waits for: the reason, the other party and the condition, as `action set --status blocked` recorded them. */
1838
1876
  function waitingLine(a) {
1839
1877
  const block = a.blockedBy;
@@ -1925,14 +1963,32 @@ function duplicateClaimLines(root, sessionId, actionId) {
1925
1963
  * over into this one. Subagent sessions are left out: their work returns to the session that delegated it.
1926
1964
  */
1927
1965
  function handoffLines(root, session, cli) {
1928
- const others = loadSessions(root).sessions.filter((s) => s.id !== session.id && !s.delegatedFrom && !s.archivedAt && (s.status === "active" || s.status === "paused") && s.actions.some(isOpen));
1966
+ const others = loadSessions(root).sessions.filter((s) => s.id !== session.id && !s.delegatedFrom && !s.archivedAt && (s.status === "active" || s.status === "paused") && s.actions.some(isCarried));
1929
1967
  if (others.length === 0) return [];
1930
- return [`其他會話還有沒做完的行動。要接續的是其中一場已經停下的工作時,先用 ${cli} session handoff <會話> 把它的行動接進這場會話,再開始做:`, ...others.map((s) => `- ${s.id}${mayBeRunning(root, s) ? "(15 分鐘內還有寫入對話紀錄,可能還在執行,不要接手)" : ""}:${s.actions.filter(isOpen).map((a) => `${a.id} ${a.do}(${a.status},服務 ${a.serves.join("、") || "未指定"})`).join(";")}`)];
1968
+ return [`其他會話還有沒做完的行動。要接續的是其中一場已經停下的工作時,先用 ${cli} session handoff <會話> 把它的行動接進這場會話,再開始做:`, ...others.map((s) => `- ${s.id}${mayBeRunning(root, s) ? "(15 分鐘內還有寫入對話紀錄,可能還在執行,不要接手)" : ""}:${s.actions.filter(isCarried).map((a) => `${a.id} ${a.do}(${a.status},服務 ${a.serves.join("、") || "未指定"})`).join(";")}`)];
1931
1969
  }
1932
1970
  const servedLine = (root, action) => action.serves.map((ref) => {
1933
1971
  const s = describe(root, ref);
1934
1972
  return s ? `${ref} ${s.text}(${s.where})` : `${ref}(找不到這個項目或條件)`;
1935
1973
  }).join(";") || "沒有指定服務的 checklist 項目或解決條件";
1974
+ /**
1975
+ * Lines for each pair of overlapping touches that involves a mainline an action in progress serves: the other
1976
+ * mainline and task, and the paths they share. Empty when the session serves nothing that overlaps.
1977
+ */
1978
+ function overlapLines(root, session) {
1979
+ const mine = new Set(session.actions.filter((a) => a.status === "doing").flatMap((a) => a.serves.map((ref) => parseRef(ref))).flatMap((parsed) => parsed && "mainline" in parsed ? [parsed.mainline] : []));
1980
+ if (mine.size === 0) return [];
1981
+ const hub = loadHub(root, { remotes: false });
1982
+ const lines = [];
1983
+ for (const o of touchOverlaps(hub.mainlines, dependencyGraph(hub).edges)) {
1984
+ const [own, other] = mine.has(o.tasks[0].split("/")[0]) ? [0, 1] : mine.has(o.tasks[1].split("/")[0]) ? [1, 0] : [-1, -1];
1985
+ if (own < 0) continue;
1986
+ const paths = o.paths.map(([a, b]) => own === 0 ? [a, b] : [b, a]).map(([a, b]) => a === b ? a : `${a} ~ ${b}`).join("、");
1987
+ const title = hub.mainlines.find((m) => m.id === o.tasks[other].split("/")[0])?.title;
1988
+ lines.push(`- ${o.tasks[own]} 和 ${o.tasks[other]}${title ? `(${title})` : ""} 都會動到 ${paths}(倉庫 ${o.repo}),兩邊沒有 depends-on`);
1989
+ }
1990
+ return lines;
1991
+ }
1936
1992
  /** Full briefing for the start of a session, after resuming, clearing or compacting. */
1937
1993
  function sessionBriefing(root, session, options) {
1938
1994
  const { cli } = options;
@@ -1957,13 +2013,18 @@ function sessionBriefing(root, session, options) {
1957
2013
  if (blocked.length > 0) lines.push(`等外部條件:${blocked.map(waitingLine).join(";")}`);
1958
2014
  }
1959
2015
  lines.push(...waiterLines(root, session));
2016
+ const overlaps = overlapLines(root, session);
2017
+ if (overlaps.length > 0) lines.push("目前行動服務的主脈絡和其他進行中的主脈絡預計動到同樣的路徑,開始改之前先和對方協調,或加 depends-on:", ...overlaps);
1960
2018
  if (!session.actions.some(isOpen)) lines.push(...handoffLines(root, session, cli));
1961
2019
  const warnings = queueWarnings(root, session, cli);
1962
2020
  if (warnings.length > 0) lines.push("需要處理:", ...warnings.map((w) => `- ${w}`));
1963
2021
  lines.push("工作方式:", `- 開始一件新工作前,先列進佇列:${cli} action add "<做法>" --serves <m/t#c>。主脈絡還沒拆任務時(查核與規劃階段),--serves 指向解決條件 <m#r>。`, `- 開始做標 doing,做完標 done:${cli} action set <a編號> --status doing。`, `- 勾選 checklist 要附證據:${cli} item check <m/t#c> --evidence <commit、檔案:行號或指令輸出>。`, `- 內容直接寫進檔案:意圖或需求寫進 mainline.md 的內文,任務的目標與完成定義寫進 task.md,選擇寫成決策(${cli} new decision <m> <slug> --title <標題> 建檔後再寫內文)。`, `- 改完 .mainflow/ 的檔案跑 ${cli} check,把它列出的問題修到沒有。`, "- 用完其他 skill 或子流程後,回到上面的目前行動再繼續。", "- 要開主脈絡、拆任務、記決策、延後或收尾時,用 mainflow skill,它依序帶過並告訴你資訊該寫在哪裡。");
1964
2022
  return lines.join("\n");
1965
2023
  }
1966
- /** One or two lines for every user turn; null when the session has no actions and the hub has no open mainline. */
2024
+ /**
2025
+ * One or two lines for every user turn; null when the session has no actions and the hub has no open mainline.
2026
+ * Records a digest of the grants, waits and warnings it lists in the session file, only when the digest changes.
2027
+ */
1967
2028
  function turnReminder(root, session, options) {
1968
2029
  if (session.actions.length === 0) return openMainlines(root).length > 0 ? `[mainflow] ${session.id} 還沒有行動。這一輪要推進主脈絡時,先用 ${options.cli} action add 列出行動並標為 doing,或用 ${options.cli} session handoff <會話> 接手上一場沒做完的行動,再開始做。回覆結尾的標頭只看這場會話進行中的行動。` : null;
1969
2030
  const doing = current(session);
@@ -1973,13 +2034,28 @@ function turnReminder(root, session, options) {
1973
2034
  const header = headerLine(session);
1974
2035
  const marker = header ? [`- 這一輪最後一則回覆的最後一行寫「${header}」,回覆範例等其他內容都放在它前面。有顯示 hook 時,它會換成標題與連結。`] : [];
1975
2036
  const grants = grantLines(root, session);
1976
- const granted = grants.length > 0 ? ["- 目前行動服務的主脈絡預先授權了這些動作:", ...grants.map((g) => ` ${g}`)] : [];
2037
+ const waiters = waiterLines(root, session);
2038
+ const warnings = queueWarnings(root, session, options.cli);
2039
+ const digest = contentHash(JSON.stringify([
2040
+ doing?.id ?? null,
2041
+ grants,
2042
+ waiters,
2043
+ warnings
2044
+ ]));
2045
+ const repeated = session.reminderDigest === digest;
2046
+ if (!repeated && (session.reminderDigest !== void 0 || grants.length + waiters.length + warnings.length > 0) && readSession(root, session.id)) try {
2047
+ setReminderDigest(root, session.id, digest);
2048
+ } catch {}
2049
+ const once = (lines, summary, full) => lines.length === 0 ? [] : repeated ? [summary] : full;
2050
+ const granted = once(grants, `- 預先授權同上一輪,共 ${grants.length} 條。`, ["- 目前行動服務的主脈絡預先授權了這些動作:", ...grants.map((g) => ` ${g}`)]);
2051
+ const waiting = once(waiters, `- 等待行同上一輪,共 ${waiters.length} 條。`, waiters.map((w) => `- ${w}`));
2052
+ const warned = once(warnings, `- 佇列提醒同上一輪,共 ${warnings.length} 條。`, warnings.map((w) => `- ${w}`));
1977
2053
  return [
1978
2054
  head,
1979
- ...waiterLines(root, session).map((w) => `- ${w}`),
2055
+ ...waiting,
1980
2056
  ...granted,
1981
2057
  ...marker,
1982
- ...queueWarnings(root, session, options.cli).map((w) => `- ${w}`)
2058
+ ...warned
1983
2059
  ].join("\n");
1984
2060
  }
1985
2061
  //#endregion
@@ -2203,7 +2279,7 @@ function setCriterion(root, refInput, done, evidence, expectedVersion) {
2203
2279
  return mutateFrontMatter(root, mainlineFile(root, ref.mainline), mainlineSchema, (m) => {
2204
2280
  const criterion = m.criteria.find((c) => c.id === ref.criterion);
2205
2281
  if (!criterion) throw new WriteError(`criterion ${refInput} not found`);
2206
- setListItem(criterion, done, evidence);
2282
+ setListItem(criterion, done, evidence, `${refInput} is dropped; remove dropped and supersededBy in ${MAINLINE_FILE} to bring it back before checking it`);
2207
2283
  }, expectedVersion);
2208
2284
  }
2209
2285
  /** Rewrite a criterion's text or doneWhen, leaving done and evidence as they are. */
@@ -2217,14 +2293,17 @@ function setCriterionText(root, refInput, patch, expectedVersion) {
2217
2293
  if (patch.doneWhen !== void 0) criterion.doneWhen = patch.doneWhen;
2218
2294
  }, expectedVersion);
2219
2295
  }
2220
- /** Check or uncheck a criterion or checklist item; checking clears a drop. */
2221
- function setListItem(item, done, evidence) {
2296
+ /**
2297
+ * Check or uncheck a criterion or checklist item. A dropped item cannot be checked, because a drop is a
2298
+ * ruling that the item is no longer needed; `droppedHint` says how to bring it back. Neither direction
2299
+ * touches dropped or supersededBy.
2300
+ */
2301
+ function setListItem(item, done, evidence, droppedHint) {
2302
+ if (done && item.dropped) throw new WriteError(droppedHint);
2222
2303
  item.done = done;
2223
2304
  if (done) {
2224
2305
  item.evidence = evidence;
2225
2306
  item.doneAt = today();
2226
- delete item.dropped;
2227
- delete item.supersededBy;
2228
2307
  } else {
2229
2308
  delete item.evidence;
2230
2309
  delete item.doneAt;
@@ -2299,7 +2378,7 @@ function setChecklistItem(root, refInput, done, evidence, expectedVersion) {
2299
2378
  return mutateFrontMatter(root, taskFile(root, ref.mainline, ref.task), taskSchema, (t) => {
2300
2379
  const item = t.checklist.find((c) => c.id === ref.item);
2301
2380
  if (!item) throw new WriteError(`checklist item ${refInput} not found`);
2302
- setListItem(item, done, evidence);
2381
+ setListItem(item, done, evidence, `${refInput} is dropped; run item undrop ${refInput} to bring it back before checking it`);
2303
2382
  }, expectedVersion);
2304
2383
  }
2305
2384
  /** Mark an item as no longer needed, naming what replaced it when something did. */
@@ -2314,6 +2393,20 @@ function dropChecklistItem(root, refInput, input = {}, expectedVersion) {
2314
2393
  if (input.note) item.note = input.note;
2315
2394
  }, expectedVersion);
2316
2395
  }
2396
+ /** Bring a dropped item back as an open one: clears dropped and supersededBy, keeps the note. */
2397
+ function undropChecklistItem(root, refInput, expectedVersion) {
2398
+ const ref = requireRef(refInput, "checklistItem");
2399
+ return mutateFrontMatter(root, taskFile(root, ref.mainline, ref.task), taskSchema, (t) => {
2400
+ const item = t.checklist.find((c) => c.id === ref.item);
2401
+ if (!item) throw new WriteError(`checklist item ${refInput} not found`);
2402
+ if (!item.dropped) throw new WriteError(`${refInput} is not dropped`);
2403
+ delete item.dropped;
2404
+ delete item.supersededBy;
2405
+ item.done = false;
2406
+ delete item.evidence;
2407
+ delete item.doneAt;
2408
+ }, expectedVersion);
2409
+ }
2317
2410
  function createDecision(root, mainline, input) {
2318
2411
  requireRef(mainline, "mainline");
2319
2412
  requireSlug(input.slug);
@@ -3172,4 +3265,4 @@ function openInApp(hub, target, closeAfterMs = 150) {
3172
3265
  return true;
3173
3266
  }
3174
3267
  //#endregion
3175
- export { createFinding as $, duplicateClaimLines as A, setSessionStatus as B, setCriterion as C, supersedeDecision as D, setMainlineTitle as E, handoffSession as F, grantScope as G, startSession as H, loadSessions as I, endsWithMarker as J, matchGrants as K, readSession as L, turnReminder as M, addAction as N, updateTask as O, ensureSession as P, closeIssue as Q, setSessionArchived as R, setChecklistItem as S, setMainlineStatus as T, transcriptPath as U, settleSession as V, updateAction as W, mayDecorate as X, headerLine as Y, appendOps as Z, createMainline as _, DATA_DIR as _t, openInApp as a, setIssueArchived as at, initHub as b, worktreesOf as bt, syncHost as c, dependencyGraph as ct, ARCHIVE_KINDS as d, refIndex as dt, createIssue as et, setArchivedMany as f, MAINLINE_FILE as ft, createDecision as g, readYaml as gt, addRepo as h, WriteError as ht, hostName as i, setFindingStatus as it, sessionBriefing as j, untilState as k, migrateHub as l, mainlineEdges as lt, addHub as m, ConflictError as mt, DEFAULT_UI_PORT as n, reopenIssue as nt, removeHostLink as o, HubError as ot, acceptDecision as p, TASK_FILE as pt, decorateReply as q, caddySites as r, setFindingArchived as rt, siteBlock as s, loadHub as st, APP_BUNDLE_ID as t, planIssue as tt, retroReport as u, linksInto as ut, createTask as v, currentBranch as vt, setCriterionText as w, setArchived as x, dropChecklistItem as y, findHubRoot as yt, setSessionInfo as z };
3268
+ export { appendOps as $, untilState as A, setSessionArchived as B, setCriterion as C, supersedeDecision as D, setMainlineTitle as E, ensureSession as F, transcriptPath as G, setSessionStatus as H, handoffSession as I, matchGrants as J, updateAction as K, loadSessions as L, sessionBriefing as M, turnReminder as N, undropChecklistItem as O, addAction as P, mayDecorate as Q, readSession as R, setChecklistItem as S, worktreesOf as St, setMainlineStatus as T, settleSession as U, setSessionInfo as V, startSession as W, endsWithMarker as X, decorateReply as Y, headerLine as Z, createMainline as _, WriteError as _t, openInApp as a, setFindingArchived as at, initHub as b, currentBranch as bt, syncHost as c, HubError as ct, ARCHIVE_KINDS as d, mainlineEdges as dt, closeIssue as et, setArchivedMany as f, linksInto as ft, createDecision as g, ConflictError as gt, addRepo as h, TASK_FILE as ht, hostName as i, reopenIssue as it, duplicateClaimLines as j, updateTask as k, migrateHub as l, loadHub as lt, addHub as m, MAINLINE_FILE as mt, DEFAULT_UI_PORT as n, createIssue as nt, removeHostLink as o, setFindingStatus as ot, acceptDecision as p, refIndex as pt, grantScope as q, caddySites as r, planIssue as rt, siteBlock as s, setIssueArchived as st, APP_BUNDLE_ID as t, createFinding as tt, retroReport as u, dependencyGraph as ut, createTask as v, readYaml as vt, setCriterionText as w, setArchived as x, findHubRoot as xt, dropChecklistItem as y, DATA_DIR as yt, setReminderDigest as z };
package/dist/host.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as openInApp, c as syncHost, i as hostName, n as DEFAULT_UI_PORT, o as removeHostLink, r as caddySites, s as siteBlock, t as APP_BUNDLE_ID } from "./host-sSWrUJeC.js";
1
+ import { a as openInApp, c as syncHost, i as hostName, n as DEFAULT_UI_PORT, o as removeHostLink, r as caddySites, s as siteBlock, t as APP_BUNDLE_ID } from "./host-CeN3dvXg.js";
2
2
  export { APP_BUNDLE_ID, DEFAULT_UI_PORT, caddySites, hostName, openInApp, removeHostLink, siteBlock, syncHost };
@@ -1,4 +1,4 @@
1
- import { _t as DATA_DIR, c as syncHost, ht as WriteError, i as hostName, n as DEFAULT_UI_PORT, o as removeHostLink } from "./host-sSWrUJeC.js";
1
+ import { _t as WriteError, c as syncHost, i as hostName, n as DEFAULT_UI_PORT, o as removeHostLink, yt as DATA_DIR } from "./host-CeN3dvXg.js";
2
2
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { spawn, spawnSync } from "node:child_process";
@@ -176,7 +176,7 @@ async function launchUi(root, hub, configured, options) {
176
176
  }
177
177
  let server;
178
178
  if (!reuse) {
179
- const { serveUi } = await import("./ui-DcBLvgqY.js");
179
+ const { serveUi } = await import("./ui-D_9x6XNs.js");
180
180
  server = await serveUi(root, choice.port);
181
181
  writeUiRecord(root, {
182
182
  port: choice.port,
package/dist/launch.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as portCandidates, c as watchHubGone, i as networkProbe, n as choosePort, o as readUiRecord, r as launchUi, s as startUiInBackground, t as PORT_SPAN } from "./launch-1i4OAsD6.js";
1
+ import { a as portCandidates, c as watchHubGone, i as networkProbe, n as choosePort, o as readUiRecord, r as launchUi, s as startUiInBackground, t as PORT_SPAN } from "./launch-BKKoCyTj.js";
2
2
  export { PORT_SPAN, choosePort, launchUi, networkProbe, portCandidates, readUiRecord, startUiInBackground, watchHubGone };
@@ -1,6 +1,6 @@
1
- import { C as setCriterion, E as setMainlineTitle, I as loadSessions, L as readSession, O as updateTask, R as setSessionArchived, S as setChecklistItem, T as setMainlineStatus, U as transcriptPath, _t as DATA_DIR, a as openInApp, at as setIssueArchived, ct as dependencyGraph, d as ARCHIVE_KINDS, dt as refIndex, f as setArchivedMany, ht as WriteError, lt as mainlineEdges, mt as ConflictError, rt as setFindingArchived, st as loadHub, ut as linksInto, x as setArchived } from "./host-sSWrUJeC.js";
1
+ import { B as setSessionArchived, C as setCriterion, E as setMainlineTitle, G as transcriptPath, L as loadSessions, R as readSession, S as setChecklistItem, T as setMainlineStatus, _t as WriteError, a as openInApp, at as setFindingArchived, d as ARCHIVE_KINDS, dt as mainlineEdges, f as setArchivedMany, ft as linksInto, gt as ConflictError, k as updateTask, lt as loadHub, pt as refIndex, st as setIssueArchived, ut as dependencyGraph, x as setArchived, yt as DATA_DIR } from "./host-CeN3dvXg.js";
2
2
  import { t as listProgress } from "./progress-1BfPg2zN.js";
3
- import { l as CLI_VERSION } from "./launch-1i4OAsD6.js";
3
+ import { l as CLI_VERSION } from "./launch-BKKoCyTj.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.27.0",
3
+ "version": "0.28.0",
4
4
  "description": "本機多 AI 協作的主脈絡任務管理:CLI 與介面",
5
5
  "license": "MIT",
6
6
  "type": "module",