brainfile 0.21.0 → 0.21.2

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.
Files changed (38) hide show
  1. package/dist/{chunk-FOLBAGJS.mjs → chunk-7KU2R7QC.mjs} +245 -101
  2. package/dist/cli.mjs +324 -207
  3. package/dist/commands/brief.js +1 -1
  4. package/dist/commands/brief.js.map +1 -1
  5. package/dist/commands/log.d.ts.map +1 -1
  6. package/dist/commands/log.js +4 -1
  7. package/dist/commands/log.js.map +1 -1
  8. package/dist/commands/sync.d.ts +3 -1
  9. package/dist/commands/sync.d.ts.map +1 -1
  10. package/dist/commands/sync.js +23 -14
  11. package/dist/commands/sync.js.map +1 -1
  12. package/dist/commands/where.d.ts +4 -1
  13. package/dist/commands/where.d.ts.map +1 -1
  14. package/dist/commands/where.js +9 -3
  15. package/dist/commands/where.js.map +1 -1
  16. package/dist/mcp/helpers.d.ts +3 -2
  17. package/dist/mcp/helpers.d.ts.map +1 -1
  18. package/dist/mcp/helpers.js +5 -6
  19. package/dist/mcp/helpers.js.map +1 -1
  20. package/dist/mcp/tools/brief_tool.js +1 -1
  21. package/dist/mcp/tools/brief_tool.js.map +1 -1
  22. package/dist/{tui-M2KWIG2E.mjs → tui-3Z7ZJV7M.mjs} +1 -1
  23. package/dist/utils/actor.d.ts +22 -0
  24. package/dist/utils/actor.d.ts.map +1 -0
  25. package/dist/utils/actor.js +200 -0
  26. package/dist/utils/actor.js.map +1 -0
  27. package/dist/utils/board-commit-hooks.d.ts.map +1 -1
  28. package/dist/utils/board-commit-hooks.js +3 -2
  29. package/dist/utils/board-commit-hooks.js.map +1 -1
  30. package/dist/utils/board-repo.d.ts +33 -9
  31. package/dist/utils/board-repo.d.ts.map +1 -1
  32. package/dist/utils/board-repo.js +162 -61
  33. package/dist/utils/board-repo.js.map +1 -1
  34. package/dist/utils/board-sync.d.ts +26 -11
  35. package/dist/utils/board-sync.d.ts.map +1 -1
  36. package/dist/utils/board-sync.js +141 -56
  37. package/dist/utils/board-sync.js.map +1 -1
  38. package/package.json +2 -2
@@ -1003,7 +1003,7 @@ var require_ansi_styles = __commonJS({
1003
1003
  });
1004
1004
  };
1005
1005
  var colorConvert;
