@shipers-dev/multi 0.63.0 → 0.64.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.
Files changed (2) hide show
  1. package/dist/index.js +110 -22
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -17116,29 +17116,49 @@ async function ensureWorktree(workingDir, issueKey, issueId, opts) {
17116
17116
  return { path: workingDir, branch: "", created: false };
17117
17117
  }
17118
17118
  ensureGitignoreEntry(workingDir, ".multi/");
17119
- const key = normalizeKey(issueKey);
17120
- const branch = `multi/${key}`;
17119
+ const slug = normalizeKey(opts?.worktreeName ?? issueKey);
17120
+ const branch = `multi/${slug}`;
17121
17121
  const wtDir = join5(workingDir, ".multi", "worktrees");
17122
- const wtPath = join5(wtDir, key);
17122
+ const wtPath = join5(wtDir, slug);
17123
+ let baseRef = "HEAD";
17124
+ const explicit = opts?.baseBranch || opts?.parentBranch || null;
17125
+ if (explicit) {
17126
+ const localOk = await branchExists(workingDir, explicit);
17127
+ if (localOk) {
17128
+ baseRef = explicit;
17129
+ } else {
17130
+ const fetched = await run3(workingDir, "git", ["fetch", "origin", `${explicit}:${explicit}`]);
17131
+ if (fetched.code === 0)
17132
+ baseRef = explicit;
17133
+ }
17134
+ }
17123
17135
  if (existsSync3(wtPath)) {
17136
+ if (await isDirty(wtPath)) {
17137
+ throw new WorktreeDirtyError({ path: wtPath, branch, reason: "dirty" });
17138
+ }
17139
+ if (opts?.baseBranch) {
17140
+ const mergeBase = await run3(wtPath, "git", ["merge-base", branch, opts.baseBranch]);
17141
+ if (mergeBase.code === 0 && mergeBase.stdout) {
17142
+ const forkOnBase = await run3(workingDir, "git", ["merge-base", "--is-ancestor", mergeBase.stdout, opts.baseBranch]);
17143
+ if (forkOnBase.code === 1) {
17144
+ const baseHead = await run3(workingDir, "git", ["rev-parse", "--verify", `${opts.baseBranch}^{commit}`]);
17145
+ throw new WorktreeDirtyError({
17146
+ path: wtPath,
17147
+ branch,
17148
+ reason: "base_mismatch",
17149
+ expectedBase: opts.baseBranch,
17150
+ actualBase: baseHead.code === 0 ? baseHead.stdout : mergeBase.stdout
17151
+ });
17152
+ }
17153
+ }
17154
+ }
17124
17155
  if (issueId)
17125
- setWorktreeIndexEntry(workingDir, key, issueId);
17156
+ setWorktreeIndexEntry(workingDir, slug, issueId);
17126
17157
  return { path: wtPath, branch, created: false };
17127
17158
  }
17128
17159
  try {
17129
17160
  mkdirSync3(wtDir, { recursive: true });
17130
17161
  } catch {}
17131
- let baseRef = "HEAD";
17132
- if (opts?.parentBranch) {
17133
- const localOk = await branchExists(workingDir, opts.parentBranch);
17134
- if (localOk) {
17135
- baseRef = opts.parentBranch;
17136
- } else {
17137
- const fetched = await run3(workingDir, "git", ["fetch", "origin", `${opts.parentBranch}:${opts.parentBranch}`]);
17138
- if (fetched.code === 0)
17139
- baseRef = opts.parentBranch;
17140
- }
17141
- }
17142
17162
  const exists4 = await branchExists(workingDir, branch);
17143
17163
  const args2 = exists4 ? ["worktree", "add", wtPath, branch] : ["worktree", "add", "-b", branch, wtPath, baseRef];
17144
17164
  const r = await run3(workingDir, "git", args2);
@@ -17147,7 +17167,7 @@ async function ensureWorktree(workingDir, issueKey, issueId, opts) {
17147
17167
  }
17148
17168
  await linkIgnoredFiles(workingDir, wtPath);
17149
17169
  if (issueId)
17150
- setWorktreeIndexEntry(workingDir, key, issueId);
17170
+ setWorktreeIndexEntry(workingDir, slug, issueId);
17151
17171
  return { path: wtPath, branch, created: true };
17152
17172
  }
