@kw-yeh/vibeflow 4.1.0 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/vibeflow.mjs CHANGED
@@ -555,14 +555,17 @@ ${entry}
555
555
  }
556
556
  }
557
557
  function writeAttachments(worktreePath, inputs) {
558
+ if (inputs.length === 0) return [];
559
+ ensureAttachmentExclude(worktreePath);
560
+ return writeAttachmentsTo(path10.join(worktreePath, ATTACHMENTS_DIR), inputs);
561
+ }
562
+ function writeAttachmentsTo(attachmentDir, inputs) {
558
563
  if (inputs.length === 0) return [];
559
564
  const decodedInputs = inputs.map((input) => ({
560
565
  input,
561
566
  name: safeAttachmentName(input.name),
562
567
  bytes: decodeAttachment(input)
563
568
  }));
564
- ensureAttachmentExclude(worktreePath);
565
- const attachmentDir = path10.join(worktreePath, ATTACHMENTS_DIR);
566
569
  fs6.mkdirSync(attachmentDir, { recursive: true });
567
570
  return decodedInputs.map(({ input, name, bytes }) => {
568
571
  const id = randomUUID().slice(0, 8);
@@ -598,16 +601,15 @@ var init_attachments = __esm({
598
601
 
599
602
  // packages/tui/src/task-form.ts
600
603
  import { spawnSync } from "child_process";
601
- import fs19 from "fs";
604
+ import fs20 from "fs";
602
605
  import os6 from "os";
603
- import path23 from "path";
606
+ import path24 from "path";
604
607
  import React, { useEffect, useRef, useState } from "react";
605
608
  import { Box, Text, render, useApp, useInput } from "ink";
606
609
  function newFormState(defaultAutoMode, agents) {
607
610
  const agentCli = agents && agents.length > 0 && !agents.some((a) => a.id === "claude") ? agents[0].id : "claude";
608
611
  return {
609
612
  mode: "new",
610
- launched: false,
611
613
  title: "",
612
614
  description: "",
613
615
  projectPath: null,
@@ -627,11 +629,11 @@ function editFormState(task, defaultAutoMode) {
627
629
  return {
628
630
  mode: "edit",
629
631
  taskId: task.id,
630
- launched: Boolean(task.launchedAt),
631
632
  title: task.title,
632
633
  description: task.description ?? "",
633
634
  projectPath: task.projectPath ?? null,
634
635
  originalProjectPath: task.projectPath ?? null,
636
+ worktreePath: task.worktreePath,
635
637
  gitInfo: null,
636
638
  gitInfoPath: null,
637
639
  baseBranch: task.baseBranch ?? "",
@@ -662,8 +664,8 @@ function visibleFields(s, ctx) {
662
664
  return fields2;
663
665
  }
664
666
  const fields = ["title", "description", "effort", "autoMode", "project"];
665
- if (projectChanged(s) && isRepo && hasRemote) fields.push("baseBranch");
666
- fields.push("agent", "model", "submit");
667
+ if (isRepo && hasRemote) fields.push("baseBranch");
668
+ fields.push("branch", "agent", "model", "submit");
667
669
  return fields;
668
670
  }
669
671
  function modelOptions(ctx, agentCli, current) {
@@ -712,24 +714,25 @@ function updatePayload(s) {
712
714
  model: s.model || void 0,
713
715
  effort: s.effort,
714
716
  autoMode: s.autoMode,
715
- ...!s.launched && s.projectPath ? { projectPath: s.projectPath, baseBranch: s.baseBranch || null } : {}
717
+ ...s.projectPath ? { projectPath: s.projectPath, baseBranch: s.baseBranch || null } : {},
718
+ branch: s.branch.trim()
716
719
  };
717
720
  }
718
721
  function editorCommand(env, platform) {
719
722
  return env.VISUAL?.trim() || env.EDITOR?.trim() || (platform === "win32" ? "notepad" : "vi");
720
723
  }
721
724
  function editInEditor(text) {
722
- const dir = fs19.mkdtempSync(path23.join(os6.tmpdir(), "vibeflow-"));
723
- const file = path23.join(dir, "description.md");
725
+ const dir = fs20.mkdtempSync(path24.join(os6.tmpdir(), "vibeflow-"));
726
+ const file = path24.join(dir, "description.md");
724
727
  try {
725
- fs19.writeFileSync(file, text);
728
+ fs20.writeFileSync(file, text);
726
729
  const cmd = editorCommand(process.env, process.platform);
727
730
  const r = spawnSync(`${cmd} "${file}"`, { stdio: "inherit", shell: true });
728
731
  if (r.error) return { error: `\u7121\u6CD5\u555F\u52D5\u7DE8\u8F2F\u5668\uFF08${cmd}\uFF09\uFF1A${r.error.message}` };
729
732
  if (r.status !== 0) return { error: `\u7DE8\u8F2F\u5668\uFF08${cmd}\uFF09\u7D50\u675F\u78BC ${r.status}\uFF0C\u63CF\u8FF0\u672A\u8B8A\u66F4` };
730
- return { text: fs19.readFileSync(file, "utf8").replace(/\r?\n$/, "") };
733
+ return { text: fs20.readFileSync(file, "utf8").replace(/\r?\n$/, "") };
731
734
  } finally {
732
- fs19.rmSync(dir, { recursive: true, force: true });
735
+ fs20.rmSync(dir, { recursive: true, force: true });
733
736
  }
734
737
  }
735
738
  function cycle(list, current, step) {
@@ -754,16 +757,20 @@ function FormView({ api, ctx, initial, initialFocus, initialMessage, pristine, o
754
757
  const lookup = useRef(0);
755
758
  const fields = visibleFields(s, ctx);
756
759
  const field = fields[Math.min(focus, fields.length - 1)];
757
- const projectLocked = s.mode === "edit" && s.launched;
758
760
  const missing = isProjectMissing(ctx.recent, s.projectPath);
759
761
  useEffect(() => {
760
- if (!s.projectPath || missing || s.gitInfoPath === s.projectPath || s.mode === "edit" && !projectChanged(s)) return;
762
+ if (!s.projectPath || missing || s.gitInfoPath === s.projectPath) return;
761
763
  const target = s.projectPath;
762
764
  const id = ++lookup.current;
763
765
  setLoadingGit(true);
764
766
  void api.getGitInfo(target).then((info) => {
765
767
  if (id !== lookup.current) return;
766
- setS((prev) => ({ ...prev, gitInfo: info, gitInfoPath: target, baseBranch: info.defaultBase ?? "" }));
768
+ setS((prev) => ({
769
+ ...prev,
770
+ gitInfo: info,
771
+ gitInfoPath: target,
772
+ baseBranch: prev.mode === "edit" && target === prev.originalProjectPath && prev.baseBranch ? prev.baseBranch : info.defaultBase ?? ""
773
+ }));
767
774
  }).catch((err) => id === lookup.current && setMessage(`\u5075\u6E2C Git \u5931\u6557\uFF1A${err.message}`)).finally(() => id === lookup.current && setLoadingGit(false));
768
775
  }, [s.projectPath]);
769
776
  const leave = (result) => {
@@ -771,7 +778,6 @@ function FormView({ api, ctx, initial, initialFocus, initialMessage, pristine, o
771
778
  exit();
772
779
  };
773
780
  const setProject = (projectPath) => {
774
- if (projectLocked) return;
775
781
  setS((prev) => ({ ...prev, projectPath, gitInfo: null, gitInfoPath: null, baseBranch: "" }));
776
782
  };
777
783
  const submit = async () => {
@@ -797,10 +803,10 @@ function FormView({ api, ctx, initial, initialFocus, initialMessage, pristine, o
797
803
  if (editing === "title") setS((prev) => ({ ...prev, title: draft }));
798
804
  else if (editing === "branch") setS((prev) => ({ ...prev, branch: value2 }));
799
805
  else if (editing === "project") {
800
- if (value2) setProject(path23.resolve(value2.replace(/^~(?=$|\/)/, os6.homedir())));
806
+ if (value2) setProject(path24.resolve(value2.replace(/^~(?=$|\/)/, os6.homedir())));
801
807
  } else if (editing === "attachments" && value2) {
802
- const file = path23.resolve(value2.replace(/^~(?=$|\/)/, os6.homedir()));
803
- if (!fs19.existsSync(file) || !fs19.statSync(file).isFile()) {
808
+ const file = path24.resolve(value2.replace(/^~(?=$|\/)/, os6.homedir()));
809
+ if (!fs20.existsSync(file) || !fs20.statSync(file).isFile()) {
804
810
  setMessage(`\u627E\u4E0D\u5230\u6A94\u6848\uFF1A${file}`);
805
811
  return;
806
812
  }
@@ -833,7 +839,7 @@ function FormView({ api, ctx, initial, initialFocus, initialMessage, pristine, o
833
839
  if (key.downArrow || key.tab) return setFocus((f) => Math.min(fields.length - 1, f + 1));
834
840
  const step = key.leftArrow ? -1 : key.rightArrow ? 1 : 0;
835
841
  if (step) {
836
- if (field === "project" && !projectLocked) {
842
+ if (field === "project") {
837
843
  const paths = ctx.recent.map((p) => p.path);
838
844
  if (paths.length) setProject(cycle(paths, s.projectPath ?? paths[paths.length - 1], step));
839
845
  } else if (field === "baseBranch") {
@@ -859,7 +865,7 @@ function FormView({ api, ctx, initial, initialFocus, initialMessage, pristine, o
859
865
  void api.initRepository(s.projectPath).then((info) => setS((prev) => ({ ...prev, gitInfo: info, gitInfoPath: prev.projectPath, baseBranch: info.defaultBase ?? "" }))).catch((err) => setMessage(err.message)).finally(() => setBusy(null));
860
866
  return;
861
867
  }
862
- if (TEXT_FIELDS.includes(field) && !(field === "project" && projectLocked)) {
868
+ if (TEXT_FIELDS.includes(field)) {
863
869
  setEditing(field);
864
870
  setDraft(field === "title" ? s.title : field === "branch" ? s.branch : field === "project" ? s.projectPath ?? "" : "");
865
871
  }
@@ -870,7 +876,6 @@ function FormView({ api, ctx, initial, initialFocus, initialMessage, pristine, o
870
876
  case "title":
871
877
  return s.title || "\uFF08\u4F8B\u5982\uFF1A\u5BE6\u4F5C\u767B\u5165\u9801\u9762\uFF09";
872
878
  case "project": {
873
- if (projectLocked) return `${s.projectPath ? path23.basename(s.projectPath) : "\u2014"}\uFF08\u4EFB\u52D9\u5DF2\u958B\u59CB\uFF0C\u9396\u5B9A\uFF09`;
874
879
  if (!s.projectPath) return ctx.recent.length ? "\u2190/\u2192 \u9078\u64C7\u4F7F\u7528\u904E\u7684\u5C08\u6848\uFF0C\u6216 Enter \u8F38\u5165\u8DEF\u5F91" : "Enter \u8F38\u5165\u5C08\u6848\u8CC7\u6599\u593E\u7684\u7D55\u5C0D\u8DEF\u5F91";
875
880
  return `${s.projectPath}${missing ? "\uFF08\u8DEF\u5F91\u907A\u5931\uFF09" : ""}`;
876
881
  }
@@ -881,7 +886,7 @@ function FormView({ api, ctx, initial, initialFocus, initialMessage, pristine, o
881
886
  case "branch":
882
887
  return s.branch || "\uFF08\u7559\u7A7A\u5247\u81EA\u52D5\u547D\u540D\uFF0C\u4F8B\u5982 feature/login-fix\uFF09";
883
888
  case "attachments":
884
- return s.attachments.length ? s.attachments.map((a) => path23.basename(a)).join("\u3001") : "\uFF08Enter \u8F38\u5165\u6A94\u6848\u8DEF\u5F91\u52A0\u5165\uFF09";
889
+ return s.attachments.length ? s.attachments.map((a) => path24.basename(a)).join("\u3001") : "\uFF08Enter \u8F38\u5165\u6A94\u6848\u8DEF\u5F91\u52A0\u5165\uFF09";
885
890
  case "description": {
886
891
  if (!s.description) return "\uFF08Enter \u7528\u7DE8\u8F2F\u5668\u64B0\u5BEB\uFF09";
887
892
  const lines = s.description.split("\n");
@@ -906,15 +911,18 @@ function FormView({ api, ctx, initial, initialFocus, initialMessage, pristine, o
906
911
  };
907
912
  const note = () => {
908
913
  const info = s.gitInfo;
909
- if (field === "project" && !projectLocked) {
914
+ if (field === "project") {
910
915
  if (loadingGit) return "\u5075\u6E2C Git \u72C0\u614B\u4E2D\u2026";
911
916
  if (missing) return `\u627E\u4E0D\u5230 ${s.projectPath}\uFF0C\u5C08\u6848\u53EF\u80FD\u5DF2\u88AB\u79FB\u52D5\u6216\u522A\u9664\u3002`;
912
917
  if (info && !info.isRepo) return "\u9019\u500B\u8CC7\u6599\u593E\u4E0D\u662F Git repository\u3002\u8ACB\u6539\u9078\u4E00\u500B Git \u5C08\u6848\uFF0C\u6216\u5728\u4E0B\u65B9\u521D\u59CB\u5316\u3002";
913
- if (s.mode === "edit" && projectChanged(s)) return "\u66F4\u63DB\u5C08\u6848\u6703\u5728\u65B0\u5C08\u6848\u91CD\u5EFA worktree\uFF08\u6B64\u4EFB\u52D9\u5C1A\u672A\u958B\u59CB\uFF0C\u7121\u8B8A\u66F4\u6703\u907A\u5931\uFF09\u3002";
918
+ if (s.mode === "edit" && projectChanged(s)) return "\u5206\u652F\u6703\u5728\u65B0\u5C08\u6848\u3001\u5361\u7247\u958B\u59CB\u57F7\u884C\u6642\u624D\u5EFA\u7ACB\u3002";
914
919
  if (info?.isRepo && !info.hasRemote) return `\u6B64 repository \u6C92\u6709 remote\uFF0C\u5C07\u4EE5\u76EE\u524D\u5206\u652F (${info.currentBranch ?? "HEAD"}) \u70BA\u57FA\u6E96\u5EFA\u7ACB\u672C\u5730 worktree\u3002`;
915
- if (info?.isRepo && s.projectPath) return `worktree \u6703\u5EFA\u5728 ${ctx.workstationPath || "~/Desktop"}/${path23.basename(s.projectPath)}`;
920
+ if (info?.isRepo && s.projectPath) return `worktree \u6703\u5EFA\u5728 ${ctx.workstationPath || "~/Desktop"}/${path24.basename(s.projectPath)}`;
921
+ }
922
+ if (field === "branch") {
923
+ const when = info?.hasRemote ? "\u958B\u59CB\u57F7\u884C\u6642\u6703\u5EFA\u7ACB\u540C\u540D\u5206\u652F\u4E26\u63A8\u5230 origin\uFF1B\u82E5 remote \u5DF2\u6709\u9019\u500B\u5206\u652F\uFF0C\u5247\u76F4\u63A5\u53D6\u56DE\u4E26\u63A5\u7E8C\u4E0A\u9762\u7684\u5DE5\u4F5C\u3002" : "\u958B\u59CB\u57F7\u884C\u6642\u6703\u5EFA\u7ACB\u540C\u540D\u5206\u652F\u3002";
924
+ return s.mode === "edit" && s.worktreePath ? `${when} \u9019\u5F35\u5361\u5DF2\u6709 worktree\uFF1A\u66F4\u6539\u5206\u652F\u3001\u5C08\u6848\u6216\u57FA\u6E96\u5206\u652F\u6703\u79FB\u9664\u5B83\uFF08\u6709\u672A\u5B8C\u6210\u8B8A\u66F4\u6642\u7121\u6CD5\u5132\u5B58\uFF09\u3002` : when;
916
925
  }
917
- if (field === "branch") return info?.hasRemote ? "\u586B\u5BEB\u5F8C\u6703\u5EFA\u7ACB\u540C\u540D\u5206\u652F\uFF1B\u82E5 remote \u5DF2\u6709\u9019\u500B\u5206\u652F\uFF0C\u5247\u76F4\u63A5\u53D6\u56DE\u4E26\u63A5\u7E8C\u4E0A\u9762\u7684\u5DE5\u4F5C\u3002" : "\u586B\u5BEB\u5F8C\u6703\u5EFA\u7ACB\u540C\u540D\u5206\u652F\u3002";
918
926
  if (field === "effort") return `${EFFORTS.find((x) => x.value === s.effort).description} Claude Code \u8207 Codex \u6703\u5728\u555F\u52D5\u6B64\u4EFB\u52D9\u6642\u5957\u7528\uFF1B\u5BE6\u969B\u53EF\u7528\u7D1A\u8DDD\u4F9D model \u800C\u5B9A\u3002`;
919
927
  if (field === "autoMode") return s.autoMode ? "\u958B\u555F\uFF1AAgent \u6703\u76F4\u63A5\u4FEE\u6539\u6A94\u6848\u3001\u57F7\u884C\u6307\u4EE4\uFF0C\u4E0D\u6703\u9010\u6B21\u5411\u4F60\u78BA\u8A8D\u3002" : "\u95DC\u9589\uFF1AAgent \u6BCF\u6B21\u8981\u52D5\u4F5C\u524D\uFF0C\u90FD\u6703\u5728\u7D42\u7AEF\u6A5F\u88E1\u7B49\u4F60\u5141\u8A31\u3002";
920
928
  if (field === "submit") return blockReason(s, ctx, loadingGit);
@@ -1029,7 +1037,7 @@ __export(src_exports, {
1029
1037
  showDiff: () => showDiff
1030
1038
  });
1031
1039
  import { spawnSync as spawnSync2 } from "child_process";
1032
- import path24 from "path";
1040
+ import path25 from "path";
1033
1041
  import React2, { useEffect as useEffect2, useState as useState2 } from "react";
1034
1042
  import { Box as Box2, Text as Text2, render as render2, useApp as useApp2, useInput as useInput2, useStdout } from "ink";
1035
1043
  function findDetachKey(text) {
@@ -1130,6 +1138,9 @@ function BoardView({ opts, initial, initialMessage, onAction }) {
1130
1138
  }
1131
1139
  setSel({ column: sel.column + direction, index: 0 });
1132
1140
  if (move.effect === "launch") {
1141
+ if (!move.task.worktreePath) {
1142
+ setMessage(`\u6B63\u5728\u70BA\u300C${move.task.title}\u300D\u5F9E\u6700\u65B0\u7684 ${move.task.baseBranch ?? "\u57FA\u6E96\u5206\u652F"} \u5EFA\u7ACB\u5206\u652F ${move.task.branch} \u8207 worktree\uFF0C\u9700\u8981\u5E7E\u79D2\u9418\u2026`);
1143
+ }
1133
1144
  await api.term.start({ taskId: move.task.id, launch: {} });
1134
1145
  setMessage(`\u5DF2\u555F\u52D5\u300C${move.task.title}\u300D\u7684 agent\u3002Enter \u53EF\u9032\u5165\u7D42\u7AEF\u3002`);
1135
1146
  } else if (move.effect === "cleanup") {
@@ -1189,8 +1200,14 @@ function BoardView({ opts, initial, initialMessage, onAction }) {
1189
1200
  return;
1190
1201
  }
1191
1202
  if (!selected) return;
1192
- if (input === "e") return leave({ type: "edit", task: selected });
1203
+ if (input === "e") {
1204
+ if (COLUMNS2[sel.column] !== "backlog") return setMessage("\u57F7\u884C\u4E2D\u8207\u5DF2\u5B8C\u6210\u7684\u5361\u7247\u53EA\u80FD\u67E5\u770B\uFF0C\u7121\u6CD5\u7DE8\u8F2F");
1205
+ return leave({ type: "edit", task: selected });
1206
+ }
1193
1207
  if (key.return) {
1208
+ if (!selected.worktreePath && COLUMNS2[sel.column] === "backlog") {
1209
+ return setMessage("\u9019\u5F35\u5361\u5C1A\u672A\u958B\u59CB\u57F7\u884C\uFF0C\u5206\u652F\u8207 worktree \u6703\u5728\u958B\u59CB\u6642\u5EFA\u7ACB");
1210
+ }
1194
1211
  if (!selected.worktreePath && !selected.projectPath) return setMessage("\u9019\u5F35\u5361\u6C92\u6709\u5DE5\u4F5C\u76EE\u9304");
1195
1212
  if (COLUMNS2[sel.column] === "done") return setMessage("\u5DF2\u5B8C\u6210\u7684\u5361\u7247\u6C92\u6709\u7D42\u7AEF");
1196
1213
  return leave({ type: "attach", task: selected });
@@ -1291,7 +1308,7 @@ function showDiff(task) {
1291
1308
  });
1292
1309
  }
1293
1310
  function tmuxAttach(opts, sessionName) {
1294
- const conf = path24.join(opts.userDataDir, "tmux.conf");
1311
+ const conf = path25.join(opts.userDataDir, "tmux.conf");
1295
1312
  const inOurServer = (process.env.TMUX ?? "").split(",")[0].endsWith(`/${TMUX_SOCKET}`);
1296
1313
  const args = inOurServer ? ["-L", TMUX_SOCKET, "-f", conf, "switch-client", "-t", `=${sessionName}`] : ["-L", TMUX_SOCKET, "-f", conf, "attach-session", "-t", `=${sessionName}`];
1297
1314
  spawnSync2("tmux", args, { stdio: "inherit", env: { ...process.env, TMUX: inOurServer ? process.env.TMUX : "" } });
@@ -1421,8 +1438,8 @@ var init_src = __esm({
1421
1438
  });
1422
1439
 
1423
1440
  // packages/cli/src/main.ts
1424
- import fs20 from "fs";
1425
- import path25 from "path";
1441
+ import fs21 from "fs";
1442
+ import path26 from "path";
1426
1443
  import { fileURLToPath } from "url";
1427
1444
  import { parseArgs as parseArgs2 } from "node:util";
1428
1445
 
@@ -1814,8 +1831,8 @@ function createBridge(t) {
1814
1831
  * in-app preview will not inline. Resolves to '' on success, else a reason.
1815
1832
  */
1816
1833
  openArtifactsDir: (taskId) => t.invoke("task:openArtifactsDir", taskId),
1817
- /** The task's decision record; null when it has no workspace folder. */
1818
- getDecisions: (taskId) => t.invoke("task:getDecisions", taskId),
1834
+ /** Every spec.md the task's branch added or changed; empty when there is none. */
1835
+ getSpecs: (taskId) => t.invoke("task:getSpecs", taskId),
1819
1836
  /** Store dir + CLI paths for launch injection, so an agent can write cards. */
1820
1837
  getBoardCliLaunchInfo: () => t.invoke("board:getCliLaunchInfo"),
1821
1838
  /** VibeFlow's own skill / prompt / script store. */
@@ -3200,6 +3217,9 @@ import { join as join2 } from "path";
3200
3217
  function projectWorkstationPath(workstationRoot, projectName) {
3201
3218
  return join2(workstationRoot, projectName);
3202
3219
  }
3220
+ function taskAttachmentStagingPath(workspacePath, taskId) {
3221
+ return join2(workspacePath, ".attachments", taskId);
3222
+ }
3203
3223
 
3204
3224
  // packages/core/src/agents.ts
3205
3225
  init_env();
@@ -3278,25 +3298,21 @@ async function createTaskFromInput(input) {
3278
3298
  if (explicitBranch) {
3279
3299
  await assertBranchAvailable(projectPath, workspacePath, explicitBranch);
3280
3300
  }
3281
- const preferredBranch = explicitBranch ?? await generateBranchName(input.title, input.description);
3282
- let provisionResult;
3283
- try {
3284
- provisionResult = await provisionWorktree(
3285
- projectPath,
3286
- workspacePath,
3287
- taskId,
3288
- input.baseBranch ?? null,
3289
- preferredBranch,
3290
- { explicitBranch: Boolean(explicitBranch) }
3291
- );
3292
- } catch (err) {
3293
- throw Object.assign(
3294
- new Error(`Worktree \u5EFA\u7ACB\u5931\u6557\uFF1A${err.message}`),
3295
- { code: "WORKTREE_CREATE_FAILED" }
3296
- );
3297
- }
3298
- const attachments = writeAttachments(
3299
- provisionResult.worktreePath,
3301
+ const branch = explicitBranch ?? await generateBranchName(input.title, input.description);
3302
+ const draft = {
3303
+ id: taskId,
3304
+ title: input.title.trim() || `Task ${taskId}`,
3305
+ branch: branch ?? fallbackBranchName(taskId),
3306
+ branchExplicit: explicitBranch ? true : void 0,
3307
+ projectPath,
3308
+ projectName,
3309
+ workspacePath,
3310
+ baseBranch: input.baseBranch || info.defaultBase || info.currentBranch || void 0
3311
+ };
3312
+ const col = input.status ?? "backlog";
3313
+ const provisioned = col === "backlog" ? null : await provisionTaskWorktree(draft);
3314
+ const attachments = provisioned?.worktreePath ? writeAttachments(provisioned.worktreePath, input.attachments ?? []) : writeAttachmentsTo(
3315
+ taskAttachmentStagingPath(workspacePath, taskId),
3300
3316
  input.attachments ?? []
3301
3317
  );
3302
3318
  const attachmentLines = attachments.map(
@@ -3305,16 +3321,9 @@ async function createTaskFromInput(input) {
3305
3321
  const baseDescription = input.description?.trim();
3306
3322
  const description = attachmentLines.length ? [baseDescription, attachmentLines.join("\n")].filter(Boolean).join("\n\n") : baseDescription || void 0;
3307
3323
  const task = {
3308
- id: taskId,
3309
- title: input.title.trim() || `Task ${taskId}`,
3324
+ ...draft,
3325
+ ...provisioned,
3310
3326
  description,
3311
- branch: provisionResult.branch,
3312
- projectPath,
3313
- projectName,
3314
- worktreePath: provisionResult.worktreePath,
3315
- workspacePath,
3316
- baseBranch: provisionResult.baseBranch,
3317
- pushed: provisionResult.pushed,
3318
3327
  createdAt: Date.now(),
3319
3328
  agentCli: input.agentCli ?? "claude",
3320
3329
  model: input.model || void 0,
@@ -3323,7 +3332,6 @@ async function createTaskFromInput(input) {
3323
3332
  };
3324
3333
  try {
3325
3334
  const board = store.get("board");
3326
- const col = input.status ?? "backlog";
3327
3335
  board[col] = [task, ...board[col]];
3328
3336
  store.set("board", board);
3329
3337
  recordRecentProject(store, projectPath);
@@ -3335,6 +3343,38 @@ async function createTaskFromInput(input) {
3335
3343
  }
3336
3344
  return { task, storePath: store.path };
3337
3345
  }
3346
+ async function provisionTaskWorktree(task) {
3347
+ if (!task.projectPath || !task.workspacePath) {
3348
+ throw Object.assign(new Error("\u4EFB\u52D9\u6C92\u6709\u5C08\u6848\u8CC7\u6599\u593E\uFF0C\u7121\u6CD5\u5EFA\u7ACB worktree"), {
3349
+ code: "WORKTREE_CREATE_FAILED"
3350
+ });
3351
+ }
3352
+ await fs9.mkdir(task.workspacePath, { recursive: true });
3353
+ if (task.branchExplicit) {
3354
+ await assertBranchAvailable(task.projectPath, task.workspacePath, task.branch);
3355
+ }
3356
+ try {
3357
+ const result = await provisionWorktree(
3358
+ task.projectPath,
3359
+ task.workspacePath,
3360
+ task.id,
3361
+ task.baseBranch ?? null,
3362
+ task.branch,
3363
+ { explicitBranch: task.branchExplicit === true }
3364
+ );
3365
+ return {
3366
+ branch: result.branch,
3367
+ worktreePath: result.worktreePath,
3368
+ baseBranch: result.baseBranch,
3369
+ pushed: result.pushed
3370
+ };
3371
+ } catch (err) {
3372
+ throw Object.assign(
3373
+ new Error(`Worktree \u5EFA\u7ACB\u5931\u6557\uFF1A${err.message}`),
3374
+ { code: "WORKTREE_CREATE_FAILED" }
3375
+ );
3376
+ }
3377
+ }
3338
3378
  function updateTaskFromInput(input) {
3339
3379
  const store = input.storePath ? getStoreAtPath(input.storePath) : getStore();
3340
3380
  const board = store.get("board");
@@ -3521,7 +3561,7 @@ async function taskCommand(argv) {
3521
3561
  title: task2.title,
3522
3562
  description: task2.description,
3523
3563
  branch: task2.branch,
3524
- worktreePath: task2.worktreePath
3564
+ worktreePath: task2.worktreePath ?? null
3525
3565
  }
3526
3566
  };
3527
3567
  }
@@ -3563,7 +3603,7 @@ async function taskCommand(argv) {
3563
3603
  title: task.title,
3564
3604
  projectPath: task.projectPath,
3565
3605
  branch: task.branch,
3566
- worktreePath: task.worktreePath,
3606
+ worktreePath: task.worktreePath ?? null,
3567
3607
  baseBranch: task.baseBranch,
3568
3608
  agentCli: task.agentCli,
3569
3609
  model: task.model,
@@ -3574,8 +3614,8 @@ async function taskCommand(argv) {
3574
3614
  }
3575
3615
 
3576
3616
  // packages/cli/src/host.ts
3577
- import fs17 from "fs";
3578
- import path21 from "path";
3617
+ import fs18 from "fs";
3618
+ import path22 from "path";
3579
3619
  import { randomBytes as randomBytes2 } from "crypto";
3580
3620
 
3581
3621
  // packages/core/src/events.ts
@@ -4113,9 +4153,9 @@ async function syncBuiltinsAtStartup() {
4113
4153
  }
4114
4154
 
4115
4155
  // packages/core/src/service.ts
4116
- import fs15 from "fs";
4156
+ import fs16 from "fs";
4117
4157
  import os5 from "os";
4118
- import path19 from "path";
4158
+ import path20 from "path";
4119
4159
  import { randomUUID as randomUUID4 } from "crypto";
4120
4160
 
4121
4161
  // packages/core/src/agent-models.ts
@@ -4364,34 +4404,81 @@ async function boardCliLaunchInfo() {
4364
4404
  import fs14 from "fs";
4365
4405
  import path18 from "path";
4366
4406
  var DECISIONS_FILE_SUFFIX = ".DECISIONS.md";
4367
- var MAX_DECISION_BYTES = 256 * 1024;
4368
4407
  function decisionsKey(worktreePath, branch) {
4369
4408
  return worktreePath ? path18.basename(worktreePath) : branch.replace(/\//g, "-");
4370
4409
  }
4371
4410
  function decisionsPath(workspacePath, key) {
4372
4411
  return path18.join(workspacePath, `${key}${DECISIONS_FILE_SUFFIX}`);
4373
4412
  }
4374
- function readDecisions(workspacePath, key) {
4375
- const target = decisionsPath(workspacePath, key);
4413
+ function deleteDecisions(workspacePath, key) {
4376
4414
  try {
4377
- const stat = fs14.statSync(target);
4378
- if (!stat.isFile()) return { path: target, markdown: null };
4379
- const buf = fs14.readFileSync(target);
4380
- return {
4381
- path: target,
4382
- markdown: buf.subarray(0, MAX_DECISION_BYTES).toString("utf8"),
4383
- updatedAt: stat.mtimeMs,
4384
- ...buf.byteLength > MAX_DECISION_BYTES ? { truncated: true } : {}
4385
- };
4415
+ fs14.rmSync(decisionsPath(workspacePath, key), { force: true });
4386
4416
  } catch {
4387
- return { path: target, markdown: null };
4388
4417
  }
4389
4418
  }
4390
- function deleteDecisions(workspacePath, key) {
4419
+
4420
+ // packages/core/src/specs.ts
4421
+ import fs15 from "fs";
4422
+ import path19 from "path";
4423
+ var SPEC_FILE_NAME = "spec.md";
4424
+ var MAX_SPEC_BYTES = 256 * 1024;
4425
+ function isSpecPath(p) {
4426
+ return path19.posix.basename(p) === SPEC_FILE_NAME;
4427
+ }
4428
+ function clip2(buf) {
4429
+ return {
4430
+ markdown: buf.subarray(0, MAX_SPEC_BYTES).toString("utf8"),
4431
+ ...buf.byteLength > MAX_SPEC_BYTES ? { truncated: true } : {}
4432
+ };
4433
+ }
4434
+ async function readWorktreeSpecs(worktreePath, baseBranch) {
4435
+ const baseRef = await resolveBaseRef(worktreePath, baseBranch);
4436
+ const entries = await collectDiffEntries(worktreePath, baseRef);
4437
+ const specs = [];
4438
+ for (const entry of entries) {
4439
+ if (entry.status === "D" || !isSpecPath(entry.path)) continue;
4440
+ try {
4441
+ const full = path19.join(worktreePath, entry.path);
4442
+ const stat = fs15.statSync(full);
4443
+ if (!stat.isFile()) continue;
4444
+ specs.push({ path: entry.path, ...clip2(fs15.readFileSync(full)), updatedAt: stat.mtimeMs });
4445
+ } catch {
4446
+ }
4447
+ }
4448
+ return specs.sort((a, b) => a.path.localeCompare(b.path));
4449
+ }
4450
+ async function readBranchSpecs(projectPath, branch, baseBranch, changedPaths) {
4451
+ const ref = `refs/heads/${branch}`;
4391
4452
  try {
4392
- fs14.rmSync(decisionsPath(workspacePath, key), { force: true });
4453
+ await git(projectPath, ["rev-parse", "--verify", ref]);
4393
4454
  } catch {
4455
+ return [];
4456
+ }
4457
+ let paths = changedPaths;
4458
+ if (!paths) {
4459
+ const baseRef = await resolveBaseRef(projectPath, baseBranch);
4460
+ const names = await git(projectPath, [
4461
+ "diff",
4462
+ "--name-only",
4463
+ "--diff-filter=d",
4464
+ `${baseRef}...${ref}`
4465
+ ]).catch(() => "");
4466
+ paths = names.split("\n").map((l) => l.trim());
4467
+ }
4468
+ const specs = [];
4469
+ for (const p of paths.filter(isSpecPath)) {
4470
+ try {
4471
+ const content = await git(projectPath, ["show", `${ref}:${p}`]);
4472
+ const ct = await git(projectPath, ["log", "-1", "--format=%ct", ref, "--", p]);
4473
+ specs.push({
4474
+ path: p,
4475
+ ...clip2(Buffer.from(content, "utf8")),
4476
+ ...ct ? { updatedAt: Number(ct) * 1e3 } : {}
4477
+ });
4478
+ } catch {
4479
+ }
4394
4480
  }
4481
+ return specs.sort((a, b) => a.path.localeCompare(b.path));
4395
4482
  }
4396
4483
 
4397
4484
  // packages/core/src/chat-session.ts
@@ -4705,22 +4792,6 @@ function buildArtifactPrompt(artifactsDir = ARTIFACTS_FALLBACK_DIR2) {
4705
4792
  "5. Artifact \u8CC7\u6599\u593E\u4F4D\u65BC worktree \u4E4B\u5916\uFF1B\u4E0D\u8981\u628A\u5176\u4E2D\u5167\u5BB9\u52A0\u5165 git commit\uFF0C\u4E5F\u4E0D\u8981\u628A\u6700\u7D42\u4EA4\u4ED8\u7269\u53EA\u7559\u5728\u9019\u88E1\u3002"
4706
4793
  ].join("\n");
4707
4794
  }
4708
- var DECISIONS_FILE_SUFFIX2 = ".DECISIONS.md";
4709
- function taskDecisionsPath(worktreePath, workspacePath) {
4710
- if (!worktreePath || !workspacePath) return null;
4711
- return `${toShellPath(workspacePath)}/${pathBasename(worktreePath)}${DECISIONS_FILE_SUFFIX2}`;
4712
- }
4713
- function buildDecisionPrompt(decisionsPath2) {
4714
- return [
4715
- "\u6C7A\u7B56\u66F8\uFF08\u672C\u6B21 session \u5168\u7A0B\u9069\u7528\uFF09\uFF1A",
4716
- `1. \u672C\u4EFB\u52D9\u7684\u6C7A\u7B56\u66F8\u662F ${decisionsPath2}\uFF1B\u505A\u51FA\u65B0\u6C7A\u7B56\u3001\u6216\u8ABF\u6574\u65E2\u6709\u6C7A\u7B56\u6642\uFF0C\u7576\u4E0B\u5C31\u66F4\u65B0\u5B83\uFF08\u4E0D\u5B58\u5728\u8ACB\u5EFA\u7ACB\uFF09\u3002`,
4717
- "2. \u53EA\u5BEB\u300C\u9019\u6B21\u65B0\u589E\u4E86\u4EC0\u9EBC\u6C7A\u7B56\u3001\u8ABF\u6574\u4E86\u4EC0\u9EBC\u65E2\u6709\u6C7A\u7B56\u300D\u8207\u7406\u7531\u3002\u9700\u6C42\u8986\u8FF0\u3001\u9032\u5EA6\u56DE\u5831\u3001\u5BE6\u4F5C\u6D41\u6C34\u5E33\u3001\u672C\u4F86\u5C31\u6210\u7ACB\u7684\u6163\u4F8B\u90FD\u4E0D\u8981\u5BEB\u3002",
4718
- "3. \u6BCF\u5247\u6C7A\u7B56\u4E00\u500B `## <\u4E00\u53E5\u8A71\u8B1B\u5B8C\u7684\u6C7A\u7B56>` \u6BB5\u843D\uFF0C\u5E95\u4E0B 2-4 \u884C\u4EA4\u4EE3\uFF1A\u70BA\u4EC0\u9EBC\u9019\u6A23\u9078\u3001\u5426\u6C7A\u4E86\u4EC0\u9EBC\u66FF\u4EE3\u65B9\u6848\u3001\u8AB0\u4E4B\u5F8C\u6703\u88AB\u9019\u500B\u6C7A\u7B56\u7D81\u4F4F\u3002",
4719
- "4. \u8981\u8AAA\u660E\u6D41\u7A0B\u3001\u72C0\u614B\u8F49\u63DB\u6216\u6A21\u7D44\u95DC\u4FC2\u6642\uFF0C\u7528 mermaid fence \u756B\u5716\uFF0CVibeFlow \u6703\u76F4\u63A5\u6E32\u67D3\u3002",
4720
- "5. \u9019\u6B21\u6C92\u6709\u4EFB\u4F55\u6C7A\u7B56\u5C31\u4E0D\u8981\u5EFA\u7ACB\u9019\u500B\u6A94\u6848\uFF1B\u5BE7\u53EF\u7559\u767D\uFF0C\u4E0D\u8981\u6E4A\u5167\u5BB9\u3002",
4721
- "6. \u7528\u7E41\u9AD4\u4E2D\u6587\u66F8\u5BEB\uFF0C\u7A0B\u5F0F\u8B58\u5225\u5B57\u4FDD\u7559\u82F1\u6587\u3002"
4722
- ].join("\n");
4723
- }
4724
4795
  var AUTO_PERMISSION_MODE = "auto";
4725
4796
  function claudePermissionFlag(autoMode) {
4726
4797
  return autoMode ? ` --permission-mode ${AUTO_PERMISSION_MODE}` : "";
@@ -4849,11 +4920,9 @@ function buildAgentCommand(task, systemPrompt, opts, workspacePath) {
4849
4920
  const model = taskModel(task);
4850
4921
  const artifactsDir = agentArtifactsDir(task.worktreePath, workspacePath) ?? ARTIFACTS_FALLBACK_DIR2;
4851
4922
  const builtInPrompt = buildArtifactPrompt(artifactsDir);
4852
- const decisionsPath2 = taskDecisionsPath(task.worktreePath, workspacePath);
4853
- const decisionPrompt = decisionsPath2 ? buildDecisionPrompt(decisionsPath2) : "";
4854
4923
  const libraryPrompt = opts?.library?.promptText?.trim();
4855
4924
  const customPrompt = resolveSystemPrompt(systemPrompt);
4856
- const sys = [builtInPrompt, decisionPrompt, libraryPrompt, customPrompt].filter(Boolean).join("\n\n");
4925
+ const sys = [builtInPrompt, libraryPrompt, customPrompt].filter(Boolean).join("\n\n");
4857
4926
  const includeTaskPrompt = opts?.includeTaskPrompt !== false;
4858
4927
  const prompt = includeTaskPrompt && !opts?.resume ? buildPrompt(task) : "";
4859
4928
  const sessionId = agent === "claude" && includeTaskPrompt ? executorSessionId(task.id, task.runId) : void 0;
@@ -4962,15 +5031,54 @@ function createCore({ sessions, bus, version }) {
4962
5031
  }
4963
5032
  function claudeSessionFile(cwd, sessionId) {
4964
5033
  const munged = cwd.replace(/[^a-zA-Z0-9]/g, "-");
4965
- return path19.join(os5.homedir(), ".claude", "projects", munged, `${sessionId}.jsonl`);
5034
+ return path20.join(os5.homedir(), ".claude", "projects", munged, `${sessionId}.jsonl`);
4966
5035
  }
4967
5036
  async function removeTaskWorktree(task) {
4968
5037
  if (!task.projectPath || !task.worktreePath) return;
4969
5038
  const branch = task.branch || fallbackBranchName(task.id);
4970
5039
  await removeWorktree(task.projectPath, task.worktreePath);
4971
- deleteArtifacts(task.workspacePath ?? path19.dirname(task.worktreePath), task.worktreePath);
5040
+ deleteArtifacts(task.workspacePath ?? path20.dirname(task.worktreePath), task.worktreePath);
4972
5041
  await deleteBranch(task.projectPath, branch);
4973
5042
  }
5043
+ function columnOf(taskId) {
5044
+ const board = getState().board;
5045
+ return COLUMNS.find((col) => board[col].some((t) => t.id === taskId)) ?? null;
5046
+ }
5047
+ const provisioning = /* @__PURE__ */ new Map();
5048
+ function ensureProvisioned(task) {
5049
+ if (task.worktreePath) return Promise.resolve(task);
5050
+ const pending = provisioning.get(task.id);
5051
+ if (pending) return pending;
5052
+ const run2 = (async () => {
5053
+ try {
5054
+ updateTask(task.id, { ...await provisionTaskWorktree(task), launchError: void 0 });
5055
+ bus.emit("state:changed", getState());
5056
+ } catch (err) {
5057
+ returnUnstarted(task.id, err.message);
5058
+ throw err;
5059
+ }
5060
+ const provisioned = findTask(task.id);
5061
+ if (!provisioned) throw new Error("\u627E\u4E0D\u5230\u8981\u57F7\u884C\u7684\u4EFB\u52D9");
5062
+ return provisioned;
5063
+ })().finally(() => provisioning.delete(task.id));
5064
+ provisioning.set(task.id, run2);
5065
+ return run2;
5066
+ }
5067
+ function returnUnstarted(taskId, launchError) {
5068
+ const current = findTask(taskId);
5069
+ if (!current) return;
5070
+ const board = getState().board;
5071
+ const next = {
5072
+ backlog: [
5073
+ { ...current, launchedAt: void 0, launchError },
5074
+ ...board.backlog.filter((t) => t.id !== taskId)
5075
+ ],
5076
+ in_progress: board.in_progress.filter((t) => t.id !== taskId),
5077
+ done: board.done.filter((t) => t.id !== taskId)
5078
+ };
5079
+ setBoard(next);
5080
+ bus.emit("state:changed", getState());
5081
+ }
4974
5082
  const handlers = {
4975
5083
  "vibeflow:getState": () => getState(),
4976
5084
  "app:getVersion": () => version,
@@ -5007,8 +5115,8 @@ function createCore({ sessions, bus, version }) {
5007
5115
  "projects:listRecent": () => listRecentProjects(getStore()),
5008
5116
  // `vibeflow open <path>` while a host runs: only the host writes the store.
5009
5117
  "projects:record": (projectPath) => {
5010
- const p = path19.resolve(str(projectPath, "projectPath"));
5011
- if (!fs15.existsSync(p) || !fs15.statSync(p).isDirectory()) invalid(`not a directory: ${p}`);
5118
+ const p = path20.resolve(str(projectPath, "projectPath"));
5119
+ if (!fs16.existsSync(p) || !fs16.statSync(p).isDirectory()) invalid(`not a directory: ${p}`);
5012
5120
  recordRecentProject(getStore(), p);
5013
5121
  return listRecentProjects(getStore());
5014
5122
  },
@@ -5040,15 +5148,21 @@ function createCore({ sessions, bus, version }) {
5040
5148
  },
5041
5149
  // Discard everything the last run left behind — the PTY session, the
5042
5150
  // sub-agent timeline, the conversation id, the code it changed, its artifacts
5043
- // and its decision record — so the next launch starts over as if the card
5151
+ // and its legacy decision record — so the next launch starts over as if the card
5044
5152
  // were new. Launching again is the caller's decision.
5045
5153
  "vibeflow:resetTaskRun": async (taskId) => {
5046
5154
  const task = requireTask(taskId);
5047
- if (!task.worktreePath) throw new Error("\u4EFB\u52D9\u6C92\u6709\u53EF\u7528\u7684 worktree");
5155
+ if (!task.worktreePath) {
5156
+ await teardownTask(task.id);
5157
+ updateTask(task.id, { launchedAt: Date.now(), runId: randomUUID4() });
5158
+ const reset2 = findTask(task.id);
5159
+ if (!reset2) throw new Error("\u627E\u4E0D\u5230\u8981\u91CD\u7F6E\u7684\u4EFB\u52D9");
5160
+ return { state: getState(), task: reset2 };
5161
+ }
5048
5162
  if (!task.baseBranch) throw new Error("\u4EFB\u52D9\u6C92\u6709\u8A18\u9304 base branch\uFF0C\u7121\u6CD5\u9084\u539F worktree");
5049
5163
  await teardownTask(task.id);
5050
5164
  await resetWorktreeToBase(task.worktreePath, task.baseBranch);
5051
- const workspacePath = task.workspacePath ?? path19.dirname(task.worktreePath);
5165
+ const workspacePath = task.workspacePath ?? path20.dirname(task.worktreePath);
5052
5166
  deleteArtifacts(workspacePath, task.worktreePath);
5053
5167
  deleteDecisions(workspacePath, decisionsKey(task.worktreePath, task.branch));
5054
5168
  resetSubAgents(task.worktreePath);
@@ -5058,21 +5172,25 @@ function createCore({ sessions, bus, version }) {
5058
5172
  bus.emit("subagents:update", { taskId: task.id, subAgents: [] });
5059
5173
  return { state: getState(), task: reset };
5060
5174
  },
5061
- // Edit an existing card's fields. Most are plain metadata read at launch time
5062
- // (title / description / agent / model / workspace). The project folder
5063
- // and base branch are git-bound: re-selecting them rebuilds the worktree, which
5064
- // is only allowed for not-yet-launched tasks (no work to lose).
5175
+ // Edit an existing card's fields. Only a Backlog card is editable: once it
5176
+ // runs, what it was asked to do and where it does it are history. The
5177
+ // git-bound fields (project, base, branch) are just stored — the worktree is
5178
+ // built when the card starts. A Backlog card that already has one (made
5179
+ // before provisioning moved to launch, or returned from a run) gives it up,
5180
+ // but only while it holds no changes.
5065
5181
  "vibeflow:updateTask": async (payload) => {
5066
5182
  const p = obj(payload, "payload");
5067
5183
  const existing = requireTask(p.taskId);
5184
+ if (columnOf(existing.id) !== "backlog") {
5185
+ throw new Error("\u53EA\u6709 Backlog \u7684\u4EFB\u52D9\u53EF\u4EE5\u7DE8\u8F2F");
5186
+ }
5068
5187
  let gitPatch = {};
5069
5188
  const nextProject = p.projectPath || existing.projectPath;
5189
+ const nextBranch = p.branch === void 0 ? "" : str(p.branch, "branch").trim();
5070
5190
  const projectChanged2 = Boolean(p.projectPath && p.projectPath !== existing.projectPath);
5071
5191
  const baseChanged = p.baseBranch != null && p.baseBranch !== (existing.baseBranch ?? null);
5072
- if (nextProject && (projectChanged2 || baseChanged)) {
5073
- if (existing.launchedAt) {
5074
- throw new Error("\u4EFB\u52D9\u5DF2\u958B\u59CB\u57F7\u884C\uFF0C\u7121\u6CD5\u66F4\u63DB\u5C08\u6848\u8CC7\u6599\u593E\u6216\u57FA\u6E96\u5206\u652F");
5075
- }
5192
+ const branchChanged = Boolean(nextBranch && nextBranch !== existing.branch);
5193
+ if (nextProject && (projectChanged2 || baseChanged || branchChanged)) {
5076
5194
  if (existing.worktreePath) {
5077
5195
  const diff = await getWorktreeDiffEntries(
5078
5196
  existing.worktreePath,
@@ -5080,35 +5198,30 @@ function createCore({ sessions, bus, version }) {
5080
5198
  { fetch: false }
5081
5199
  );
5082
5200
  if (diff.length > 0) {
5083
- throw new Error("\u76EE\u524D worktree \u5DF2\u6709\u8B8A\u66F4\uFF0C\u7121\u6CD5\u66F4\u63DB\u5C08\u6848\u8CC7\u6599\u593E\u6216\u57FA\u6E96\u5206\u652F");
5201
+ throw new Error("\u76EE\u524D worktree \u5DF2\u6709\u8B8A\u66F4\uFF0C\u7121\u6CD5\u66F4\u63DB\u5C08\u6848\u8CC7\u6599\u593E\u3001\u57FA\u6E96\u5206\u652F\u6216\u5206\u652F\u540D\u7A31");
5084
5202
  }
5085
5203
  }
5086
5204
  const info = await getGitInfo(nextProject);
5087
5205
  if (!info.isRepo) throw new Error("\u76EE\u6A19\u8CC7\u6599\u593E\u4E0D\u662F git repository");
5206
+ const projectName = path20.basename(nextProject);
5207
+ const workspacePath = projectWorkstationPath(resolveWorkstationPath(getSettings()), projectName);
5088
5208
  await teardownTask(existing.id);
5089
5209
  await removeTaskWorktree(existing);
5090
- const projectName = path19.basename(nextProject);
5091
- const workspacePath = projectWorkstationPath(resolveWorkstationPath(getSettings()), projectName);
5092
- await fs15.promises.mkdir(workspacePath, { recursive: true });
5093
- const result = await provisionWorktree(
5094
- nextProject,
5095
- workspacePath,
5096
- existing.id,
5097
- p.baseBranch ?? existing.baseBranch ?? null,
5098
- existing.branch
5099
- );
5210
+ if (branchChanged) await assertBranchAvailable(nextProject, workspacePath, nextBranch);
5100
5211
  recordRecentProject(getStore(), nextProject);
5101
5212
  gitPatch = {
5102
5213
  projectPath: nextProject,
5103
5214
  projectName,
5104
- branch: result.branch,
5105
- worktreePath: result.worktreePath,
5106
5215
  workspacePath,
5107
- baseBranch: result.baseBranch,
5108
- pushed: result.pushed
5216
+ baseBranch: (p.baseBranch ?? (projectChanged2 ? null : existing.baseBranch)) || info.defaultBase || info.currentBranch || void 0,
5217
+ ...branchChanged ? { branch: nextBranch, branchExplicit: true } : {},
5218
+ worktreePath: void 0,
5219
+ pushed: void 0,
5220
+ launchedAt: void 0
5109
5221
  };
5110
5222
  }
5111
5223
  updateTask(existing.id, {
5224
+ launchError: void 0,
5112
5225
  title: (p.title ?? "").trim() || `Task ${existing.id}`,
5113
5226
  description: p.description?.trim() || void 0,
5114
5227
  agentCli: p.agentCli,
@@ -5124,10 +5237,12 @@ function createCore({ sessions, bus, version }) {
5124
5237
  // string or a cwd, so a connected client cannot run anything else.
5125
5238
  "pty:start": async (payload) => {
5126
5239
  const p = obj(payload, "payload");
5127
- const task = requireTask(p.taskId);
5128
- const key = sessionKeyFor(task.id, p.sessionKey);
5129
- const cwd = task.worktreePath ?? task.projectPath;
5130
- if (!cwd) throw new Error("\u4EFB\u52D9\u6C92\u6709\u53EF\u7528\u7684\u5DE5\u4F5C\u76EE\u9304");
5240
+ const requested = requireTask(p.taskId);
5241
+ const key = sessionKeyFor(requested.id, p.sessionKey);
5242
+ const pending = provisioning.get(requested.id);
5243
+ const task = p.launch ? await ensureProvisioned(requested) : pending ? await pending : requested;
5244
+ const cwd = task.worktreePath;
5245
+ if (!cwd) throw new Error("\u4EFB\u52D9\u5C1A\u672A\u958B\u59CB\u57F7\u884C\uFF0C\u9084\u6C92\u6709 worktree");
5131
5246
  let command;
5132
5247
  let fresh = p.fresh === true;
5133
5248
  if (p.launch) {
@@ -5177,7 +5292,7 @@ function createCore({ sessions, bus, version }) {
5177
5292
  "claude:sessionExists": (taskId) => {
5178
5293
  const task = requireTask(taskId);
5179
5294
  if (!task.worktreePath) return false;
5180
- return fs15.existsSync(claudeSessionFile(task.worktreePath, executorSessionId(task.id, task.runId)));
5295
+ return fs16.existsSync(claudeSessionFile(task.worktreePath, executorSessionId(task.id, task.runId)));
5181
5296
  },
5182
5297
  "sessions:list": async () => {
5183
5298
  const keys = await sessions.list();
@@ -5224,13 +5339,16 @@ function createCore({ sessions, bus, version }) {
5224
5339
  const task = requireTask(taskId);
5225
5340
  if (!task.worktreePath || !task.workspacePath) return "task has no worktree";
5226
5341
  const dir = agentArtifactsPath(task.workspacePath, task.worktreePath);
5227
- if (!fs15.existsSync(dir)) return "artifacts directory does not exist yet";
5342
+ if (!fs16.existsSync(dir)) return "artifacts directory does not exist yet";
5228
5343
  return getPlatform().openPath(dir);
5229
5344
  },
5230
- "task:getDecisions": (taskId) => {
5345
+ "task:getSpecs": async (taskId) => {
5231
5346
  const task = requireTask(taskId);
5232
- if (!task.workspacePath) return null;
5233
- return readDecisions(task.workspacePath, decisionsKey(task.worktreePath, task.branch));
5347
+ const base = task.baseBranch ?? "main";
5348
+ if (task.worktreePath) return readWorktreeSpecs(task.worktreePath, base);
5349
+ if (!task.projectPath || !task.launchedAt) return [];
5350
+ const changed = task.outcome?.files.filter((f) => f.status !== "D").map((f) => f.path);
5351
+ return readBranchSpecs(task.projectPath, task.branch, base, changed);
5234
5352
  },
5235
5353
  "board:getCliLaunchInfo": () => boardCliLaunchInfo(),
5236
5354
  "library:list": async () => listLibrary(await libraryRoot(), builtinSkillsDir()),
@@ -5333,6 +5451,9 @@ function createCore({ sessions, bus, version }) {
5333
5451
  deleteDecisions(task.workspacePath, decisionsKey(task.worktreePath, task.branch));
5334
5452
  }
5335
5453
  await removeTaskWorktree(task);
5454
+ if (task.workspacePath) {
5455
+ fs16.rmSync(taskAttachmentStagingPath(task.workspacePath, task.id), { recursive: true, force: true });
5456
+ }
5336
5457
  clearConversation(task.id);
5337
5458
  removeTask(task.id);
5338
5459
  return getState();
@@ -5340,9 +5461,11 @@ function createCore({ sessions, bus, version }) {
5340
5461
  "attachments:write": (payload) => {
5341
5462
  const p = obj(payload, "payload");
5342
5463
  const task = requireTask(p.taskId);
5343
- if (!task.worktreePath) throw new Error("\u627E\u4E0D\u5230\u4EFB\u52D9 worktree");
5344
5464
  if (!Array.isArray(p.attachments)) invalid("attachments must be an array");
5345
- return writeAttachments(task.worktreePath, p.attachments);
5465
+ const inputs = p.attachments;
5466
+ if (task.worktreePath) return writeAttachments(task.worktreePath, inputs);
5467
+ if (!task.workspacePath) throw new Error("\u627E\u4E0D\u5230\u4EFB\u52D9 worktree");
5468
+ return writeAttachmentsTo(taskAttachmentStagingPath(task.workspacePath, task.id), inputs);
5346
5469
  },
5347
5470
  "chat:load": (taskId) => loadConversation(requireTask(taskId).id),
5348
5471
  "chat:cancel": (taskId) => {
@@ -5382,9 +5505,9 @@ function createCore({ sessions, bus, version }) {
5382
5505
  // Watch the containing directory, not the file: JsonStore renames a temp
5383
5506
  // file over the target and so replaces the inode on every save.
5384
5507
  watchStore() {
5385
- const file = path19.basename(getStorePath());
5508
+ const file = path20.basename(getStorePath());
5386
5509
  let debounce = null;
5387
- storeWatcher = fs15.watch(path19.dirname(getStorePath()), (_event, filename) => {
5510
+ storeWatcher = fs16.watch(path20.dirname(getStorePath()), (_event, filename) => {
5388
5511
  if (filename !== file) return;
5389
5512
  if (debounce) clearTimeout(debounce);
5390
5513
  debounce = setTimeout(() => {
@@ -5416,9 +5539,9 @@ function createSessionBackend(sink, choice) {
5416
5539
  }
5417
5540
 
5418
5541
  // packages/core/src/web-server.ts
5419
- import fs16 from "fs";
5542
+ import fs17 from "fs";
5420
5543
  import http from "http";
5421
- import path20 from "path";
5544
+ import path21 from "path";
5422
5545
  import { timingSafeEqual } from "crypto";
5423
5546
  import { WebSocketServer } from "ws";
5424
5547
  var TOKEN_COOKIE = "vf_token";
@@ -5471,14 +5594,14 @@ function resolveStatic(root, urlPath) {
5471
5594
  return null;
5472
5595
  }
5473
5596
  if (decoded.includes("\0")) return null;
5474
- const rel = path20.posix.normalize(decoded).replace(/^\/+/, "");
5475
- const base = path20.resolve(root);
5476
- const candidates = [rel, path20.posix.join(rel, "index.html"), `${rel}.html`];
5597
+ const rel = path21.posix.normalize(decoded).replace(/^\/+/, "");
5598
+ const base = path21.resolve(root);
5599
+ const candidates = [rel, path21.posix.join(rel, "index.html"), `${rel}.html`];
5477
5600
  for (const c of candidates) {
5478
- const full = path20.resolve(base, c);
5479
- if (full !== base && !full.startsWith(base + path20.sep)) return null;
5601
+ const full = path21.resolve(base, c);
5602
+ if (full !== base && !full.startsWith(base + path21.sep)) return null;
5480
5603
  try {
5481
- if (fs16.statSync(full).isFile()) return full;
5604
+ if (fs17.statSync(full).isFile()) return full;
5482
5605
  } catch {
5483
5606
  }
5484
5607
  }
@@ -5539,7 +5662,7 @@ async function startWebServer(options) {
5539
5662
  send(res, 404, "Not found");
5540
5663
  return;
5541
5664
  }
5542
- const ext = path20.extname(file).toLowerCase();
5665
+ const ext = path21.extname(file).toLowerCase();
5543
5666
  const immutable = url.pathname.startsWith("/_next/static/");
5544
5667
  res.writeHead(200, {
5545
5668
  "Content-Type": CONTENT_TYPES[ext] ?? "application/octet-stream",
@@ -5552,7 +5675,7 @@ async function startWebServer(options) {
5552
5675
  res.end();
5553
5676
  return;
5554
5677
  }
5555
- fs16.createReadStream(file).pipe(res);
5678
+ fs17.createReadStream(file).pipe(res);
5556
5679
  }
5557
5680
  const wss = new WebSocketServer({ noServer: true, maxPayload: MAX_PAYLOAD });
5558
5681
  function reject(socket, status, reason) {
@@ -5724,7 +5847,7 @@ function loginUrl(lock, pathname = "/home/") {
5724
5847
  return `http://127.0.0.1:${lock.port}${pathname}?token=${encodeURIComponent(lock.token)}`;
5725
5848
  }
5726
5849
  function isWebDir(dir) {
5727
- return fs17.existsSync(path21.join(dir, "home", "index.html"));
5850
+ return fs18.existsSync(path22.join(dir, "home", "index.html"));
5728
5851
  }
5729
5852
 
5730
5853
  // packages/cli/src/remote.ts
@@ -5841,8 +5964,8 @@ function connectToHost(lock) {
5841
5964
  }
5842
5965
 
5843
5966
  // packages/cli/src/doctor.ts
5844
- import fs18 from "fs";
5845
- import path22 from "path";
5967
+ import fs19 from "fs";
5968
+ import path23 from "path";
5846
5969
  import { execFile as execFile7 } from "child_process";
5847
5970
  import { promisify as promisify5 } from "util";
5848
5971
  init_env();
@@ -5982,10 +6105,10 @@ async function runDoctor(options) {
5982
6105
  });
5983
6106
  }
5984
6107
  try {
5985
- fs18.mkdirSync(options.userDataDir, { recursive: true });
5986
- const probe = path22.join(options.userDataDir, `.doctor-${process.pid}`);
5987
- fs18.writeFileSync(probe, "");
5988
- fs18.rmSync(probe);
6108
+ fs19.mkdirSync(options.userDataDir, { recursive: true });
6109
+ const probe = path23.join(options.userDataDir, `.doctor-${process.pid}`);
6110
+ fs19.writeFileSync(probe, "");
6111
+ fs19.rmSync(probe);
5989
6112
  checks.push({ id: "data", name: "\u8CC7\u6599\u76EE\u9304", level: "ok", detail: options.userDataDir });
5990
6113
  } catch (err) {
5991
6114
  checks.push({
@@ -6039,24 +6162,24 @@ Options:
6039
6162
  `.trim();
6040
6163
  function detectLayout() {
6041
6164
  const file = fileURLToPath(import.meta.url);
6042
- const here = path25.dirname(file);
6043
- const bundledWeb = path25.join(here, "web");
6044
- if (isWebDir(bundledWeb) || path25.basename(here) === "dist") {
6165
+ const here = path26.dirname(file);
6166
+ const bundledWeb = path26.join(here, "web");
6167
+ if (isWebDir(bundledWeb) || path26.basename(here) === "dist") {
6045
6168
  return {
6046
- version: readVersion(path25.join(here, "..", "package.json")),
6169
+ version: readVersion(path26.join(here, "..", "package.json")),
6047
6170
  webDir: isWebDir(bundledWeb) ? bundledWeb : null,
6048
6171
  sourceRoot: null,
6049
6172
  cliEntry: file
6050
6173
  };
6051
6174
  }
6052
- const root = path25.resolve(here, "..", "..", "..");
6175
+ const root = path26.resolve(here, "..", "..", "..");
6053
6176
  const override = process.env.VIBEFLOW_WEB_DIR;
6054
- const webDir = override && isWebDir(override) ? override : isWebDir(path25.join(root, "app")) ? path25.join(root, "app") : null;
6055
- return { version: readVersion(path25.join(root, "package.json")), webDir, sourceRoot: root, cliEntry: null };
6177
+ const webDir = override && isWebDir(override) ? override : isWebDir(path26.join(root, "app")) ? path26.join(root, "app") : null;
6178
+ return { version: readVersion(path26.join(root, "package.json")), webDir, sourceRoot: root, cliEntry: null };
6056
6179
  }
6057
6180
  function readVersion(pkgPath) {
6058
6181
  try {
6059
- return JSON.parse(fs20.readFileSync(pkgPath, "utf8")).version;
6182
+ return JSON.parse(fs21.readFileSync(pkgPath, "utf8")).version;
6060
6183
  } catch {
6061
6184
  return "0.0.0";
6062
6185
  }
@@ -6262,8 +6385,8 @@ async function cmdOpen(values, target, layout, userDataDir) {
6262
6385
  console.error("\u7528\u6CD5\uFF1Avibeflow open <path>");
6263
6386
  return 1;
6264
6387
  }
6265
- const dir = path25.resolve(target);
6266
- if (!fs20.existsSync(dir) || !fs20.statSync(dir).isDirectory()) {
6388
+ const dir = path26.resolve(target);
6389
+ if (!fs21.existsSync(dir) || !fs21.statSync(dir).isDirectory()) {
6267
6390
  console.error(`\u4E0D\u662F\u8CC7\u6599\u593E\uFF1A${dir}`);
6268
6391
  return 1;
6269
6392
  }