@kw-yeh/vibeflow 4.11.30 → 4.11.33

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/vibeflow.mjs CHANGED
@@ -594,6 +594,38 @@ function buildClaudeSettings(worktreePath) {
594
594
  function resolveSystemPrompt(custom) {
595
595
  return custom && custom.trim() ? custom : "";
596
596
  }
597
+ function masterSystemPrompt(opts) {
598
+ const lines = [
599
+ "\u4F60\u662F VibeFlow \u7684 Master agent\uFF0C\u4E0D\u5C6C\u65BC\u4EFB\u4F55 repo\uFF0C\u5DE5\u4F5C\u76EE\u9304\u53EA\u662F\u4F60\u7684\u7B46\u8A18\u5340\u3002",
600
+ "\u4F60\u7684\u5DE5\u4F5C\u662F\u628A\u4F7F\u7528\u8005\u7684\u4EFB\u52D9\u62C6\u89E3\u3001\u6D3E\u7D66 slave \u5361\u7247\u57F7\u884C\u3001\u8FFD\u8E64\u5230\u5B8C\u6210\uFF0C\u800C\u4E0D\u662F\u81EA\u5DF1\u6539\u7A0B\u5F0F\u78BC\u3002",
601
+ ""
602
+ ];
603
+ if (!opts.canWrite) {
604
+ lines.push(
605
+ "\u9019\u6B21\u555F\u52D5\u7121\u6CD5\u4F7F\u7528 board CLI\uFF08\u627E\u4E0D\u5230 VIBEFLOW_CLI\uFF09\uFF0C\u4F60\u4E0D\u80FD\u5EFA\u7ACB\u6216\u555F\u52D5\u5361\u7247\u3002",
606
+ "\u8ACB\u628A\u62C6\u89E3\u7D50\u679C\u6574\u7406\u6210\u5361\u7247\u6E05\u55AE\uFF08\u6A19\u984C\u3001\u5C08\u6848\u8DEF\u5F91\u3001prompt\u3001\u5148\u5F8C\u9806\u5E8F\uFF09\u4EA4\u7D66\u4F7F\u7528\u8005\u624B\u52D5\u5EFA\u7ACB\u3002"
607
+ );
608
+ return lines.join("\n");
609
+ }
610
+ lines.push(
611
+ '\u5148\u5B9A\u7FA9\u9019\u500B shell \u51FD\u5F0F\uFF0C\u4E4B\u5F8C\u6240\u6709\u6307\u4EE4\u90FD\u7528\u5B83\uFF0C\u4E26\u4E14\u4E00\u5F8B\u5E36\u4E0A --store-path "$VIBEFLOW_STORE_DIR"\uFF1A',
612
+ ' VF() { if [ -n "$VIBEFLOW_CLI_LOADER" ]; then node --experimental-strip-types --import "$VIBEFLOW_CLI_LOADER" "$VIBEFLOW_CLI" "$@"; else node "$VIBEFLOW_CLI" "$@"; fi; }',
613
+ "",
614
+ "1. \u5224\u65B7\uFF1A\u4E00\u5F35\u5361\u505A\u5F97\u5B8C\u5C31\u53EA\u5EFA\u4E00\u5F35\uFF1B\u6D89\u53CA\u591A\u500B repo \u6216\u53EF\u5E73\u884C\u7684\u7368\u7ACB\u5DE5\u4F5C\u624D\u62C6\u6210\u591A\u5F35\u3002\u6BCF\u5F35\u53EA\u52D5\u4E00\u500B repo\uFF0C\u5404\u81EA\u80FD\u7368\u7ACB\u9A57\u6536\u3002",
615
+ '2. \u6D3E\u5DE5\uFF1AVF task create --store-path "$VIBEFLOW_STORE_DIR" --master "$VIBEFLOW_TASK_ID" --project <repo \u7D55\u5C0D\u8DEF\u5F91> --title <\u6A19\u984C> --prompt <\u5167\u5BB9>',
616
+ " prompt \u5FC5\u9808\u81EA\u5305\u542B\uFF1A\u80CC\u666F\u3001\u7BC4\u570D\u3001\u9A57\u6536\u689D\u4EF6\u3001\u8207\u5176\u4ED6\u5361\u7684\u4ECB\u9762\u7D04\u5B9A\u2014\u2014slave \u770B\u4E0D\u5230\u4F60\u7684\u5C0D\u8A71\u3002",
617
+ " \u9700\u8981\u6642\u53EF\u52A0 --agent / --model / --effort / --base-branch\u3002"
618
+ );
619
+ lines.push(
620
+ opts.autoMode ? '3. \u555F\u52D5\uFF1A\u6C92\u6709\u524D\u7F6E\u4F9D\u8CF4\u7684\u5361\u76F4\u63A5 VF task start --store-path "$VIBEFLOW_STORE_DIR" --task <id>\uFF1B\u6709\u4F9D\u8CF4\u7684\u5148\u7559\u5728 Backlog\uFF0C\u524D\u7F6E\u5361\u5B8C\u6210\u5F8C\u518D\u555F\u52D5\u3002' : "3. \u555F\u52D5\uFF1A\u4E0D\u8981\u81EA\u5DF1\u555F\u52D5\u5361\u7247\u3002\u5217\u51FA\u4F60\u5EFA\u4E86\u54EA\u4E9B\u5361\u3001\u5EFA\u8B70\u7684\u555F\u52D5\u9806\u5E8F\uFF0C\u7B49\u4F7F\u7528\u8005\u5728 VibeFlow \u6309\u958B\u59CB\u3002"
621
+ );
622
+ lines.push(
623
+ '4. \u76E3\u807D\uFF1AVF task list --store-path "$VIBEFLOW_STORE_DIR" --master "$VIBEFLOW_TASK_ID" \u6703\u56DE\u50B3\u6BCF\u5F35\u5361\u7684\u6B04\u4F4D\u3001\u662F\u5426\u5728\u8DD1\u3001todo \u9032\u5EA6\u3001\u6D3B\u52D5\u72C0\u614B\u8207 PR\u3002',
624
+ " \u5B9A\u671F\u67E5\u770B\uFF1B\u5361\u5728\u7B49\u5F85\u6B0A\u9650\u3001\u6709 launchError\u3001\u6216\u9577\u6642\u9593\u6C92\u6709\u6D3B\u52D5\u6642\uFF0C\u544A\u8A34\u4F7F\u7528\u8005\u4E26\u63D0\u51FA\u8655\u7406\u65B9\u5F0F\u3002",
625
+ "5. \u4F60\u4E0D\u6703\u628A\u5361\u7247\u79FB\u5230 Done\u3001\u4E0D\u6703\u5408\u4F75 PR\u3002\u5168\u90E8 slave \u5B8C\u6210\u5F8C\uFF0C\u6574\u7406\u6BCF\u5F35\u7684\u7D50\u679C\u8207 PR \u9023\u7D50\uFF0C\u4EA4\u7D66\u4F7F\u7528\u8005\u505A\u6700\u5F8C\u78BA\u8A8D\u3002"
626
+ );
627
+ return lines.join("\n");
628
+ }
597
629
  function buildPrompt(task) {
598
630
  const lines = [`\u4EFB\u52D9\u6A19\u984C\uFF1A${task.title}`];
599
631
  const description = task.description?.trim();
@@ -654,7 +686,8 @@ function taskModel(task) {
654
686
  function buildAgentCommand(task, systemPrompt, opts, workspacePath) {
655
687
  const agent = taskAgent(task);
656
688
  const model = taskModel(task);
657
- const sys = resolveSystemPrompt(systemPrompt);
689
+ const custom = resolveSystemPrompt(systemPrompt);
690
+ const sys = task.role === "master" ? [custom, masterSystemPrompt({ canWrite: Boolean(opts?.boardCli?.cliPath), autoMode: opts?.autoMode === true })].filter(Boolean).join("\n\n") : custom;
658
691
  const includeTaskPrompt = opts?.includeTaskPrompt !== false;
659
692
  const prompt = includeTaskPrompt && !opts?.resume ? buildPrompt(task) : "";
660
693
  const sessionId = agent === "claude" && includeTaskPrompt ? executorSessionId(task.id, task.runId) : void 0;
@@ -1116,6 +1149,7 @@ function newFormState(defaultAutoMode, agents) {
1116
1149
  const agentCli = agents && agents.length > 0 && !agents.some((a) => a.id === "claude") ? agents[0].id : "claude";
1117
1150
  return {
1118
1151
  mode: "new",
1152
+ role: "slave",
1119
1153
  title: "",
1120
1154
  description: "",
1121
1155
  projectPath: null,
@@ -1135,6 +1169,7 @@ function editFormState(task, defaultAutoMode) {
1135
1169
  return {
1136
1170
  mode: "edit",
1137
1171
  taskId: task.id,
1172
+ role: task.role === "master" ? "master" : "slave",
1138
1173
  title: task.title,
1139
1174
  description: task.description ?? "",
1140
1175
  projectPath: task.projectPath ?? null,
@@ -1161,8 +1196,11 @@ function visibleFields(s, ctx) {
1161
1196
  const missing = isProjectMissing(ctx.recent, s.projectPath);
1162
1197
  const isRepo = s.gitInfo?.isRepo ?? false;
1163
1198
  const hasRemote = s.gitInfo?.hasRemote ?? false;
1199
+ if (s.role === "master") {
1200
+ return s.mode === "new" ? ["role", "title", "attachments", "description", "effort", "autoMode", "agent", "model", "submit"] : ["title", "description", "effort", "autoMode", "agent", "model", "submit"];
1201
+ }
1164
1202
  if (s.mode === "new") {
1165
- const fields2 = ["title", "project"];
1203
+ const fields2 = ["role", "title", "project"];
1166
1204
  if (s.gitInfo && !missing && !isRepo) fields2.push("gitInit");
1167
1205
  if (isRepo && hasRemote) fields2.push("baseBranch");
1168
1206
  if (isRepo) fields2.push("branch");
@@ -1189,6 +1227,7 @@ function fitEffort(ctx, s) {
1189
1227
  }
1190
1228
  function blockReason(s, ctx, loadingGit) {
1191
1229
  if (!s.title.trim()) return "\u8ACB\u8F38\u5165\u4EFB\u52D9\u6A19\u984C";
1230
+ if (s.role === "master") return null;
1192
1231
  if (loadingGit) return "\u5075\u6E2C Git \u72C0\u614B\u4E2D\u2026";
1193
1232
  const missing = isProjectMissing(ctx.recent, s.projectPath);
1194
1233
  if (s.mode === "new") {
@@ -1205,12 +1244,15 @@ function blockReason(s, ctx, loadingGit) {
1205
1244
  }
1206
1245
  function createPayload(s) {
1207
1246
  const hasRemote = s.gitInfo?.hasRemote ?? false;
1247
+ const target = s.role === "master" ? { role: "master" } : {
1248
+ projectPath: s.projectPath,
1249
+ baseBranch: hasRemote ? s.baseBranch || null : null,
1250
+ branch: s.branch.trim() || void 0
1251
+ };
1208
1252
  return {
1209
1253
  title: s.title.trim(),
1210
1254
  description: s.description.trim(),
1211
- projectPath: s.projectPath,
1212
- baseBranch: hasRemote ? s.baseBranch || null : null,
1213
- branch: s.branch.trim() || void 0,
1255
+ ...target,
1214
1256
  agentCli: s.agentCli,
1215
1257
  model: s.model || void 0,
1216
1258
  effort: s.effort,
@@ -1227,8 +1269,10 @@ function updatePayload(s) {
1227
1269
  model: s.model || void 0,
1228
1270
  effort: s.effort,
1229
1271
  autoMode: s.autoMode,
1230
- ...s.projectPath ? { projectPath: s.projectPath, baseBranch: s.baseBranch || null } : {},
1231
- branch: s.branch.trim()
1272
+ ...s.role === "master" ? {} : {
1273
+ ...s.projectPath ? { projectPath: s.projectPath, baseBranch: s.baseBranch || null } : {},
1274
+ branch: s.branch.trim()
1275
+ }
1232
1276
  };
1233
1277
  }
1234
1278
  function editorCommand(env, platform) {
@@ -1254,7 +1298,7 @@ function cycle(list, current, step) {
1254
1298
  return list[((i < 0 ? 0 : i + step) % list.length + list.length) % list.length];
1255
1299
  }
1256
1300
  function sameForm(a, b) {
1257
- const pick = (s) => [s.title, s.description, s.projectPath, s.baseBranch, s.branch, s.attachments.join("\n"), s.effort, s.autoMode, s.agentCli, s.model];
1301
+ const pick = (s) => [s.role, s.title, s.description, s.projectPath, s.baseBranch, s.branch, s.attachments.join("\n"), s.effort, s.autoMode, s.agentCli, s.model];
1258
1302
  return JSON.stringify(pick(a)) === JSON.stringify(pick(b));
1259
1303
  }
1260
1304
  function FormView({ api, ctx, initial, initialFocus, initialMessage, pristine, onResult }) {
@@ -1352,7 +1396,9 @@ function FormView({ api, ctx, initial, initialFocus, initialMessage, pristine, o
1352
1396
  if (key.downArrow || key.tab) return setFocus((f) => Math.min(fields.length - 1, f + 1));
1353
1397
  const step = key.leftArrow ? -1 : key.rightArrow ? 1 : 0;
1354
1398
  if (step) {
1355
- if (field === "project") {
1399
+ if (field === "role") {
1400
+ setS((prev) => ({ ...prev, role: prev.role === "master" ? "slave" : "master" }));
1401
+ } else if (field === "project") {
1356
1402
  const paths = ctx.recent.map((p) => p.path);
1357
1403
  if (paths.length) setProject(cycle(paths, s.projectPath ?? paths[paths.length - 1], step));
1358
1404
  } else if (field === "baseBranch") {
@@ -1389,6 +1435,8 @@ function FormView({ api, ctx, initial, initialFocus, initialMessage, pristine, o
1389
1435
  const value = (f) => {
1390
1436
  if (editing === f) return `${draft}\u258F`;
1391
1437
  switch (f) {
1438
+ case "role":
1439
+ return s.role === "master" ? "\u2039 Master\uFF1A\u4E0D\u7D81 repo\uFF0C\u62C6\u89E3\u4E26\u6D3E\u5DE5\u7D66\u5176\u4ED6\u5361\u7247 \u203A" : "\u2039 Slave\uFF1A\u5728\u4E00\u500B Git \u5C08\u6848\u88E1\u57F7\u884C \u203A";
1392
1440
  case "title":
1393
1441
  return s.title || "\uFF08\u4F8B\u5982\uFF1A\u5BE6\u4F5C\u767B\u5165\u9801\u9762\uFF09";
1394
1442
  case "project": {
@@ -1534,6 +1582,7 @@ var init_task_form = __esm({
1534
1582
  ];
1535
1583
  DEFAULT_EFFORT = "auto";
1536
1584
  LABELS = {
1585
+ role: "\u89D2\u8272",
1537
1586
  title: "\u4EFB\u52D9\u6A19\u984C",
1538
1587
  project: "\u5C08\u6848\u8CC7\u6599\u593E",
1539
1588
  gitInit: "\u521D\u59CB\u5316 Git",
@@ -1811,13 +1860,16 @@ function BoardView({ opts, initial, initialMessage, onAction }) {
1811
1860
  ...tasks.length === 0 ? [h2(Text2, { key: "empty", dimColor: true }, "\u2014")] : tasks.map((t, ti) => {
1812
1861
  const isSel = active && ti === sel.index;
1813
1862
  const mark = running.has(t.id) ? "\u25CF " : " ";
1863
+ const role = t.role === "master" ? "\u25C6 " : t.masterId ? "\u21B3 " : "";
1864
+ const masterTitle = t.masterId ? COLUMNS2.flatMap((c) => board[c]).find((m) => m.id === t.masterId)?.title : void 0;
1865
+ const detail = t.role === "master" ? `Master \xB7 ${COLUMNS2.flatMap((c) => board[c]).filter((x) => x.masterId === t.id).length} \u500B slave` : `${t.projectName ?? ""} \xB7 ${t.branch}${masterTitle ? ` \xB7 \u25C6 ${masterTitle}` : ""}`;
1814
1866
  const summary = col === "backlog" ? "" : progressSummary(col === "in_progress" ? progress[t.id] : null, t.usage);
1815
1867
  return h2(
1816
1868
  Box2,
1817
1869
  { key: t.id, flexDirection: "column" },
1818
- h2(Text2, { inverse: isSel, color: running.has(t.id) ? "green" : void 0 }, truncate2(`${mark}${t.title}`, colWidth - 4)),
1870
+ h2(Text2, { inverse: isSel, color: running.has(t.id) ? "green" : t.role === "master" ? "magenta" : void 0 }, truncate2(`${mark}${role}${t.title}`, colWidth - 4)),
1819
1871
  summary ? h2(Text2, { dimColor: !isSel, color: isSel ? "cyan" : void 0 }, truncate2(` ${summary}`, colWidth - 4)) : null,
1820
- isSel ? h2(Text2, { dimColor: true }, truncate2(` ${t.projectName ?? ""} \xB7 ${t.branch}`, colWidth - 4)) : null
1872
+ isSel ? h2(Text2, { dimColor: true }, truncate2(` ${detail}`, colWidth - 4)) : null
1821
1873
  );
1822
1874
  })
1823
1875
  );
@@ -2331,6 +2383,9 @@ function recordRecentProject(store, projectPath) {
2331
2383
  }
2332
2384
 
2333
2385
  // packages/core/src/store.ts
2386
+ function isMasterTask(task) {
2387
+ return task.role === "master";
2388
+ }
2334
2389
  var DEFAULT_NOTIFICATION_SETTINGS = {
2335
2390
  enabled: true,
2336
2391
  stepCompleted: false,
@@ -2454,7 +2509,7 @@ function updateTask(taskId, patch) {
2454
2509
  function removeTask(taskId) {
2455
2510
  const board = getStore().get("board");
2456
2511
  Object.keys(board).forEach((col) => {
2457
- board[col] = board[col].filter((t) => t.id !== taskId);
2512
+ board[col] = board[col].filter((t) => t.id !== taskId).map((t) => t.masterId === taskId ? { ...t, masterId: void 0 } : t);
2458
2513
  });
2459
2514
  getStore().set("board", board);
2460
2515
  }
@@ -2520,6 +2575,10 @@ function createBridge(t) {
2520
2575
  listAgentModels: (agentId2, refresh = false) => t.invoke("agents:listModels", { agentId: agentId2, refresh }),
2521
2576
  createTask: (payload) => t.invoke("vibeflow:createTask", payload),
2522
2577
  updateTask: (payload) => t.invoke("vibeflow:updateTask", payload),
2578
+ /** Move a Backlog card to In Progress and start its agent on the host. */
2579
+ startTask: (taskId) => t.invoke("vibeflow:startTask", taskId),
2580
+ /** Cards as a master sees them; filter by master or a single card. */
2581
+ taskSummaries: (filter) => t.invoke("task:summaries", filter),
2523
2582
  /** Clear prior execution state and prepare a fresh run of this task. */
2524
2583
  resetTaskRun: (taskId) => t.invoke("vibeflow:resetTaskRun", taskId),
2525
2584
  removeTask: (taskId) => t.invoke("vibeflow:removeTask", taskId),
@@ -2602,6 +2661,7 @@ function createBridge(t) {
2602
2661
  // Interactive terminal bridge (Phase 3).
2603
2662
  term: {
2604
2663
  readFile: (sessionKey, path33) => t.invoke("terminal:readFile", { sessionKey, path: path33 }),
2664
+ existingFiles: (sessionKey, paths) => t.invoke("terminal:existingFiles", { sessionKey, paths }),
2605
2665
  openFile: (sessionKey, path33) => t.invoke("terminal:openFile", { sessionKey, path: path33 }),
2606
2666
  /**
2607
2667
  * Start a terminal for a task: its agent when `launch` is given, else an
@@ -3181,6 +3241,7 @@ function linkFromRef(ref, state) {
3181
3241
  return { kind: ref.kind, number: ref.number, url: ref.url, state };
3182
3242
  }
3183
3243
  function hasRealBranch(task) {
3244
+ if (task.role === "master") return false;
3184
3245
  return Boolean(task.provisionedAt || task.worktreePath || task.outcome);
3185
3246
  }
3186
3247
  function resolveTaskLinks(tasks, repoOfProject, prIndex) {
@@ -4352,8 +4413,28 @@ async function detectAgents() {
4352
4413
 
4353
4414
  // packages/core/src/tasks.ts
4354
4415
  init_attachments();
4416
+ function mastersRoot(storeFile) {
4417
+ return path13.join(path13.dirname(storeFile), "masters");
4418
+ }
4419
+ function taskError(message, code2) {
4420
+ return Object.assign(new Error(message), { code: code2 });
4421
+ }
4422
+ function assertMasterRef(board, masterId, taskId) {
4423
+ const master = Object.values(board).flat().find((t) => t.id === masterId);
4424
+ if (!master) throw taskError(`\u627E\u4E0D\u5230 master \u5361\u7247\uFF1A${masterId}`, "MASTER_NOT_FOUND");
4425
+ if (!isMasterTask(master)) throw taskError(`\u5361\u7247 ${masterId} \u4E0D\u662F master`, "NOT_A_MASTER");
4426
+ if (taskId && masterId === taskId) throw taskError("\u5361\u7247\u4E0D\u80FD\u6307\u5B9A\u81EA\u5DF1\u70BA master", "NOT_A_MASTER");
4427
+ }
4428
+ async function assertDirectory(dir) {
4429
+ const stat = await fs10.stat(dir).catch(() => null);
4430
+ if (!path13.isAbsolute(dir) || !stat?.isDirectory()) {
4431
+ throw taskError(`\u627E\u4E0D\u5230\u8CC7\u6599\u593E\uFF1A${dir}`, "WORK_DIR_NOT_FOUND");
4432
+ }
4433
+ }
4355
4434
  async function createTaskFromInput(input) {
4356
- const { projectPath } = input;
4435
+ if (input.role === "master") return createMasterTask(input);
4436
+ const projectPath = input.projectPath;
4437
+ if (!projectPath) throw taskError("\u5C1A\u672A\u9078\u64C7\u5C08\u6848\u8CC7\u6599\u593E", "PROJECT_NOT_FOUND");
4357
4438
  try {
4358
4439
  await fs10.access(projectPath);
4359
4440
  } catch {
@@ -4369,6 +4450,7 @@ async function createTaskFromInput(input) {
4369
4450
  const taskId = randomUUID2().slice(0, 8);
4370
4451
  const explicitBranch = input.branch?.trim() || null;
4371
4452
  const store = input.storePath ? getStoreAtPath(input.storePath) : getStore();
4453
+ if (input.masterId) assertMasterRef(store.get("board"), input.masterId);
4372
4454
  const projectName = path13.basename(projectPath);
4373
4455
  const workspacePath = projectWorkstationPath(
4374
4456
  resolveWorkstationPath(store.get("settings")),
@@ -4409,6 +4491,7 @@ async function createTaskFromInput(input) {
4409
4491
  model: input.model || void 0,
4410
4492
  effort: input.effort ?? DEFAULT_TASK_EFFORT,
4411
4493
  autoMode: input.autoMode,
4494
+ ...input.masterId ? { masterId: input.masterId } : {},
4412
4495
  ...input.github ? { github: input.github } : {},
4413
4496
  ...input.jira ? { jira: input.jira } : {}
4414
4497
  };
@@ -4425,7 +4508,54 @@ async function createTaskFromInput(input) {
4425
4508
  }
4426
4509
  return { task, storePath: store.path };
4427
4510
  }
4511
+ async function createMasterTask(input) {
4512
+ if (input.masterId) throw taskError("master \u5361\u7247\u4E0D\u80FD\u518D\u96B8\u5C6C\u5176\u4ED6 master", "NOT_A_MASTER");
4513
+ const workDir = input.workDir?.trim() || void 0;
4514
+ if (workDir) await assertDirectory(workDir);
4515
+ const store = input.storePath ? getStoreAtPath(input.storePath) : getStore();
4516
+ const taskId = randomUUID2().slice(0, 8);
4517
+ const workspacePath = path13.join(mastersRoot(store.path), taskId);
4518
+ const draft = {
4519
+ id: taskId,
4520
+ title: input.title.trim() || `Master ${taskId}`,
4521
+ branch: "",
4522
+ role: "master",
4523
+ workspacePath,
4524
+ ...workDir ? { workDir } : {}
4525
+ };
4526
+ const col = input.status ?? "backlog";
4527
+ const provisioned = col === "backlog" ? null : await provisionTaskWorktree(draft);
4528
+ const attachments = provisioned?.worktreePath ? writeAttachments(provisioned.worktreePath, input.attachments ?? []) : writeAttachmentsTo(taskAttachmentStagingPath(workspacePath, taskId), input.attachments ?? []);
4529
+ const attachmentLines = attachments.map((attachment) => `[\u9644\u4EF6: ${attachment.path}]`);
4530
+ const baseDescription = input.description?.trim();
4531
+ const description = attachmentLines.length ? [baseDescription, attachmentLines.join("\n")].filter(Boolean).join("\n\n") : baseDescription || void 0;
4532
+ const task = {
4533
+ ...draft,
4534
+ ...provisioned,
4535
+ description,
4536
+ createdAt: Date.now(),
4537
+ agentCli: input.agentCli ?? "claude",
4538
+ model: input.model || void 0,
4539
+ effort: input.effort ?? DEFAULT_TASK_EFFORT,
4540
+ autoMode: input.autoMode
4541
+ };
4542
+ try {
4543
+ const board = store.get("board");
4544
+ board[col] = [task, ...board[col]];
4545
+ store.set("board", board);
4546
+ } catch (err) {
4547
+ throw taskError(`Store \u5BEB\u5165\u5931\u6557\uFF1A${err.message}`, "STORE_WRITE_FAILED");
4548
+ }
4549
+ return { task, storePath: store.path };
4550
+ }
4428
4551
  async function provisionTaskWorktree(task) {
4552
+ if (isMasterTask(task)) {
4553
+ if (!task.workspacePath) throw taskError("master \u5361\u7247\u6C92\u6709\u5DE5\u4F5C\u8CC7\u6599\u593E", "WORKTREE_CREATE_FAILED");
4554
+ const worktreePath = task.workDir ?? path13.join(task.workspacePath, "work");
4555
+ await fs10.mkdir(worktreePath, { recursive: true });
4556
+ await fs10.mkdir(task.workspacePath, { recursive: true });
4557
+ return { branch: "", worktreePath, provisionedAt: Date.now() };
4558
+ }
4429
4559
  if (!task.projectPath || !task.workspacePath) {
4430
4560
  throw Object.assign(new Error("\u4EFB\u52D9\u6C92\u6709\u5C08\u6848\u8CC7\u6599\u593E\uFF0C\u7121\u6CD5\u5EFA\u7ACB worktree"), {
4431
4561
  code: "WORKTREE_CREATE_FAILED"
@@ -4469,12 +4599,17 @@ function updateTaskFromInput(input) {
4469
4599
  });
4470
4600
  }
4471
4601
  const current = board[from].find((t) => t.id === input.taskId);
4602
+ if (input.masterId) {
4603
+ if (isMasterTask(current)) throw taskError("master \u5361\u7247\u4E0D\u80FD\u518D\u96B8\u5C6C\u5176\u4ED6 master", "NOT_A_MASTER");
4604
+ assertMasterRef(board, input.masterId, current.id);
4605
+ }
4472
4606
  const title = input.title?.trim();
4473
4607
  const description = input.description?.trim();
4474
4608
  const task = {
4475
4609
  ...current,
4476
4610
  ...title ? { title } : {},
4477
- ...input.description !== void 0 ? { description: description || void 0 } : {}
4611
+ ...input.description !== void 0 ? { description: description || void 0 } : {},
4612
+ ...input.masterId !== void 0 ? { masterId: input.masterId || void 0 } : {}
4478
4613
  };
4479
4614
  const to = input.status ?? from;
4480
4615
  if (to === from) {
@@ -4496,9 +4631,125 @@ function updateTaskFromInput(input) {
4496
4631
 
4497
4632
  // packages/cli/src/task-command.ts
4498
4633
  init_attachments();
4634
+
4635
+ // packages/cli/src/remote.ts
4636
+ import WebSocket from "ws";
4637
+
4638
+ // packages/core/src/ws-transport.ts
4639
+ var OPEN = 1;
4640
+ var MAX_BACKOFF_MS = 5e3;
4641
+ function createWsTransport(options) {
4642
+ let socket = null;
4643
+ let nextId = 1;
4644
+ let connectedOnce = false;
4645
+ let closed = false;
4646
+ let backoff = 250;
4647
+ const queue = [];
4648
+ const pending = /* @__PURE__ */ new Map();
4649
+ const listeners = /* @__PURE__ */ new Map();
4650
+ function flush() {
4651
+ while (socket && socket.readyState === OPEN && queue.length > 0) socket.send(queue.shift());
4652
+ }
4653
+ function open() {
4654
+ if (closed) return;
4655
+ const ws = options.connect();
4656
+ socket = ws;
4657
+ ws.onopen = () => {
4658
+ backoff = 250;
4659
+ options.onStatus?.(true);
4660
+ flush();
4661
+ if (connectedOnce) options.onReconnect?.();
4662
+ connectedOnce = true;
4663
+ };
4664
+ ws.onmessage = (ev) => {
4665
+ let msg;
4666
+ try {
4667
+ msg = JSON.parse(String(ev.data));
4668
+ } catch {
4669
+ return;
4670
+ }
4671
+ if (typeof msg.event === "string") {
4672
+ dispatch(msg.event, msg.payload);
4673
+ return;
4674
+ }
4675
+ const entry = typeof msg.id === "number" ? pending.get(msg.id) : void 0;
4676
+ if (!entry) return;
4677
+ pending.delete(msg.id);
4678
+ if (msg.error) entry.reject(Object.assign(new Error(msg.error.message), { code: msg.error.code }));
4679
+ else entry.resolve(msg.result === null ? void 0 : msg.result);
4680
+ };
4681
+ ws.onerror = () => {
4682
+ };
4683
+ ws.onclose = () => {
4684
+ if (socket !== ws) return;
4685
+ socket = null;
4686
+ options.onStatus?.(false);
4687
+ for (const [id, entry] of Array.from(pending)) {
4688
+ pending.delete(id);
4689
+ entry.reject(new Error("\u8207 VibeFlow core \u7684\u9023\u7DDA\u4E2D\u65B7"));
4690
+ }
4691
+ if (closed) return;
4692
+ setTimeout(open, backoff);
4693
+ backoff = Math.min(backoff * 2, MAX_BACKOFF_MS);
4694
+ };
4695
+ }
4696
+ function dispatch(channel, payload) {
4697
+ for (const cb of Array.from(listeners.get(channel) ?? [])) cb(payload);
4698
+ }
4699
+ function post(message) {
4700
+ const data = JSON.stringify(message);
4701
+ if (socket && socket.readyState === OPEN) socket.send(data);
4702
+ else queue.push(data);
4703
+ }
4704
+ open();
4705
+ return {
4706
+ kind: "ws",
4707
+ invoke(method, ...params) {
4708
+ const id = nextId++;
4709
+ return new Promise((resolve, reject) => {
4710
+ pending.set(id, { resolve, reject });
4711
+ post({ id, method, params });
4712
+ });
4713
+ },
4714
+ send(method, ...params) {
4715
+ post({ method, params });
4716
+ },
4717
+ on(channel, callback) {
4718
+ let set = listeners.get(channel);
4719
+ if (!set) listeners.set(channel, set = /* @__PURE__ */ new Set());
4720
+ set.add(callback);
4721
+ return () => {
4722
+ set.delete(callback);
4723
+ };
4724
+ },
4725
+ close() {
4726
+ closed = true;
4727
+ socket?.close();
4728
+ },
4729
+ dispatch
4730
+ };
4731
+ }
4732
+
4733
+ // packages/cli/src/remote.ts
4734
+ function connectToHost(lock) {
4735
+ const url = `ws://127.0.0.1:${lock.port}/ws`;
4736
+ const transport = createWsTransport({
4737
+ connect: () => new WebSocket(url, {
4738
+ headers: { Authorization: `Bearer ${lock.token}`, Host: `127.0.0.1:${lock.port}` }
4739
+ })
4740
+ });
4741
+ return {
4742
+ api: createBridge(transport),
4743
+ invoke: (method, ...params) => transport.invoke(method, ...params),
4744
+ close: () => transport.close()
4745
+ };
4746
+ }
4747
+
4748
+ // packages/cli/src/task-command.ts
4499
4749
  var AGENT_IDS = AGENT_CLIS.map((agent) => agent.id);
4500
4750
  var STATUSES = ["backlog", "in_progress", "done"];
4501
- var SUBCOMMANDS = ["create", "update", "artifact"];
4751
+ var SUBCOMMANDS = ["create", "update", "start", "list", "show", "artifact"];
4752
+ var ROLES = ["slave", "master"];
4502
4753
  var MODES = ["existing", "new"];
4503
4754
  var AUTO_MODES = ["on", "off"];
4504
4755
  var TASK_USAGE = `
@@ -4507,10 +4758,18 @@ VibeFlow task commands
4507
4758
  Usage:
4508
4759
  vibeflow task create [options]
4509
4760
  vibeflow task update [options]
4761
+ vibeflow task start --task <id> [options]
4762
+ vibeflow task list [--master <id>] [options]
4763
+ vibeflow task show --task <id> [options]
4510
4764
  vibeflow task artifact add --file <path> [options]
4511
4765
 
4512
4766
  task create options:
4513
- --project <path> Target project directory (required)
4767
+ --role <role> ${ROLES.join(" | ")} (default: slave). A master has no project:
4768
+ it runs in its own folder and dispatches other cards.
4769
+ --project <path> Target project directory (required for a slave)
4770
+ --work-dir <path> Master only: run in this existing folder (git or not)
4771
+ instead of one VibeFlow creates
4772
+ --master <id> Dispatch the new card under this master card
4514
4773
  --title <text> Task title (required)
4515
4774
  --prompt <text> Task prompt / description (required)
4516
4775
  --status <column> ${STATUSES.join(" | ")} (default: backlog)
@@ -4534,11 +4793,19 @@ task update options:
4534
4793
  --title <text> New card title
4535
4794
  --prompt <text> New card description (empty string clears it)
4536
4795
  --status <column> ${STATUSES.join(" | ")} \u2014 moves the card between columns
4796
+ --master <id|none> Put the card under a master, or "none" to take it out
4537
4797
  --store-path <dir> Explicit store directory
4538
4798
  --profile <name> dev | prod \u2014 shorthand for common store paths
4539
4799
 
4540
- At least one of --title / --prompt / --status is required. Branch, worktree
4541
- and project path are provisioned on disk and cannot be changed here.
4800
+ At least one of --title / --prompt / --status / --master is required. Branch,
4801
+ worktree and project path are provisioned on disk and cannot be changed here.
4802
+
4803
+ task start / list / show (need the running VibeFlow host):
4804
+ start --task <id> Move a Backlog card to In Progress and start its agent,
4805
+ the same as dragging it in the app
4806
+ list [--master <id>] Every card, or one master's cards, with column, whether
4807
+ its agent is running, todo progress, activity and PR
4808
+ show --task <id> The same summary for one card
4542
4809
 
4543
4810
  task artifact add options:
4544
4811
  --file <path> Copy a file into the card's user-facing Artifacts folder
@@ -4548,7 +4815,7 @@ task artifact add options:
4548
4815
 
4549
4816
  Notes:
4550
4817
  A card created with --status in_progress does NOT start running by itself.
4551
- Every launch is started by hand from the app.
4818
+ Use "task start" on a Backlog card, or start it from the app.
4552
4819
 
4553
4820
  Examples:
4554
4821
  vibeflow task create \\
@@ -4601,6 +4868,9 @@ async function taskCommand(argv) {
4601
4868
  args: argv,
4602
4869
  options: {
4603
4870
  project: { type: "string" },
4871
+ role: { type: "string" },
4872
+ "work-dir": { type: "string" },
4873
+ master: { type: "string" },
4604
4874
  title: { type: "string" },
4605
4875
  prompt: { type: "string" },
4606
4876
  task: { type: "string" },
@@ -4628,7 +4898,7 @@ async function taskCommand(argv) {
4628
4898
  const [sub, action] = positionals;
4629
4899
  if (values.help || !sub) return null;
4630
4900
  const storePath = resolveStorePath(
4631
- values["store-path"] ?? (sub === "artifact" ? process.env.VIBEFLOW_STORE_DIR : void 0),
4901
+ values["store-path"] ?? (sub === "artifact" || HOST_COMMANDS.includes(sub) ? process.env.VIBEFLOW_STORE_DIR : void 0),
4632
4902
  values.profile
4633
4903
  );
4634
4904
  setPlatform(createNodePlatform({ userDataDir: storePath }));
@@ -4643,17 +4913,19 @@ async function taskCommand(argv) {
4643
4913
  if (!task2) fail("TASK_NOT_FOUND", `Task not found: ${taskId}`);
4644
4914
  return { taskId, path: addTaskArtifact(task2, values.file) };
4645
4915
  }
4916
+ if (HOST_COMMANDS.includes(sub)) return hostCommand(sub, values, storePath);
4646
4917
  if (sub === "update") {
4647
4918
  if (!values.task) fail("MISSING_ARGUMENT", "Missing required arguments: --task");
4648
4919
  requireOneOf("--status", values.status, STATUSES);
4649
- if (values.title === void 0 && values.prompt === void 0 && values.status === void 0) {
4650
- fail("MISSING_ARGUMENT", "task update needs at least one of: --title, --prompt, --status");
4920
+ if (values.title === void 0 && values.prompt === void 0 && values.status === void 0 && values.master === void 0) {
4921
+ fail("MISSING_ARGUMENT", "task update needs at least one of: --title, --prompt, --status, --master");
4651
4922
  }
4652
4923
  const { task: task2, storePath: resolvedPath2 } = updateTaskFromInput({
4653
4924
  taskId: values.task,
4654
4925
  title: values.title,
4655
4926
  description: values.prompt,
4656
4927
  status: values.status,
4928
+ masterId: values.master === void 0 ? void 0 : values.master === "none" ? null : values.master,
4657
4929
  storePath
4658
4930
  });
4659
4931
  return {
@@ -4663,12 +4935,19 @@ async function taskCommand(argv) {
4663
4935
  title: task2.title,
4664
4936
  description: task2.description,
4665
4937
  branch: task2.branch,
4666
- worktreePath: task2.worktreePath ?? null
4938
+ worktreePath: task2.worktreePath ?? null,
4939
+ masterId: task2.masterId ?? null
4667
4940
  }
4668
4941
  };
4669
4942
  }
4943
+ requireOneOf("--role", values.role, ROLES);
4944
+ const master = values.role === "master";
4945
+ if (!master && values["work-dir"] !== void 0) fail("INVALID_ARGUMENT", "--work-dir is only for --role master");
4946
+ if (master && (values.project || values["base-branch"] || values.branch || values.mode)) {
4947
+ fail("INVALID_ARGUMENT", "A master card has no project: drop --project / --base-branch / --branch / --mode");
4948
+ }
4670
4949
  const missing = [];
4671
- if (!values.project) missing.push("--project");
4950
+ if (!master && !values.project) missing.push("--project");
4672
4951
  if (!values.title) missing.push("--title");
4673
4952
  if (!values.prompt) missing.push("--prompt");
4674
4953
  if (missing.length) fail("MISSING_ARGUMENT", `Missing required arguments: ${missing.join(", ")}`);
@@ -4685,6 +4964,9 @@ async function taskCommand(argv) {
4685
4964
  }
4686
4965
  const { task, storePath: resolvedPath } = await createTaskFromInput({
4687
4966
  projectPath: values.project,
4967
+ role: master ? "master" : void 0,
4968
+ workDir: values["work-dir"],
4969
+ masterId: values.master,
4688
4970
  title: values.title,
4689
4971
  description: values.prompt,
4690
4972
  status: values.status ?? "backlog",
@@ -4703,7 +4985,9 @@ async function taskCommand(argv) {
4703
4985
  task: {
4704
4986
  id: task.id,
4705
4987
  title: task.title,
4706
- projectPath: task.projectPath,
4988
+ role: task.role ?? "slave",
4989
+ masterId: task.masterId ?? null,
4990
+ projectPath: task.projectPath ?? null,
4707
4991
  branch: task.branch,
4708
4992
  worktreePath: task.worktreePath ?? null,
4709
4993
  baseBranch: task.baseBranch,
@@ -4714,6 +4998,28 @@ async function taskCommand(argv) {
4714
4998
  }
4715
4999
  };
4716
5000
  }
5001
+ var HOST_COMMANDS = ["start", "list", "show"];
5002
+ async function hostCommand(sub, values, storePath) {
5003
+ if (sub !== "list" && !values.task) fail("MISSING_ARGUMENT", "Missing required arguments: --task");
5004
+ const lock = liveHost(storePath);
5005
+ if (!lock) fail("HOST_NOT_RUNNING", `No VibeFlow host is running for ${storePath}. Start it with \`vibeflow\` first.`);
5006
+ const remote = connectToHost(lock);
5007
+ try {
5008
+ if (sub === "start") {
5009
+ const { task } = await remote.api.startTask(values.task);
5010
+ return { task: { id: task.id, title: task.title, worktreePath: task.worktreePath ?? null } };
5011
+ }
5012
+ if (sub === "show") {
5013
+ const [task] = await remote.api.taskSummaries({ taskId: values.task });
5014
+ return { task };
5015
+ }
5016
+ return { tasks: await remote.api.taskSummaries(values.master ? { masterId: values.master } : void 0) };
5017
+ } catch (err) {
5018
+ fail("HOST_ERROR", err.message);
5019
+ } finally {
5020
+ remote.close();
5021
+ }
5022
+ }
4717
5023
 
4718
5024
  // packages/cli/src/host.ts
4719
5025
  import fs25 from "fs";
@@ -5158,6 +5464,17 @@ function resolveTerminalFile(cwd, input) {
5158
5464
  if (target.includes("\0")) throw new Error("\u6A94\u6848\u8DEF\u5F91\u7121\u6548\u3002");
5159
5465
  return path16.resolve(cwd, target);
5160
5466
  }
5467
+ var MAX_FILE_CANDIDATES = 128;
5468
+ async function existingTerminalFiles(cwd, inputs) {
5469
+ const found2 = await Promise.all(inputs.slice(0, MAX_FILE_CANDIDATES).map(async (input) => {
5470
+ try {
5471
+ return (await fs13.stat(resolveTerminalFile(cwd, input))).isFile();
5472
+ } catch {
5473
+ return false;
5474
+ }
5475
+ }));
5476
+ return inputs.filter((_, i) => found2[i]);
5477
+ }
5161
5478
  async function readTerminalFile(cwd, input, metadataOnly = false) {
5162
5479
  let target;
5163
5480
  try {
@@ -6966,6 +7283,9 @@ function createCore({ sessions, bus, version, homeDir, ghRunner, githubAuthStatu
6966
7283
  bus.emit("progress:notify", { taskId, title: task.title, notifications });
6967
7284
  }
6968
7285
  });
7286
+ function gitWorktree(task) {
7287
+ return isMasterTask(task) ? void 0 : task.worktreePath;
7288
+ }
6969
7289
  function progressTarget(task) {
6970
7290
  if (!task.worktreePath) return null;
6971
7291
  const agent = taskAgent(task);
@@ -7143,6 +7463,87 @@ function createCore({ sessions, bus, version, homeDir, ghRunner, githubAuthStatu
7143
7463
  authStatus: jiraAuthStatus ?? (() => getJiraAuthStatus(acliRunner)),
7144
7464
  pointFields: () => getSettings().jira?.storyPointsFields?.length ? getSettings().jira.storyPointsFields : DEFAULT_POINT_FIELDS
7145
7465
  });
7466
+ async function startTaskSession(requested, key, p) {
7467
+ const pending = provisioning.get(requested.id);
7468
+ const task = p.launch ? await ensureProvisioned(requested) : pending ? await pending : requested;
7469
+ const cwd = task.worktreePath;
7470
+ if (!cwd) throw new Error("\u4EFB\u52D9\u5C1A\u672A\u958B\u59CB\u57F7\u884C\uFF0C\u9084\u6C92\u6709 worktree");
7471
+ let command;
7472
+ let fresh = p.fresh === true;
7473
+ if (p.launch) {
7474
+ const intent = obj(p.launch, "launch");
7475
+ if (intent.resume && await sessions.isAlive(key)) {
7476
+ fresh = false;
7477
+ } else {
7478
+ command = (await launchCommand(task, intent)).replace(/\r$/, "");
7479
+ }
7480
+ }
7481
+ if (p.launch) ensureTaskArtifactDirs(task);
7482
+ const result = await sessions.start(key, {
7483
+ cwd,
7484
+ command,
7485
+ fresh,
7486
+ cols: typeof p.cols === "number" ? p.cols : void 0,
7487
+ rows: typeof p.rows === "number" ? p.rows : void 0,
7488
+ // Session ended (natural exit included), so stop its watcher.
7489
+ onExit: () => unwatchSubAgents(key)
7490
+ });
7491
+ terminalDirectories.set(key, cwd);
7492
+ watchSubAgents(key, cwd, (subAgents) => {
7493
+ bus.emit("subagents:update", { taskId: task.id, subAgents });
7494
+ });
7495
+ if (p.launch) {
7496
+ const current = findTask(task.id);
7497
+ if (current) trackProgress(current);
7498
+ }
7499
+ return result;
7500
+ }
7501
+ async function summarizeTasks(filter) {
7502
+ const board = getState().board;
7503
+ const alive = new Set(await sessions.list());
7504
+ const summaries = [];
7505
+ for (const column of COLUMNS) {
7506
+ for (const task of board[column]) {
7507
+ if (filter.taskId && task.id !== filter.taskId) continue;
7508
+ if (filter.masterId && task.masterId !== filter.masterId) continue;
7509
+ if (column === "in_progress" && !progress.isTracking(task.id)) trackProgress(task);
7510
+ const snapshot = progress.isTracking(task.id) ? progress.snapshot(task.id) : null;
7511
+ const todos = snapshot?.todos;
7512
+ const current = todos?.find((t) => t.status === "in_progress");
7513
+ const pr = task.outcome?.pr;
7514
+ summaries.push({
7515
+ id: task.id,
7516
+ title: task.title,
7517
+ column,
7518
+ ...task.role ? { role: task.role } : {},
7519
+ ...task.masterId ? { masterId: task.masterId } : {},
7520
+ ...task.projectName ? { projectName: task.projectName } : {},
7521
+ ...task.branch ? { branch: task.branch } : {},
7522
+ running: alive.has(task.id),
7523
+ ...task.launchedAt ? { launchedAt: task.launchedAt } : {},
7524
+ ...task.launchError ? { launchError: task.launchError } : {},
7525
+ ...todos ? {
7526
+ todos: {
7527
+ done: todos.filter((t) => t.status === "completed").length,
7528
+ total: todos.length,
7529
+ ...current ? { current: current.activeForm ?? current.content } : {}
7530
+ }
7531
+ } : {},
7532
+ ...snapshot ? { activity: snapshot.activity } : {},
7533
+ ...pr ? { pr: { number: pr.number, url: pr.url, state: pr.state } } : {},
7534
+ ...task.outcome ? {
7535
+ outcome: {
7536
+ commits: task.outcome.commits.length,
7537
+ files: task.outcome.files.length,
7538
+ additions: task.outcome.additions,
7539
+ deletions: task.outcome.deletions
7540
+ }
7541
+ } : {}
7542
+ });
7543
+ }
7544
+ }
7545
+ return summaries;
7546
+ }
7146
7547
  const handlers = {
7147
7548
  "vibeflow:getState": () => getState(),
7148
7549
  // A card's progress right now (todo list, usage, activity), for a frontend
@@ -7242,8 +7643,13 @@ function createCore({ sessions, bus, version, homeDir, ghRunner, githubAuthStatu
7242
7643
  },
7243
7644
  "vibeflow:createTask": async (payload) => {
7244
7645
  const p = obj(payload, "payload");
7245
- if (!p.projectPath) throw new Error("\u5C1A\u672A\u9078\u64C7\u5C08\u6848\u8CC7\u6599\u593E");
7246
- str(p.projectPath, "projectPath");
7646
+ if (p.role !== void 0 && p.role !== "master") invalid("unknown role");
7647
+ if (p.role !== "master") {
7648
+ if (!p.projectPath) throw new Error("\u5C1A\u672A\u9078\u64C7\u5C08\u6848\u8CC7\u6599\u593E");
7649
+ str(p.projectPath, "projectPath");
7650
+ }
7651
+ if (p.workDir !== void 0) str(p.workDir, "workDir");
7652
+ if (p.masterId !== void 0) str(p.masterId, "masterId");
7247
7653
  str(p.title ?? "", "title");
7248
7654
  if (p.effort !== void 0 && !AGENT_EFFORTS.includes(p.effort)) invalid("unknown effort");
7249
7655
  if (p.agentCli !== void 0 && !AGENT_IDS2.includes(p.agentCli)) invalid("unknown agent");
@@ -7251,6 +7657,9 @@ function createCore({ sessions, bus, version, homeDir, ghRunner, githubAuthStatu
7251
7657
  if (p.jira !== void 0 && !isJiraRef(p.jira)) invalid("jira must be a Jira ticket ref");
7252
7658
  const { task } = await createTaskFromInput({
7253
7659
  projectPath: p.projectPath,
7660
+ role: p.role,
7661
+ workDir: p.workDir,
7662
+ masterId: p.masterId,
7254
7663
  title: p.title,
7255
7664
  description: p.description,
7256
7665
  baseBranch: p.baseBranch,
@@ -7278,6 +7687,18 @@ function createCore({ sessions, bus, version, homeDir, ghRunner, githubAuthStatu
7278
7687
  // were new. Launching again is the caller's decision.
7279
7688
  "vibeflow:resetTaskRun": async (taskId) => {
7280
7689
  const task = requireTask(taskId);
7690
+ if (isMasterTask(task) && task.worktreePath) {
7691
+ await teardownTask(task.id);
7692
+ if (task.workspacePath) deleteArtifacts(task.workspacePath, task.worktreePath);
7693
+ resetSubAgents(task.worktreePath);
7694
+ const usage2 = usageThroughCurrentRun(task);
7695
+ progress.untrack(task.id);
7696
+ updateTask(task.id, { launchedAt: Date.now(), runId: randomUUID5(), jevRoute: void 0, ...usage2 ? { usage: usage2 } : {} });
7697
+ const reset2 = findTask(task.id);
7698
+ if (!reset2) throw new Error("\u627E\u4E0D\u5230\u8981\u91CD\u7F6E\u7684\u4EFB\u52D9");
7699
+ bus.emit("subagents:update", { taskId: task.id, subAgents: [] });
7700
+ return { state: getState(), task: reset2 };
7701
+ }
7281
7702
  if (!task.worktreePath) {
7282
7703
  await teardownTask(task.id);
7283
7704
  progress.untrack(task.id);
@@ -7314,8 +7735,16 @@ function createCore({ sessions, bus, version, homeDir, ghRunner, githubAuthStatu
7314
7735
  throw new Error("\u53EA\u6709 Backlog \u7684\u4EFB\u52D9\u53EF\u4EE5\u7DE8\u8F2F");
7315
7736
  }
7316
7737
  if (p.effort !== void 0 && !AGENT_EFFORTS.includes(p.effort)) invalid("unknown effort");
7738
+ let masterPatch = {};
7739
+ if (p.masterId !== void 0) {
7740
+ if (p.masterId !== null) {
7741
+ if (isMasterTask(existing)) invalid("a master cannot belong to another master");
7742
+ assertMasterRef(getState().board, str(p.masterId, "masterId"), existing.id);
7743
+ }
7744
+ masterPatch = { masterId: p.masterId || void 0 };
7745
+ }
7317
7746
  let gitPatch = {};
7318
- const nextProject = p.projectPath || existing.projectPath;
7747
+ const nextProject = isMasterTask(existing) ? void 0 : p.projectPath || existing.projectPath;
7319
7748
  const nextBranch = p.branch === void 0 ? "" : str(p.branch, "branch").trim();
7320
7749
  const projectChanged2 = Boolean(p.projectPath && p.projectPath !== existing.projectPath);
7321
7750
  const baseChanged = p.baseBranch != null && p.baseBranch !== (existing.baseBranch ?? null);
@@ -7360,6 +7789,7 @@ function createCore({ sessions, bus, version, homeDir, ghRunner, githubAuthStatu
7360
7789
  jevRoute: void 0,
7361
7790
  effort: p.effort,
7362
7791
  autoMode: p.autoMode,
7792
+ ...masterPatch,
7363
7793
  ...gitPatch
7364
7794
  });
7365
7795
  return getState();
@@ -7370,40 +7800,35 @@ function createCore({ sessions, bus, version, homeDir, ghRunner, githubAuthStatu
7370
7800
  "pty:start": async (payload) => {
7371
7801
  const p = obj(payload, "payload");
7372
7802
  const requested = requireTask(p.taskId);
7373
- const key = sessionKeyFor(requested.id, p.sessionKey);
7374
- const pending = provisioning.get(requested.id);
7375
- const task = p.launch ? await ensureProvisioned(requested) : pending ? await pending : requested;
7376
- const cwd = task.worktreePath;
7377
- if (!cwd) throw new Error("\u4EFB\u52D9\u5C1A\u672A\u958B\u59CB\u57F7\u884C\uFF0C\u9084\u6C92\u6709 worktree");
7378
- let command;
7379
- let fresh = p.fresh === true;
7380
- if (p.launch) {
7381
- const intent = obj(p.launch, "launch");
7382
- if (intent.resume && await sessions.isAlive(key)) {
7383
- fresh = false;
7384
- } else {
7385
- command = (await launchCommand(task, intent)).replace(/\r$/, "");
7386
- }
7387
- }
7388
- if (p.launch) ensureTaskArtifactDirs(task);
7389
- const result = await sessions.start(key, {
7390
- cwd,
7391
- command,
7392
- fresh,
7393
- cols: typeof p.cols === "number" ? p.cols : void 0,
7394
- rows: typeof p.rows === "number" ? p.rows : void 0,
7395
- // Session ended (natural exit included), so stop its watcher.
7396
- onExit: () => unwatchSubAgents(key)
7803
+ return startTaskSession(requested, sessionKeyFor(requested.id, p.sessionKey), p);
7804
+ },
7805
+ // What dragging a card into In Progress does, without a frontend: the card
7806
+ // moves, provisions and starts its agent here, so whoever opens it later
7807
+ // attaches to a run that is already going. This is how a master starts the
7808
+ // cards it dispatched.
7809
+ "vibeflow:startTask": async (taskId) => {
7810
+ const task = requireTask(taskId);
7811
+ const column = columnOf(task.id);
7812
+ if (column !== "backlog") throw new Error(`\u53EA\u6709 Backlog \u7684\u5361\u7247\u53EF\u4EE5\u555F\u52D5\uFF08\u76EE\u524D\u5728 ${column}\uFF09`);
7813
+ const board = getState().board;
7814
+ const started = { ...task, launchedAt: Date.now(), launchError: void 0 };
7815
+ setBoard({
7816
+ backlog: board.backlog.filter((t) => t.id !== task.id),
7817
+ in_progress: [started, ...board.in_progress],
7818
+ done: board.done
7397
7819
  });
7398
- terminalDirectories.set(key, cwd);
7399
- watchSubAgents(key, cwd, (subAgents) => {
7400
- bus.emit("subagents:update", { taskId: task.id, subAgents });
7820
+ bus.emit("state:changed", getState());
7821
+ await startTaskSession(started, task.id, { taskId: task.id, launch: {} });
7822
+ return { state: getState(), task: findTask(task.id) ?? started };
7823
+ },
7824
+ // Every card on the board, or one master's cards, as the CLI and a master's
7825
+ // agent read them.
7826
+ "task:summaries": (payload) => {
7827
+ const p = payload === void 0 ? {} : obj(payload, "payload");
7828
+ return summarizeTasks({
7829
+ masterId: p.masterId === void 0 ? void 0 : requireTask(p.masterId).id,
7830
+ taskId: p.taskId === void 0 ? void 0 : requireTask(p.taskId).id
7401
7831
  });
7402
- if (p.launch) {
7403
- const current = findTask(task.id);
7404
- if (current) trackProgress(current);
7405
- }
7406
- return result;
7407
7832
  },
7408
7833
  "terminal:start": async (payload) => {
7409
7834
  const p = obj(payload, "payload");
@@ -7419,8 +7844,11 @@ function createCore({ sessions, bus, version, homeDir, ghRunner, githubAuthStatu
7419
7844
  if (p.taskId) {
7420
7845
  const requested = requireTask(p.taskId);
7421
7846
  const column = columnOf(requested.id);
7422
- const task = column === "in_progress" && !requested.worktreePath ? await ensureProvisioned(requested) : requested;
7423
- cwd = str(column === "backlog" ? task.projectPath : task.worktreePath ?? task.projectPath, "directory");
7847
+ const task = !requested.worktreePath && (column === "in_progress" || isMasterTask(requested)) ? await ensureProvisioned(requested) : requested;
7848
+ cwd = str(
7849
+ isMasterTask(task) ? task.worktreePath : column === "backlog" ? task.projectPath : task.worktreePath ?? task.projectPath,
7850
+ "directory"
7851
+ );
7424
7852
  projectPath = task.projectPath ?? cwd;
7425
7853
  } else if (p.projectPath) {
7426
7854
  cwd = str(p.projectPath, "projectPath");
@@ -7452,6 +7880,11 @@ function createCore({ sessions, bus, version, homeDir, ghRunner, githubAuthStatu
7452
7880
  const p = obj(payload, "payload");
7453
7881
  return readTerminalFile(terminalDirectory(p.sessionKey), str(p.path, "path"));
7454
7882
  },
7883
+ "terminal:existingFiles": async (payload) => {
7884
+ const p = obj(payload, "payload");
7885
+ if (!Array.isArray(p.paths)) invalid("paths must be an array");
7886
+ return existingTerminalFiles(terminalDirectory(p.sessionKey), p.paths.map((value) => str(value, "path")));
7887
+ },
7455
7888
  "terminal:openFile": async (payload) => {
7456
7889
  const p = obj(payload, "payload");
7457
7890
  const file = await readTerminalFile(terminalDirectory(p.sessionKey), str(p.path, "path"), true);
@@ -7509,8 +7942,9 @@ function createCore({ sessions, bus, version, homeDir, ghRunner, githubAuthStatu
7509
7942
  },
7510
7943
  "git:getDiff": (taskId) => {
7511
7944
  const task = requireTask(taskId);
7512
- if (!task.worktreePath) return [];
7513
- return getWorktreeDiff(task.worktreePath, task.baseBranch ?? "HEAD");
7945
+ const worktree = gitWorktree(task);
7946
+ if (!worktree) return [];
7947
+ return getWorktreeDiff(worktree, task.baseBranch ?? "HEAD");
7514
7948
  },
7515
7949
  /**
7516
7950
  * Changed-file list without content — cheap enough for the sidebar to poll.
@@ -7519,16 +7953,18 @@ function createCore({ sessions, bus, version, homeDir, ghRunner, githubAuthStatu
7519
7953
  */
7520
7954
  "git:getDiffEntries": (taskId, opts) => {
7521
7955
  const task = requireTask(taskId);
7522
- if (!task.worktreePath) return [];
7956
+ const worktree = gitWorktree(task);
7957
+ if (!worktree) return [];
7523
7958
  const fetch2 = opts && typeof opts === "object" ? opts.fetch : void 0;
7524
- return getWorktreeDiffEntries(task.worktreePath, task.baseBranch ?? "HEAD", {
7959
+ return getWorktreeDiffEntries(worktree, task.baseBranch ?? "HEAD", {
7525
7960
  fetch: typeof fetch2 === "boolean" ? fetch2 : void 0
7526
7961
  });
7527
7962
  },
7528
7963
  "git:getDiffFile": (taskId, filePath) => {
7529
7964
  const task = requireTask(taskId);
7530
- if (!task.worktreePath) return null;
7531
- return getWorktreeDiffFile(task.worktreePath, task.baseBranch ?? "HEAD", str(filePath, "filePath"));
7965
+ const worktree = gitWorktree(task);
7966
+ if (!worktree) return null;
7967
+ return getWorktreeDiffFile(worktree, task.baseBranch ?? "HEAD", str(filePath, "filePath"));
7532
7968
  },
7533
7969
  "task:listArtifacts": (taskId) => {
7534
7970
  const task = requireTask(taskId);
@@ -7554,6 +7990,7 @@ function createCore({ sessions, bus, version, homeDir, ghRunner, githubAuthStatu
7554
7990
  "task:getSpecs": async (taskId) => {
7555
7991
  const task = requireTask(taskId);
7556
7992
  const base2 = task.baseBranch ?? "main";
7993
+ if (isMasterTask(task)) return [];
7557
7994
  if (task.worktreePath) return readWorktreeSpecs(task.worktreePath, base2);
7558
7995
  const since = task.provisionedAt ?? task.launchedAt ?? (task.pushed !== void 0 ? task.createdAt : void 0);
7559
7996
  if (!task.projectPath || !since) return [];
@@ -7609,25 +8046,29 @@ function createCore({ sessions, bus, version, homeDir, ghRunner, githubAuthStatu
7609
8046
  "git:approve": async (payload) => {
7610
8047
  const p = obj(payload, "payload");
7611
8048
  const task = requireTask(p.taskId);
7612
- if (!task.worktreePath) throw new Error("\u627E\u4E0D\u5230\u6B64\u4EFB\u52D9\u7684 worktree");
7613
- const result = await commitAndPush(task.worktreePath, str(p.message, "message"));
8049
+ const worktree = gitWorktree(task);
8050
+ if (!worktree) throw new Error("\u627E\u4E0D\u5230\u6B64\u4EFB\u52D9\u7684 worktree");
8051
+ const result = await commitAndPush(worktree, str(p.message, "message"));
7614
8052
  if (result.pushed) updateTask(task.id, { pushed: true });
7615
8053
  return { result, state: getState() };
7616
8054
  },
7617
8055
  "git:generateCommitMessage": (taskId) => {
7618
8056
  const task = requireTask(taskId);
7619
- if (!task.worktreePath) throw new Error("\u627E\u4E0D\u5230\u6B64\u4EFB\u52D9\u7684 worktree");
7620
- return generateCommitMessage(task.worktreePath, task.baseBranch ?? "main", task.agentCli ?? "claude");
8057
+ const worktree = gitWorktree(task);
8058
+ if (!worktree) throw new Error("\u627E\u4E0D\u5230\u6B64\u4EFB\u52D9\u7684 worktree");
8059
+ return generateCommitMessage(worktree, task.baseBranch ?? "main", task.agentCli ?? "claude");
7621
8060
  },
7622
8061
  "git:getPrStatus": (taskId) => {
7623
8062
  const task = requireTask(taskId);
7624
- if (!task.worktreePath) return null;
7625
- return getPrStatus(task.worktreePath);
8063
+ const worktree = gitWorktree(task);
8064
+ if (!worktree) return null;
8065
+ return getPrStatus(worktree);
7626
8066
  },
7627
8067
  "git:getGithubCompareUrl": (taskId) => {
7628
8068
  const task = requireTask(taskId);
7629
- if (!task.worktreePath) return null;
7630
- return getGithubCompareUrl(task.worktreePath, task.baseBranch ?? "main");
8069
+ const worktree = gitWorktree(task);
8070
+ if (!worktree) return null;
8071
+ return getGithubCompareUrl(worktree, task.baseBranch ?? "main");
7631
8072
  },
7632
8073
  // One dialog at a time: a second click while it is open joins it instead
7633
8074
  // of stacking another window on the host.
@@ -7655,8 +8096,9 @@ function createCore({ sessions, bus, version, homeDir, ghRunner, githubAuthStatu
7655
8096
  await teardownTask(task.id);
7656
8097
  cancelChatSend(task.id);
7657
8098
  let outcome;
7658
- if (task.worktreePath) {
7659
- outcome = await captureTaskOutcome(task.worktreePath, task.baseBranch ?? "main") ?? void 0;
8099
+ const worktree = gitWorktree(task);
8100
+ if (worktree) {
8101
+ outcome = await captureTaskOutcome(worktree, task.baseBranch ?? "main") ?? void 0;
7660
8102
  }
7661
8103
  const usage = usageThroughCurrentRun(task);
7662
8104
  progress.untrack(task.id);
@@ -7685,6 +8127,9 @@ function createCore({ sessions, bus, version, homeDir, ghRunner, githubAuthStatu
7685
8127
  if (task.workspacePath) {
7686
8128
  fs21.rmSync(taskAttachmentStagingPath(task.workspacePath, task.id), { recursive: true, force: true });
7687
8129
  }
8130
+ if (isMasterTask(task) && task.workspacePath) {
8131
+ fs21.rmSync(task.workspacePath, { recursive: true, force: true });
8132
+ }
7688
8133
  clearConversation(task.id);
7689
8134
  removeTask(task.id);
7690
8135
  return getState();
@@ -8395,119 +8840,6 @@ function isWebDir(dir) {
8395
8840
  return fs25.existsSync(path28.join(dir, "home", "index.html"));
8396
8841
  }
8397
8842
 
8398
- // packages/cli/src/remote.ts
8399
- import WebSocket from "ws";
8400
-
8401
- // packages/core/src/ws-transport.ts
8402
- var OPEN = 1;
8403
- var MAX_BACKOFF_MS = 5e3;
8404
- function createWsTransport(options) {
8405
- let socket = null;
8406
- let nextId = 1;
8407
- let connectedOnce = false;
8408
- let closed = false;
8409
- let backoff = 250;
8410
- const queue = [];
8411
- const pending = /* @__PURE__ */ new Map();
8412
- const listeners = /* @__PURE__ */ new Map();
8413
- function flush() {
8414
- while (socket && socket.readyState === OPEN && queue.length > 0) socket.send(queue.shift());
8415
- }
8416
- function open() {
8417
- if (closed) return;
8418
- const ws = options.connect();
8419
- socket = ws;
8420
- ws.onopen = () => {
8421
- backoff = 250;
8422
- options.onStatus?.(true);
8423
- flush();
8424
- if (connectedOnce) options.onReconnect?.();
8425
- connectedOnce = true;
8426
- };
8427
- ws.onmessage = (ev) => {
8428
- let msg;
8429
- try {
8430
- msg = JSON.parse(String(ev.data));
8431
- } catch {
8432
- return;
8433
- }
8434
- if (typeof msg.event === "string") {
8435
- dispatch(msg.event, msg.payload);
8436
- return;
8437
- }
8438
- const entry = typeof msg.id === "number" ? pending.get(msg.id) : void 0;
8439
- if (!entry) return;
8440
- pending.delete(msg.id);
8441
- if (msg.error) entry.reject(Object.assign(new Error(msg.error.message), { code: msg.error.code }));
8442
- else entry.resolve(msg.result === null ? void 0 : msg.result);
8443
- };
8444
- ws.onerror = () => {
8445
- };
8446
- ws.onclose = () => {
8447
- if (socket !== ws) return;
8448
- socket = null;
8449
- options.onStatus?.(false);
8450
- for (const [id, entry] of Array.from(pending)) {
8451
- pending.delete(id);
8452
- entry.reject(new Error("\u8207 VibeFlow core \u7684\u9023\u7DDA\u4E2D\u65B7"));
8453
- }
8454
- if (closed) return;
8455
- setTimeout(open, backoff);
8456
- backoff = Math.min(backoff * 2, MAX_BACKOFF_MS);
8457
- };
8458
- }
8459
- function dispatch(channel, payload) {
8460
- for (const cb of Array.from(listeners.get(channel) ?? [])) cb(payload);
8461
- }
8462
- function post(message) {
8463
- const data = JSON.stringify(message);
8464
- if (socket && socket.readyState === OPEN) socket.send(data);
8465
- else queue.push(data);
8466
- }
8467
- open();
8468
- return {
8469
- kind: "ws",
8470
- invoke(method, ...params) {
8471
- const id = nextId++;
8472
- return new Promise((resolve, reject) => {
8473
- pending.set(id, { resolve, reject });
8474
- post({ id, method, params });
8475
- });
8476
- },
8477
- send(method, ...params) {
8478
- post({ method, params });
8479
- },
8480
- on(channel, callback) {
8481
- let set = listeners.get(channel);
8482
- if (!set) listeners.set(channel, set = /* @__PURE__ */ new Set());
8483
- set.add(callback);
8484
- return () => {
8485
- set.delete(callback);
8486
- };
8487
- },
8488
- close() {
8489
- closed = true;
8490
- socket?.close();
8491
- },
8492
- dispatch
8493
- };
8494
- }
8495
-
8496
- // packages/cli/src/remote.ts
8497
- function connectToHost(lock) {
8498
- const url = `ws://127.0.0.1:${lock.port}/ws`;
8499
- const transport = createWsTransport({
8500
- connect: () => new WebSocket(url, {
8501
- headers: { Authorization: `Bearer ${lock.token}`, Host: `127.0.0.1:${lock.port}` }
8502
- })
8503
- });
8504
- return {
8505
- api: createBridge(transport),
8506
- invoke: (method, ...params) => transport.invoke(method, ...params),
8507
- close: () => transport.close()
8508
- };
8509
- }
8510
-
8511
8843
  // packages/cli/src/doctor.ts
8512
8844
  import fs26 from "fs";
8513
8845
  import path29 from "path";