runcastle 1.0.7 → 1.0.8

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 (3) hide show
  1. package/bin/runcastle.js +33 -22
  2. package/index.js +33 -22
  3. package/package.json +1 -1
package/bin/runcastle.js CHANGED
@@ -2116,11 +2116,14 @@ async function reattachWorktree(path, branch) {
2116
2116
  await git(path).checkout(branch);
2117
2117
  } catch {}
2118
2118
  }
2119
+ function tempBranchSlugSegment(slug) {
2120
+ return slug.slice(0, TEMP_BRANCH_SLUG_MAX).replace(/-+$/, "");
2121
+ }
2119
2122
  function researchBranchName(slug, waypointSeq, unique) {
2120
- return `${RESEARCH_BRANCH_PREFIX}${slug}/${waypointSeq}-${unique}`;
2123
+ return `${RESEARCH_BRANCH_PREFIX}${tempBranchSlugSegment(slug)}/${waypointSeq}-${unique}`;
2121
2124
  }
2122
2125
  function ticketBranchName(slug, ticketSeq, unique) {
2123
- return `${TICKET_BRANCH_PREFIX}${slug}/${ticketSeq}-${unique}`;
2126
+ return `${TICKET_BRANCH_PREFIX}${tempBranchSlugSegment(slug)}/${ticketSeq}-${unique}`;
2124
2127
  }