1006
- var makeDynamicStyles = (wrap, targetSpace, identity2, isBackground) => {
1006
+ var makeDynamicStyles = (wrap, targetSpace, identity, isBackground) => {
1007
1007
  if (colorConvert === void 0) {
1008
1008
  colorConvert = require_color_convert();
1009
1009
  }
@@ -1012,7 +1012,7 @@ var require_ansi_styles = __commonJS({
1012
1012
  for (const [sourceSpace, suite] of Object.entries(colorConvert)) {
1013
1013
  const name = sourceSpace === "ansi16" ? "ansi" : sourceSpace;
1014
1014
  if (sourceSpace === targetSpace) {
1015
- styles[name] = wrap(identity2, offset);
1015
+ styles[name] = wrap(identity, offset);
1016
1016
  } else if (typeof suite === "object") {
1017
1017
  styles[name] = wrap(suite[targetSpace], offset);
1018
1018
  }
@@ -5622,7 +5622,7 @@ var require_frontmatter = __commonJS({
5622
5622
  }
5623
5623
  let endIndex = -1;
5624
5624
  for (let i = 1; i < lines.length; i++) {
5625
- if (lines[i].trim() === "---") {
5625
+ if (lines[i].trimEnd() === "---") {
5626
5626
  endIndex = i;
5627
5627
  break;
5628
5628
  }
@@ -8577,6 +8577,7 @@ var require_workspace = __commonJS({
8577
8577
  exports.getLogFilePath = getLogFilePath2;
8578
8578
  exports.findV2Task = findV2Task2;
8579
8579
  exports.extractDescription = extractDescription2;
8580
+ exports.replaceDescription = replaceDescription;
8580
8581
  exports.extractLog = extractLog2;
8581
8582
  exports.composeBody = composeBody2;
8582
8583
  exports.readV2BoardConfig = readV2BoardConfig2;
@@ -8646,10 +8647,27 @@ var require_workspace = __commonJS({
8646
8647
  }
8647
8648
  return null;
8648
8649
  }
8650
+ var DESCRIPTION_SECTION = /## Description\n([\s\S]*?)(?=\n## (?:Log|Child Tasks)[ \t]*(?:\n|$)|\n*$)/;
8649
8651
  function extractDescription2(body) {
8650
- const match = body.match(/## Description\n([\s\S]*?)(?=\n## |\n*$)/);
8652
+ const match = body.match(DESCRIPTION_SECTION);
8651
8653
  return match ? match[1].trim() || void 0 : void 0;
8652
8654
  }
8655
+ function replaceDescription(body, description) {
8656
+ const match = DESCRIPTION_SECTION.exec(body);
8657
+ if (!match)
8658
+ return null;
8659
+ const before = body.slice(0, match.index);
8660
+ const rest = body.slice(match.index + match[0].length).replace(/^\n+/, "");
8661
+ const text = description?.trim();
8662
+ if (!text)
8663
+ return `${before}${rest}`;
8664
+ return rest ? `${before}## Description
8665
+ ${text}
8666
+
8667
+ ${rest}` : `${before}## Description
8668
+ ${text}
8669
+ `;
8670
+ }
8653
8671
  function extractLog2(body) {
8654
8672
  const match = body.match(/## Log\n([\s\S]*?)(?=\n## |\n*$)/);
8655
8673
  return match ? match[1].trim() || void 0 : void 0;
@@ -9471,6 +9489,7 @@ var require_taskOperations = __commonJS({
9471
9489
  exports.getEffectiveState = getEffectiveState;
9472
9490
  var fs11 = __importStar(__require("fs"));
9473
9491
  var path11 = __importStar(__require("path"));
9492
+ var workspace_1 = require_workspace();
9474
9493
  var boardValidation_1 = require_boardValidation();
9475
9494
  var contractSpec_1 = require_contractSpec();
9476
9495
  var idGen_1 = require_idGen();
@@ -9772,7 +9791,14 @@ ${section}
9772
9791
  task[key] = value;
9773
9792
  }
9774
9793
  };
9775
- applyField("description", patch.description);
9794
+ let body = doc.body;
9795
+ const inBody = patch.description === void 0 ? null : (0, workspace_1.replaceDescription)(body, patch.description);
9796
+ if (inBody !== null) {
9797
+ body = inBody;
9798
+ delete task.description;
9799
+ } else {
9800
+ applyField("description", patch.description);
9801
+ }
9776
9802
  applyField("priority", patch.priority);
9777
9803
  applyField("tags", patch.tags);
9778
9804
  applyField("assignee", patch.assignee);
@@ -9782,7 +9808,7 @@ ${section}
9782
9808
  applyField("status", patch.status);
9783
9809
  task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
9784
9810
  try {
9785
- (0, taskFile_1.writeTaskFile)(taskPath, task, doc.body);
9811
+ (0, taskFile_1.writeTaskFile)(taskPath, task, body);
9786
9812
  return { success: true, task, filePath: taskPath };
9787
9813
  } catch (err) {
9788
9814
  return { success: false, error: `Failed to write task file: ${err}` };
@@ -10677,7 +10703,7 @@ var require_dist = __commonJS({
10677
10703
  exports.patchTasks = exports.moveTasks = exports.setAllSubtasksCompleted = exports.setSubtasksCompleted = exports.updateSubtask = exports.deleteSubtask = exports.addSubtask = exports.patchTask = exports.restoreTask = exports.archiveTask = exports.updateStatsConfig = exports.updateBoardTitle = exports.toggleSubtask = exports.deleteTask = exports.updateTask = exports.addTask = exports.moveTask = exports.getAllTemplateIds = exports.getTemplateById = exports.processTemplate = exports.generateSubtaskId = exports.generateTaskId = exports.BUILT_IN_TEMPLATES = exports.hashBoardContent = exports.hashBoard = exports.diffBoards = exports.BrainfileLinter = exports.BrainfileValidator = exports.serializeFrontmatter = exports.parseFrontmatter = exports.BrainfileSerializer = exports.loadSchemaHints = exports.parseSchemaHints = exports.inferRenderer = exports.inferType = exports.BrainfileParser = exports.buildContract = exports.parseDeliverableSpec = exports.normalizeToArray = exports.ALLOWED_DELIVERABLE_TYPES = exports.removeTaskContractConstraint = exports.addTaskContractConstraint = exports.removeTaskContractValidationCommand = exports.addTaskContractValidationCommand = exports.removeTaskContractDeliverable = exports.addTaskContractDeliverable = exports.patchTaskContract = exports.setTaskContractStatus = exports.clearTaskContract = exports.setTaskContract = void 0;
10678
10704
  exports.BRAINFILE_BASENAME = exports.DOT_BRAINFILE_DIRNAME = exports.atomicWriteFileSync = exports.ensureDotBrainfileGitignore = exports.ensureDotBrainfileDir = exports.getDotBrainfileGitignorePath = exports.getBrainfileStatePath = exports.getBrainfileStateDir = exports.resolveBrainfilePath = exports.findBrainfile = exports.EXCLUDE_DIRS = exports.BRAINFILE_GLOBS = exports.BRAINFILE_PATTERNS = exports.extractBrainfileSuffix = exports.isBrainfileName = exports.watchBrainfiles = exports.findNearestBrainfile = exports.findPrimaryBrainfile = exports.discover = exports.getParentTaskId = exports.isValidSubtaskId = exports.isValidTaskId = exports.generateNextSubtaskId = exports.generateSubtaskIdFromIndex = exports.generateNextTaskId = exports.getMaxTaskIdNumber = exports.extractTaskIdNumber = exports.getOverdueTasks = exports.getIncompleteSubtasksSummary = exports.getTasksWithIncompleteSubtasks = exports.isCompletionColumn = exports.findCompletionColumn = exports.columnExists = exports.getTotalTaskCount = exports.getColumnTaskCount = exports.searchTasks = exports.getTasksByAssignee = exports.getTasksByPriority = exports.getTasksByTag = exports.getAllTasks = exports.taskIdExists = exports.findTaskById = exports.findColumnByName = exports.findColumnById = exports.validateColumn = exports.validateType = exports.isTypeCompletable = exports.getBoardTypes = exports.archiveTasks = exports.deleteTasks = void 0;
10679
10705
  exports.activateTaskContract = exports.attachTaskContract = exports.failTaskContract = exports.completeTaskContract = exports.deliverTaskContract = exports.pickupTaskContract = exports.updateSubtasksInFile = exports.toggleSubtasksInFile = exports.deleteSubtasksFromFile = exports.addSubtasksToFile = exports.searchLogs = exports.searchTaskFiles = exports.findTask = exports.listTasks = exports.appendLog = exports.deleteTaskFile = exports.completeTaskFile = exports.patchTaskFile = exports.moveTaskFileToColumn = exports.moveTaskFile = exports.addTaskFile = exports.generateNextFileTaskId = exports.DEFAULT_CONTRACT_COLUMN_MAP = exports.taskFileName = exports.readTasksDir = exports.writeTaskFile = exports.readTaskFile = exports.serializeTaskContent = exports.parseTaskContent = exports.writeBoardConfig = exports.readBoardConfig = exports.serializeBoardConfig = exports.parseBoardConfig = exports.formatTaskForLinear = exports.formatTaskForGitHub = exports.BRIEF_STATE_VERSION = exports.sanitizeAgentFilename = exports.getBriefStatePath = exports.getBriefStateDir = exports.writeBriefState = exports.readBriefState = exports.relativeTime = exports.parseNotes = exports.parseNoteLine = exports.isEmptyBrief = exports.buildBrief = exports.DOT_BRAINFILE_STATE_IGNORE_ENTRY = exports.DOT_BRAINFILE_STATE_DIRNAME = exports.DOT_BRAINFILE_GITIGNORE_BASENAME = exports.BRAINFILE_STATE_BASENAME = void 0;
10680
- exports.Brainfile = exports.buildBoardFromV2 = exports.readV2BoardConfig = exports.composeBody = exports.extractLog = exports.extractDescription = exports.findV2Task = exports.getLogFilePath = exports.getTaskFilePath = exports.ensureV2Dirs = exports.isV2 = exports.getV2Dirs = exports.topologicalSort = exports.DependencyCycleError = exports.MissingDependencyError = exports.ledgerRecordToCompletedRef = exports.taskToCompletedRef = exports.isLedgerContractStatus = exports.normalizePathValue = exports.getTaskContext = exports.getFileHistory = exports.queryLedger = exports.readLedger = exports.appendLedgerRecord = exports.buildLedgerRecord = exports.searchTasksRanked = exports.scoreTaskDocument = exports.taskMatchesFilters = exports.parseSearchQuery = exports.getEffectiveState = exports.activateTaskContractsByParent = void 0;
10706
+ exports.Brainfile = exports.buildBoardFromV2 = exports.readV2BoardConfig = exports.composeBody = exports.extractLog = exports.replaceDescription = exports.extractDescription = exports.findV2Task = exports.getLogFilePath = exports.getTaskFilePath = exports.ensureV2Dirs = exports.isV2 = exports.getV2Dirs = exports.topologicalSort = exports.DependencyCycleError = exports.MissingDependencyError = exports.ledgerRecordToCompletedRef = exports.taskToCompletedRef = exports.isLedgerContractStatus = exports.normalizePathValue = exports.getTaskContext = exports.getFileHistory = exports.queryLedger = exports.readLedger = exports.appendLedgerRecord = exports.buildLedgerRecord = exports.searchTasksRanked = exports.scoreTaskDocument = exports.taskMatchesFilters = exports.parseSearchQuery = exports.getEffectiveState = exports.activateTaskContractsByParent = void 0;
10681
10707
  __exportStar(require_types2(), exports);
10682
10708
  __exportStar(require_contract(), exports);
10683
10709
  var contract_1 = require_contract2();
@@ -11236,6 +11262,9 @@ var require_dist = __commonJS({
11236
11262
  Object.defineProperty(exports, "extractDescription", { enumerable: true, get: function() {
11237
11263
  return workspace_1.extractDescription;
11238
11264
  } });
11265
+ Object.defineProperty(exports, "replaceDescription", { enumerable: true, get: function() {
11266
+ return workspace_1.replaceDescription;
11267
+ } });
11239
11268
  Object.defineProperty(exports, "extractLog", { enumerable: true, get: function() {
11240
11269
  return workspace_1.extractLog;
11241
11270
  } });
@@ -11549,18 +11578,34 @@ import * as path2 from "path";
11549
11578
  import { spawnSync } from "child_process";
11550
11579
  var DEFAULT_BOARD_BRANCH = "brainfile";
11551
11580
  var BOARD_BRANCH_CONFIG_KEY = "brainfile.branch";
11581
+ var BOARD_REF_NAMESPACE = "refs/brainfile";
11582
+ var BOARD_NAME_CONFIG_KEY = "brainfile.boardName";
11583
+ var LEGACY_REMOTE_BRANCH_CONFIG_KEY = "brainfile.remoteBranch";
11584
+ var DEFAULT_BOARD_NAME = "board";
11585
+ function remoteBoardRef(name) {
11586
+ return `${BOARD_REF_NAMESPACE}/${name}`;
11587
+ }
11588
+ function boardTrackingRef(remote, name) {
11589
+ return `${BOARD_REF_NAMESPACE}/remotes/${remote}/${name}`;
11590
+ }
11591
+ var NETWORK_TIMEOUT_MS = 8e3;
11592
+ var NO_PROMPT_ENV = { GIT_TERMINAL_PROMPT: "0", GIT_ASKPASS: "echo", SSH_ASKPASS: "echo" };
11593
+ function gitNetwork(args, cwd) {
11594
+ return git(args, cwd, NO_PROMPT_ENV, NETWORK_TIMEOUT_MS);
11595
+ }
11552
11596
  var EXCLUDE_ENTRY = ".brainfile/";
11553
11597
  var EXCLUDE_MARKER = "# brainfile: board worktree (managed by `brainfile`)";
11554
11598
  var FALLBACK_IDENTITY_NAME = "brainfile";
11555
11599
  var FALLBACK_IDENTITY_EMAIL = "brainfile@localhost";
11556
11600
  var MAX_MESSAGE_LENGTH = 72;
11557
11601
  var BOARD_SUBDIRS = ["board", "logs"];
11558
- function git(args, cwd, env) {
11602
+ function git(args, cwd, env, timeoutMs) {
11559
11603
  const result = spawnSync("git", args, {
11560
11604
  cwd,
11561
11605
  encoding: "utf-8",
11562
11606
  env: env ? { ...process.env, ...env } : process.env,
11563
- stdio: ["ignore", "pipe", "pipe"]
11607
+ stdio: ["ignore", "pipe", "pipe"],
11608
+ ...timeoutMs ? { timeout: timeoutMs, killSignal: "SIGKILL" } : {}
11564
11609
  });
11565
11610
  return {
11566
11611
  ok: result.status === 0,
@@ -11625,25 +11670,75 @@ function findTrackedBoardDir(cwd) {
11625
11670
  if (!hit) return null;
11626
11671
  return fs2.existsSync(path2.join(hit.path, "brainfile.md")) ? hit.path : null;
11627
11672
  }
11628
- function materializeBoardWorktree(cwd) {
11629
- const branch = boardBranch(cwd);
11630
- const hasLocal = git(["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`], cwd).ok;
11631
- const configuredRemote = git(["config", "--get", "brainfile.remote"], cwd).stdout || null;
11632
- const configuredBranch = git(["config", "--get", "brainfile.remoteBranch"], cwd).stdout || branch;
11633
- let upstream = null;
11634
- if (!hasLocal) {
11635
- if (configuredRemote) {
11636
- const ref = `${configuredRemote}/${configuredBranch}`;
11637
- if (!git(["rev-parse", "--verify", "--quiet", `refs/remotes/${ref}`], cwd).ok) {
11638
- git(["fetch", "--quiet", configuredRemote, configuredBranch], cwd);
11639
- }
11640
- if (git(["rev-parse", "--verify", "--quiet", `refs/remotes/${ref}`], cwd).ok) upstream = ref;
11641
- }
11642
- if (!upstream && git(["rev-parse", "--verify", "--quiet", `refs/remotes/origin/${branch}`], cwd).ok) {
11643
- upstream = `origin/${branch}`;
11644
- }
11673
+ var PROBE_MISS_TTL_MS = 10 * 6e4;
11674
+ function probeMissFile(cwd) {
11675
+ const common = gitCommonDir(cwd);
11676
+ return common ? path2.join(common, "brainfile-probe.json") : null;
11677
+ }
11678
+ function recentProbeMiss(cwd, remote) {
11679
+ const file = probeMissFile(cwd);
11680
+ if (!file) return false;
11681
+ try {
11682
+ const miss = JSON.parse(fs2.readFileSync(file, "utf-8"));
11683
+ return miss.remote === remote && typeof miss.at === "number" && Date.now() - miss.at < PROBE_MISS_TTL_MS;
11684
+ } catch {
11685
+ return false;
11686
+ }
11687
+ }
11688
+ function recordProbeMiss(cwd, remote) {
11689
+ const file = probeMissFile(cwd);
11690
+ if (!file) return;
11691
+ try {
11692
+ fs2.writeFileSync(file, JSON.stringify({ remote, at: Date.now() }), "utf-8");
11693
+ } catch {
11645
11694
  }
11646
- if (!hasLocal && !upstream) return null;
11695
+ }
11696
+ function clearProbeMiss(cwd) {
11697
+ const file = probeMissFile(cwd);
11698
+ if (file) fs2.rmSync(file, { force: true });
11699
+ }
11700
+ function listRemoteBoards(cwd, remote) {
11701
+ const r = gitNetwork(["ls-remote", "--quiet", remote, `${BOARD_REF_NAMESPACE}/*`], cwd);
11702
+ if (!r.ok) return null;
11703
+ const names = [];
11704
+ for (const line of r.stdout.split("\n")) {
11705
+ const ref = line.split(" ")[1] ?? "";
11706
+ const name = ref.slice(BOARD_REF_NAMESPACE.length + 1);
11707
+ if (ref.startsWith(`${BOARD_REF_NAMESPACE}/`) && name && !name.includes("/")) names.push(name);
11708
+ }
11709
+ return names;
11710
+ }
11711
+ function findBoardStart(cwd, branch, options) {
11712
+ if (git(["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`], cwd).ok) return { ref: branch, remote: null };
11713
+ const configuredRemote = git(["config", "--get", "brainfile.remote"], cwd).stdout || null;
11714
+ const remote = configuredRemote ?? (git(["remote", "get-url", "origin"], cwd).ok ? "origin" : null);
11715
+ if (!remote) return null;
11716
+ const configuredName = git(["config", "--get", BOARD_NAME_CONFIG_KEY], cwd).stdout || git(["config", "--get", LEGACY_REMOTE_BRANCH_CONFIG_KEY], cwd).stdout || null;
11717
+ const knownName = configuredName ?? DEFAULT_BOARD_NAME;
11718
+ const known = boardTrackingRef(remote, knownName);
11719
+ if (git(["rev-parse", "--verify", "--quiet", known], cwd).ok) return { ref: known, remote, name: knownName };
11720
+ if (options.fresh || !recentProbeMiss(cwd, remote)) {
11721
+ const names = listRemoteBoards(cwd, remote);
11722
+ const name = names === null ? null : configuredName ? names.includes(configuredName) ? configuredName : null : names.includes(DEFAULT_BOARD_NAME) ? DEFAULT_BOARD_NAME : names.length === 1 ? names[0] : null;
11723
+ if (name) {
11724
+ const tracking = boardTrackingRef(remote, name);
11725
+ if (gitNetwork(["fetch", "--quiet", "--no-tags", remote, `+${remoteBoardRef(name)}:${tracking}`], cwd).ok) {
11726
+ clearProbeMiss(cwd);
11727
+ return { ref: tracking, remote, name };
11728
+ }
11729
+ }
11730
+ }
11731
+ const legacyBranch = configuredName ?? branch;
11732
+ const legacy = `refs/remotes/${remote}/${legacyBranch}`;
11733
+ if (!git(["rev-parse", "--verify", "--quiet", legacy], cwd).ok && configuredRemote && (options.fresh || !recentProbeMiss(cwd, remote))) {
11734
+ gitNetwork(["fetch", "--quiet", "--no-tags", remote, `+refs/heads/${legacyBranch}:${legacy}`], cwd);
11735
+ }
11736
+ if (git(["rev-parse", "--verify", "--quiet", legacy], cwd).ok) return { ref: legacy, remote };
11737
+ recordProbeMiss(cwd, remote);
11738
+ return null;
11739
+ }
11740
+ function materializeBoardWorktree(cwd, options = {}) {
11741
+ const branch = boardBranch(cwd);
11647
11742
  const worktrees = listWorktrees(cwd);
11648
11743
  if (worktrees.length === 0) return null;
11649
11744
  if (worktrees.some((w) => w.branch === `refs/heads/${branch}`)) return null;
@@ -11657,15 +11752,21 @@ function materializeBoardWorktree(cwd) {
11657
11752
  target = path2.join(main.path, ".brainfile");
11658
11753
  }
11659
11754
  if (fs2.existsSync(target)) return null;
11660
- const r = hasLocal ? git(["worktree", "add", "--quiet", target, branch], cwd) : git(["worktree", "add", "--quiet", "--track", "-b", branch, target, upstream], cwd);
11755
+ const start = findBoardStart(cwd, branch, options);
11756
+ if (!start) return null;
11757
+ const r = start.remote ? git(["worktree", "add", "--quiet", "--no-track", "-b", branch, target, start.ref], cwd) : git(["worktree", "add", "--quiet", target, branch], cwd);
11661
11758
  if (!r.ok) return null;
11662
11759
  ensureExcludeEntry(cwd);
11663
11760
  for (const sub of BOARD_SUBDIRS) fs2.mkdirSync(path2.join(target, sub), { recursive: true });
11664
- const cameFrom = upstream ? upstream.slice(0, upstream.indexOf("/")) : null;
11665
- if (cameFrom && !configuredRemote) git(["config", "brainfile.remote", cameFrom], cwd);
11761
+ if (start.remote) {
11762
+ if (!git(["config", "--get", "brainfile.remote"], cwd).stdout) git(["config", "brainfile.remote", start.remote], cwd);
11763
+ if (start.name && start.name !== DEFAULT_BOARD_NAME && !git(["config", "--get", BOARD_NAME_CONFIG_KEY], cwd).stdout) {
11764
+ git(["config", BOARD_NAME_CONFIG_KEY, start.name], cwd);
11765
+ }
11766
+ }
11666
11767
  const relative3 = path2.relative(process.cwd(), target) || ".";
11667
11768
  process.stderr.write(
11668
- cameFrom ? `Checked out the shared board from ${cameFrom} into ${relative3}/
11769
+ start.remote ? `Checked out the shared board from ${start.remote} into ${relative3}/
11669
11770
  ` : `Checked out the board from the '${branch}' branch into ${relative3}/
11670
11771
  `
11671
11772
  );
@@ -11731,9 +11832,6 @@ function identityArgs(dotDir) {
11731
11832
  if (!git(["config", "--get", "user.email"], dotDir).stdout) args.push("-c", `user.email=${FALLBACK_IDENTITY_EMAIL}`);
11732
11833
  return args;
11733
11834
  }
11734
- function sanitizeAgent(agent) {
11735
- return agent.replace(/[<>\r\n]/g, "").trim();
11736
- }
11737
11835
  var commitListeners = [];
11738
11836
  function onBoardCommitted(listener) {
11739
11837
  if (!commitListeners.includes(listener)) commitListeners.push(listener);
@@ -11768,17 +11866,12 @@ function commitBoard(dotDir, options) {
11768
11866
  if (unmergedFiles(dotDir).length > 0) return false;
11769
11867
  if (!git(["add", "-A"], dotDir).ok) return false;
11770
11868
  if (!boardIsDirty(dotDir)) return false;
11771
- const env = {};
11772
- const agent = options.agent ? sanitizeAgent(options.agent) : "";
11773
- if (agent) {
11774
- env.GIT_AUTHOR_NAME = agent;
11775
- env.GIT_AUTHOR_EMAIL = `${agent}@brainfile.local`;
11776
- }
11777
- const message = oneLine(options.message);
11869
+ const agent = options.agent?.replace(/[[\]<>\r\n]/g, "").trim();
11870
+ const suffix = agent ? ` [${agent}]` : "";
11871
+ const message = oneLine(options.message, suffix);
11778
11872
  const r = git(
11779
11873
  [...identityArgs(dotDir), "-c", "commit.gpgsign=false", "commit", "--quiet", "--no-verify", "-m", message],
11780
- dotDir,
11781
- env
11874
+ dotDir
11782
11875
  );
11783
11876
  if (r.ok) notifyCommitted(dotDir);
11784
11877
  return r.ok;
@@ -11794,13 +11887,6 @@ function commitHandEdits(dotDir) {
11794
11887
  const summary = files.length <= 3 ? files.join(", ") : `${files.length} files`;
11795
11888
  return commitBoard(dotDir, { message: `edit: ${summary}` });
11796
11889
  }
11797
- function resolveAgentName(argv = process.argv) {
11798
- if (process.env.BRAINFILE_AGENT) return process.env.BRAINFILE_AGENT;
11799
- const flag = argv.indexOf("--agent");
11800
- if (flag >= 0 && argv[flag + 1] && !argv[flag + 1].startsWith("-")) return argv[flag + 1];
11801
- const inline = argv.find((a) => a.startsWith("--agent="));
11802
- return inline ? inline.slice("--agent=".length) : null;
11803
- }
11804
11890
  function homeBoardDir() {
11805
11891
  return path2.join(os.homedir(), ".brainfile");
11806
11892
  }
@@ -11808,9 +11894,10 @@ function isGlobalBoardRequested(argv = process.argv) {
11808
11894
  if (process.env.BRAINFILE_GLOBAL === "1") return true;
11809
11895
  return argv.includes("-g") || argv.includes("--global");
11810
11896
  }
11811
- function oneLine(message) {
11897
+ function oneLine(message, suffix = "") {
11812
11898
  const flat = message.replace(/\s+/g, " ").trim();
11813
- return flat.length > MAX_MESSAGE_LENGTH ? `${flat.slice(0, MAX_MESSAGE_LENGTH - 1)}\u2026` : flat;
11899
+ const room = Math.max(1, MAX_MESSAGE_LENGTH - suffix.length);
11900
+ return `${flat.length > room ? `${flat.slice(0, room - 1)}\u2026` : flat}${suffix}`;
11814
11901
  }
11815
11902
  function describeCommandForCommit(command) {
11816
11903
  const names = [];
@@ -11833,7 +11920,7 @@ var exitHookInstalled = false;
11833
11920
  function scheduleBoardCommit(brainfilePath, message, delayMs = 2e3) {
11834
11921
  const dotDir = path2.dirname(path2.resolve(brainfilePath));
11835
11922
  if (!isTrackedBoard(dotDir)) return;
11836
- const entry = pendingCommits.get(dotDir) ?? { messages: [], agent: resolveAgentName() };
11923
+ const entry = pendingCommits.get(dotDir) ?? { messages: [] };
11837
11924
  entry.messages.push(message);
11838
11925
  pendingCommits.set(dotDir, entry);
11839
11926
  if (flushTimer) clearTimeout(flushTimer);
@@ -11852,7 +11939,7 @@ function flushBoardCommits() {
11852
11939
  for (const [dotDir, entry] of pendingCommits) {
11853
11940
  const [first, ...rest] = entry.messages;
11854
11941
  const message = rest.length === 0 ? first : `${first} (+${rest.length} more)`;
11855
- commitBoard(dotDir, { message, agent: entry.agent });
11942
+ commitBoard(dotDir, { message });
11856
11943
  }
11857
11944
  pendingCommits.clear();
11858
11945
  }
@@ -12130,7 +12217,7 @@ import * as fs7 from "fs";
12130
12217
  import * as os3 from "os";
12131
12218
  import * as path7 from "path";
12132
12219
  var REMOTE_CONFIG_KEY = "brainfile.remote";
12133
- var REMOTE_BRANCH_CONFIG_KEY = "brainfile.remoteBranch";
12220
+ var LEGACY_CHECKED_CONFIG_KEY = "brainfile.legacyBranchChecked";
12134
12221
  var AUTOSYNC_CONFIG_KEY = "brainfile.autosync";
12135
12222
  var URL_REMOTE_NAME = "board";
12136
12223
  var MERGE_DRIVER_NAME = "brainfile";
@@ -12152,14 +12239,23 @@ function configSet(dotDir, key, value) {
12152
12239
  function boardRemote(dotDir) {
12153
12240
  return configGet(dotDir, REMOTE_CONFIG_KEY);
12154
12241
  }
12155
- function boardRemoteBranch(dotDir) {
12156
- const configured = configGet(dotDir, REMOTE_BRANCH_CONFIG_KEY);
12242
+ function boardRemoteName(dotDir) {
12243
+ const configured = configGet(dotDir, BOARD_NAME_CONFIG_KEY) ?? configGet(dotDir, LEGACY_REMOTE_BRANCH_CONFIG_KEY);
12157
12244
  if (configured) return configured;
12245
+ return defaultName(dotDir, DEFAULT_BOARD_NAME);
12246
+ }
12247
+ function defaultName(dotDir, inRepo) {
12158
12248
  if (path7.resolve(dotDir) === path7.resolve(homeBoardDir())) return "home";
12159
12249
  if (boardRepoKind(dotDir) === "standalone" && path7.basename(path7.resolve(dotDir)) === ".brainfile") {
12160
- return path7.basename(path7.dirname(path7.resolve(dotDir))) || boardBranch(dotDir);
12250
+ return path7.basename(path7.dirname(path7.resolve(dotDir))) || inRepo;
12161
12251
  }
12162
- return boardBranch(dotDir);
12252
+ return inRepo;
12253
+ }
12254
+ function boardRemoteRef(dotDir) {
12255
+ return remoteBoardRef(boardRemoteName(dotDir));
12256
+ }
12257
+ function legacyRemoteBranch(dotDir) {
12258
+ return configGet(dotDir, LEGACY_REMOTE_BRANCH_CONFIG_KEY) ?? defaultName(dotDir, boardBranch(dotDir));
12163
12259
  }
12164
12260
  function boardAutosync(dotDir) {
12165
12261
  const value = configGet(dotDir, AUTOSYNC_CONFIG_KEY);
@@ -12176,7 +12272,7 @@ function looksLikeRemoteUrl(target) {
12176
12272
  if (target.includes("/") && fs7.existsSync(target)) return true;
12177
12273
  return false;
12178
12274
  }
12179
- function setBoardRemote(cwd, target, remoteBranch) {
12275
+ function setBoardRemote(cwd, target, boardName) {
12180
12276
  const trimmed = target.trim();
12181
12277
  if (!trimmed) throw new Error("remote is required");
12182
12278
  let remote;
@@ -12194,13 +12290,16 @@ function setBoardRemote(cwd, target, remoteBranch) {
12194
12290
  throw new Error(`'${remote}' is not a git remote here.${hint} Pass a URL to add one.`);
12195
12291
  }
12196
12292
  }
12293
+ if (boardName !== void 0 && !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(boardName)) {
12294
+ throw new Error(`'${boardName}' is not a usable board name: use letters, digits, '.', '_' or '-'.`);
12295
+ }
12197
12296
  configSet(cwd, REMOTE_CONFIG_KEY, remote);
12198
- if (remoteBranch) configSet(cwd, REMOTE_BRANCH_CONFIG_KEY, remoteBranch);
12297
+ if (boardName) configSet(cwd, BOARD_NAME_CONFIG_KEY, boardName);
12199
12298
  registerMergeDriver(cwd);
12200
12299
  return {
12201
12300
  remote,
12202
12301
  url: git(["remote", "get-url", remote], cwd).stdout || null,
12203
- remoteBranch: remoteBranch ?? boardRemoteBranch(cwd)
12302
+ remoteRef: boardRemoteRef(cwd)
12204
12303
  };
12205
12304
  }
12206
12305
  function registerMergeDriver(cwd, command) {
@@ -12255,18 +12354,12 @@ function commitCount(dotDir, range) {
12255
12354
  const r = git(["rev-list", "--count", range], dotDir);
12256
12355
  return r.ok ? Number.parseInt(r.stdout, 10) || 0 : 0;
12257
12356
  }
12258
- function identity(dotDir, agent) {
12357
+ function identityArgs2(dotDir) {
12259
12358
  const args = ["-c", "commit.gpgsign=false"];
12260
12359
  if (!git(["config", "--get", "user.email"], dotDir).ok) {
12261
12360
  args.push("-c", "user.name=brainfile", "-c", "user.email=brainfile@localhost");
12262
12361
  }
12263
- const env = {};
12264
- const name = agent?.replace(/[\r\n<>]/g, "").trim();
12265
- if (name) {
12266
- env.GIT_AUTHOR_NAME = name;
12267
- env.GIT_AUTHOR_EMAIL = `${name}@brainfile.local`;
12268
- }
12269
- return { args, env };
12362
+ return args;
12270
12363
  }
12271
12364
  function resolveCompleteVersusEdit(dotDir) {
12272
12365
  const status = git(["status", "--porcelain", "-z"], dotDir);
@@ -12310,16 +12403,49 @@ ${entryLine}
12310
12403
  }
12311
12404
  return resolved;
12312
12405
  }
12406
+ function mergeIncoming(dotDir, incoming, result) {
12407
+ if (git(["merge-base", "--is-ancestor", incoming, "HEAD"], dotDir).ok) return { ok: true, pulled: 0 };
12408
+ const before = git(["rev-parse", "HEAD"], dotDir).stdout;
12409
+ const id = identityArgs2(dotDir);
12410
+ const merge = git([...id, "merge", "--quiet", "--no-edit", incoming], dotDir);
12411
+ if (!merge.ok) {
12412
+ resolveCompleteVersusEdit(dotDir);
12413
+ const remaining = unmergedFiles(dotDir);
12414
+ if (remaining.length > 0) {
12415
+ return {
12416
+ ok: false,
12417
+ result: {
12418
+ ...result,
12419
+ ok: false,
12420
+ failure: "conflict",
12421
+ conflicts: remaining,
12422
+ warning: `merge left conflicts in ${remaining.join(", ")} \u2014 resolve them in ${dotDir}, then run sync again`
12423
+ }
12424
+ };
12425
+ }
12426
+ const commit = git([...id, "commit", "--quiet", "--no-edit", "--no-verify"], dotDir);
12427
+ if (!commit.ok) {
12428
+ return { ok: false, result: { ...result, ok: false, failure: "error", warning: `merge commit failed: ${firstLine(commit.stderr)}` } };
12429
+ }
12430
+ }
12431
+ return { ok: true, pulled: commitCount(dotDir, `${before}..${incoming}`) };
12432
+ }
12433
+ function legacyChecked(dotDir, remote) {
12434
+ return configGet(dotDir, LEGACY_CHECKED_CONFIG_KEY) === remote;
12435
+ }
12313
12436
  function syncBoard(dotDir, options = {}) {
12314
- const pull = options.pull ?? true;
12315
12437
  const push = options.push ?? true;
12316
12438
  const result = { ok: true, pulled: 0, pushed: 0, conflicts: [] };
12317
12439
  if (!isTrackedBoard(dotDir)) return { ...result, skipped: "not-tracked" };
12318
12440
  const remote = boardRemote(dotDir);
12319
12441
  if (!remote) return { ...result, skipped: "no-remote" };
12320
- const remoteBranch = boardRemoteBranch(dotDir);
12442
+ const name = boardRemoteName(dotDir);
12443
+ const remoteRef = remoteBoardRef(name);
12444
+ const tracking = boardTrackingRef(remote, name);
12321
12445
  result.remote = remote;
12322
- result.remoteBranch = remoteBranch;
12446
+ result.remoteRef = remoteRef;
12447
+ const checkLegacy = !legacyChecked(dotDir, remote);
12448
+ const pull = (options.pull ?? true) || checkLegacy;
12323
12449
  const stuck = unmergedFiles(dotDir);
12324
12450
  if (stuck.length > 0) {
12325
12451
  return finish(dotDir, { ...result, ok: false, failure: "conflict", conflicts: stuck, warning: `unresolved conflicts: ${stuck.join(", ")}` });
@@ -12328,41 +12454,38 @@ function syncBoard(dotDir, options = {}) {
12328
12454
  if (ensureBoardAttributes(dotDir)) commitBoard(dotDir, { message: "chore: merge attributes" });
12329
12455
  registerMergeDriver(dotDir);
12330
12456
  let fetched = false;
12457
+ let legacy = null;
12458
+ let legacyAbsent = false;
12331
12459
  if (pull) {
12332
- const fetch2 = git(["fetch", "--quiet", remote, remoteBranch], dotDir);
12460
+ const fetch2 = git(["fetch", "--quiet", "--no-tags", remote, `+${remoteRef}:${tracking}`], dotDir);
12333
12461
  if (fetch2.ok) {
12334
12462
  fetched = true;
12335
12463
  } else if (!isMissingRemoteRef(fetch2.stderr)) {
12336
12464
  return finish(dotDir, { ...result, ok: false, failure: "offline", warning: `fetch from ${remote} failed: ${firstLine(fetch2.stderr)}` });
12465
+ } else {
12466
+ git(["update-ref", "-d", tracking], dotDir);
12337
12467
  }
12338
- if (fetched && !git(["merge-base", "--is-ancestor", "FETCH_HEAD", "HEAD"], dotDir).ok) {
12339
- const before = git(["rev-parse", "HEAD"], dotDir).stdout;
12340
- const id = identity(dotDir, options.agent);
12341
- const merge = git([...id.args, "merge", "--quiet", "--no-edit", "FETCH_HEAD"], dotDir, id.env);
12342
- if (!merge.ok) {
12343
- resolveCompleteVersusEdit(dotDir);
12344
- const remaining = unmergedFiles(dotDir);
12345
- if (remaining.length > 0) {
12346
- return finish(dotDir, {
12347
- ...result,
12348
- ok: false,
12349
- failure: "conflict",
12350
- conflicts: remaining,
12351
- warning: `merge left conflicts in ${remaining.join(", ")} \u2014 resolve them in ${dotDir}, then run sync again`
12352
- });
12353
- }
12354
- const commit = git([...id.args, "commit", "--quiet", "--no-edit", "--no-verify"], dotDir, id.env);
12355
- if (!commit.ok) {
12356
- return finish(dotDir, { ...result, ok: false, failure: "error", warning: `merge commit failed: ${firstLine(commit.stderr)}` });
12357
- }
12468
+ if (checkLegacy) {
12469
+ const branch = legacyRemoteBranch(dotDir);
12470
+ const old = git(["fetch", "--quiet", "--no-tags", remote, `refs/heads/${branch}`], dotDir);
12471
+ if (old.ok) {
12472
+ legacy = { branch, sha: git(["rev-parse", "FETCH_HEAD"], dotDir).stdout };
12473
+ } else if (isMissingRemoteRef(old.stderr)) {
12474
+ legacyAbsent = true;
12358
12475
  }
12359
- result.pulled = commitCount(dotDir, `${before}..FETCH_HEAD`);
12476
+ }
12477
+ for (const incoming of [fetched ? tracking : null, legacy?.sha ?? null]) {
12478
+ if (!incoming) continue;
12479
+ const merged = mergeIncoming(dotDir, incoming, result);
12480
+ if (!merged.ok) return finish(dotDir, merged.result);
12481
+ result.pulled += merged.pulled;
12360
12482
  }
12361
12483
  }
12362
12484
  if (push) {
12363
- const ahead = fetched ? commitCount(dotDir, "FETCH_HEAD..HEAD") : pendingChanges(dotDir);
12485
+ const known = git(["rev-parse", "--verify", "--quiet", tracking], dotDir).ok;
12486
+ const ahead = commitCount(dotDir, known ? `${tracking}..HEAD` : "HEAD");
12364
12487
  if (ahead > 0) {
12365
- const pushed = git(["push", "--quiet", remote, `HEAD:refs/heads/${remoteBranch}`], dotDir);
12488
+ const pushed = git(["push", "--quiet", remote, `HEAD:${remoteRef}`], dotDir);
12366
12489
  if (!pushed.ok) {
12367
12490
  const rejected = /rejected|non-fast-forward|fetch first/i.test(pushed.stderr);
12368
12491
  return finish(dotDir, {
@@ -12372,8 +12495,24 @@ function syncBoard(dotDir, options = {}) {
12372
12495
  warning: rejected ? `push to ${remote} rejected (the remote moved) \u2014 run sync again` : `push to ${remote} failed: ${firstLine(pushed.stderr)}`
12373
12496
  });
12374
12497
  }
12498
+ git(["update-ref", tracking, "HEAD"], dotDir);
12375
12499
  result.pushed = ahead;
12376
12500
  }
12501
+ if (legacy && git(["merge-base", "--is-ancestor", legacy.sha, tracking], dotDir).ok) {
12502
+ const removed = git(
12503
+ ["push", "--quiet", `--force-with-lease=refs/heads/${legacy.branch}:${legacy.sha}`, remote, `:refs/heads/${legacy.branch}`],
12504
+ dotDir
12505
+ );
12506
+ if (removed.ok) {
12507
+ git(["update-ref", "-d", `refs/remotes/${remote}/${legacy.branch}`], dotDir);
12508
+ result.movedFromBranch = legacy.branch;
12509
+ configSet(dotDir, LEGACY_CHECKED_CONFIG_KEY, remote);
12510
+ }
12511
+ }
12512
+ }
12513
+ if (legacyAbsent) {
12514
+ git(["update-ref", "-d", `refs/remotes/${remote}/${legacyRemoteBranch(dotDir)}`], dotDir);
12515
+ configSet(dotDir, LEGACY_CHECKED_CONFIG_KEY, remote);
12377
12516
  }
12378
12517
  return finish(dotDir, result);
12379
12518
  }
@@ -12381,7 +12520,7 @@ function finish(dotDir, result) {
12381
12520
  writeSyncState(dotDir, {
12382
12521
  at: (/* @__PURE__ */ new Date()).toISOString(),
12383
12522
  remote: result.remote ?? "",
12384
- remoteBranch: result.remoteBranch ?? "",
12523
+ remoteRef: result.remoteRef ?? "",
12385
12524
  ok: result.ok,
12386
12525
  ...result.warning ? { warning: result.warning } : {}
12387
12526
  });
@@ -12406,10 +12545,15 @@ function syncStatusLabel(dotDir, now = Date.now()) {
12406
12545
  function pendingChanges(dotDir) {
12407
12546
  const remote = boardRemote(dotDir);
12408
12547
  if (!remote) return 0;
12409
- const tracking = `refs/remotes/${remote}/${boardRemoteBranch(dotDir)}`;
12548
+ const tracking = boardTrackingRef(remote, boardRemoteName(dotDir));
12410
12549
  const known = git(["rev-parse", "--verify", "--quiet", tracking], dotDir).ok;
12411
12550
  return commitCount(dotDir, known ? `${tracking}..HEAD` : "HEAD");
12412
12551
  }
12552
+ function boardWebUrl(remoteUrl, remoteRef) {
12553
+ if (!remoteUrl) return null;
12554
+ const m = /^(?:https:\/\/(?:[^@/]+@)?github\.com\/|git@github\.com:|ssh:\/\/git@github\.com\/)([^/]+)\/([^/]+?)(?:\.git)?\/?$/.exec(remoteUrl);
12555
+ return m ? `https://github.com/${m[1]}/${m[2]}/commits/${remoteRef}` : null;
12556
+ }
12413
12557
  var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
12414
12558
  function syncMessage(result) {
12415
12559
  if (result.skipped === "not-tracked") {
@@ -13442,7 +13586,6 @@ export {
13442
13586
  boardIsDirty,
13443
13587
  commitBoard,
13444
13588
  commitHandEdits,
13445
- resolveAgentName,
13446
13589
  homeBoardDir,
13447
13590
  isGlobalBoardRequested,
13448
13591
  describeCommandForCommit,
@@ -13456,7 +13599,7 @@ export {
13456
13599
  import_core5 as import_core,
13457
13600
  mergeDriverCommand,
13458
13601
  boardRemote,
13459
- boardRemoteBranch,
13602
+ boardRemoteRef,
13460
13603
  boardAutosync,
13461
13604
  setBoardAutosync,
13462
13605
  setBoardRemote,
@@ -13467,6 +13610,7 @@ export {
13467
13610
  secondsSinceSync,
13468
13611
  syncStatusLabel,
13469
13612
  pendingChanges,
13613
+ boardWebUrl,
13470
13614
  syncMessage,
13471
13615
  effectiveAutosync,
13472
13616
  clearPushLock,