17153
17173
  async function linkIgnoredFiles(workingDir, wtPath) {
@@ -17274,7 +17294,17 @@ async function squashMergeChild(wtPath, childBranch, childKey) {
17274
17294
  result.message = `${childKey} squash-merged (${stagedFiles.length} file${stagedFiles.length === 1 ? "" : "s"})`;
17275
17295
  return result;
17276
17296
  }
17277
- var init_worktree = () => {};
17297
+ var WorktreeDirtyError;
17298
+ var init_worktree = __esm(() => {
17299
+ WorktreeDirtyError = class WorktreeDirtyError extends Error {
17300
+ details;
17301
+ code = "E_WORKTREE_DIRTY";
17302
+ constructor(details) {
17303
+ super(`worktree dirty or base-mismatched at ${details.path} (${details.reason})`);
17304
+ this.details = details;
17305
+ }
17306
+ };
17307
+ });
17278
17308
 
17279
17309
  // ../../node_modules/zod/v4/core/core.js
17280
17310
  function $constructor(name, initializer, params) {
@@ -34022,7 +34052,8 @@ var init_streams = __esm(() => {
34022
34052
  "stopped",
34023
34053
  "queued",
34024
34054
  "worktree_created",
34025
- "worktree_error"
34055
+ "worktree_error",
34056
+ "worktree_dirty"
34026
34057
  ];
34027
34058
  StreamEventTypeSchema = exports_external.enum(STREAM_EVENT_TYPES);
34028
34059
  StreamEventInputSchema = exports_external.object({
@@ -34220,7 +34251,7 @@ function parsePlanBlocks(text) {
34220
34251
  }
34221
34252
  return { actions, errors: errors3 };
34222
34253
  }
34223
- var PLAN_SCHEMA_VERSION = 10, Priority, AssigneeType, IssueStatus, SessionRole, SkillFile, EvalPolicy, PlanActionSchema, PlanEnvelopeSchema, UiBlockSchema, UI_FENCE_RE, FENCE_RE;
34254
+ var PLAN_SCHEMA_VERSION = 11, Priority, AssigneeType, IssueStatus, SessionRole, SkillFile, EvalPolicy, PlanActionSchema, PlanEnvelopeSchema, UiBlockSchema, UI_FENCE_RE, FENCE_RE;
34224
34255
  var init_plans = __esm(() => {
34225
34256
  init_zod();
34226
34257
  Priority = exports_external.enum(["low", "medium", "high"]);
@@ -34243,6 +34274,8 @@ var init_plans = __esm(() => {
34243
34274
  priority: Priority.optional(),
34244
34275
  assignee_type: AssigneeType.optional(),
34245
34276
  assignee_id: exports_external.string().optional(),
34277
+ base_ref: exports_external.string().min(1).max(200).optional(),
34278
+ worktree: exports_external.string().min(1).max(60).regex(/^[a-z0-9][a-z0-9_\-\/]*$/i).optional(),
34246
34279
  parent_id: exports_external.string().optional(),
34247
34280
  blocked_by: exports_external.array(exports_external.string().min(1)).optional(),
34248
34281
  await_children: exports_external.boolean().optional(),
@@ -34853,9 +34886,54 @@ async function handleRunTask(apiUrl, deviceId, task, detected, ctx) {
34853
34886
  const ISSUE_BASE = tenantWsId && projectId ? `${apiUrl}/api/workspaces/${tenantWsId}/projects/${projectId}/issues/${issueId}` : null;
34854
34887
  let workingDir = baseWorkingDir;
34855
34888
  let worktreeBranch = "";
34856
- if (baseWorkingDir) {
34889
+ const inlineMode = task.inline_mode === true;
34890
+ const baseBranch = task.base_branch ?? null;
34891
+ const worktreeName = task.worktree_name ?? null;
34892
+ let inlineRestoreHead = null;
34893
+ const blockOnDirty = async (reason, extra = {}) => {
34894
+ await postStream(apiUrl, issueId, "worktree_dirty", { reason, ...extra });
34895
+ if (tenantWsId) {
34896
+ try {
34897
+ await patchIssueStatus(apiUrl, tenantWsId, issueId, "blocked");
34898
+ } catch {}
34899
+ }
34900
+ await postStream(apiUrl, issueId, "run_finished", { stopReason: "blocked", duration_ms: 0 });
34901
+ };
34902
+ if (baseWorkingDir && inlineMode) {
34857
34903
  try {
34858
- const wt = await ensureWorktree(baseWorkingDir, task.key || issueId, issueId, { parentBranch: task.parent_branch ?? undefined });
34904
+ const status3 = Bun.spawnSync(["git", "status", "--porcelain"], { cwd: baseWorkingDir, stdout: "pipe" });
34905
+ const dirty = (status3.stdout?.toString() || "").trim().length > 0;
34906
+ if (dirty) {
34907
+ await blockOnDirty("inline_dirty", { path: baseWorkingDir });
34908
+ return;
34909
+ }
34910
+ if (baseBranch) {
34911
+ const origSymRef = Bun.spawnSync(["git", "symbolic-ref", "--quiet", "HEAD"], { cwd: baseWorkingDir, stdout: "pipe" });
34912
+ if (origSymRef.exitCode === 0) {
34913
+ inlineRestoreHead = (origSymRef.stdout?.toString() || "").trim().replace(/^refs\/heads\//, "") || null;
34914
+ } else {
34915
+ const origSha = Bun.spawnSync(["git", "rev-parse", "HEAD"], { cwd: baseWorkingDir, stdout: "pipe" });
34916
+ inlineRestoreHead = (origSha.stdout?.toString() || "").trim() || null;
34917
+ }
34918
+ const co = Bun.spawnSync(["git", "checkout", baseBranch], { cwd: baseWorkingDir, stdout: "pipe", stderr: "pipe" });
34919
+ if (co.exitCode !== 0) {
34920
+ await postStream(apiUrl, issueId, "worktree_error", { mode: "inline", message: co.stderr?.toString() || "checkout failed" });
34921
+ await blockOnDirty("inline_checkout_failed", { baseBranch });
34922
+ return;
34923
+ }
34924
+ worktreeBranch = baseBranch;
34925
+ }
34926
+ await postStream(apiUrl, issueId, "worktree_created", { path: baseWorkingDir, branch: worktreeBranch, reused: true, inline: true });
34927
+ } catch (e) {
34928
+ await postStream(apiUrl, issueId, "worktree_error", { mode: "inline", message: fmtError(e) });
34929
+ }
34930
+ } else if (baseWorkingDir) {
34931
+ try {
34932
+ const wt = await ensureWorktree(baseWorkingDir, task.key || issueId, issueId, {
34933
+ baseBranch: baseBranch ?? undefined,
34934
+ parentBranch: task.parent_branch ?? undefined,
34935
+ worktreeName: worktreeName ?? undefined
34936
+ });
34859
34937
  workingDir = wt.path;
34860
34938
  worktreeBranch = wt.branch;
34861
34939
  await postStream(apiUrl, issueId, "worktree_created", { path: wt.path, branch: wt.branch, reused: !wt.created });
@@ -34867,6 +34945,10 @@ async function handleRunTask(apiUrl, deviceId, task, detected, ctx) {
34867
34945
  }
34868
34946
  }
34869
34947
  } catch (e) {
34948
+ if (e?.code === "E_WORKTREE_DIRTY") {
34949
+ await blockOnDirty("worktree_dirty", { details: e.details });
34950
+ return;
34951
+ }
34870
34952
  await postStream(apiUrl, issueId, "worktree_error", { message: fmtError(e) });
34871
34953
  }
34872
34954
  }
@@ -35398,6 +35480,12 @@ ${userPart}` : userPart;
35398
35480
  await apiClient.post(`${ISSUE_BASE}/fail`, {});
35399
35481
  log3(` ✗ ${task.key} failed: ${msg}`);
35400
35482
  }
35483
+ } finally {
35484
+ if (inlineMode && inlineRestoreHead && baseWorkingDir) {
35485
+ try {
35486
+ Bun.spawnSync(["git", "checkout", inlineRestoreHead], { cwd: baseWorkingDir, stdout: "pipe", stderr: "pipe" });
35487
+ } catch {}
35488
+ }
35401
35489
  }
35402
35490
  }
35403
35491
  async function buildPlanningPreamble(apiUrl, task, _wsId) {
@@ -38681,7 +38769,7 @@ import { parseArgs } from "util";
38681
38769
  // package.json
38682
38770
  var package_default = {
38683
38771
  name: "@shipers-dev/multi",
38684
- version: "0.63.0",
38772
+ version: "0.64.0",
38685
38773
  type: "module",
38686
38774
  bin: {
38687
38775
  "multi-agent": "./dist/index.js"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipers-dev/multi",
3
- "version": "0.63.0",
3
+ "version": "0.64.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "multi-agent": "./dist/index.js"