2125
2128
  async function mergeTempBranch(repoPath, featureBranchName, tempBranch) {
2126
2129
  const g = git(repoPath);
@@ -2188,17 +2191,23 @@ async function cleanupTempBranches(repoPath) {
2188
2191
  const prefix = tempBranchPrefix(name);
2189
2192
  if (!prefix)
2190
2193
  continue;
2191
- const slug = name.slice(prefix.length).split("/")[0];
2192
- const target = featureBranch(slug);
2194
+ const seg = name.slice(prefix.length).split("/")[0];
2195
+ const targets = seg ? all.filter((b) => {
2196
+ if (!b.startsWith("feature/"))
2197
+ return false;
2198
+ const featureSlug = b.slice("feature/".length);
2199
+ return featureSlug === seg || tempBranchSlugSegment(featureSlug) === seg;
2200
+ }) : [];
2193
2201
  let merged = false;
2194
- if (slug && all.includes(target)) {
2202
+ for (const target of targets) {
2195
2203
  try {
2196
2204
  const tip = (await g.revparse([name])).trim();
2197
2205
  const base = (await g.raw(["merge-base", name, target])).trim();
2198
- merged = tip.length > 0 && tip === base;
2199
- } catch {
2200
- merged = false;
2201
- }
2206
+ if (tip.length > 0 && tip === base) {
2207
+ merged = true;
2208
+ break;
2209
+ }
2210
+ } catch {}
2202
2211
  }
2203
2212
  if (merged && await deleteBranchDetachingWorktrees(g, repoPath, name)) {
2204
2213
  deleted.push(name);
@@ -2345,7 +2354,7 @@ async function mergeInProgress(g) {
2345
2354
  return false;
2346
2355
  }
2347
2356
  }
2348
- var DOCS_PATHSPEC = "docs/features", DENY_DIRTY = "Working tree has uncommitted changes \u2014 commit or stash first", DENY_ACTIVE = "A test drive is already active \u2014 stop it first", DENY_ACTIVE_RUN = "Feature has an active run \u2014 wait for it to finish", DENY_NONE_ACTIVE = "No test drive is active", RESEARCH_BRANCH_PREFIX = "runcastle/research/", TICKET_BRANCH_PREFIX = "runcastle/ticket/", TEMP_BRANCH_PREFIXES, testDriveState;
2357
+ var DOCS_PATHSPEC = "docs/features", DENY_DIRTY = "Working tree has uncommitted changes \u2014 commit or stash first", DENY_ACTIVE = "A test drive is already active \u2014 stop it first", DENY_ACTIVE_RUN = "Feature has an active run \u2014 wait for it to finish", DENY_NONE_ACTIVE = "No test drive is active", RESEARCH_BRANCH_PREFIX = "runcastle/research/", TICKET_BRANCH_PREFIX = "runcastle/ticket/", TEMP_BRANCH_PREFIXES, TEMP_BRANCH_SLUG_MAX = 16, testDriveState;
2349
2358
  var init_git = __esm(() => {
2350
2359
  init_paths();
2351
2360
  init_errors();
@@ -2653,10 +2662,11 @@ function createSerialQueue() {
2653
2662
  return next;
2654
2663
  };
2655
2664
  }
2656
- function firstLine(s) {
2657
- const i = s.indexOf(`
2658
- `);
2659
- return (i === -1 ? s : s.slice(0, i)).trim();
2665
+ function errorHeadline(s) {
2666
+ const lines = s.split(`
2667
+ `).map((l) => l.trim()).filter(Boolean);
2668
+ const causes = lines.filter((l) => /^(fatal|error):/i.test(l));
2669
+ return causes.at(-1) ?? lines[0] ?? "";
2660
2670
  }
2661
2671
  async function burnTickets(ctx, tickets2, execute, concurrency = 1) {
2662
2672
  const width = Math.max(1, Math.floor(concurrency));
@@ -2711,7 +2721,7 @@ async function burnTickets(ctx, tickets2, execute, concurrency = 1) {
2711
2721
  failTicket(seq, outcome.error, outcome.event);
2712
2722
  ctx.emitEvent({
2713
2723
  type: "ticket.failed",
2714
- message: `ticket ${t.seq} failed: ${firstLine(outcome.error)}`,
2724
+ message: `ticket ${t.seq} failed: ${errorHeadline(outcome.error)}`,
2715
2725
  ticketId: t.id,
2716
2726
  data: { error: outcome.error }
2717
2727
  });
@@ -2919,7 +2929,7 @@ async function realExecuteTicketRun(ctx, ticket, config2, token, model, land) {
2919
2929
  }
2920
2930
  return {
2921
2931
  status: "failed",
2922
- error: `ticket ${ticket.seq} committed to ${tempBranch} but landing on ${feature.branch} failed: ${firstLine(detail)} \u2014 the branch is preserved for manual recovery`
2932
+ error: `ticket ${ticket.seq} committed to ${tempBranch} but landing on ${feature.branch} failed: ${errorHeadline(detail)} \u2014 the branch is preserved for manual recovery`
2923
2933
  };
2924
2934
  }
2925
2935
  return outcome;
@@ -3004,15 +3014,16 @@ async function researchRun(ctx, deps) {
3004
3014
  }
3005
3015
  ctx.emitEvent({
3006
3016
  type: "research.failed",
3007
- message: `waypoint ${waypoint.seq} research failed: ${firstLine2(outcome.error)}`,
3017
+ message: `waypoint ${waypoint.seq} research failed: ${errorHeadline2(outcome.error)}`,
3008
3018
  data: { waypointId: waypoint.id, error: outcome.error }
3009
3019
  });
3010
3020
  return { status: "failed", summary: outcome.error };
3011
3021
  }
3012
- function firstLine2(s) {
3013
- const i = s.indexOf(`
3014
- `);
3015
- return (i === -1 ? s : s.slice(0, i)).trim();
3022
+ function errorHeadline2(s) {
3023
+ const lines = s.split(`
3024
+ `).map((l) => l.trim()).filter(Boolean);
3025
+ const causes = lines.filter((l) => /^(fatal|error):/i.test(l));
3026
+ return causes.at(-1) ?? lines[0] ?? "";
3016
3027
  }
3017
3028
  function createResearchStreamThrottle(emit2, opts = {}) {
3018
3029
  return createStreamThrottle((e) => emit2({ ...e, type: e.type.replace(/^burn\./, "research.") }), opts);
@@ -3089,7 +3100,7 @@ async function realExecuteResearchRun(ctx, waypoint, config2, token, model) {
3089
3100
  const detail = merge.error ?? "merge failed";
3090
3101
  ctx.emitEvent({
3091
3102
  type: merge.conflict ? "merge.conflict.needs-human" : "research.merge_failed",
3092
- message: merge.conflict ? `research branch ${tempBranch} conflicts with ${feature.branch} \u2014 merge it manually (git merge ${tempBranch}; resolve; git branch -D ${tempBranch}), then resolve the waypoint` : `could not land ${tempBranch} on ${feature.branch}: ${firstLine2(detail)} \u2014 the branch is preserved for manual recovery`,
3103
+ message: merge.conflict ? `research branch ${tempBranch} conflicts with ${feature.branch} \u2014 merge it manually (git merge ${tempBranch}; resolve; git branch -D ${tempBranch}), then resolve the waypoint` : `could not land ${tempBranch} on ${feature.branch}: ${errorHeadline2(detail)} \u2014 the branch is preserved for manual recovery`,
3093
3104
  data: { tempBranch, featureBranch: feature.branch, conflict: merge.conflict ?? false, error: detail }
3094
3105
  });
3095
3106
  return {
package/index.js CHANGED
@@ -2115,11 +2115,14 @@ async function reattachWorktree(path, branch) {
2115
2115
  await git(path).checkout(branch);
2116
2116
  } catch {}
2117
2117
  }
2118
+ function tempBranchSlugSegment(slug) {
2119
+ return slug.slice(0, TEMP_BRANCH_SLUG_MAX).replace(/-+$/, "");
2120
+ }
2118
2121
  function researchBranchName(slug, waypointSeq, unique) {
2119
- return `${RESEARCH_BRANCH_PREFIX}${slug}/${waypointSeq}-${unique}`;
2122
+ return `${RESEARCH_BRANCH_PREFIX}${tempBranchSlugSegment(slug)}/${waypointSeq}-${unique}`;
2120
2123
  }
2121
2124
  function ticketBranchName(slug, ticketSeq, unique) {
2122
- return `${TICKET_BRANCH_PREFIX}${slug}/${ticketSeq}-${unique}`;
2125
+ return `${TICKET_BRANCH_PREFIX}${tempBranchSlugSegment(slug)}/${ticketSeq}-${unique}`;
2123
2126
  }
2124
2127
  async function mergeTempBranch(repoPath, featureBranchName, tempBranch) {
2125
2128
  const g = git(repoPath);
@@ -2187,17 +2190,23 @@ async function cleanupTempBranches(repoPath) {
2187
2190
  const prefix = tempBranchPrefix(name);
2188
2191
  if (!prefix)
2189
2192
  continue;
2190
- const slug = name.slice(prefix.length).split("/")[0];
2191
- const target = featureBranch(slug);
2193
+ const seg = name.slice(prefix.length).split("/")[0];
2194
+ const targets = seg ? all.filter((b) => {
2195
+ if (!b.startsWith("feature/"))
2196
+ return false;
2197
+ const featureSlug = b.slice("feature/".length);
2198
+ return featureSlug === seg || tempBranchSlugSegment(featureSlug) === seg;
2199
+ }) : [];
2192
2200
  let merged = false;
2193
- if (slug && all.includes(target)) {
2201
+ for (const target of targets) {
2194
2202
  try {
2195
2203
  const tip = (await g.revparse([name])).trim();
2196
2204
  const base = (await g.raw(["merge-base", name, target])).trim();
2197
- merged = tip.length > 0 && tip === base;
2198
- } catch {
2199
- merged = false;
2200
- }
2205
+ if (tip.length > 0 && tip === base) {
2206
+ merged = true;
2207
+ break;
2208
+ }
2209
+ } catch {}
2201
2210
  }
2202
2211
  if (merged && await deleteBranchDetachingWorktrees(g, repoPath, name)) {
2203
2212
  deleted.push(name);
@@ -2344,7 +2353,7 @@ async function mergeInProgress(g) {
2344
2353
  return false;
2345
2354
  }
2346
2355
  }
2347
- var DOCS_PATHSPEC = "docs/features", DENY_DIRTY = "Working tree has uncommitted changes \u2014 commit or stash first", DENY_ACTIVE = "A test drive is already active \u2014 stop it first", DENY_ACTIVE_RUN = "Feature has an active run \u2014 wait for it to finish", DENY_NONE_ACTIVE = "No test drive is active", RESEARCH_BRANCH_PREFIX = "runcastle/research/", TICKET_BRANCH_PREFIX = "runcastle/ticket/", TEMP_BRANCH_PREFIXES, testDriveState;
2356
+ var DOCS_PATHSPEC = "docs/features", DENY_DIRTY = "Working tree has uncommitted changes \u2014 commit or stash first", DENY_ACTIVE = "A test drive is already active \u2014 stop it first", DENY_ACTIVE_RUN = "Feature has an active run \u2014 wait for it to finish", DENY_NONE_ACTIVE = "No test drive is active", RESEARCH_BRANCH_PREFIX = "runcastle/research/", TICKET_BRANCH_PREFIX = "runcastle/ticket/", TEMP_BRANCH_PREFIXES, TEMP_BRANCH_SLUG_MAX = 16, testDriveState;
2348
2357
  var init_git = __esm(() => {
2349
2358
  init_paths();
2350
2359
  init_errors();
@@ -2652,10 +2661,11 @@ function createSerialQueue() {
2652
2661
  return next;
2653
2662
  };
2654
2663
  }
2655
- function firstLine(s) {
2656
- const i = s.indexOf(`
2657
- `);
2658
- return (i === -1 ? s : s.slice(0, i)).trim();
2664
+ function errorHeadline(s) {
2665
+ const lines = s.split(`
2666
+ `).map((l) => l.trim()).filter(Boolean);
2667
+ const causes = lines.filter((l) => /^(fatal|error):/i.test(l));
2668
+ return causes.at(-1) ?? lines[0] ?? "";
2659
2669
  }
2660
2670
  async function burnTickets(ctx, tickets2, execute, concurrency = 1) {
2661
2671
  const width = Math.max(1, Math.floor(concurrency));
@@ -2710,7 +2720,7 @@ async function burnTickets(ctx, tickets2, execute, concurrency = 1) {
2710
2720
  failTicket(seq, outcome.error, outcome.event);
2711
2721
  ctx.emitEvent({
2712
2722
  type: "ticket.failed",
2713
- message: `ticket ${t.seq} failed: ${firstLine(outcome.error)}`,
2723
+ message: `ticket ${t.seq} failed: ${errorHeadline(outcome.error)}`,
2714
2724
  ticketId: t.id,
2715
2725
  data: { error: outcome.error }
2716
2726
  });
@@ -2918,7 +2928,7 @@ async function realExecuteTicketRun(ctx, ticket, config2, token, model, land) {
2918
2928
  }
2919
2929
  return {
2920
2930
  status: "failed",
2921
- error: `ticket ${ticket.seq} committed to ${tempBranch} but landing on ${feature.branch} failed: ${firstLine(detail)} \u2014 the branch is preserved for manual recovery`
2931
+ error: `ticket ${ticket.seq} committed to ${tempBranch} but landing on ${feature.branch} failed: ${errorHeadline(detail)} \u2014 the branch is preserved for manual recovery`
2922
2932
  };
2923
2933
  }
2924
2934
  return outcome;
@@ -3003,15 +3013,16 @@ async function researchRun(ctx, deps) {
3003
3013
  }
3004
3014
  ctx.emitEvent({
3005
3015
  type: "research.failed",
3006
- message: `waypoint ${waypoint.seq} research failed: ${firstLine2(outcome.error)}`,
3016
+ message: `waypoint ${waypoint.seq} research failed: ${errorHeadline2(outcome.error)}`,
3007
3017
  data: { waypointId: waypoint.id, error: outcome.error }
3008
3018
  });
3009
3019
  return { status: "failed", summary: outcome.error };
3010
3020
  }
3011
- function firstLine2(s) {
3012
- const i = s.indexOf(`
3013
- `);
3014
- return (i === -1 ? s : s.slice(0, i)).trim();
3021
+ function errorHeadline2(s) {
3022
+ const lines = s.split(`
3023
+ `).map((l) => l.trim()).filter(Boolean);
3024
+ const causes = lines.filter((l) => /^(fatal|error):/i.test(l));
3025
+ return causes.at(-1) ?? lines[0] ?? "";
3015
3026
  }
3016
3027
  function createResearchStreamThrottle(emit2, opts = {}) {
3017
3028
  return createStreamThrottle((e) => emit2({ ...e, type: e.type.replace(/^burn\./, "research.") }), opts);
@@ -3088,7 +3099,7 @@ async function realExecuteResearchRun(ctx, waypoint, config2, token, model) {
3088
3099
  const detail = merge.error ?? "merge failed";
3089
3100
  ctx.emitEvent({
3090
3101
  type: merge.conflict ? "merge.conflict.needs-human" : "research.merge_failed",
3091
- message: merge.conflict ? `research branch ${tempBranch} conflicts with ${feature.branch} \u2014 merge it manually (git merge ${tempBranch}; resolve; git branch -D ${tempBranch}), then resolve the waypoint` : `could not land ${tempBranch} on ${feature.branch}: ${firstLine2(detail)} \u2014 the branch is preserved for manual recovery`,
3102
+ message: merge.conflict ? `research branch ${tempBranch} conflicts with ${feature.branch} \u2014 merge it manually (git merge ${tempBranch}; resolve; git branch -D ${tempBranch}), then resolve the waypoint` : `could not land ${tempBranch} on ${feature.branch}: ${errorHeadline2(detail)} \u2014 the branch is preserved for manual recovery`,
3092
3103
  data: { tempBranch, featureBranch: feature.branch, conflict: merge.conflict ?? false, error: detail }
3093
3104
  });
3094
3105
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runcastle",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "description": "Burn tickets into shipped features with Claude Code.",
5
5
  "license": "MIT",
6
6
  "type": "module",