runcastle 1.0.5 → 1.0.7
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 +289 -37
- package/index.js +286 -36
- package/package.json +1 -1
- package/skills/packs/runcastle/skills/revisit/SKILL.md +38 -0
- package/web/assets/index-EKuI75rm.css +1 -0
- package/web/assets/{index-CRhFvrAu.js → index-Y3ehCYIg.js} +2 -2
- package/web/index.html +2 -2
- package/web/assets/index-ClNOSIVg.css +0 -1
package/bin/runcastle.js
CHANGED
|
@@ -62,6 +62,9 @@ var init_paths = () => {};
|
|
|
62
62
|
|
|
63
63
|
// ../core/src/config.ts
|
|
64
64
|
import * as z from "zod";
|
|
65
|
+
function resolveSandboxImage(config) {
|
|
66
|
+
return config.sandboxImage ?? DEFAULT_SANDBOX_IMAGE;
|
|
67
|
+
}
|
|
65
68
|
function resolveModel(step, config, project, runOverride) {
|
|
66
69
|
return runOverride ?? config.stepModels[step] ?? project?.model ?? config.model;
|
|
67
70
|
}
|
|
@@ -77,13 +80,14 @@ var MODEL_STEPS, ModelStep, DEFAULT_SMOKE_MODEL = "claude-haiku-4-5-20251001", f
|
|
|
77
80
|
}
|
|
78
81
|
delete obj.smokeModel;
|
|
79
82
|
return obj;
|
|
80
|
-
}, RuncastleConfig;
|
|
83
|
+
}, RuncastleConfig, DEFAULT_SANDBOX_IMAGE = "sandcastle:runcastle";
|
|
81
84
|
var init_config = __esm(() => {
|
|
82
85
|
MODEL_STEPS = [
|
|
83
86
|
"ideation",
|
|
84
87
|
"qa",
|
|
85
88
|
"waypoint",
|
|
86
89
|
"converge",
|
|
90
|
+
"revisit",
|
|
87
91
|
"research",
|
|
88
92
|
"implement",
|
|
89
93
|
"smoke"
|
|
@@ -95,7 +99,8 @@ var init_config = __esm(() => {
|
|
|
95
99
|
stepModels: z.partialRecord(ModelStep, z.string()).default({ smoke: DEFAULT_SMOKE_MODEL }),
|
|
96
100
|
sandbox: z.enum(["docker", "podman", "noSandbox"]).default("docker"),
|
|
97
101
|
mainBranch: z.string().default("main"),
|
|
98
|
-
sandboxImage: z.string().optional()
|
|
102
|
+
sandboxImage: z.string().optional(),
|
|
103
|
+
burnConcurrency: z.number().int().min(1).max(8).default(3)
|
|
99
104
|
}));
|
|
100
105
|
});
|
|
101
106
|
|
|
@@ -128,6 +133,9 @@ function loadConfig(env = process.env) {
|
|
|
128
133
|
overrides.mainBranch = env.RUNCASTLE_MAIN_BRANCH;
|
|
129
134
|
if (env.RUNCASTLE_SANDBOX_IMAGE)
|
|
130
135
|
overrides.sandboxImage = env.RUNCASTLE_SANDBOX_IMAGE;
|
|
136
|
+
if (env.RUNCASTLE_BURN_CONCURRENCY) {
|
|
137
|
+
overrides.burnConcurrency = Number(env.RUNCASTLE_BURN_CONCURRENCY);
|
|
138
|
+
}
|
|
131
139
|
const base = typeof fileConfig === "object" && fileConfig !== null ? fileConfig : {};
|
|
132
140
|
return RuncastleConfig.parse({ ...base, ...overrides });
|
|
133
141
|
}
|
|
@@ -171,8 +179,8 @@ var init_schemas = __esm(() => {
|
|
|
171
179
|
"shipped"
|
|
172
180
|
]);
|
|
173
181
|
FeatureSize = z2.enum(["full", "collapsed"]);
|
|
174
|
-
TicketStatus = z2.enum(["pending", "burning", "done", "failed"]);
|
|
175
|
-
SessionKind = z2.enum(["ideation", "qa", "waypoint", "converge"]);
|
|
182
|
+
TicketStatus = z2.enum(["pending", "burning", "done", "failed", "cancelled"]);
|
|
183
|
+
SessionKind = z2.enum(["ideation", "qa", "waypoint", "converge", "revisit"]);
|
|
176
184
|
RunStatus = z2.enum(["running", "succeeded", "failed", "cancelled"]);
|
|
177
185
|
FeatureStatus = z2.enum(["active", "shipped"]);
|
|
178
186
|
SessionStatus = z2.enum(["launching", "live", "ended"]);
|
|
@@ -1456,7 +1464,7 @@ var init_waypoints = __esm(() => {
|
|
|
1456
1464
|
});
|
|
1457
1465
|
|
|
1458
1466
|
// src/launcher/sessions.ts
|
|
1459
|
-
import { and as and4, desc as desc2, eq as eq4, inArray, sql as sql2 } from "drizzle-orm";
|
|
1467
|
+
import { and as and4, desc as desc2, eq as eq4, inArray, isNotNull, sql as sql2 } from "drizzle-orm";
|
|
1460
1468
|
function createSessionRow(ctx, input) {
|
|
1461
1469
|
const row = ctx.db.insert(sessions).values({
|
|
1462
1470
|
id: newId("sess"),
|
|
@@ -1535,6 +1543,10 @@ function markSessionEnded(ctx, id) {
|
|
|
1535
1543
|
ctx.db.update(sessions).set({ status: "ended" }).where(eq4(sessions.id, id)).run();
|
|
1536
1544
|
return getSessionRow(ctx, id);
|
|
1537
1545
|
}
|
|
1546
|
+
function mostRecentResumableSession(ctx, featureId) {
|
|
1547
|
+
const row = ctx.db.select().from(sessions).where(and4(eq4(sessions.featureId, featureId), eq4(sessions.status, "ended"), isNotNull(sessions.ccSessionId))).orderBy(desc2(sql2`rowid`)).limit(1).get();
|
|
1548
|
+
return row ? rowToSession(row) : null;
|
|
1549
|
+
}
|
|
1538
1550
|
function mostRecentLiveSession(ctx) {
|
|
1539
1551
|
const row = ctx.db.select().from(sessions).where(eq4(sessions.status, "live")).orderBy(desc2(sql2`rowid`)).limit(1).get();
|
|
1540
1552
|
return row ? rowToSession(row) : null;
|
|
@@ -1675,6 +1687,55 @@ function storeTickets(ctx, featureId, inputs) {
|
|
|
1675
1687
|
});
|
|
1676
1688
|
return rows.map(rowToTicket);
|
|
1677
1689
|
}
|
|
1690
|
+
function getTicket(ctx, id) {
|
|
1691
|
+
const row = ctx.db.select().from(tickets).where(eq5(tickets.id, id)).get();
|
|
1692
|
+
if (!row)
|
|
1693
|
+
throw new NotFoundError(`ticket ${id} not found`);
|
|
1694
|
+
return rowToTicket(row);
|
|
1695
|
+
}
|
|
1696
|
+
function assertMutable(t, verb) {
|
|
1697
|
+
if (!MUTABLE_STATUSES.has(t.status)) {
|
|
1698
|
+
throw new InvalidInputError(`cannot ${verb} ticket ${t.seq} \u2014 it is ${t.status}; only pending or failed tickets can be ${verb}${verb.endsWith("l") ? "led" : "ed"}`);
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
function editTicket(ctx, id, patch) {
|
|
1702
|
+
const current2 = getTicket(ctx, id);
|
|
1703
|
+
assertMutable(current2, "edit");
|
|
1704
|
+
const set = {};
|
|
1705
|
+
if (patch.title !== undefined)
|
|
1706
|
+
set.title = patch.title;
|
|
1707
|
+
if (patch.goal !== undefined)
|
|
1708
|
+
set.goal = patch.goal;
|
|
1709
|
+
if (patch.context !== undefined)
|
|
1710
|
+
set.context = patch.context;
|
|
1711
|
+
if (patch.acceptanceCriteria !== undefined)
|
|
1712
|
+
set.acceptanceCriteria = patch.acceptanceCriteria;
|
|
1713
|
+
if (patch.seams !== undefined)
|
|
1714
|
+
set.seams = patch.seams;
|
|
1715
|
+
if (Object.keys(set).length === 0) {
|
|
1716
|
+
throw new InvalidInputError("nothing to edit \u2014 pass at least one content field");
|
|
1717
|
+
}
|
|
1718
|
+
ctx.db.update(tickets).set(set).where(eq5(tickets.id, id)).run();
|
|
1719
|
+
emit(ctx, current2.featureId, {
|
|
1720
|
+
type: "ticket.edited",
|
|
1721
|
+
message: `ticket ${current2.seq} content edited (${Object.keys(set).join(", ")})`,
|
|
1722
|
+
ticketId: id,
|
|
1723
|
+
data: { fields: Object.keys(set) }
|
|
1724
|
+
});
|
|
1725
|
+
return getTicket(ctx, id);
|
|
1726
|
+
}
|
|
1727
|
+
function cancelTicket(ctx, id, reason) {
|
|
1728
|
+
const current2 = getTicket(ctx, id);
|
|
1729
|
+
assertMutable(current2, "cancel");
|
|
1730
|
+
ctx.db.update(tickets).set({ status: "cancelled", error: reason?.trim() ? reason.trim() : null }).where(eq5(tickets.id, id)).run();
|
|
1731
|
+
emit(ctx, current2.featureId, {
|
|
1732
|
+
type: "ticket.cancelled",
|
|
1733
|
+
message: `ticket ${current2.seq} cancelled${reason?.trim() ? `: ${reason.trim()}` : ""}`,
|
|
1734
|
+
ticketId: id,
|
|
1735
|
+
data: { reason: reason ?? null }
|
|
1736
|
+
});
|
|
1737
|
+
return getTicket(ctx, id);
|
|
1738
|
+
}
|
|
1678
1739
|
function updateTicket(ctx, id, patch) {
|
|
1679
1740
|
const current2 = ctx.db.select().from(tickets).where(eq5(tickets.id, id)).get();
|
|
1680
1741
|
if (!current2)
|
|
@@ -1696,11 +1757,13 @@ function updateTicket(ctx, id, patch) {
|
|
|
1696
1757
|
const updated = ctx.db.select().from(tickets).where(eq5(tickets.id, id)).get();
|
|
1697
1758
|
return rowToTicket(updated);
|
|
1698
1759
|
}
|
|
1760
|
+
var MUTABLE_STATUSES;
|
|
1699
1761
|
var init_tickets = __esm(() => {
|
|
1700
1762
|
init_src();
|
|
1701
1763
|
init_schema();
|
|
1702
1764
|
init_errors();
|
|
1703
1765
|
init_events();
|
|
1766
|
+
MUTABLE_STATUSES = new Set(["pending", "failed"]);
|
|
1704
1767
|
});
|
|
1705
1768
|
|
|
1706
1769
|
// src/services/gates.ts
|
|
@@ -1725,14 +1788,14 @@ function checkGate(ctx, check, feature) {
|
|
|
1725
1788
|
return { satisfied: true };
|
|
1726
1789
|
return fileGate(ctx, feature, "spec.md", "write the spec (spec.md) before breaking into tickets");
|
|
1727
1790
|
case "tickets-approved": {
|
|
1728
|
-
const count = listByFeature2(ctx, feature.id).length;
|
|
1791
|
+
const count = listByFeature2(ctx, feature.id).filter((t) => t.status !== "cancelled").length;
|
|
1729
1792
|
return count >= 1 ? { satisfied: true } : { satisfied: false, reason: "no tickets to burn" };
|
|
1730
1793
|
}
|
|
1731
1794
|
case "all-tickets-terminal": {
|
|
1732
1795
|
const tickets2 = listByFeature2(ctx, feature.id);
|
|
1733
1796
|
if (tickets2.length === 0)
|
|
1734
1797
|
return { satisfied: false, reason: "no tickets have run yet" };
|
|
1735
|
-
const open = tickets2.filter((t) => t.status !== "done" && t.status !== "failed");
|
|
1798
|
+
const open = tickets2.filter((t) => t.status !== "done" && t.status !== "failed" && t.status !== "cancelled");
|
|
1736
1799
|
return open.length === 0 ? { satisfied: true } : {
|
|
1737
1800
|
satisfied: false,
|
|
1738
1801
|
reason: notYetTerminal("ticket", open.map((t) => t.status), ["pending", "burning"])
|
|
@@ -2056,7 +2119,10 @@ async function reattachWorktree(path, branch) {
|
|
|
2056
2119
|
function researchBranchName(slug, waypointSeq, unique) {
|
|
2057
2120
|
return `${RESEARCH_BRANCH_PREFIX}${slug}/${waypointSeq}-${unique}`;
|
|
2058
2121
|
}
|
|
2059
|
-
|
|
2122
|
+
function ticketBranchName(slug, ticketSeq, unique) {
|
|
2123
|
+
return `${TICKET_BRANCH_PREFIX}${slug}/${ticketSeq}-${unique}`;
|
|
2124
|
+
}
|
|
2125
|
+
async function mergeTempBranch(repoPath, featureBranchName, tempBranch) {
|
|
2060
2126
|
const g = git(repoPath);
|
|
2061
2127
|
let branches;
|
|
2062
2128
|
try {
|
|
@@ -2105,7 +2171,10 @@ async function deleteBranchDetachingWorktrees(g, repoPath, branch) {
|
|
|
2105
2171
|
return false;
|
|
2106
2172
|
}
|
|
2107
2173
|
}
|
|
2108
|
-
|
|
2174
|
+
function tempBranchPrefix(name) {
|
|
2175
|
+
return TEMP_BRANCH_PREFIXES.find((p) => name.startsWith(p));
|
|
2176
|
+
}
|
|
2177
|
+
async function cleanupTempBranches(repoPath) {
|
|
2109
2178
|
const deleted = [];
|
|
2110
2179
|
const kept = [];
|
|
2111
2180
|
const g = git(repoPath);
|
|
@@ -2116,9 +2185,10 @@ async function cleanupResearchBranches(repoPath) {
|
|
|
2116
2185
|
return { deleted, kept };
|
|
2117
2186
|
}
|
|
2118
2187
|
for (const name of all) {
|
|
2119
|
-
|
|
2188
|
+
const prefix = tempBranchPrefix(name);
|
|
2189
|
+
if (!prefix)
|
|
2120
2190
|
continue;
|
|
2121
|
-
const slug = name.slice(
|
|
2191
|
+
const slug = name.slice(prefix.length).split("/")[0];
|
|
2122
2192
|
const target = featureBranch(slug);
|
|
2123
2193
|
let merged = false;
|
|
2124
2194
|
if (slug && all.includes(target)) {
|
|
@@ -2275,13 +2345,14 @@ async function mergeInProgress(g) {
|
|
|
2275
2345
|
return false;
|
|
2276
2346
|
}
|
|
2277
2347
|
}
|
|
2278
|
-
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/", testDriveState;
|
|
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;
|
|
2279
2349
|
var init_git = __esm(() => {
|
|
2280
2350
|
init_paths();
|
|
2281
2351
|
init_errors();
|
|
2282
2352
|
init_dev_pane();
|
|
2283
2353
|
init_events();
|
|
2284
2354
|
init_repo();
|
|
2355
|
+
TEMP_BRANCH_PREFIXES = [RESEARCH_BRANCH_PREFIX, TICKET_BRANCH_PREFIX];
|
|
2285
2356
|
});
|
|
2286
2357
|
|
|
2287
2358
|
// src/services/knowledge.ts
|
|
@@ -2572,16 +2643,28 @@ function isMergeConflictError(err) {
|
|
|
2572
2643
|
const msg = err instanceof Error ? err.message : typeof err === "string" ? err : "";
|
|
2573
2644
|
return /conflict/i.test(msg) || /git branch -D/i.test(msg) || /automatic merge failed/i.test(msg);
|
|
2574
2645
|
}
|
|
2646
|
+
function createSerialQueue() {
|
|
2647
|
+
let tail = Promise.resolve();
|
|
2648
|
+
return (task) => {
|
|
2649
|
+
const next = tail.then(task);
|
|
2650
|
+
tail = next.catch(() => {
|
|
2651
|
+
return;
|
|
2652
|
+
});
|
|
2653
|
+
return next;
|
|
2654
|
+
};
|
|
2655
|
+
}
|
|
2575
2656
|
function firstLine(s) {
|
|
2576
2657
|
const i = s.indexOf(`
|
|
2577
2658
|
`);
|
|
2578
2659
|
return (i === -1 ? s : s.slice(0, i)).trim();
|
|
2579
2660
|
}
|
|
2580
|
-
async function burnTickets(ctx, tickets2, execute) {
|
|
2661
|
+
async function burnTickets(ctx, tickets2, execute, concurrency = 1) {
|
|
2662
|
+
const width = Math.max(1, Math.floor(concurrency));
|
|
2581
2663
|
const bySeq = indexBySeq(tickets2);
|
|
2582
2664
|
const status = new Map(tickets2.map((t) => [t.seq, t.status]));
|
|
2583
2665
|
const pending = new Set(tickets2.filter((t) => t.status === "pending").map((t) => t.seq));
|
|
2584
2666
|
const inFlight = new Map;
|
|
2667
|
+
const satisfied = (s) => s === "done" || s === "cancelled";
|
|
2585
2668
|
const readyState = (seq) => {
|
|
2586
2669
|
const t = bySeq.get(seq);
|
|
2587
2670
|
if (!t)
|
|
@@ -2592,7 +2675,7 @@ async function burnTickets(ctx, tickets2, execute) {
|
|
|
2592
2675
|
if (!present || bs === "failed")
|
|
2593
2676
|
return { blockedBy: b, present };
|
|
2594
2677
|
}
|
|
2595
|
-
return t.blockedBy.every((b) => status.get(b)
|
|
2678
|
+
return t.blockedBy.every((b) => satisfied(status.get(b))) ? "ready" : "wait";
|
|
2596
2679
|
};
|
|
2597
2680
|
const failTicket = (seq, error, extra) => {
|
|
2598
2681
|
const t = bySeq.get(seq);
|
|
@@ -2653,7 +2736,7 @@ async function burnTickets(ctx, tickets2, execute) {
|
|
|
2653
2736
|
}
|
|
2654
2737
|
}
|
|
2655
2738
|
}
|
|
2656
|
-
while (inFlight.size <
|
|
2739
|
+
while (inFlight.size < width) {
|
|
2657
2740
|
const readySeq = [...pending].find((seq) => readyState(seq) === "ready");
|
|
2658
2741
|
if (readySeq === undefined)
|
|
2659
2742
|
break;
|
|
@@ -2664,7 +2747,12 @@ async function burnTickets(ctx, tickets2, execute) {
|
|
|
2664
2747
|
inFlight.set(readySeq, p);
|
|
2665
2748
|
}
|
|
2666
2749
|
if (inFlight.size > 0) {
|
|
2667
|
-
|
|
2750
|
+
try {
|
|
2751
|
+
await Promise.race(inFlight.values());
|
|
2752
|
+
} catch (err) {
|
|
2753
|
+
await Promise.allSettled([...inFlight.values()]);
|
|
2754
|
+
throw err;
|
|
2755
|
+
}
|
|
2668
2756
|
} else if (pending.size > 0) {
|
|
2669
2757
|
for (const seq of [...pending]) {
|
|
2670
2758
|
pending.delete(seq);
|
|
@@ -2688,12 +2776,14 @@ async function burnTickets(ctx, tickets2, execute) {
|
|
|
2688
2776
|
}
|
|
2689
2777
|
async function burnRun(ctx, deps) {
|
|
2690
2778
|
const tickets2 = ctx.tickets;
|
|
2691
|
-
const
|
|
2779
|
+
const burnable = tickets2.filter((t) => t.status !== "cancelled");
|
|
2780
|
+
const cancelled = tickets2.length - burnable.length;
|
|
2781
|
+
const total = burnable.length;
|
|
2692
2782
|
if (deps.config.sandbox !== "noSandbox" && !deps.hasAuthToken) {
|
|
2693
2783
|
ctx.emitEvent({ type: AUTH_MISSING_EVENT, message: AUTH_MISSING_MESSAGE });
|
|
2694
2784
|
return { status: "failed", summary: "burn aborted: auth token missing" };
|
|
2695
2785
|
}
|
|
2696
|
-
const cycle = detectCycle(
|
|
2786
|
+
const cycle = detectCycle(burnable);
|
|
2697
2787
|
if (cycle) {
|
|
2698
2788
|
const path = cycle.join(" \u2192 ");
|
|
2699
2789
|
ctx.emitEvent({
|
|
@@ -2703,9 +2793,9 @@ async function burnRun(ctx, deps) {
|
|
|
2703
2793
|
});
|
|
2704
2794
|
return { status: "failed", summary: `dependency cycle: ${path}` };
|
|
2705
2795
|
}
|
|
2706
|
-
const done = await burnTickets(ctx, tickets2, deps.executeTicketRun);
|
|
2707
|
-
const summary = `${done}/${total} tickets done`;
|
|
2708
|
-
ctx.emitEvent({ type: "burn.summary", message: summary, data: { done, total } });
|
|
2796
|
+
const done = await burnTickets(ctx, tickets2, deps.executeTicketRun, deps.concurrency);
|
|
2797
|
+
const summary = cancelled > 0 ? `${done}/${total} tickets done (${cancelled} cancelled)` : `${done}/${total} tickets done`;
|
|
2798
|
+
ctx.emitEvent({ type: "burn.summary", message: summary, data: { done, total, cancelled } });
|
|
2709
2799
|
return { status: done === total ? "succeeded" : "failed", summary };
|
|
2710
2800
|
}
|
|
2711
2801
|
function burnerTemplatePath() {
|
|
@@ -2756,7 +2846,7 @@ function buildBurnAgent(config2, token, model) {
|
|
|
2756
2846
|
return agent;
|
|
2757
2847
|
}
|
|
2758
2848
|
function selectSandbox(config2) {
|
|
2759
|
-
const imageOpts =
|
|
2849
|
+
const imageOpts = { imageName: resolveSandboxImage(config2) };
|
|
2760
2850
|
switch (config2.sandbox) {
|
|
2761
2851
|
case "docker":
|
|
2762
2852
|
return docker(imageOpts);
|
|
@@ -2766,8 +2856,9 @@ function selectSandbox(config2) {
|
|
|
2766
2856
|
return noSandbox();
|
|
2767
2857
|
}
|
|
2768
2858
|
}
|
|
2769
|
-
async function realExecuteTicketRun(ctx, ticket, config2, token, model) {
|
|
2859
|
+
async function realExecuteTicketRun(ctx, ticket, config2, token, model, land) {
|
|
2770
2860
|
const { project, feature } = ctx;
|
|
2861
|
+
const tempBranch = ticketBranchName(feature.slug, ticket.seq, newId("b").slice(2, 10));
|
|
2771
2862
|
const template = readFileSync4(burnerTemplatePath(), "utf8");
|
|
2772
2863
|
const prompt = renderTicketPrompt(template, {
|
|
2773
2864
|
TICKET_JSON: buildTicketJson(ticket),
|
|
@@ -2783,7 +2874,7 @@ async function realExecuteTicketRun(ctx, ticket, config2, token, model) {
|
|
|
2783
2874
|
sandbox: selectSandbox(config2),
|
|
2784
2875
|
cwd: project.repoPath,
|
|
2785
2876
|
prompt,
|
|
2786
|
-
branchStrategy: { type: "branch", branch:
|
|
2877
|
+
branchStrategy: { type: "branch", branch: tempBranch, baseBranch: feature.branch },
|
|
2787
2878
|
signal: ctx.signal,
|
|
2788
2879
|
name: `ticket-${ticket.seq}`,
|
|
2789
2880
|
logging: { type: "file", path: logFilePath, onAgentStreamEvent: throttle.onEvent }
|
|
@@ -2810,16 +2901,39 @@ async function realExecuteTicketRun(ctx, ticket, config2, token, model) {
|
|
|
2810
2901
|
}
|
|
2811
2902
|
throttle.flush();
|
|
2812
2903
|
const blocked = readBlockedFile([result.preservedWorktreePath, project.repoPath]);
|
|
2813
|
-
|
|
2904
|
+
const outcome = interpretRunResult(result, blocked);
|
|
2905
|
+
if (outcome.status !== "done")
|
|
2906
|
+
return outcome;
|
|
2907
|
+
const merge = await land(() => mergeTempBranch(project.repoPath, feature.branch, tempBranch));
|
|
2908
|
+
if (!merge.ok) {
|
|
2909
|
+
const detail = merge.error ?? "merge failed";
|
|
2910
|
+
if (merge.conflict) {
|
|
2911
|
+
return {
|
|
2912
|
+
status: "failed",
|
|
2913
|
+
error: `ticket ${ticket.seq} committed to ${tempBranch} but landing on ${feature.branch} hit a conflict: ${detail}`,
|
|
2914
|
+
event: {
|
|
2915
|
+
type: "merge.conflict.needs-human",
|
|
2916
|
+
message: `ticket ${ticket.seq}: ${tempBranch} conflicts with ${feature.branch} \u2014 merge it manually (git merge ${tempBranch}; resolve; git branch -D ${tempBranch}), then re-burn`
|
|
2917
|
+
}
|
|
2918
|
+
};
|
|
2919
|
+
}
|
|
2920
|
+
return {
|
|
2921
|
+
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`
|
|
2923
|
+
};
|
|
2924
|
+
}
|
|
2925
|
+
return outcome;
|
|
2814
2926
|
}
|
|
2815
2927
|
function resolveBurnDeps(ctx) {
|
|
2816
2928
|
const config2 = loadConfig();
|
|
2817
2929
|
const token = readTokenFromEnvFile(envPath());
|
|
2818
2930
|
const model = resolveModel("implement", config2, ctx.project, ctx.modelOverride);
|
|
2931
|
+
const land = createSerialQueue();
|
|
2819
2932
|
return {
|
|
2820
2933
|
config: config2,
|
|
2821
2934
|
hasAuthToken: token !== undefined,
|
|
2822
|
-
|
|
2935
|
+
concurrency: config2.burnConcurrency,
|
|
2936
|
+
executeTicketRun: (c, ticket) => realExecuteTicketRun(c, ticket, config2, token, model, land)
|
|
2823
2937
|
};
|
|
2824
2938
|
}
|
|
2825
2939
|
function readTokenFromEnvFile(path) {
|
|
@@ -2834,12 +2948,13 @@ function readTokenFromEnvFile(path) {
|
|
|
2834
2948
|
const token = fromFile && fromFile.length > 0 ? fromFile : process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
2835
2949
|
return token && token.length > 0 ? token : undefined;
|
|
2836
2950
|
}
|
|
2837
|
-
var
|
|
2951
|
+
var AUTH_MISSING_EVENT = "auth.missing", AUTH_MISSING_MESSAGE = "run `claude setup-token` and put CLAUDE_CODE_OAUTH_TOKEN in ~/.runcastle/.env", PLACEHOLDERS, ticketBurner;
|
|
2838
2952
|
var init_ticket_burner = __esm(() => {
|
|
2839
2953
|
init_src();
|
|
2840
2954
|
init_config_load();
|
|
2841
2955
|
init_paths();
|
|
2842
2956
|
init_skills_root();
|
|
2957
|
+
init_git();
|
|
2843
2958
|
PLACEHOLDERS = ["TICKET_JSON", "FEATURE_BRIEF", "DOCS_DIGEST", "COMMIT_CONVENTION"];
|
|
2844
2959
|
ticketBurner = {
|
|
2845
2960
|
id: "ticket-burner",
|
|
@@ -2948,7 +3063,7 @@ async function realExecuteResearchRun(ctx, waypoint, config2, token, model) {
|
|
|
2948
3063
|
const throttle = createResearchStreamThrottle((e) => ctx.emitEvent(e));
|
|
2949
3064
|
const runOptions = {
|
|
2950
3065
|
agent: buildBurnAgent(config2, token, model),
|
|
2951
|
-
sandbox: config2.sandbox === "docker" ? docker2(
|
|
3066
|
+
sandbox: config2.sandbox === "docker" ? docker2({ imageName: resolveSandboxImage(config2) }) : noSandbox2(),
|
|
2952
3067
|
cwd: project.repoPath,
|
|
2953
3068
|
prompt,
|
|
2954
3069
|
branchStrategy: { type: "branch", branch: tempBranch, baseBranch: feature.branch },
|
|
@@ -2969,7 +3084,7 @@ async function realExecuteResearchRun(ctx, waypoint, config2, token, model) {
|
|
|
2969
3084
|
const outcome = interpretResearchResult(result, docRelPath);
|
|
2970
3085
|
if (outcome.status !== "done")
|
|
2971
3086
|
return outcome;
|
|
2972
|
-
const merge = await
|
|
3087
|
+
const merge = await mergeTempBranch(project.repoPath, feature.branch, tempBranch);
|
|
2973
3088
|
if (!merge.ok) {
|
|
2974
3089
|
const detail = merge.error ?? "merge failed";
|
|
2975
3090
|
ctx.emitEvent({
|
|
@@ -3247,7 +3362,8 @@ function list(ctx, projectId) {
|
|
|
3247
3362
|
pending: tickets2.filter((t) => t.status === "pending").length,
|
|
3248
3363
|
burning: tickets2.filter((t) => t.status === "burning").length,
|
|
3249
3364
|
done: tickets2.filter((t) => t.status === "done").length,
|
|
3250
|
-
failed: tickets2.filter((t) => t.status === "failed").length
|
|
3365
|
+
failed: tickets2.filter((t) => t.status === "failed").length,
|
|
3366
|
+
cancelled: tickets2.filter((t) => t.status === "cancelled").length
|
|
3251
3367
|
};
|
|
3252
3368
|
return { ...feature, ticketCounts: counts, activeRun: hasActiveRun(ctx, feature.id) };
|
|
3253
3369
|
});
|
|
@@ -3276,13 +3392,19 @@ async function burn(ctx, featureId, opts = {}) {
|
|
|
3276
3392
|
const why = feature.phase === "implementation" ? "a run is already burning this feature" : `feature must be in the tickets phase to burn (currently ${feature.phase})`;
|
|
3277
3393
|
throw new GateError(why);
|
|
3278
3394
|
}
|
|
3279
|
-
|
|
3280
|
-
|
|
3395
|
+
const tickets2 = listByFeature2(ctx, featureId);
|
|
3396
|
+
if (tickets2.filter((t) => t.status !== "cancelled").length < 1) {
|
|
3397
|
+
throw new GateError(tickets2.length > 0 ? "no burnable tickets \u2014 every ticket is cancelled" : "no tickets to burn");
|
|
3281
3398
|
}
|
|
3282
3399
|
if (restarting) {
|
|
3400
|
+
const failed = tickets2.filter((t) => t.status === "failed");
|
|
3401
|
+
for (const t of failed) {
|
|
3402
|
+
updateTicket(ctx, t.id, { status: "pending", error: null });
|
|
3403
|
+
}
|
|
3283
3404
|
emit(ctx, featureId, {
|
|
3284
3405
|
type: "burn.restarted",
|
|
3285
|
-
message: "restarting burn (previous run cancelled or crashed)"
|
|
3406
|
+
message: failed.length > 0 ? `restarting burn \u2014 retrying ${failed.length} failed ticket(s)` : "restarting burn (previous run cancelled or crashed)",
|
|
3407
|
+
data: { retried: failed.map((t) => t.seq) }
|
|
3286
3408
|
});
|
|
3287
3409
|
} else {
|
|
3288
3410
|
setPhase(ctx, featureId, "implementation", "burn.started", "burning tickets");
|
|
@@ -3394,6 +3516,22 @@ function toolEmitTickets(ctx, session, input) {
|
|
|
3394
3516
|
const stored = storeTickets(ctx, feature.id, input.tickets);
|
|
3395
3517
|
return { stored: stored.length, ids: stored.map((t) => t.id) };
|
|
3396
3518
|
}
|
|
3519
|
+
function requireOwnTicket(ctx, session, ticketId) {
|
|
3520
|
+
const ticket = getTicket(ctx, ticketId);
|
|
3521
|
+
if (ticket.featureId !== session.featureId) {
|
|
3522
|
+
throw new GateError(`ticket ${ticketId} does not belong to this session's feature`);
|
|
3523
|
+
}
|
|
3524
|
+
return ticket;
|
|
3525
|
+
}
|
|
3526
|
+
function toolUpdateTicket(ctx, session, input) {
|
|
3527
|
+
requireOwnTicket(ctx, session, input.id);
|
|
3528
|
+
const { id, ...patch } = input;
|
|
3529
|
+
return { ok: true, ticket: editTicket(ctx, id, patch) };
|
|
3530
|
+
}
|
|
3531
|
+
function toolCancelTicket(ctx, session, input) {
|
|
3532
|
+
requireOwnTicket(ctx, session, input.id);
|
|
3533
|
+
return { ok: true, ticket: cancelTicket(ctx, input.id, input.reason) };
|
|
3534
|
+
}
|
|
3397
3535
|
function toolEscalateToMap(ctx, session, input) {
|
|
3398
3536
|
return escalateToMap(ctx, session.featureId, input);
|
|
3399
3537
|
}
|
|
@@ -3493,6 +3631,37 @@ function buildMcpServer() {
|
|
|
3493
3631
|
await commitDocsCheckpoint(rs.ctx, rs.session, `runcastle: tickets emitted (${result.stored})`);
|
|
3494
3632
|
return ok(result);
|
|
3495
3633
|
});
|
|
3634
|
+
server.registerTool("update_ticket", {
|
|
3635
|
+
title: "Update ticket",
|
|
3636
|
+
description: "Rewrite a stored ticket's content \u2014 title, goal, context, acceptanceCriteria, seams (any subset). Only pending or failed tickets can be edited; done/cancelled tickets are history and burning tickets are already running. Use during a revisit when a decision change makes a ticket stale. Get ids from get_feature_context.",
|
|
3637
|
+
inputSchema: {
|
|
3638
|
+
id: z3.string(),
|
|
3639
|
+
title: z3.string().optional(),
|
|
3640
|
+
goal: z3.string().optional(),
|
|
3641
|
+
context: z3.string().optional(),
|
|
3642
|
+
acceptanceCriteria: z3.array(z3.string()).optional(),
|
|
3643
|
+
seams: z3.array(z3.string()).optional()
|
|
3644
|
+
}
|
|
3645
|
+
}, async (args, extra) => {
|
|
3646
|
+
const rs = await resolveCtxSession(extra);
|
|
3647
|
+
if (!rs)
|
|
3648
|
+
return noSession();
|
|
3649
|
+
const result = toolUpdateTicket(rs.ctx, rs.session, args);
|
|
3650
|
+
await commitDocsCheckpoint(rs.ctx, rs.session, `runcastle: ticket ${result.ticket.seq} updated`);
|
|
3651
|
+
return ok(result);
|
|
3652
|
+
});
|
|
3653
|
+
server.registerTool("cancel_ticket", {
|
|
3654
|
+
title: "Cancel ticket",
|
|
3655
|
+
description: "Cancel a ticket that is no longer needed (terminal state; only pending or failed tickets). The burner skips cancelled tickets, and tickets blocked by a cancelled one still burn \u2014 cancellation counts as satisfied. Pass a short reason so the timeline explains why.",
|
|
3656
|
+
inputSchema: { id: z3.string(), reason: z3.string().optional() }
|
|
3657
|
+
}, async (args, extra) => {
|
|
3658
|
+
const rs = await resolveCtxSession(extra);
|
|
3659
|
+
if (!rs)
|
|
3660
|
+
return noSession();
|
|
3661
|
+
const result = toolCancelTicket(rs.ctx, rs.session, args);
|
|
3662
|
+
await commitDocsCheckpoint(rs.ctx, rs.session, `runcastle: ticket ${result.ticket.seq} cancelled`);
|
|
3663
|
+
return ok(result);
|
|
3664
|
+
});
|
|
3496
3665
|
server.registerTool("escalate_to_map", {
|
|
3497
3666
|
title: "Escalate to map",
|
|
3498
3667
|
description: "Escalate this grilling session into a map when the feature outgrows one context window. Flips the feature to mapped and scaffolds docs/features/<slug>/map.md, seeding Destination + Notes from your arguments (Not-yet-specified and Out-of-scope start empty). Idempotent: calling it on an already-mapped feature warns and changes nothing.",
|
|
@@ -3890,6 +4059,8 @@ function renderSystemPrompt(feature, kind, waypoint) {
|
|
|
3890
4059
|
return renderWaypointPrompt(feature, waypoint);
|
|
3891
4060
|
if (kind === "converge")
|
|
3892
4061
|
return renderConvergePrompt(feature);
|
|
4062
|
+
if (kind === "revisit")
|
|
4063
|
+
return renderRevisitPrompt(feature);
|
|
3893
4064
|
const docs = featureDocsRel(feature.slug);
|
|
3894
4065
|
const entry = kind === "ideation" ? "Begin by invoking the `/runcastle:ideate` skill and drive the ideation session to completion." : "This is a Q&A session: invoke `/runcastle:qa`. Answer questions from the docs + code. Do NOT advance phases or emit tickets.";
|
|
3895
4066
|
return [
|
|
@@ -4015,6 +4186,49 @@ function renderConvergePrompt(feature) {
|
|
|
4015
4186
|
].join(`
|
|
4016
4187
|
`);
|
|
4017
4188
|
}
|
|
4189
|
+
function renderRevisitPrompt(feature) {
|
|
4190
|
+
const docs = featureDocsRel(feature.slug);
|
|
4191
|
+
return [
|
|
4192
|
+
`# runcastle \u2014 ${feature.title} (revisit session)`,
|
|
4193
|
+
"",
|
|
4194
|
+
feature.oneLiner,
|
|
4195
|
+
"",
|
|
4196
|
+
"This is a **revisit session**: the human came back to a feature whose",
|
|
4197
|
+
"earlier sessions are finished \u2014 they remembered something, or a decision",
|
|
4198
|
+
"changed. Where possible this terminal RESUMES the previous conversation, so",
|
|
4199
|
+
"you may already have its context above. Your job: fold the new information",
|
|
4200
|
+
"into the record, then reconcile the tickets with it. The docs are the",
|
|
4201
|
+
"artifact \u2014 later phases read them, never the transcripts.",
|
|
4202
|
+
"",
|
|
4203
|
+
"## Feature",
|
|
4204
|
+
`- Slug: \`${feature.slug}\``,
|
|
4205
|
+
`- Branch: \`${feature.branch}\``,
|
|
4206
|
+
`- Current phase: **${feature.phase}** (size: ${feature.size})`,
|
|
4207
|
+
"",
|
|
4208
|
+
"## Knowledge (versioned in the target repo)",
|
|
4209
|
+
`Feature docs live at \`${docs}/\`:`,
|
|
4210
|
+
`- \`${docs}/decisions.md\` \u2014 append the new/changed decisions with a dated "revisited" note.`,
|
|
4211
|
+
`- \`${docs}/spec.md\` \u2014 if it exists, amend the affected sections in place.`,
|
|
4212
|
+
`- \`${docs}/map.md\` \u2014 if the feature is mapped, keep the map honest too.`,
|
|
4213
|
+
"",
|
|
4214
|
+
"## runcastle MCP tools",
|
|
4215
|
+
"- `get_feature_context()` \u2014 feature + phase + docs + ALL tickets (with ids and statuses).",
|
|
4216
|
+
"- `update_ticket({ id, ...fields })` \u2014 rewrite a stale pending/failed ticket's content.",
|
|
4217
|
+
"- `cancel_ticket({ id, reason })` \u2014 cancel a pending/failed ticket that is now unnecessary.",
|
|
4218
|
+
"- `emit_tickets({ tickets })` \u2014 add tickets for NEW work the change requires.",
|
|
4219
|
+
"- `record_event({ type, message })` \u2014 drop a timeline note summarising the revisit.",
|
|
4220
|
+
"",
|
|
4221
|
+
"## Rules",
|
|
4222
|
+
"- Do NOT call `complete_phase` \u2014 a revisit never moves the pipeline.",
|
|
4223
|
+
"- Do NOT touch `done`/`burning` tickets; if done work is now wrong, emit a new ticket that fixes it.",
|
|
4224
|
+
"- Docs first, tickets second: capture the decision prose before any ticket surgery.",
|
|
4225
|
+
"",
|
|
4226
|
+
"## Your task",
|
|
4227
|
+
"Invoke the `/runcastle:revisit` skill and work through what the human brings up.",
|
|
4228
|
+
""
|
|
4229
|
+
].join(`
|
|
4230
|
+
`);
|
|
4231
|
+
}
|
|
4018
4232
|
function renderSettings(hookClient) {
|
|
4019
4233
|
const cmd = (event) => ({
|
|
4020
4234
|
type: "command",
|
|
@@ -4060,6 +4274,8 @@ var init_artifacts = __esm(() => {
|
|
|
4060
4274
|
RUNCASTLE_MCP_ALLOW_RULES = [
|
|
4061
4275
|
"mcp__runcastle__get_feature_context",
|
|
4062
4276
|
"mcp__runcastle__emit_tickets",
|
|
4277
|
+
"mcp__runcastle__update_ticket",
|
|
4278
|
+
"mcp__runcastle__cancel_ticket",
|
|
4063
4279
|
"mcp__runcastle__record_event",
|
|
4064
4280
|
"mcp__runcastle__complete_phase",
|
|
4065
4281
|
"mcp__runcastle__escalate_to_map",
|
|
@@ -4226,11 +4442,32 @@ async function launchSession(ctx, input, opts = {}) {
|
|
|
4226
4442
|
throw e;
|
|
4227
4443
|
}
|
|
4228
4444
|
}
|
|
4445
|
+
if (input.kind === "revisit") {
|
|
4446
|
+
try {
|
|
4447
|
+
assertSpawnable(ctx, feature, session.id);
|
|
4448
|
+
} catch (e) {
|
|
4449
|
+
markSessionEnded(ctx, session.id);
|
|
4450
|
+
throw e;
|
|
4451
|
+
}
|
|
4452
|
+
const prior = mostRecentResumableSession(ctx, feature.id);
|
|
4453
|
+
if (prior?.ccSessionId) {
|
|
4454
|
+
resumeSessionId = prior.ccSessionId;
|
|
4455
|
+
} else {
|
|
4456
|
+
resumeUnavailableFrom = "revisit";
|
|
4457
|
+
}
|
|
4458
|
+
}
|
|
4229
4459
|
emit(ctx, feature.id, {
|
|
4230
4460
|
type: "session.launching",
|
|
4231
4461
|
message: `launching ${input.kind} session`,
|
|
4232
4462
|
data: { sessionId: session.id, kind: input.kind, worktreePath, waypointId: waypoint?.id }
|
|
4233
4463
|
});
|
|
4464
|
+
if (input.kind === "revisit" && resumeUnavailableFrom) {
|
|
4465
|
+
emit(ctx, feature.id, {
|
|
4466
|
+
type: "session.resume_unavailable",
|
|
4467
|
+
message: "no resumable conversation for this feature \u2014 revisiting fresh from the docs",
|
|
4468
|
+
data: { sessionId: session.id }
|
|
4469
|
+
});
|
|
4470
|
+
}
|
|
4234
4471
|
if (waypoint && resumeUnavailableFrom) {
|
|
4235
4472
|
emit(ctx, feature.id, {
|
|
4236
4473
|
type: "session.resume_unavailable",
|
|
@@ -4828,6 +5065,14 @@ var init_settings = __esm(() => {
|
|
|
4828
5065
|
valueSchema: z9.string().min(1),
|
|
4829
5066
|
parseEnv: idEnv
|
|
4830
5067
|
},
|
|
5068
|
+
{
|
|
5069
|
+
key: "burnConcurrency",
|
|
5070
|
+
configKey: "burnConcurrency",
|
|
5071
|
+
envVar: "RUNCASTLE_BURN_CONCURRENCY",
|
|
5072
|
+
restartRequired: false,
|
|
5073
|
+
valueSchema: z9.number().int().min(1).max(8),
|
|
5074
|
+
parseEnv: (raw) => Number(raw)
|
|
5075
|
+
},
|
|
4831
5076
|
{
|
|
4832
5077
|
key: "mainBranch",
|
|
4833
5078
|
configKey: "mainBranch",
|
|
@@ -4996,7 +5241,7 @@ function afkTokenProbe(env) {
|
|
|
4996
5241
|
async function runDoctor(env) {
|
|
4997
5242
|
const { exec } = env;
|
|
4998
5243
|
const processEnv = env.env ?? process.env;
|
|
4999
|
-
const imageName = env.imageName ??
|
|
5244
|
+
const imageName = env.imageName ?? DEFAULT_SANDBOX_IMAGE;
|
|
5000
5245
|
const results = [
|
|
5001
5246
|
await bunProbe(exec),
|
|
5002
5247
|
await nodeProbe(exec),
|
|
@@ -5018,6 +5263,9 @@ function exitCodeFor(report, mode) {
|
|
|
5018
5263
|
return report.tier1Ok ? 0 : 1;
|
|
5019
5264
|
return report.ok ? 0 : 1;
|
|
5020
5265
|
}
|
|
5266
|
+
var init_doctor = __esm(() => {
|
|
5267
|
+
init_src();
|
|
5268
|
+
});
|
|
5021
5269
|
|
|
5022
5270
|
// src/doctor/system-exec.ts
|
|
5023
5271
|
import { spawn as spawn2 } from "child_process";
|
|
@@ -5201,6 +5449,7 @@ function fileAfkTokenIo(exec) {
|
|
|
5201
5449
|
var AFK_TOKEN_KEY = "CLAUDE_CODE_OAUTH_TOKEN";
|
|
5202
5450
|
var init_setup = __esm(() => {
|
|
5203
5451
|
init_paths();
|
|
5452
|
+
init_doctor();
|
|
5204
5453
|
init_errors();
|
|
5205
5454
|
init_asset_paths();
|
|
5206
5455
|
});
|
|
@@ -5210,6 +5459,7 @@ import * as z11 from "zod";
|
|
|
5210
5459
|
var setupRouter;
|
|
5211
5460
|
var init_setup2 = __esm(() => {
|
|
5212
5461
|
init_src();
|
|
5462
|
+
init_doctor();
|
|
5213
5463
|
init_system_exec();
|
|
5214
5464
|
init_registry();
|
|
5215
5465
|
init_setup();
|
|
@@ -5227,7 +5477,7 @@ var init_setup2 = __esm(() => {
|
|
|
5227
5477
|
const exec = createSystemExec();
|
|
5228
5478
|
const preferred = ctx.config.sandbox === "podman" ? "podman" : "docker";
|
|
5229
5479
|
const runtime = await resolveRuntime(exec, preferred);
|
|
5230
|
-
const imageName = ctx.config
|
|
5480
|
+
const imageName = resolveSandboxImage(ctx.config);
|
|
5231
5481
|
const sandcastleBin = input.kind === "build-image" ? resolveSandcastleBin() ?? undefined : undefined;
|
|
5232
5482
|
const spec = terminalSpec(input.kind, { runtime, imageName, sandcastleBin });
|
|
5233
5483
|
const sessionId = newId("setup");
|
|
@@ -5417,7 +5667,7 @@ async function reconcileStaleRuns(ctx) {
|
|
|
5417
5667
|
}
|
|
5418
5668
|
for (const project of allProjects(ctx)) {
|
|
5419
5669
|
try {
|
|
5420
|
-
await
|
|
5670
|
+
await cleanupTempBranches(project.repoPath);
|
|
5421
5671
|
} catch {}
|
|
5422
5672
|
}
|
|
5423
5673
|
return reconciled;
|
|
@@ -5585,7 +5835,7 @@ function envWithToken() {
|
|
|
5585
5835
|
function resolveDoctorEnv() {
|
|
5586
5836
|
let imageName;
|
|
5587
5837
|
try {
|
|
5588
|
-
imageName = loadConfig()
|
|
5838
|
+
imageName = resolveSandboxImage(loadConfig());
|
|
5589
5839
|
} catch {
|
|
5590
5840
|
imageName = undefined;
|
|
5591
5841
|
}
|
|
@@ -5604,9 +5854,11 @@ async function runCli(argv, log = console.log) {
|
|
|
5604
5854
|
return exitCodeFor(report, mode);
|
|
5605
5855
|
}
|
|
5606
5856
|
var init_cli = __esm(() => {
|
|
5857
|
+
init_src();
|
|
5607
5858
|
init_paths();
|
|
5608
5859
|
init_config_load();
|
|
5609
5860
|
init_ticket_burner();
|
|
5861
|
+
init_doctor();
|
|
5610
5862
|
init_report();
|
|
5611
5863
|
init_system_exec();
|
|
5612
5864
|
if (false) {}
|