@kw-yeh/vibeflow 4.11.32 → 4.11.34

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),
@@ -3182,6 +3241,7 @@ function linkFromRef(ref, state) {
3182
3241
  return { kind: ref.kind, number: ref.number, url: ref.url, state };
3183
3242
  }
3184
3243
  function hasRealBranch(task) {
3244
+ if (task.role === "master") return false;
3185
3245
  return Boolean(task.provisionedAt || task.worktreePath || task.outcome);
3186
3246
  }
3187
3247
  function resolveTaskLinks(tasks, repoOfProject, prIndex) {
@@ -4353,8 +4413,28 @@ async function detectAgents() {
4353
4413
 
4354
4414
  // packages/core/src/tasks.ts
4355
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
+ }
4356
4434
  async function createTaskFromInput(input) {
4357
- 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");
4358
4438
  try {
4359
4439
  await fs10.access(projectPath);
4360
4440
  } catch {
@@ -4370,6 +4450,7 @@ async function createTaskFromInput(input) {
4370
4450
  const taskId = randomUUID2().slice(0, 8);
4371
4451
  const explicitBranch = input.branch?.trim() || null;
4372
4452
  const store = input.storePath ? getStoreAtPath(input.storePath) : getStore();
4453
+ if (input.masterId) assertMasterRef(store.get("board"), input.masterId);
4373
4454
  const projectName = path13.basename(projectPath);
4374
4455
  const workspacePath = projectWorkstationPath(
4375
4456
  resolveWorkstationPath(store.get("settings")),
@@ -4410,6 +4491,7 @@ async function createTaskFromInput(input) {
4410
4491
  model: input.model || void 0,
4411
4492
  effort: input.effort ?? DEFAULT_TASK_EFFORT,
4412
4493
  autoMode: input.autoMode,
4494
+ ...input.masterId ? { masterId: input.masterId } : {},
4413
4495
  ...input.github ? { github: input.github } : {},
4414
4496
  ...input.jira ? { jira: input.jira } : {}
4415
4497
  };
@@ -4426,7 +4508,54 @@ async function createTaskFromInput(input) {
4426
4508
  }
4427
4509
  return { task, storePath: store.path };
4428
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
+ }
4429
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
+ }
4430
4559
  if (!task.projectPath || !task.workspacePath) {
4431
4560
  throw Object.assign(new Error("\u4EFB\u52D9\u6C92\u6709\u5C08\u6848\u8CC7\u6599\u593E\uFF0C\u7121\u6CD5\u5EFA\u7ACB worktree"), {
4432
4561
  code: "WORKTREE_CREATE_FAILED"
@@ -4470,12 +4599,17 @@ function updateTaskFromInput(input) {
4470
4599
  });
4471
4600
  }
4472
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
+ }
4473
4606
  const title = input.title?.trim();
4474
4607
  const description = input.description?.trim();
4475
4608
  const task = {
4476
4609
  ...current,
4477
4610
  ...title ? { title } : {},
4478
- ...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 } : {}
4479
4613
  };
4480
4614
  const to = input.status ?? from;
4481
4615
  if (to === from) {
@@ -4497,9 +4631,125 @@ function updateTaskFromInput(input) {
4497
4631
 
4498
4632
  // packages/cli/src/task-command.ts
4499
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
4500
4749
  var AGENT_IDS = AGENT_CLIS.map((agent) => agent.id);
4501
4750
  var STATUSES = ["backlog", "in_progress", "done"];
4502
- var SUBCOMMANDS = ["create", "update", "artifact"];
4751
+ var SUBCOMMANDS = ["create", "update", "start", "list", "show", "artifact"];
4752
+ var ROLES = ["slave", "master"];
4503
4753
  var MODES = ["existing", "new"];
4504
4754
  var AUTO_MODES = ["on", "off"];
4505
4755
  var TASK_USAGE = `
@@ -4508,10 +4758,18 @@ VibeFlow task commands
4508
4758
  Usage:
4509
4759
  vibeflow task create [options]
4510
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]
4511
4764
  vibeflow task artifact add --file <path> [options]
4512
4765
 
4513
4766
  task create options:
4514
- --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
4515
4773
  --title <text> Task title (required)
4516
4774
  --prompt <text> Task prompt / description (required)
4517
4775
  --status <column> ${STATUSES.join(" | ")} (default: backlog)
@@ -4535,11 +4793,19 @@ task update options:
4535
4793
  --title <text> New card title
4536
4794
  --prompt <text> New card description (empty string clears it)
4537
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
4538
4797
  --store-path <dir> Explicit store directory
4539
4798
  --profile <name> dev | prod \u2014 shorthand for common store paths
4540
4799
 
4541
- At least one of --title / --prompt / --status is required. Branch, worktree
4542
- 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
4543
4809
 
4544
4810
  task artifact add options:
4545
4811
  --file <path> Copy a file into the card's user-facing Artifacts folder
@@ -4549,7 +4815,7 @@ task artifact add options:
4549
4815
 
4550
4816
  Notes:
4551
4817
  A card created with --status in_progress does NOT start running by itself.
4552
- Every launch is started by hand from the app.
4818
+ Use "task start" on a Backlog card, or start it from the app.
4553
4819
 
4554
4820
  Examples:
4555
4821
  vibeflow task create \\
@@ -4602,6 +4868,9 @@ async function taskCommand(argv) {
4602
4868
  args: argv,
4603
4869
  options: {
4604
4870
  project: { type: "string" },
4871
+ role: { type: "string" },
4872
+ "work-dir": { type: "string" },
4873
+ master: { type: "string" },
4605
4874
  title: { type: "string" },
4606
4875
  prompt: { type: "string" },
4607
4876
  task: { type: "string" },
@@ -4629,7 +4898,7 @@ async function taskCommand(argv) {
4629
4898
  const [sub, action] = positionals;
4630
4899
  if (values.help || !sub) return null;
4631
4900
  const storePath = resolveStorePath(
4632
- 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),
4633
4902
  values.profile
4634
4903
  );
4635
4904
  setPlatform(createNodePlatform({ userDataDir: storePath }));
@@ -4644,17 +4913,19 @@ async function taskCommand(argv) {
4644
4913
  if (!task2) fail("TASK_NOT_FOUND", `Task not found: ${taskId}`);
4645
4914
  return { taskId, path: addTaskArtifact(task2, values.file) };
4646
4915
  }
4916
+ if (HOST_COMMANDS.includes(sub)) return hostCommand(sub, values, storePath);
4647
4917
  if (sub === "update") {
4648
4918
  if (!values.task) fail("MISSING_ARGUMENT", "Missing required arguments: --task");
4649
4919
  requireOneOf("--status", values.status, STATUSES);
4650
- if (values.title === void 0 && values.prompt === void 0 && values.status === void 0) {
4651
- 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");
4652
4922
  }
4653
4923
  const { task: task2, storePath: resolvedPath2 } = updateTaskFromInput({
4654
4924
  taskId: values.task,
4655
4925
  title: values.title,
4656
4926
  description: values.prompt,
4657
4927
  status: values.status,
4928
+ masterId: values.master === void 0 ? void 0 : values.master === "none" ? null : values.master,
4658
4929
  storePath
4659
4930
  });
4660
4931
  return {
@@ -4664,12 +4935,19 @@ async function taskCommand(argv) {
4664
4935
  title: task2.title,
4665
4936
  description: task2.description,
4666
4937
  branch: task2.branch,
4667
- worktreePath: task2.worktreePath ?? null
4938
+ worktreePath: task2.worktreePath ?? null,
4939
+ masterId: task2.masterId ?? null
4668
4940
  }
4669
4941
  };
4670
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
+ }
4671
4949
  const missing = [];
4672
- if (!values.project) missing.push("--project");
4950
+ if (!master && !values.project) missing.push("--project");
4673
4951
  if (!values.title) missing.push("--title");
4674
4952
  if (!values.prompt) missing.push("--prompt");
4675
4953
  if (missing.length) fail("MISSING_ARGUMENT", `Missing required arguments: ${missing.join(", ")}`);
@@ -4686,6 +4964,9 @@ async function taskCommand(argv) {
4686
4964
  }
4687
4965
  const { task, storePath: resolvedPath } = await createTaskFromInput({
4688
4966
  projectPath: values.project,
4967
+ role: master ? "master" : void 0,
4968
+ workDir: values["work-dir"],
4969
+ masterId: values.master,
4689
4970
  title: values.title,
4690
4971
  description: values.prompt,
4691
4972
  status: values.status ?? "backlog",
@@ -4704,7 +4985,9 @@ async function taskCommand(argv) {
4704
4985
  task: {
4705
4986
  id: task.id,
4706
4987
  title: task.title,
4707
- projectPath: task.projectPath,
4988
+ role: task.role ?? "slave",
4989
+ masterId: task.masterId ?? null,
4990
+ projectPath: task.projectPath ?? null,
4708
4991
  branch: task.branch,
4709
4992
  worktreePath: task.worktreePath ?? null,
4710
4993
  baseBranch: task.baseBranch,
@@ -4715,6 +4998,28 @@ async function taskCommand(argv) {
4715
4998
  }
4716
4999
  };
4717
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
+ }
4718
5023
 
4719
5024
  // packages/cli/src/host.ts
4720
5025
  import fs25 from "fs";
@@ -6978,6 +7283,9 @@ function createCore({ sessions, bus, version, homeDir, ghRunner, githubAuthStatu
6978
7283
  bus.emit("progress:notify", { taskId, title: task.title, notifications });
6979
7284
  }
6980
7285
  });
7286
+ function gitWorktree(task) {
7287
+ return isMasterTask(task) ? void 0 : task.worktreePath;
7288
+ }
6981
7289
  function progressTarget(task) {
6982
7290
  if (!task.worktreePath) return null;
6983
7291
  const agent = taskAgent(task);
@@ -7155,6 +7463,87 @@ function createCore({ sessions, bus, version, homeDir, ghRunner, githubAuthStatu
7155
7463
  authStatus: jiraAuthStatus ?? (() => getJiraAuthStatus(acliRunner)),
7156
7464
  pointFields: () => getSettings().jira?.storyPointsFields?.length ? getSettings().jira.storyPointsFields : DEFAULT_POINT_FIELDS
7157
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
+ }
7158
7547
  const handlers = {
7159
7548
  "vibeflow:getState": () => getState(),
7160
7549
  // A card's progress right now (todo list, usage, activity), for a frontend
@@ -7254,8 +7643,13 @@ function createCore({ sessions, bus, version, homeDir, ghRunner, githubAuthStatu
7254
7643
  },
7255
7644
  "vibeflow:createTask": async (payload) => {
7256
7645
  const p = obj(payload, "payload");
7257
- if (!p.projectPath) throw new Error("\u5C1A\u672A\u9078\u64C7\u5C08\u6848\u8CC7\u6599\u593E");
7258
- 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");
7259
7653
  str(p.title ?? "", "title");
7260
7654
  if (p.effort !== void 0 && !AGENT_EFFORTS.includes(p.effort)) invalid("unknown effort");
7261
7655
  if (p.agentCli !== void 0 && !AGENT_IDS2.includes(p.agentCli)) invalid("unknown agent");
@@ -7263,6 +7657,9 @@ function createCore({ sessions, bus, version, homeDir, ghRunner, githubAuthStatu
7263
7657
  if (p.jira !== void 0 && !isJiraRef(p.jira)) invalid("jira must be a Jira ticket ref");
7264
7658
  const { task } = await createTaskFromInput({
7265
7659
  projectPath: p.projectPath,
7660
+ role: p.role,
7661
+ workDir: p.workDir,
7662
+ masterId: p.masterId,
7266
7663
  title: p.title,
7267
7664
  description: p.description,
7268
7665
  baseBranch: p.baseBranch,
@@ -7290,6 +7687,18 @@ function createCore({ sessions, bus, version, homeDir, ghRunner, githubAuthStatu
7290
7687
  // were new. Launching again is the caller's decision.
7291
7688
  "vibeflow:resetTaskRun": async (taskId) => {
7292
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
+ }
7293
7702
  if (!task.worktreePath) {
7294
7703
  await teardownTask(task.id);
7295
7704
  progress.untrack(task.id);
@@ -7326,8 +7735,16 @@ function createCore({ sessions, bus, version, homeDir, ghRunner, githubAuthStatu
7326
7735
  throw new Error("\u53EA\u6709 Backlog \u7684\u4EFB\u52D9\u53EF\u4EE5\u7DE8\u8F2F");
7327
7736
  }
7328
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
+ }
7329
7746
  let gitPatch = {};
7330
- const nextProject = p.projectPath || existing.projectPath;
7747
+ const nextProject = isMasterTask(existing) ? void 0 : p.projectPath || existing.projectPath;
7331
7748
  const nextBranch = p.branch === void 0 ? "" : str(p.branch, "branch").trim();
7332
7749
  const projectChanged2 = Boolean(p.projectPath && p.projectPath !== existing.projectPath);
7333
7750
  const baseChanged = p.baseBranch != null && p.baseBranch !== (existing.baseBranch ?? null);
@@ -7372,6 +7789,7 @@ function createCore({ sessions, bus, version, homeDir, ghRunner, githubAuthStatu
7372
7789
  jevRoute: void 0,
7373
7790
  effort: p.effort,
7374
7791
  autoMode: p.autoMode,
7792
+ ...masterPatch,
7375
7793
  ...gitPatch
7376
7794
  });
7377
7795
  return getState();
@@ -7382,40 +7800,35 @@ function createCore({ sessions, bus, version, homeDir, ghRunner, githubAuthStatu
7382
7800
  "pty:start": async (payload) => {
7383
7801
  const p = obj(payload, "payload");
7384
7802
  const requested = requireTask(p.taskId);
7385
- const key = sessionKeyFor(requested.id, p.sessionKey);
7386
- const pending = provisioning.get(requested.id);
7387
- const task = p.launch ? await ensureProvisioned(requested) : pending ? await pending : requested;
7388
- const cwd = task.worktreePath;
7389
- if (!cwd) throw new Error("\u4EFB\u52D9\u5C1A\u672A\u958B\u59CB\u57F7\u884C\uFF0C\u9084\u6C92\u6709 worktree");
7390
- let command;
7391
- let fresh = p.fresh === true;
7392
- if (p.launch) {
7393
- const intent = obj(p.launch, "launch");
7394
- if (intent.resume && await sessions.isAlive(key)) {
7395
- fresh = false;
7396
- } else {
7397
- command = (await launchCommand(task, intent)).replace(/\r$/, "");
7398
- }
7399
- }
7400
- if (p.launch) ensureTaskArtifactDirs(task);
7401
- const result = await sessions.start(key, {
7402
- cwd,
7403
- command,
7404
- fresh,
7405
- cols: typeof p.cols === "number" ? p.cols : void 0,
7406
- rows: typeof p.rows === "number" ? p.rows : void 0,
7407
- // Session ended (natural exit included), so stop its watcher.
7408
- 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
7409
7819
  });
7410
- terminalDirectories.set(key, cwd);
7411
- watchSubAgents(key, cwd, (subAgents) => {
7412
- 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
7413
7831
  });
7414
- if (p.launch) {
7415
- const current = findTask(task.id);
7416
- if (current) trackProgress(current);
7417
- }
7418
- return result;
7419
7832
  },
7420
7833
  "terminal:start": async (payload) => {
7421
7834
  const p = obj(payload, "payload");
@@ -7431,8 +7844,11 @@ function createCore({ sessions, bus, version, homeDir, ghRunner, githubAuthStatu
7431
7844
  if (p.taskId) {
7432
7845
  const requested = requireTask(p.taskId);
7433
7846
  const column = columnOf(requested.id);
7434
- const task = column === "in_progress" && !requested.worktreePath ? await ensureProvisioned(requested) : requested;
7435
- 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
+ );
7436
7852
  projectPath = task.projectPath ?? cwd;
7437
7853
  } else if (p.projectPath) {
7438
7854
  cwd = str(p.projectPath, "projectPath");
@@ -7526,8 +7942,9 @@ function createCore({ sessions, bus, version, homeDir, ghRunner, githubAuthStatu
7526
7942
  },
7527
7943
  "git:getDiff": (taskId) => {
7528
7944
  const task = requireTask(taskId);
7529
- if (!task.worktreePath) return [];
7530
- return getWorktreeDiff(task.worktreePath, task.baseBranch ?? "HEAD");
7945
+ const worktree = gitWorktree(task);
7946
+ if (!worktree) return [];
7947
+ return getWorktreeDiff(worktree, task.baseBranch ?? "HEAD");
7531
7948
  },
7532
7949
  /**
7533
7950
  * Changed-file list without content — cheap enough for the sidebar to poll.
@@ -7536,16 +7953,18 @@ function createCore({ sessions, bus, version, homeDir, ghRunner, githubAuthStatu
7536
7953
  */
7537
7954
  "git:getDiffEntries": (taskId, opts) => {
7538
7955
  const task = requireTask(taskId);
7539
- if (!task.worktreePath) return [];
7956
+ const worktree = gitWorktree(task);
7957
+ if (!worktree) return [];
7540
7958
  const fetch2 = opts && typeof opts === "object" ? opts.fetch : void 0;
7541
- return getWorktreeDiffEntries(task.worktreePath, task.baseBranch ?? "HEAD", {
7959
+ return getWorktreeDiffEntries(worktree, task.baseBranch ?? "HEAD", {
7542
7960
  fetch: typeof fetch2 === "boolean" ? fetch2 : void 0
7543
7961
  });
7544
7962
  },
7545
7963
  "git:getDiffFile": (taskId, filePath) => {
7546
7964
  const task = requireTask(taskId);
7547
- if (!task.worktreePath) return null;
7548
- 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"));
7549
7968
  },
7550
7969
  "task:listArtifacts": (taskId) => {
7551
7970
  const task = requireTask(taskId);
@@ -7571,6 +7990,7 @@ function createCore({ sessions, bus, version, homeDir, ghRunner, githubAuthStatu
7571
7990
  "task:getSpecs": async (taskId) => {
7572
7991
  const task = requireTask(taskId);
7573
7992
  const base2 = task.baseBranch ?? "main";
7993
+ if (isMasterTask(task)) return [];
7574
7994
  if (task.worktreePath) return readWorktreeSpecs(task.worktreePath, base2);
7575
7995
  const since = task.provisionedAt ?? task.launchedAt ?? (task.pushed !== void 0 ? task.createdAt : void 0);
7576
7996
  if (!task.projectPath || !since) return [];
@@ -7626,25 +8046,29 @@ function createCore({ sessions, bus, version, homeDir, ghRunner, githubAuthStatu
7626
8046
  "git:approve": async (payload) => {
7627
8047
  const p = obj(payload, "payload");
7628
8048
  const task = requireTask(p.taskId);
7629
- if (!task.worktreePath) throw new Error("\u627E\u4E0D\u5230\u6B64\u4EFB\u52D9\u7684 worktree");
7630
- 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"));
7631
8052
  if (result.pushed) updateTask(task.id, { pushed: true });
7632
8053
  return { result, state: getState() };
7633
8054
  },
7634
8055
  "git:generateCommitMessage": (taskId) => {
7635
8056
  const task = requireTask(taskId);
7636
- if (!task.worktreePath) throw new Error("\u627E\u4E0D\u5230\u6B64\u4EFB\u52D9\u7684 worktree");
7637
- 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");
7638
8060
  },
7639
8061
  "git:getPrStatus": (taskId) => {
7640
8062
  const task = requireTask(taskId);
7641
- if (!task.worktreePath) return null;
7642
- return getPrStatus(task.worktreePath);
8063
+ const worktree = gitWorktree(task);
8064
+ if (!worktree) return null;
8065
+ return getPrStatus(worktree);
7643
8066
  },
7644
8067
  "git:getGithubCompareUrl": (taskId) => {
7645
8068
  const task = requireTask(taskId);
7646
- if (!task.worktreePath) return null;
7647
- return getGithubCompareUrl(task.worktreePath, task.baseBranch ?? "main");
8069
+ const worktree = gitWorktree(task);
8070
+ if (!worktree) return null;
8071
+ return getGithubCompareUrl(worktree, task.baseBranch ?? "main");
7648
8072
  },
7649
8073
  // One dialog at a time: a second click while it is open joins it instead
7650
8074
  // of stacking another window on the host.
@@ -7672,8 +8096,9 @@ function createCore({ sessions, bus, version, homeDir, ghRunner, githubAuthStatu
7672
8096
  await teardownTask(task.id);
7673
8097
  cancelChatSend(task.id);
7674
8098
  let outcome;
7675
- if (task.worktreePath) {
7676
- 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;
7677
8102
  }
7678
8103
  const usage = usageThroughCurrentRun(task);
7679
8104
  progress.untrack(task.id);
@@ -7702,6 +8127,9 @@ function createCore({ sessions, bus, version, homeDir, ghRunner, githubAuthStatu
7702
8127
  if (task.workspacePath) {
7703
8128
  fs21.rmSync(taskAttachmentStagingPath(task.workspacePath, task.id), { recursive: true, force: true });
7704
8129
  }
8130
+ if (isMasterTask(task) && task.workspacePath) {
8131
+ fs21.rmSync(task.workspacePath, { recursive: true, force: true });
8132
+ }
7705
8133
  clearConversation(task.id);
7706
8134
  removeTask(task.id);
7707
8135
  return getState();
@@ -8412,119 +8840,6 @@ function isWebDir(dir) {
8412
8840
  return fs25.existsSync(path28.join(dir, "home", "index.html"));
8413
8841
  }
8414
8842
 
8415
- // packages/cli/src/remote.ts
8416
- import WebSocket from "ws";
8417
-
8418
- // packages/core/src/ws-transport.ts
8419
- var OPEN = 1;
8420
- var MAX_BACKOFF_MS = 5e3;
8421
- function createWsTransport(options) {
8422
- let socket = null;
8423
- let nextId = 1;
8424
- let connectedOnce = false;
8425
- let closed = false;
8426
- let backoff = 250;
8427
- const queue = [];
8428
- const pending = /* @__PURE__ */ new Map();
8429
- const listeners = /* @__PURE__ */ new Map();
8430
- function flush() {
8431
- while (socket && socket.readyState === OPEN && queue.length > 0) socket.send(queue.shift());
8432
- }
8433
- function open() {
8434
- if (closed) return;
8435
- const ws = options.connect();
8436
- socket = ws;
8437
- ws.onopen = () => {
8438
- backoff = 250;
8439
- options.onStatus?.(true);
8440
- flush();
8441
- if (connectedOnce) options.onReconnect?.();
8442
- connectedOnce = true;
8443
- };
8444
- ws.onmessage = (ev) => {
8445
- let msg;
8446
- try {
8447
- msg = JSON.parse(String(ev.data));
8448
- } catch {
8449
- return;
8450
- }
8451
- if (typeof msg.event === "string") {
8452
- dispatch(msg.event, msg.payload);
8453
- return;
8454
- }
8455
- const entry = typeof msg.id === "number" ? pending.get(msg.id) : void 0;
8456
- if (!entry) return;
8457
- pending.delete(msg.id);
8458
- if (msg.error) entry.reject(Object.assign(new Error(msg.error.message), { code: msg.error.code }));
8459
- else entry.resolve(msg.result === null ? void 0 : msg.result);
8460
- };
8461
- ws.onerror = () => {
8462
- };
8463
- ws.onclose = () => {
8464
- if (socket !== ws) return;
8465
- socket = null;
8466
- options.onStatus?.(false);
8467
- for (const [id, entry] of Array.from(pending)) {
8468
- pending.delete(id);
8469
- entry.reject(new Error("\u8207 VibeFlow core \u7684\u9023\u7DDA\u4E2D\u65B7"));
8470
- }
8471
- if (closed) return;
8472
- setTimeout(open, backoff);
8473
- backoff = Math.min(backoff * 2, MAX_BACKOFF_MS);
8474
- };
8475
- }
8476
- function dispatch(channel, payload) {
8477
- for (const cb of Array.from(listeners.get(channel) ?? [])) cb(payload);
8478
- }
8479
- function post(message) {
8480
- const data = JSON.stringify(message);
8481
- if (socket && socket.readyState === OPEN) socket.send(data);
8482
- else queue.push(data);
8483
- }
8484
- open();
8485
- return {
8486
- kind: "ws",
8487
- invoke(method, ...params) {
8488
- const id = nextId++;
8489
- return new Promise((resolve, reject) => {
8490
- pending.set(id, { resolve, reject });
8491
- post({ id, method, params });
8492
- });
8493
- },
8494
- send(method, ...params) {
8495
- post({ method, params });
8496
- },
8497
- on(channel, callback) {
8498
- let set = listeners.get(channel);
8499
- if (!set) listeners.set(channel, set = /* @__PURE__ */ new Set());
8500
- set.add(callback);
8501
- return () => {
8502
- set.delete(callback);
8503
- };
8504
- },
8505
- close() {
8506
- closed = true;
8507
- socket?.close();
8508
- },
8509
- dispatch
8510
- };
8511
- }
8512
-
8513
- // packages/cli/src/remote.ts
8514
- function connectToHost(lock) {
8515
- const url = `ws://127.0.0.1:${lock.port}/ws`;
8516
- const transport = createWsTransport({
8517
- connect: () => new WebSocket(url, {
8518
- headers: { Authorization: `Bearer ${lock.token}`, Host: `127.0.0.1:${lock.port}` }
8519
- })
8520
- });
8521
- return {
8522
- api: createBridge(transport),
8523
- invoke: (method, ...params) => transport.invoke(method, ...params),
8524
- close: () => transport.close()
8525
- };
8526
- }
8527
-
8528
8843
  // packages/cli/src/doctor.ts
8529
8844
  import fs26 from "fs";
8530
8845
  import path29 from "path";