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.
- package/bin/runcastle.js +33 -22
- package/index.js +33 -22
- 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
|
|
2192
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
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
|
|
2657
|
-
const
|
|
2658
|
-
`);
|
|
2659
|
-
|
|
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: ${
|
|
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: ${
|
|
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: ${
|
|
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
|
|
3013
|
-
const
|
|
3014
|
-
`);
|
|
3015
|
-
|
|
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}: ${
|
|
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
|
|
2191
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
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
|
|
2656
|
-
const
|
|
2657
|
-
`);
|
|
2658
|
-
|
|
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: ${
|
|
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: ${
|
|
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: ${
|
|
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
|
|
3012
|
-
const
|
|
3013
|
-
`);
|
|
3014
|
-
|
|
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}: ${
|
|
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 {
|