runcastle 1.0.14 → 1.0.16
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 +231 -74
- package/drizzle/0008_panoramic_molecule_man.sql +1 -0
- package/drizzle/meta/0008_snapshot.json +603 -0
- package/drizzle/meta/_journal.json +7 -0
- package/index.js +231 -74
- package/package.json +1 -1
- package/skills/packs/README.md +1 -1
- package/skills/packs/runcastle/skills/converge/SKILL.md +5 -9
- package/skills/packs/runcastle/skills/ideate/SKILL.md +6 -6
- package/skills/packs/runcastle/skills/revisit/SKILL.md +9 -0
- package/skills/packs/runcastle/skills/spec/SKILL.md +1 -1
- package/skills/packs/runcastle/skills/tickets/SKILL.md +2 -2
- package/web/assets/index-9ATu1U6z.css +1 -0
- package/web/assets/index-BXaPmFdw.js +110 -0
- package/web/index.html +2 -2
- package/web/assets/index-BEeyZA5j.js +0 -109
- package/web/assets/index-CI8-AEi9.css +0 -1
package/bin/runcastle.js
CHANGED
|
@@ -186,7 +186,7 @@ var init_ids = () => {};
|
|
|
186
186
|
|
|
187
187
|
// ../core/src/schemas.ts
|
|
188
188
|
import * as z2 from "zod";
|
|
189
|
-
var Phase,
|
|
189
|
+
var Phase, TicketStatus, SessionKind, RunStatus, FeatureStatus, SessionStatus, TicketInput, Ticket, WaypointType, WaypointStatus, WaypointDisposition, WaypointInput, Waypoint, Project, Feature, SessionRow, Run, SettingSource, SettingScope, SettingField, SettingsView, SettingsUpdateInput, EventRow;
|
|
190
190
|
var init_schemas = __esm(() => {
|
|
191
191
|
Phase = z2.enum([
|
|
192
192
|
"ideation",
|
|
@@ -196,11 +196,10 @@ var init_schemas = __esm(() => {
|
|
|
196
196
|
"review",
|
|
197
197
|
"shipped"
|
|
198
198
|
]);
|
|
199
|
-
FeatureSize = z2.enum(["full", "collapsed"]);
|
|
200
199
|
TicketStatus = z2.enum(["pending", "burning", "done", "failed", "cancelled"]);
|
|
201
200
|
SessionKind = z2.enum(["ideation", "qa", "waypoint", "converge", "revisit"]);
|
|
202
201
|
RunStatus = z2.enum(["running", "succeeded", "failed", "cancelled"]);
|
|
203
|
-
FeatureStatus = z2.enum(["active", "shipped"]);
|
|
202
|
+
FeatureStatus = z2.enum(["active", "shipped", "archived"]);
|
|
204
203
|
SessionStatus = z2.enum(["launching", "live", "ended"]);
|
|
205
204
|
TicketInput = z2.object({
|
|
206
205
|
title: z2.string(),
|
|
@@ -253,7 +252,6 @@ var init_schemas = __esm(() => {
|
|
|
253
252
|
slug: z2.string(),
|
|
254
253
|
title: z2.string(),
|
|
255
254
|
oneLiner: z2.string(),
|
|
256
|
-
size: FeatureSize,
|
|
257
255
|
mapped: z2.boolean(),
|
|
258
256
|
phase: Phase,
|
|
259
257
|
branch: z2.string(),
|
|
@@ -379,11 +377,7 @@ function nextPhase(feature) {
|
|
|
379
377
|
const i = ORDER.indexOf(feature.phase);
|
|
380
378
|
if (i < 0 || i >= ORDER.length - 1)
|
|
381
379
|
return null;
|
|
382
|
-
|
|
383
|
-
if (feature.size === "collapsed" && step === "spec") {
|
|
384
|
-
return ORDER[i + 2] ?? null;
|
|
385
|
-
}
|
|
386
|
-
return step;
|
|
380
|
+
return ORDER[i + 1];
|
|
387
381
|
}
|
|
388
382
|
function nextGate(feature) {
|
|
389
383
|
const i = ORDER.indexOf(feature.phase);
|
|
@@ -394,7 +388,7 @@ function nextGate(feature) {
|
|
|
394
388
|
return MAPPED_G1;
|
|
395
389
|
return gate;
|
|
396
390
|
}
|
|
397
|
-
var PIPELINE, ORDER, MAPPED_G1;
|
|
391
|
+
var PIPELINE, ORDER, REVIEW_LOOP_BACK, MAPPED_G1;
|
|
398
392
|
var init_pipeline = __esm(() => {
|
|
399
393
|
PIPELINE = [
|
|
400
394
|
{ phase: "ideation" },
|
|
@@ -410,7 +404,7 @@ var init_pipeline = __esm(() => {
|
|
|
410
404
|
phase: "tickets",
|
|
411
405
|
gateToEnter: {
|
|
412
406
|
id: "G2",
|
|
413
|
-
description: "Spec written before breaking into tickets
|
|
407
|
+
description: "Spec written before breaking into tickets",
|
|
414
408
|
check: "spec-file-exists"
|
|
415
409
|
}
|
|
416
410
|
},
|
|
@@ -440,6 +434,7 @@ var init_pipeline = __esm(() => {
|
|
|
440
434
|
}
|
|
441
435
|
];
|
|
442
436
|
ORDER = PIPELINE.map((p) => p.phase);
|
|
437
|
+
REVIEW_LOOP_BACK = { from: "review", to: "implementation" };
|
|
443
438
|
MAPPED_G1 = {
|
|
444
439
|
id: "G1",
|
|
445
440
|
description: "Every waypoint resolved or dropped before converging",
|
|
@@ -467,7 +462,6 @@ var init_db_schema = __esm(() => {
|
|
|
467
462
|
slug: text("slug").notNull(),
|
|
468
463
|
title: text("title").notNull(),
|
|
469
464
|
oneLiner: text("one_liner").notNull(),
|
|
470
|
-
size: text("size").notNull().$type(),
|
|
471
465
|
mapped: integer("mapped", { mode: "boolean" }).notNull().default(false),
|
|
472
466
|
phase: text("phase").notNull().$type(),
|
|
473
467
|
branch: text("branch").notNull(),
|
|
@@ -1195,7 +1189,6 @@ function rowToFeature(row) {
|
|
|
1195
1189
|
slug: row.slug,
|
|
1196
1190
|
title: row.title,
|
|
1197
1191
|
oneLiner: row.oneLiner,
|
|
1198
|
-
size: row.size,
|
|
1199
1192
|
mapped: row.mapped,
|
|
1200
1193
|
phase: row.phase,
|
|
1201
1194
|
branch: row.branch,
|
|
@@ -1516,15 +1509,20 @@ function markSessionLive(ctx, id, input = {}) {
|
|
|
1516
1509
|
const firstTimeLive = existing.status !== "live";
|
|
1517
1510
|
if (firstTimeLive) {
|
|
1518
1511
|
promoteLastSession(ctx, id);
|
|
1519
|
-
|
|
1520
|
-
scheduleConvergeKickoff(ctx, existing);
|
|
1512
|
+
scheduleKickoff(ctx, existing);
|
|
1521
1513
|
}
|
|
1522
1514
|
return getSessionRow(ctx, id);
|
|
1523
1515
|
}
|
|
1524
|
-
function
|
|
1516
|
+
function kickoffLineFor(kind, override) {
|
|
1517
|
+
return override ?? KICKOFF_LINES[kind];
|
|
1518
|
+
}
|
|
1519
|
+
function setKickoffOverride(sessionId, line) {
|
|
1520
|
+
pendingKickoffOverrides.set(sessionId, line);
|
|
1521
|
+
}
|
|
1522
|
+
function writeKickoffSequence(line, io, submitDelayMs = KICKOFF_SUBMIT_DELAY_MS) {
|
|
1525
1523
|
if (!io.alive())
|
|
1526
1524
|
return;
|
|
1527
|
-
io.write(
|
|
1525
|
+
io.write(line);
|
|
1528
1526
|
const submit = setTimeout(() => {
|
|
1529
1527
|
try {
|
|
1530
1528
|
if (!io.alive())
|
|
@@ -1535,10 +1533,12 @@ function writeKickoffSequence(io, submitDelayMs = CONVERGE_KICKOFF_SUBMIT_DELAY_
|
|
|
1535
1533
|
}, submitDelayMs);
|
|
1536
1534
|
submit.unref?.();
|
|
1537
1535
|
}
|
|
1538
|
-
function
|
|
1536
|
+
function scheduleKickoff(ctx, session) {
|
|
1537
|
+
const line = kickoffLineFor(session.kind, pendingKickoffOverrides.get(session.id));
|
|
1538
|
+
pendingKickoffOverrides.delete(session.id);
|
|
1539
1539
|
const timer = setTimeout(() => {
|
|
1540
1540
|
try {
|
|
1541
|
-
writeKickoffSequence({
|
|
1541
|
+
writeKickoffSequence(line, {
|
|
1542
1542
|
write: (data) => ptyRegistry().get(session.id)?.pty.write(data),
|
|
1543
1543
|
alive: () => {
|
|
1544
1544
|
const entry = ptyRegistry().get(session.id);
|
|
@@ -1547,19 +1547,20 @@ function scheduleConvergeKickoff(ctx, session) {
|
|
|
1547
1547
|
onSubmitted: () => {
|
|
1548
1548
|
emit(ctx, session.featureId, {
|
|
1549
1549
|
type: "session.kickoff",
|
|
1550
|
-
message:
|
|
1551
|
-
data: { sessionId: session.id }
|
|
1550
|
+
message: `${session.kind} session kicked off automatically`,
|
|
1551
|
+
data: { sessionId: session.id, kind: session.kind }
|
|
1552
1552
|
});
|
|
1553
1553
|
}
|
|
1554
1554
|
});
|
|
1555
1555
|
} catch {}
|
|
1556
|
-
},
|
|
1556
|
+
}, KICKOFF_DELAY_MS);
|
|
1557
1557
|
timer.unref?.();
|
|
1558
1558
|
}
|
|
1559
1559
|
function markSessionEnded(ctx, id) {
|
|
1560
1560
|
const existing = getSessionRow(ctx, id);
|
|
1561
1561
|
if (!existing)
|
|
1562
1562
|
return null;
|
|
1563
|
+
pendingKickoffOverrides.delete(id);
|
|
1563
1564
|
ctx.db.update(sessions).set({ status: "ended" }).where(eq4(sessions.id, id)).run();
|
|
1564
1565
|
return getSessionRow(ctx, id);
|
|
1565
1566
|
}
|
|
@@ -1571,7 +1572,7 @@ function mostRecentLiveSession(ctx) {
|
|
|
1571
1572
|
const row = ctx.db.select().from(sessions).where(eq4(sessions.status, "live")).orderBy(desc2(sql2`rowid`)).limit(1).get();
|
|
1572
1573
|
return row ? rowToSession(row) : null;
|
|
1573
1574
|
}
|
|
1574
|
-
var
|
|
1575
|
+
var KICKOFF_DELAY_MS = 1500, KICKOFF_SUBMIT_DELAY_MS = 350, CONVERGE_KICKOFF_LINE, KICKOFF_LINES, pendingKickoffOverrides;
|
|
1575
1576
|
var init_sessions = __esm(() => {
|
|
1576
1577
|
init_src();
|
|
1577
1578
|
init_schema();
|
|
@@ -1580,6 +1581,14 @@ var init_sessions = __esm(() => {
|
|
|
1580
1581
|
init_waypoints();
|
|
1581
1582
|
init_repo();
|
|
1582
1583
|
CONVERGE_KICKOFF_LINE = "Proceed with your task: invoke /runcastle:converge and drive spec then tickets " + "from map.md + decisions.md, per your system prompt.";
|
|
1584
|
+
KICKOFF_LINES = {
|
|
1585
|
+
ideation: "Proceed with your task: invoke the /runcastle:ideate skill and drive the ideation session.",
|
|
1586
|
+
qa: "Proceed with your task: invoke the /runcastle:qa skill and answer questions from the " + "docs and code \u2014 do not advance phases or emit tickets.",
|
|
1587
|
+
waypoint: "Proceed with your task: invoke the /runcastle:waypoint skill and work your assigned " + "waypoint to a resolution.",
|
|
1588
|
+
converge: CONVERGE_KICKOFF_LINE,
|
|
1589
|
+
revisit: "Proceed with your task: invoke the /runcastle:revisit skill and work through what the " + "human brings up."
|
|
1590
|
+
};
|
|
1591
|
+
pendingKickoffOverrides = new Map;
|
|
1583
1592
|
});
|
|
1584
1593
|
|
|
1585
1594
|
// src/launcher/reconcile.ts
|
|
@@ -1808,8 +1817,6 @@ function checkGate(ctx, check, feature) {
|
|
|
1808
1817
|
};
|
|
1809
1818
|
}
|
|
1810
1819
|
case "spec-file-exists":
|
|
1811
|
-
if (feature.size === "collapsed")
|
|
1812
|
-
return { satisfied: true };
|
|
1813
1820
|
return fileGate(ctx, feature, "spec.md", "write the spec (spec.md) before breaking into tickets");
|
|
1814
1821
|
case "tickets-approved": {
|
|
1815
1822
|
const count = listByFeature2(ctx, feature.id).filter((t) => t.status !== "cancelled").length;
|
|
@@ -2326,6 +2333,44 @@ async function cleanupTempBranches(repoPath) {
|
|
|
2326
2333
|
}
|
|
2327
2334
|
return { deleted, kept };
|
|
2328
2335
|
}
|
|
2336
|
+
async function removeTalkWorktree(repoPath, worktreePath) {
|
|
2337
|
+
const g = git(repoPath);
|
|
2338
|
+
try {
|
|
2339
|
+
await g.raw(["worktree", "remove", "--force", worktreePath]);
|
|
2340
|
+
} catch {
|
|
2341
|
+
try {
|
|
2342
|
+
rmSync(worktreePath, { recursive: true, force: true });
|
|
2343
|
+
} catch {}
|
|
2344
|
+
}
|
|
2345
|
+
try {
|
|
2346
|
+
await g.raw(["worktree", "prune"]);
|
|
2347
|
+
} catch {}
|
|
2348
|
+
if (existsSync7(worktreePath)) {
|
|
2349
|
+
throw new InvalidInputError(`could not remove talk worktree at ${worktreePath} \u2014 a file may be locked; ` + "close anything using it and retry the delete");
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
async function deleteFeatureBranches(repoPath, slug) {
|
|
2353
|
+
const deleted = [];
|
|
2354
|
+
const kept = [];
|
|
2355
|
+
const g = git(repoPath);
|
|
2356
|
+
const featureBranchName = featureBranch(slug);
|
|
2357
|
+
const seg = tempBranchSlugSegment(slug);
|
|
2358
|
+
const tempPrefixes = TEMP_BRANCH_PREFIXES.map((p) => `${p}${seg}/`);
|
|
2359
|
+
let all;
|
|
2360
|
+
try {
|
|
2361
|
+
all = (await g.branchLocal()).all;
|
|
2362
|
+
} catch {
|
|
2363
|
+
return { deleted, kept };
|
|
2364
|
+
}
|
|
2365
|
+
const targets = all.filter((b) => b === featureBranchName || tempPrefixes.some((p) => b.startsWith(p)));
|
|
2366
|
+
for (const branch of targets) {
|
|
2367
|
+
if (await deleteBranchDetachingWorktrees(g, repoPath, branch))
|
|
2368
|
+
deleted.push(branch);
|
|
2369
|
+
else
|
|
2370
|
+
kept.push(branch);
|
|
2371
|
+
}
|
|
2372
|
+
return { deleted, kept };
|
|
2373
|
+
}
|
|
2329
2374
|
async function commitDocs(worktreePath, message) {
|
|
2330
2375
|
if (!existsSync7(resolve3(worktreePath, DOCS_PATHSPEC)))
|
|
2331
2376
|
return;
|
|
@@ -2441,9 +2486,10 @@ async function mergeFeature(project, feature) {
|
|
|
2441
2486
|
return { ok: true, target };
|
|
2442
2487
|
} catch (e) {
|
|
2443
2488
|
if (await mergeInProgress(g)) {
|
|
2489
|
+
const files = await conflictedFiles(g);
|
|
2444
2490
|
await g.raw(["merge", "--abort"]);
|
|
2445
2491
|
await restoreBranch(g, previous, target);
|
|
2446
|
-
return { ok: false, conflict: true, target };
|
|
2492
|
+
return { ok: false, conflict: true, target, files };
|
|
2447
2493
|
}
|
|
2448
2494
|
throw e instanceof Error ? e : new Error(errMsg(e));
|
|
2449
2495
|
}
|
|
@@ -2455,6 +2501,15 @@ async function restoreBranch(g, previous, target) {
|
|
|
2455
2501
|
await g.checkout(previous);
|
|
2456
2502
|
} catch {}
|
|
2457
2503
|
}
|
|
2504
|
+
async function conflictedFiles(g) {
|
|
2505
|
+
try {
|
|
2506
|
+
const out = (await g.raw(["diff", "--name-only", "--diff-filter=U"])).trim();
|
|
2507
|
+
return out ? out.split(`
|
|
2508
|
+
`).map((s) => s.trim()).filter(Boolean) : [];
|
|
2509
|
+
} catch {
|
|
2510
|
+
return [];
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2458
2513
|
async function mergeInProgress(g) {
|
|
2459
2514
|
try {
|
|
2460
2515
|
await g.raw(["rev-parse", "--verify", "--quiet", "MERGE_HEAD"]);
|
|
@@ -2569,6 +2624,33 @@ var init_knowledge = __esm(() => {
|
|
|
2569
2624
|
];
|
|
2570
2625
|
});
|
|
2571
2626
|
|
|
2627
|
+
// src/pty/end-session.ts
|
|
2628
|
+
function endSession(ctx, sessionId) {
|
|
2629
|
+
const registry = ptyRegistry();
|
|
2630
|
+
const killed = registry.kill(sessionId);
|
|
2631
|
+
registry.remove(sessionId);
|
|
2632
|
+
const session = markSessionEnded(ctx, sessionId);
|
|
2633
|
+
if (session) {
|
|
2634
|
+
releaseForSession(ctx, sessionId);
|
|
2635
|
+
emit(ctx, session.featureId, {
|
|
2636
|
+
type: "session.ended",
|
|
2637
|
+
message: "session ended by user",
|
|
2638
|
+
data: { sessionId, killed }
|
|
2639
|
+
});
|
|
2640
|
+
} else {
|
|
2641
|
+
const existing = getSessionRow(ctx, sessionId);
|
|
2642
|
+
if (!existing)
|
|
2643
|
+
return { sessionId, ok: false, killed };
|
|
2644
|
+
}
|
|
2645
|
+
return { sessionId, ok: true, killed };
|
|
2646
|
+
}
|
|
2647
|
+
var init_end_session = __esm(() => {
|
|
2648
|
+
init_sessions();
|
|
2649
|
+
init_events();
|
|
2650
|
+
init_waypoints();
|
|
2651
|
+
init_registry();
|
|
2652
|
+
});
|
|
2653
|
+
|
|
2572
2654
|
// src/launcher/skills-root.ts
|
|
2573
2655
|
import { existsSync as existsSync9 } from "fs";
|
|
2574
2656
|
import { dirname as dirname2, join as join8, resolve as resolve5 } from "path";
|
|
@@ -3778,6 +3860,7 @@ var init_runner = __esm(() => {
|
|
|
3778
3860
|
|
|
3779
3861
|
// src/services/features.ts
|
|
3780
3862
|
import { desc as desc3, eq as eq7 } from "drizzle-orm";
|
|
3863
|
+
import { rmSync as rmSync2 } from "fs";
|
|
3781
3864
|
async function createFeature(ctx, input) {
|
|
3782
3865
|
const project = requireProjectById(ctx, input.projectId);
|
|
3783
3866
|
const slug = uniqueSlug(ctx, project.id, input.title);
|
|
@@ -3790,8 +3873,7 @@ async function createFeature(ctx, input) {
|
|
|
3790
3873
|
slug,
|
|
3791
3874
|
title: input.title,
|
|
3792
3875
|
oneLiner: input.oneLiner,
|
|
3793
|
-
|
|
3794
|
-
mapped: input.mapped ?? false,
|
|
3876
|
+
mapped: false,
|
|
3795
3877
|
phase: "ideation",
|
|
3796
3878
|
branch,
|
|
3797
3879
|
baseBranch,
|
|
@@ -3874,12 +3956,21 @@ function advance(ctx, featureId) {
|
|
|
3874
3956
|
}
|
|
3875
3957
|
async function burn(ctx, featureId, opts = {}) {
|
|
3876
3958
|
const feature = getFeatureRow(ctx, featureId);
|
|
3877
|
-
const
|
|
3878
|
-
|
|
3879
|
-
|
|
3959
|
+
const running = hasActiveRun(ctx, featureId);
|
|
3960
|
+
const tickets2 = listByFeature2(ctx, featureId);
|
|
3961
|
+
const pending = tickets2.filter((t) => t.status !== "done" && t.status !== "failed" && t.status !== "cancelled");
|
|
3962
|
+
const restarting = feature.phase === "implementation" && !running;
|
|
3963
|
+
const iterating = feature.phase === "review" && !running && pending.length >= 1;
|
|
3964
|
+
if (feature.phase !== "tickets" && !restarting && !iterating) {
|
|
3965
|
+
let why;
|
|
3966
|
+
if (running)
|
|
3967
|
+
why = "a run is already burning this feature";
|
|
3968
|
+
else if (feature.phase === "review")
|
|
3969
|
+
why = "no pending tickets to burn \u2014 emit fix tickets before burning from review";
|
|
3970
|
+
else
|
|
3971
|
+
why = `feature must be in the tickets phase to burn (currently ${feature.phase})`;
|
|
3880
3972
|
throw new GateError(why);
|
|
3881
3973
|
}
|
|
3882
|
-
const tickets2 = listByFeature2(ctx, featureId);
|
|
3883
3974
|
if (tickets2.filter((t) => t.status !== "cancelled").length < 1) {
|
|
3884
3975
|
throw new GateError(tickets2.length > 0 ? "no burnable tickets \u2014 every ticket is cancelled" : "no tickets to burn");
|
|
3885
3976
|
}
|
|
@@ -3893,6 +3984,8 @@ async function burn(ctx, featureId, opts = {}) {
|
|
|
3893
3984
|
message: failed.length > 0 ? `restarting burn \u2014 retrying ${failed.length} failed ticket(s)` : "restarting burn (previous run cancelled or crashed)",
|
|
3894
3985
|
data: { retried: failed.map((t) => t.seq) }
|
|
3895
3986
|
});
|
|
3987
|
+
} else if (iterating) {
|
|
3988
|
+
setPhase(ctx, featureId, REVIEW_LOOP_BACK.to, "burn.started", "burn from review \u2014 iterating");
|
|
3896
3989
|
} else {
|
|
3897
3990
|
setPhase(ctx, featureId, "implementation", "burn.started", "burning tickets");
|
|
3898
3991
|
}
|
|
@@ -3979,6 +4072,76 @@ function escalateToMap(ctx, featureId, input) {
|
|
|
3979
4072
|
});
|
|
3980
4073
|
return { ok: true };
|
|
3981
4074
|
}
|
|
4075
|
+
function archiveFeature(ctx, featureId) {
|
|
4076
|
+
const feature = getFeatureRow(ctx, featureId);
|
|
4077
|
+
if (feature.status === "archived") {
|
|
4078
|
+
throw new GateError(`feature ${feature.slug} is already archived`);
|
|
4079
|
+
}
|
|
4080
|
+
const live = listSessionsByFeature(ctx, featureId).find((s) => s.status === "live");
|
|
4081
|
+
if (live)
|
|
4082
|
+
endSession(ctx, live.id);
|
|
4083
|
+
ctx.db.update(features).set({ status: "archived" }).where(eq7(features.id, featureId)).run();
|
|
4084
|
+
emit(ctx, featureId, {
|
|
4085
|
+
type: "feature.archived",
|
|
4086
|
+
message: `feature ${feature.slug} archived`,
|
|
4087
|
+
data: { from: feature.status }
|
|
4088
|
+
});
|
|
4089
|
+
return { ...feature, status: "archived" };
|
|
4090
|
+
}
|
|
4091
|
+
function unarchiveFeature(ctx, featureId) {
|
|
4092
|
+
const feature = getFeatureRow(ctx, featureId);
|
|
4093
|
+
if (feature.status !== "archived") {
|
|
4094
|
+
throw new GateError(`feature ${feature.slug} is not archived`);
|
|
4095
|
+
}
|
|
4096
|
+
const restored = feature.phase === "shipped" ? "shipped" : "active";
|
|
4097
|
+
ctx.db.update(features).set({ status: restored }).where(eq7(features.id, featureId)).run();
|
|
4098
|
+
emit(ctx, featureId, {
|
|
4099
|
+
type: "feature.unarchived",
|
|
4100
|
+
message: `feature ${feature.slug} unarchived (${restored})`,
|
|
4101
|
+
data: { status: restored }
|
|
4102
|
+
});
|
|
4103
|
+
return { ...feature, status: restored };
|
|
4104
|
+
}
|
|
4105
|
+
async function deleteFeature(ctx, featureId) {
|
|
4106
|
+
const feature = getFeatureRow(ctx, featureId);
|
|
4107
|
+
if (feature.status === "shipped") {
|
|
4108
|
+
throw new GateError(`feature ${feature.slug} is shipped \u2014 archive it instead (delete is for non-shipped features)`);
|
|
4109
|
+
}
|
|
4110
|
+
const project = projectForFeature(ctx, feature);
|
|
4111
|
+
for (const run3 of listRunsByFeature(ctx, featureId)) {
|
|
4112
|
+
if (run3.status === "running")
|
|
4113
|
+
cancelRun(run3.id);
|
|
4114
|
+
}
|
|
4115
|
+
const live = listSessionsByFeature(ctx, featureId).find((s) => s.status === "live");
|
|
4116
|
+
if (live)
|
|
4117
|
+
endSession(ctx, live.id);
|
|
4118
|
+
if (activeTestDriveFeatureId() === featureId) {
|
|
4119
|
+
await testDrive(ctx, project, feature, "stop");
|
|
4120
|
+
}
|
|
4121
|
+
await removeTalkWorktree(project.repoPath, worktreeDir(project.id, feature.slug));
|
|
4122
|
+
await deleteFeatureBranches(project.repoPath, feature.slug);
|
|
4123
|
+
emitProject(ctx, project.id, {
|
|
4124
|
+
type: "feature.deleted",
|
|
4125
|
+
message: `feature ${feature.slug} deleted`,
|
|
4126
|
+
data: { featureId, slug: feature.slug }
|
|
4127
|
+
});
|
|
4128
|
+
for (const s of listSessionsByFeature(ctx, featureId)) {
|
|
4129
|
+
try {
|
|
4130
|
+
rmSync2(sessionDir(s.id), { recursive: true, force: true });
|
|
4131
|
+
} catch {}
|
|
4132
|
+
}
|
|
4133
|
+
deleteFeatureRows(ctx, featureId);
|
|
4134
|
+
return { ok: true, slug: feature.slug };
|
|
4135
|
+
}
|
|
4136
|
+
function deleteFeatureRows(ctx, featureId) {
|
|
4137
|
+
ctx.db.delete(tickets).where(eq7(tickets.featureId, featureId)).run();
|
|
4138
|
+
ctx.db.delete(sessions).where(eq7(sessions.featureId, featureId)).run();
|
|
4139
|
+
ctx.db.delete(runs).where(eq7(runs.featureId, featureId)).run();
|
|
4140
|
+
ctx.db.delete(events).where(eq7(events.featureId, featureId)).run();
|
|
4141
|
+
ctx.db.delete(gateOverrides).where(eq7(gateOverrides.featureId, featureId)).run();
|
|
4142
|
+
ctx.db.delete(waypoints).where(eq7(waypoints.featureId, featureId)).run();
|
|
4143
|
+
ctx.db.delete(features).where(eq7(features.id, featureId)).run();
|
|
4144
|
+
}
|
|
3982
4145
|
function gateState(ctx, feature) {
|
|
3983
4146
|
const gate = nextGate(feature);
|
|
3984
4147
|
if (!gate)
|
|
@@ -4002,6 +4165,7 @@ function slugify(title) {
|
|
|
4002
4165
|
}
|
|
4003
4166
|
var init_features = __esm(() => {
|
|
4004
4167
|
init_src();
|
|
4168
|
+
init_paths();
|
|
4005
4169
|
init_schema();
|
|
4006
4170
|
init_errors();
|
|
4007
4171
|
init_events();
|
|
@@ -4009,6 +4173,7 @@ var init_features = __esm(() => {
|
|
|
4009
4173
|
init_git();
|
|
4010
4174
|
init_knowledge();
|
|
4011
4175
|
init_repo();
|
|
4176
|
+
init_end_session();
|
|
4012
4177
|
init_tickets();
|
|
4013
4178
|
init_waypoints();
|
|
4014
4179
|
init_runner();
|
|
@@ -4415,8 +4580,8 @@ function sessionStartContext(ctx, feature) {
|
|
|
4415
4580
|
const ticketSummary = tickets2.length === 0 ? "none yet" : `${tickets2.length} (${Object.entries(byStatus).map(([s, n]) => `${n} ${s}`).join(", ")})`;
|
|
4416
4581
|
return [
|
|
4417
4582
|
`[runcastle] ${feature.title} \u2014 ${feature.oneLiner}`,
|
|
4418
|
-
`phase: ${feature.phase}
|
|
4419
|
-
`docs: docs/features/${feature.slug}/ (brief.md, decisions.md
|
|
4583
|
+
`phase: ${feature.phase}; branch: ${feature.branch}`,
|
|
4584
|
+
`docs: docs/features/${feature.slug}/ (brief.md, decisions.md, spec.md)`,
|
|
4420
4585
|
`tickets: ${ticketSummary}`,
|
|
4421
4586
|
"Call the runcastle MCP tool `get_feature_context` for the full docs + tickets."
|
|
4422
4587
|
].join(`
|
|
@@ -4621,19 +4786,19 @@ function renderSystemPrompt(feature, kind, waypoint) {
|
|
|
4621
4786
|
"## Feature",
|
|
4622
4787
|
`- Slug: \`${feature.slug}\``,
|
|
4623
4788
|
`- Branch: \`${feature.branch}\``,
|
|
4624
|
-
`- Current phase: **${feature.phase}
|
|
4789
|
+
`- Current phase: **${feature.phase}**`,
|
|
4625
4790
|
"",
|
|
4626
4791
|
"## Pipeline",
|
|
4627
4792
|
"Features move ideation \u2192 spec \u2192 tickets \u2192 implementation \u2192 review \u2192 shipped.",
|
|
4628
|
-
"
|
|
4629
|
-
"
|
|
4630
|
-
"the
|
|
4793
|
+
"Each transition is guarded by a gate; you cross a gate by calling the",
|
|
4794
|
+
"`complete_phase` MCP tool, which runs the gate check server-side and advances",
|
|
4795
|
+
"the feature.",
|
|
4631
4796
|
"",
|
|
4632
4797
|
"## Knowledge (versioned in the target repo)",
|
|
4633
4798
|
`Feature docs live at \`${docs}/\`:`,
|
|
4634
4799
|
`- \`${docs}/brief.md\` \u2014 the seed brief (title + one-liner).`,
|
|
4635
4800
|
`- \`${docs}/decisions.md\` \u2014 decisions you capture while grilling (satisfies gate G1).`,
|
|
4636
|
-
`- \`${docs}/spec.md\` \u2014 the spec
|
|
4801
|
+
`- \`${docs}/spec.md\` \u2014 the spec (satisfies gate G2).`,
|
|
4637
4802
|
"Write these files in THIS working directory (the feature's talk worktree);",
|
|
4638
4803
|
"they are committed to the feature branch automatically at phase boundaries.",
|
|
4639
4804
|
"",
|
|
@@ -4675,7 +4840,7 @@ function renderWaypointPrompt(feature, waypoint) {
|
|
|
4675
4840
|
"## Feature",
|
|
4676
4841
|
`- Slug: \`${feature.slug}\``,
|
|
4677
4842
|
`- Branch: \`${feature.branch}\``,
|
|
4678
|
-
`- Current phase: **${feature.phase}
|
|
4843
|
+
`- Current phase: **${feature.phase}**`,
|
|
4679
4844
|
"",
|
|
4680
4845
|
"## Map + knowledge (versioned in the target repo)",
|
|
4681
4846
|
`Feature docs live at \`${docs}/\`:`,
|
|
@@ -4718,7 +4883,7 @@ function renderConvergePrompt(feature) {
|
|
|
4718
4883
|
"## Feature",
|
|
4719
4884
|
`- Slug: \`${feature.slug}\``,
|
|
4720
4885
|
`- Branch: \`${feature.branch}\``,
|
|
4721
|
-
`- Current phase: **${feature.phase}
|
|
4886
|
+
`- Current phase: **${feature.phase}**`,
|
|
4722
4887
|
"",
|
|
4723
4888
|
"## runcastle MCP tools",
|
|
4724
4889
|
"- `get_feature_context()` \u2014 full feature + phase + docs contents (map + decisions).",
|
|
@@ -4738,6 +4903,17 @@ function renderConvergePrompt(feature) {
|
|
|
4738
4903
|
}
|
|
4739
4904
|
function renderRevisitPrompt(feature) {
|
|
4740
4905
|
const docs = featureDocsRel(feature.slug);
|
|
4906
|
+
const reviewIteration = feature.phase === "review" ? [
|
|
4907
|
+
"## Review iteration",
|
|
4908
|
+
"This feature is at **review**: its tickets were burned and the human has been",
|
|
4909
|
+
"test-driving the branch. Treat this as a fix-ticket interview \u2014 read the latest",
|
|
4910
|
+
"run outcome and every ticket\u2019s state via `get_feature_context`, ask what the test",
|
|
4911
|
+
"drive surfaced (bugs, rough edges, tweaks), then emit fix tickets for that work and",
|
|
4912
|
+
"edit/cancel any stale pending tickets. Do NOT advance the phase: once the cards are",
|
|
4913
|
+
"ready, tell the human to review them and click Burn \u2014 burning from review loops the",
|
|
4914
|
+
"feature back through implementation and returns it here when the run finishes.",
|
|
4915
|
+
""
|
|
4916
|
+
] : [];
|
|
4741
4917
|
return [
|
|
4742
4918
|
`# runcastle \u2014 ${feature.title} (revisit session)`,
|
|
4743
4919
|
"",
|
|
@@ -4750,10 +4926,11 @@ function renderRevisitPrompt(feature) {
|
|
|
4750
4926
|
"into the record, then reconcile the tickets with it. The docs are the",
|
|
4751
4927
|
"artifact \u2014 later phases read them, never the transcripts.",
|
|
4752
4928
|
"",
|
|
4929
|
+
...reviewIteration,
|
|
4753
4930
|
"## Feature",
|
|
4754
4931
|
`- Slug: \`${feature.slug}\``,
|
|
4755
4932
|
`- Branch: \`${feature.branch}\``,
|
|
4756
|
-
`- Current phase: **${feature.phase}
|
|
4933
|
+
`- Current phase: **${feature.phase}**`,
|
|
4757
4934
|
"",
|
|
4758
4935
|
"## Knowledge (versioned in the target repo)",
|
|
4759
4936
|
`Feature docs live at \`${docs}/\`:`,
|
|
@@ -4844,33 +5021,6 @@ var init_artifacts = __esm(() => {
|
|
|
4844
5021
|
];
|
|
4845
5022
|
});
|
|
4846
5023
|
|
|
4847
|
-
// src/pty/end-session.ts
|
|
4848
|
-
function endSession(ctx, sessionId) {
|
|
4849
|
-
const registry = ptyRegistry();
|
|
4850
|
-
const killed = registry.kill(sessionId);
|
|
4851
|
-
registry.remove(sessionId);
|
|
4852
|
-
const session = markSessionEnded(ctx, sessionId);
|
|
4853
|
-
if (session) {
|
|
4854
|
-
releaseForSession(ctx, sessionId);
|
|
4855
|
-
emit(ctx, session.featureId, {
|
|
4856
|
-
type: "session.ended",
|
|
4857
|
-
message: "session ended by user",
|
|
4858
|
-
data: { sessionId, killed }
|
|
4859
|
-
});
|
|
4860
|
-
} else {
|
|
4861
|
-
const existing = getSessionRow(ctx, sessionId);
|
|
4862
|
-
if (!existing)
|
|
4863
|
-
return { sessionId, ok: false, killed };
|
|
4864
|
-
}
|
|
4865
|
-
return { sessionId, ok: true, killed };
|
|
4866
|
-
}
|
|
4867
|
-
var init_end_session = __esm(() => {
|
|
4868
|
-
init_sessions();
|
|
4869
|
-
init_events();
|
|
4870
|
-
init_waypoints();
|
|
4871
|
-
init_registry();
|
|
4872
|
-
});
|
|
4873
|
-
|
|
4874
5024
|
// src/launcher/launcher.ts
|
|
4875
5025
|
import { existsSync as existsSync13 } from "fs";
|
|
4876
5026
|
import { dirname as dirname5, join as join13, resolve as resolve7 } from "path";
|
|
@@ -4974,6 +5124,8 @@ async function launchSession(ctx, input, opts = {}) {
|
|
|
4974
5124
|
kind: input.kind,
|
|
4975
5125
|
worktreePath
|
|
4976
5126
|
});
|
|
5127
|
+
if (input.kickoffLine)
|
|
5128
|
+
setKickoffOverride(session.id, input.kickoffLine);
|
|
4977
5129
|
let waypoint;
|
|
4978
5130
|
let resumeSessionId;
|
|
4979
5131
|
let resumeUnavailableFrom;
|
|
@@ -5233,18 +5385,23 @@ var init_feature = __esm(() => {
|
|
|
5233
5385
|
projectId: z6.string(),
|
|
5234
5386
|
title: z6.string().min(1),
|
|
5235
5387
|
oneLiner: z6.string(),
|
|
5236
|
-
size: FeatureSize,
|
|
5237
|
-
mapped: z6.boolean().optional(),
|
|
5238
5388
|
baseBranch: z6.string().optional()
|
|
5239
5389
|
})).mutation(({ ctx, input }) => createFeature(ctx, input)),
|
|
5240
5390
|
list: publicProcedure.input(z6.object({ projectId: z6.string() })).query(({ ctx, input }) => list(ctx, input.projectId)),
|
|
5241
5391
|
get: publicProcedure.input(z6.object({ id: z6.string() })).query(({ ctx, input }) => getFeatureFull(ctx, input.id)),
|
|
5242
|
-
launchSession: publicProcedure.input(z6.object({
|
|
5392
|
+
launchSession: publicProcedure.input(z6.object({
|
|
5393
|
+
featureId: z6.string(),
|
|
5394
|
+
kind: SessionKind,
|
|
5395
|
+
kickoffLine: z6.string().min(1).optional()
|
|
5396
|
+
})).mutation(({ ctx, input }) => launchSession(ctx, input)),
|
|
5243
5397
|
workWaypoint: publicProcedure.input(z6.object({ featureId: z6.string(), waypointId: z6.string() })).mutation(({ ctx, input }) => workWaypoint(ctx, input)),
|
|
5244
5398
|
converge: publicProcedure.input(z6.object({ featureId: z6.string(), overrideReason: z6.string().min(1).optional() })).mutation(({ ctx, input }) => converge(ctx, { featureId: input.featureId, overrideReason: input.overrideReason })),
|
|
5245
5399
|
endSession: publicProcedure.input(z6.object({ sessionId: z6.string() })).mutation(({ ctx, input }) => endSession(ctx, input.sessionId)),
|
|
5246
5400
|
advance: publicProcedure.input(z6.object({ featureId: z6.string() })).mutation(({ ctx, input }) => advance(ctx, input.featureId)),
|
|
5247
5401
|
overrideGate: publicProcedure.input(z6.object({ featureId: z6.string(), gate: gateId, reason: z6.string().min(1) })).mutation(({ ctx, input }) => overrideGate(ctx, input.featureId, input.gate, input.reason)),
|
|
5402
|
+
archive: publicProcedure.input(z6.object({ featureId: z6.string() })).mutation(({ ctx, input }) => archiveFeature(ctx, input.featureId)),
|
|
5403
|
+
unarchive: publicProcedure.input(z6.object({ featureId: z6.string() })).mutation(({ ctx, input }) => unarchiveFeature(ctx, input.featureId)),
|
|
5404
|
+
delete: publicProcedure.input(z6.object({ featureId: z6.string() })).mutation(({ ctx, input }) => deleteFeature(ctx, input.featureId)),
|
|
5248
5405
|
burn: publicProcedure.input(z6.object({ featureId: z6.string(), model: z6.string().min(1).optional() })).mutation(({ ctx, input }) => burn(ctx, input.featureId, { modelOverride: input.model })),
|
|
5249
5406
|
testDrive: publicProcedure.input(z6.object({ featureId: z6.string(), action: z6.enum(["start", "stop"]) })).mutation(async ({ ctx, input }) => {
|
|
5250
5407
|
const feature = getFeatureRow(ctx, input.featureId);
|
|
@@ -5266,10 +5423,10 @@ var init_feature = __esm(() => {
|
|
|
5266
5423
|
emit(ctx, input.featureId, {
|
|
5267
5424
|
type: "merge.conflict",
|
|
5268
5425
|
message: "merge conflict \u2014 resolve and retry",
|
|
5269
|
-
data: { conflict: res.conflict }
|
|
5426
|
+
data: { conflict: res.conflict, base: res.target, files: res.files ?? [] }
|
|
5270
5427
|
});
|
|
5271
5428
|
}
|
|
5272
|
-
return { ok: res.ok, conflict: res.conflict };
|
|
5429
|
+
return { ok: res.ok, conflict: res.conflict, base: res.target, files: res.files ?? [] };
|
|
5273
5430
|
})
|
|
5274
5431
|
});
|
|
5275
5432
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ALTER TABLE `features` DROP COLUMN `size`;
|