runcastle 1.5.4 → 1.5.5
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 +376 -245
- package/drizzle/0054_careless_stellaris.sql +3 -0
- package/drizzle/meta/0054_snapshot.json +1268 -0
- package/drizzle/meta/_journal.json +7 -0
- package/index.js +376 -245
- package/package.json +1 -1
- package/skills/packs/runcastle/skills/feature/SKILL.md +2 -2
- package/skills/packs/runcastle/skills/feature/references/design.md +29 -3
- package/skills/packs/runcastle/skills/feature/references/grilling.md +6 -4
- package/skills/packs/runcastle/skills/feature/references/planning-alone.md +59 -9
- package/skills/packs/runcastle/skills/feature/references/rounds.md +2 -0
- package/skills/packs/runcastle/skills/project/SKILL.md +92 -51
- package/skills/packs/runcastle/skills/project/references/health-sweeps.md +6 -5
- package/skills/packs/runcastle/skills/project/references/triage.md +17 -12
- package/web/assets/{index-CT9k1kz0.css → index-B1MyomR6.css} +1 -1
- package/web/assets/{index-DVR7VbWW.js → index-CPgKzYyE.js} +50 -50
- package/web/index.html +2 -2
package/index.js
CHANGED
|
@@ -267,7 +267,6 @@ var init_schemas = __esm(() => {
|
|
|
267
267
|
"resolve-conflict",
|
|
268
268
|
"start-lap",
|
|
269
269
|
"self-plan",
|
|
270
|
-
"orchestrate",
|
|
271
270
|
"relay",
|
|
272
271
|
"lead-wake",
|
|
273
272
|
"scenario-setup"
|
|
@@ -407,7 +406,6 @@ var init_schemas = __esm(() => {
|
|
|
407
406
|
driveInstructions: z3.string().optional(),
|
|
408
407
|
scenarioRunner: z3.string().optional(),
|
|
409
408
|
docsCommitPrefix: z3.string().optional(),
|
|
410
|
-
orchestrator: z3.boolean().optional(),
|
|
411
409
|
closedAt: z3.number().optional()
|
|
412
410
|
});
|
|
413
411
|
ProjectFinding = z3.object({
|
|
@@ -431,7 +429,6 @@ var init_schemas = __esm(() => {
|
|
|
431
429
|
baseBranch: z3.string().optional(),
|
|
432
430
|
status: FeatureStatus,
|
|
433
431
|
createdAt: z3.number(),
|
|
434
|
-
orchestrated: z3.boolean().default(false),
|
|
435
432
|
upNext: z3.boolean().default(false),
|
|
436
433
|
stoppedAt: z3.number().nullable().default(null),
|
|
437
434
|
waitingReason: z3.string().nullable().default(null),
|
|
@@ -875,8 +872,7 @@ var init_db_schema = __esm(() => {
|
|
|
875
872
|
driveSetupCommand: text("drive_setup_command"),
|
|
876
873
|
driveStopCommand: text("drive_stop_command"),
|
|
877
874
|
scenarioRunner: text("scenario_runner"),
|
|
878
|
-
docsCommitPrefix: text("docs_commit_prefix")
|
|
879
|
-
orchestrator: integer("orchestrator", { mode: "boolean" })
|
|
875
|
+
docsCommitPrefix: text("docs_commit_prefix")
|
|
880
876
|
});
|
|
881
877
|
projectFindings = sqliteTable("project_findings", {
|
|
882
878
|
projectId: text("project_id").notNull(),
|
|
@@ -899,7 +895,6 @@ var init_db_schema = __esm(() => {
|
|
|
899
895
|
baseBranch: text("base_branch"),
|
|
900
896
|
status: text("status").notNull().$type(),
|
|
901
897
|
createdAt: integer("created_at").notNull(),
|
|
902
|
-
orchestrated: integer("orchestrated", { mode: "boolean" }).notNull().default(false),
|
|
903
898
|
upNext: integer("up_next", { mode: "boolean" }).notNull().default(false),
|
|
904
899
|
stoppedAt: integer("stopped_at"),
|
|
905
900
|
waitingReason: text("waiting_reason"),
|
|
@@ -2026,6 +2021,15 @@ async function tearDownEntry(entry) {
|
|
|
2026
2021
|
|
|
2027
2022
|
class PtyRegistry {
|
|
2028
2023
|
entries = new Map;
|
|
2024
|
+
sinksListeners = new Set;
|
|
2025
|
+
onSinksChanged(listener) {
|
|
2026
|
+
this.sinksListeners.add(listener);
|
|
2027
|
+
return () => this.sinksListeners.delete(listener);
|
|
2028
|
+
}
|
|
2029
|
+
notifySinks(entry) {
|
|
2030
|
+
for (const listener of this.sinksListeners)
|
|
2031
|
+
listener(entry.sessionId, entry.sinks.size);
|
|
2032
|
+
}
|
|
2029
2033
|
create(input) {
|
|
2030
2034
|
const existing = this.entries.get(input.sessionId);
|
|
2031
2035
|
if (existing && !existing.exited) {
|
|
@@ -2072,10 +2076,14 @@ class PtyRegistry {
|
|
|
2072
2076
|
sink.sendData(replay);
|
|
2073
2077
|
sink.sendControl(entry.exited ? { t: "status", status: "ended", exitCode: entry.exitCode ?? 0 } : { t: "status", status: "live" });
|
|
2074
2078
|
entry.sinks.add(sink);
|
|
2079
|
+
this.notifySinks(entry);
|
|
2075
2080
|
return true;
|
|
2076
2081
|
}
|
|
2077
2082
|
detach(sessionId, sink) {
|
|
2078
|
-
this.entries.get(sessionId)
|
|
2083
|
+
const entry = this.entries.get(sessionId);
|
|
2084
|
+
if (!entry?.sinks.delete(sink))
|
|
2085
|
+
return;
|
|
2086
|
+
this.notifySinks(entry);
|
|
2079
2087
|
}
|
|
2080
2088
|
kill(sessionId) {
|
|
2081
2089
|
const entry = this.entries.get(sessionId);
|
|
@@ -2548,7 +2556,6 @@ function rowToFeature(row) {
|
|
|
2548
2556
|
baseBranch: row.baseBranch ?? undefined,
|
|
2549
2557
|
status: row.status,
|
|
2550
2558
|
createdAt: row.createdAt,
|
|
2551
|
-
orchestrated: row.orchestrated,
|
|
2552
2559
|
upNext: row.upNext,
|
|
2553
2560
|
stoppedAt: row.stoppedAt,
|
|
2554
2561
|
waitingReason: row.waitingReason,
|
|
@@ -2574,7 +2581,6 @@ function rowToProject(row) {
|
|
|
2574
2581
|
driveInstructions: row.driveInstructions ?? undefined,
|
|
2575
2582
|
scenarioRunner: row.scenarioRunner ?? undefined,
|
|
2576
2583
|
docsCommitPrefix: row.docsCommitPrefix ?? undefined,
|
|
2577
|
-
orchestrator: row.orchestrator ?? undefined,
|
|
2578
2584
|
closedAt: row.closedAt ?? undefined
|
|
2579
2585
|
});
|
|
2580
2586
|
}
|
|
@@ -2670,16 +2676,6 @@ function hasActiveRun(ctx, featureId) {
|
|
|
2670
2676
|
const row = ctx.db.select({ id: runs.id }).from(runs).where(and2(eq2(runs.featureId, featureId), eq2(runs.status, "running"))).limit(1).get();
|
|
2671
2677
|
return !!row;
|
|
2672
2678
|
}
|
|
2673
|
-
function markOrchestrated(ctx, featureId, how) {
|
|
2674
|
-
const current = getFeatureRow(ctx, featureId);
|
|
2675
|
-
ctx.db.update(features).set({ orchestrated: true }).where(eq2(features.id, featureId)).run();
|
|
2676
|
-
emit(ctx, featureId, {
|
|
2677
|
-
type: "feature.orchestrated",
|
|
2678
|
-
message: how === "created" ? "created by the orchestrator" : "handed to the orchestrator",
|
|
2679
|
-
data: { how }
|
|
2680
|
-
});
|
|
2681
|
-
return { ...current, orchestrated: true };
|
|
2682
|
-
}
|
|
2683
2679
|
function setFeatureStatus(ctx, featureId, status) {
|
|
2684
2680
|
const current = getFeatureRow(ctx, featureId);
|
|
2685
2681
|
ctx.db.update(features).set({ status }).where(eq2(features.id, featureId)).run();
|
|
@@ -5750,7 +5746,7 @@ function chatKickoffFor(runtime) {
|
|
|
5750
5746
|
return `Proceed with your task: invoke ${skillRef(runtime, FEATURE_SKILL)} and continue this feature.`;
|
|
5751
5747
|
}
|
|
5752
5748
|
function selfPlanKickoffFor(runtime) {
|
|
5753
|
-
return "Proceed with your task: no human is present and nobody will reply to you. Invoke " + `${skillRef(runtime, FEATURE_SKILL)} and follow its \`${PLANNING_ALONE}\` to plan this feature ` + "by yourself in this one run: plan it, do not build it, and end by asking for the human's yes.";
|
|
5749
|
+
return "Proceed with your task: no human is present and nobody will reply to you. Invoke " + `${skillRef(runtime, FEATURE_SKILL)} and follow its \`${PLANNING_ALONE}\` to plan this feature ` + "by yourself in this one run: plan it, design its screens where it adds or reshapes them, " + "do not build it, and end by asking for the human's yes.";
|
|
5754
5750
|
}
|
|
5755
5751
|
function relayKickoffFor(runtime, message) {
|
|
5756
5752
|
return "Proceed with your task: no human is present and nobody will reply to you. The message below " + `is from the project's orchestrator, not the human. Invoke ${skillRef(runtime, FEATURE_SKILL)} ` + "and apply it by its change-handling rules: every decision it changes keeps or gains " + "its **Assumed.** line and adds **Changed by the orchestrator:** <why>, unless the message " + "says it relays the human's own answer, in which case remove both marks from that decision. " + "Redraw the preview, reconcile the spec and the tickets, and do not wait for anyone. " + `The orchestrator's message: ${message}`;
|
|
@@ -5798,7 +5794,7 @@ function leadCodeRule() {
|
|
|
5798
5794
|
function purposeLine(purpose, runtime) {
|
|
5799
5795
|
switch (purpose) {
|
|
5800
5796
|
case "self-plan":
|
|
5801
|
-
return "This is a self-planning run: plan this feature by yourself and do not build it, by the " + `${skillRef(runtime, FEATURE_SKILL)} skill's \`${PLANNING_ALONE}\`, ending on a request for the human's yes.`;
|
|
5797
|
+
return "This is a self-planning run: plan this feature by yourself, designing its screens where it adds or " + "reshapes them, and do not build it, by the " + `${skillRef(runtime, FEATURE_SKILL)} skill's \`${PLANNING_ALONE}\`, ending on a request for the human's yes.`;
|
|
5802
5798
|
case "relay":
|
|
5803
5799
|
return "This is a relay run: the project's orchestrator, not the human, sent this conversation a " + "message \u2014 apply it by the skill's change-handling rules, marking any decision it changes as the orchestrator's.";
|
|
5804
5800
|
case "resolve-conflict":
|
|
@@ -6234,20 +6230,19 @@ function renderScenarioPrompt(brief) {
|
|
|
6234
6230
|
function renderOrchestratingSection(catchUp, runtime) {
|
|
6235
6231
|
return [
|
|
6236
6232
|
`## ${ORCHESTRATING}`,
|
|
6237
|
-
"
|
|
6238
|
-
"
|
|
6239
|
-
`
|
|
6240
|
-
|
|
6241
|
-
"replaces it. In this mode it is you, not the human, who decides what to work on next.",
|
|
6233
|
+
"**You are this project's orchestrator.** You can check on, message, plan, start and merge",
|
|
6234
|
+
"any of its features, and you act on what the human asked or agreed, as far as they asked.",
|
|
6235
|
+
`Read and follow the "${ORCHESTRATING}" section of the \`${skillRef(runtime, "project")}\``,
|
|
6236
|
+
"skill \u2014 it is how you judge every step; nothing here replaces it.",
|
|
6242
6237
|
"",
|
|
6243
6238
|
"You run without permission prompts: you edit the repo and run commands without asking.",
|
|
6244
6239
|
"",
|
|
6245
|
-
...catchUp.length > 0 ? ["### Since you were last live", ...catchUp.map((line) => `- ${line}`)] : ["Nothing has happened on
|
|
6240
|
+
...catchUp.length > 0 ? ["### Since you were last live", ...catchUp.map((line) => `- ${line}`)] : ["Nothing has happened on the features you follow since you were last live."],
|
|
6246
6241
|
""
|
|
6247
6242
|
];
|
|
6248
6243
|
}
|
|
6249
6244
|
function renderProjectPrompt(brief, runtime = DEFAULT_RUNTIME) {
|
|
6250
|
-
const { project, branch, worktreePath, base, openNotes,
|
|
6245
|
+
const { project, branch, worktreePath, base, openNotes, catchUp } = brief;
|
|
6251
6246
|
return [
|
|
6252
6247
|
`# runcastle \u2014 ${project.name} (project session)`,
|
|
6253
6248
|
"",
|
|
@@ -6293,7 +6288,7 @@ function renderProjectPrompt(brief, runtime = DEFAULT_RUNTIME) {
|
|
|
6293
6288
|
"The runcastle skill pack also carries the feature sessions' entry skills; they are",
|
|
6294
6289
|
"denied to this session by settings. Yours is the one named below.",
|
|
6295
6290
|
"",
|
|
6296
|
-
...
|
|
6291
|
+
...renderOrchestratingSection(catchUp ?? [], runtime),
|
|
6297
6292
|
"## Your task",
|
|
6298
6293
|
`Invoke the \`${skillRef(runtime, "project")}\` skill, then open by asking the human what they brought.`,
|
|
6299
6294
|
"Do not explore the project first: orienting before you know the ask spends their wait on",
|
|
@@ -6334,7 +6329,7 @@ function renderSettings(hookClient, kind, skillPackDir, purpose, headless = fals
|
|
|
6334
6329
|
command: `bun run "${hookClient}" ${event}`,
|
|
6335
6330
|
timeout: event === "session-start" ? 20 : event === "session-end" ? 10 : 5
|
|
6336
6331
|
});
|
|
6337
|
-
const waiter =
|
|
6332
|
+
const waiter = kind === "project" || kind === "chat" && !headless ? [
|
|
6338
6333
|
{
|
|
6339
6334
|
type: "command",
|
|
6340
6335
|
command: `bun run "${hookClient}" ${WAIT_MESSAGES_MODE}`,
|
|
@@ -8386,7 +8381,7 @@ function paneTail(paneId) {
|
|
|
8386
8381
|
const entry = ptyRegistry().get(paneId);
|
|
8387
8382
|
return entry ? exitTail(entry.buffer.snapshot()) : "";
|
|
8388
8383
|
}
|
|
8389
|
-
async function startDesignServer(ctx, featureId, command) {
|
|
8384
|
+
async function startDesignServer(ctx, featureId, command, opts = {}) {
|
|
8390
8385
|
const worktree = talkWorktreeFor(ctx, featureId);
|
|
8391
8386
|
const given = command?.trim() || undefined;
|
|
8392
8387
|
const resolved = given ?? recordedCommand(worktree);
|
|
@@ -8399,11 +8394,13 @@ async function startDesignServer(ctx, featureId, command) {
|
|
|
8399
8394
|
if (existing) {
|
|
8400
8395
|
if (existing.command === resolved && devPaneLive(existing.paneId)) {
|
|
8401
8396
|
existing.lastHeartbeat = now();
|
|
8397
|
+
if (opts.sessionId === undefined)
|
|
8398
|
+
existing.startedBy = undefined;
|
|
8402
8399
|
return existing.ready;
|
|
8403
8400
|
}
|
|
8404
8401
|
await stopDesignServer(ctx, featureId, "stopped");
|
|
8405
8402
|
}
|
|
8406
|
-
return spawnDesignServer(ctx, featureId, worktree, resolved);
|
|
8403
|
+
return spawnDesignServer(ctx, featureId, worktree, resolved, opts.sessionId);
|
|
8407
8404
|
}
|
|
8408
8405
|
async function restartDesignServer(ctx, featureId) {
|
|
8409
8406
|
const worktree = talkWorktreeFor(ctx, featureId);
|
|
@@ -8428,7 +8425,7 @@ function withPort(url, port) {
|
|
|
8428
8425
|
return url;
|
|
8429
8426
|
}
|
|
8430
8427
|
}
|
|
8431
|
-
async function spawnDesignServer(ctx, featureId, worktree, command) {
|
|
8428
|
+
async function spawnDesignServer(ctx, featureId, worktree, command, startedBy) {
|
|
8432
8429
|
const port = await pickPort(featureId);
|
|
8433
8430
|
const paneId = designPaneId(featureId);
|
|
8434
8431
|
let settle;
|
|
@@ -8441,6 +8438,7 @@ async function spawnDesignServer(ctx, featureId, worktree, command) {
|
|
|
8441
8438
|
paneId,
|
|
8442
8439
|
port,
|
|
8443
8440
|
command,
|
|
8441
|
+
...startedBy !== undefined ? { startedBy } : {},
|
|
8444
8442
|
lastHeartbeat: now(),
|
|
8445
8443
|
ready,
|
|
8446
8444
|
abort: (reason) => {
|
|
@@ -8517,6 +8515,7 @@ function heartbeatDesignServer(featureId) {
|
|
|
8517
8515
|
if (!server)
|
|
8518
8516
|
return false;
|
|
8519
8517
|
server.lastHeartbeat = now();
|
|
8518
|
+
server.startedBy = undefined;
|
|
8520
8519
|
return true;
|
|
8521
8520
|
}
|
|
8522
8521
|
function rememberDesignPath(featureId, path) {
|
|
@@ -8543,8 +8542,16 @@ function designServerStatus(ctx, featureId) {
|
|
|
8543
8542
|
...remembered
|
|
8544
8543
|
};
|
|
8545
8544
|
}
|
|
8545
|
+
function sessionEndStopsServer(session, startedBy) {
|
|
8546
|
+
if (session.kind !== "chat" || !session.featureId)
|
|
8547
|
+
return false;
|
|
8548
|
+
return !session.headless || startedBy === session.id;
|
|
8549
|
+
}
|
|
8546
8550
|
function stopDesignServerForSession(ctx, session) {
|
|
8547
|
-
if (
|
|
8551
|
+
if (!session.featureId)
|
|
8552
|
+
return;
|
|
8553
|
+
const server = servers.get(session.featureId);
|
|
8554
|
+
if (!server || !sessionEndStopsServer(session, server.startedBy))
|
|
8548
8555
|
return;
|
|
8549
8556
|
stopDesignServer(ctx, session.featureId, "session-ended").catch(() => {});
|
|
8550
8557
|
}
|
|
@@ -9264,7 +9271,7 @@ var init_tickets = __esm(() => {
|
|
|
9264
9271
|
});
|
|
9265
9272
|
|
|
9266
9273
|
// src/services/lead-status.ts
|
|
9267
|
-
import { eq as eq8 } from "drizzle-orm";
|
|
9274
|
+
import { and as and7, desc as desc4, eq as eq8, sql as sql3 } from "drizzle-orm";
|
|
9268
9275
|
function setWaiting(ctx, featureId, why) {
|
|
9269
9276
|
const reason = why?.trim() || null;
|
|
9270
9277
|
ctx.db.update(features).set({ waitingReason: reason }).where(eq8(features.id, featureId)).run();
|
|
@@ -9274,24 +9281,28 @@ function setWaiting(ctx, featureId, why) {
|
|
|
9274
9281
|
data: { why: reason }
|
|
9275
9282
|
});
|
|
9276
9283
|
}
|
|
9277
|
-
function markHumanMessage(ctx, featureId) {
|
|
9284
|
+
function markHumanMessage(ctx, featureId, opts = {}) {
|
|
9278
9285
|
const at = Date.now();
|
|
9279
9286
|
ctx.db.update(features).set({ lastHumanMessageAt: at, stoppedAt: null, waitingReason: null }).where(eq8(features.id, featureId)).run();
|
|
9280
9287
|
emit(ctx, featureId, {
|
|
9281
9288
|
type: "lead.human_message",
|
|
9282
9289
|
message: "a human message reached the agent",
|
|
9283
|
-
data: { at }
|
|
9290
|
+
data: { at, ...opts.typed ? { typed: true } : {} }
|
|
9284
9291
|
});
|
|
9285
9292
|
}
|
|
9293
|
+
function humanTypedAt(ctx, featureId) {
|
|
9294
|
+
const row = ctx.db.select({ ts: events.ts }).from(events).where(and7(eq8(events.featureId, featureId), eq8(events.type, "lead.human_message"), sql3`json_extract(${events.data}, '$.typed') = 1`)).orderBy(desc4(events.id)).limit(1).get();
|
|
9295
|
+
return row?.ts ?? null;
|
|
9296
|
+
}
|
|
9286
9297
|
var init_lead_status = __esm(() => {
|
|
9287
9298
|
init_schema();
|
|
9288
9299
|
init_events();
|
|
9289
9300
|
});
|
|
9290
9301
|
|
|
9291
9302
|
// src/services/runs.ts
|
|
9292
|
-
import { and as
|
|
9303
|
+
import { and as and8, asc as asc4, eq as eq9, inArray as inArray6, isNotNull as isNotNull2 } from "drizzle-orm";
|
|
9293
9304
|
function runTicketIds(ctx, runId) {
|
|
9294
|
-
const rows = ctx.db.select({ ticketId: events.ticketId }).from(events).where(
|
|
9305
|
+
const rows = ctx.db.select({ ticketId: events.ticketId }).from(events).where(and8(eq9(events.runId, runId), isNotNull2(events.ticketId))).orderBy(asc4(events.id)).all();
|
|
9295
9306
|
const seen = new Set;
|
|
9296
9307
|
for (const row of rows)
|
|
9297
9308
|
if (row.ticketId)
|
|
@@ -9301,7 +9312,7 @@ function runTicketIds(ctx, runId) {
|
|
|
9301
9312
|
function runClaimedTicketIds(ctx, runId) {
|
|
9302
9313
|
const claimed = new Set(runTicketIds(ctx, runId));
|
|
9303
9314
|
const admittedSeqs = new Set;
|
|
9304
|
-
const rows = ctx.db.select({ type: events.type, data: events.data }).from(events).where(
|
|
9315
|
+
const rows = ctx.db.select({ type: events.type, data: events.data }).from(events).where(and8(eq9(events.runId, runId), inArray6(events.type, ["run.started", "burn.admitted"]))).all();
|
|
9305
9316
|
for (const row of rows) {
|
|
9306
9317
|
const data = row.data ?? {};
|
|
9307
9318
|
if (row.type === "run.started" && Array.isArray(data.ticketIds)) {
|
|
@@ -9680,8 +9691,7 @@ function projectOverrides(ctx, projectId) {
|
|
|
9680
9691
|
driveStopCommand: projects.driveStopCommand,
|
|
9681
9692
|
driveInstructions: projects.driveInstructions,
|
|
9682
9693
|
scenarioRunner: projects.scenarioRunner,
|
|
9683
|
-
docsCommitPrefix: projects.docsCommitPrefix
|
|
9684
|
-
orchestrator: projects.orchestrator
|
|
9694
|
+
docsCommitPrefix: projects.docsCommitPrefix
|
|
9685
9695
|
}).from(projects).where(eq11(projects.id, projectId)).get();
|
|
9686
9696
|
return {
|
|
9687
9697
|
model: row?.model ?? null,
|
|
@@ -9697,30 +9707,29 @@ function projectOverrides(ctx, projectId) {
|
|
|
9697
9707
|
driveStopCommand: row?.driveStopCommand ?? null,
|
|
9698
9708
|
driveInstructions: row?.driveInstructions ?? null,
|
|
9699
9709
|
scenarioRunner: row?.scenarioRunner ?? null,
|
|
9700
|
-
docsCommitPrefix: row?.docsCommitPrefix ?? null
|
|
9701
|
-
orchestrator: row?.orchestrator ?? null
|
|
9710
|
+
docsCommitPrefix: row?.docsCommitPrefix ?? null
|
|
9702
9711
|
};
|
|
9703
9712
|
}
|
|
9704
|
-
function resolveField(
|
|
9713
|
+
function resolveField(desc5, layers) {
|
|
9705
9714
|
const scoped = layers.overrides !== null;
|
|
9706
|
-
const scope = scoped &&
|
|
9707
|
-
const base = { key:
|
|
9708
|
-
if (
|
|
9709
|
-
const raw = layers.env[
|
|
9715
|
+
const scope = scoped && desc5.projectColumn ? "project" : "global";
|
|
9716
|
+
const base = { key: desc5.key, restartRequired: desc5.restartRequired, scope };
|
|
9717
|
+
if (desc5.envVar) {
|
|
9718
|
+
const raw = layers.env[desc5.envVar];
|
|
9710
9719
|
if (raw !== undefined && raw !== "") {
|
|
9711
|
-
return { ...base, value:
|
|
9720
|
+
return { ...base, value: desc5.parseEnv(raw), source: "env", editable: false };
|
|
9712
9721
|
}
|
|
9713
9722
|
}
|
|
9714
|
-
if (layers.overrides &&
|
|
9715
|
-
const ov = layers.overrides[
|
|
9723
|
+
if (layers.overrides && desc5.projectColumn) {
|
|
9724
|
+
const ov = layers.overrides[desc5.projectColumn];
|
|
9716
9725
|
if (ov !== null && ov !== "") {
|
|
9717
9726
|
return { ...base, value: ov, source: "project", editable: true };
|
|
9718
9727
|
}
|
|
9719
9728
|
}
|
|
9720
|
-
if (
|
|
9721
|
-
return { ...base, value: layers.fileRaw[
|
|
9729
|
+
if (desc5.configKey && Object.prototype.hasOwnProperty.call(layers.fileRaw, desc5.configKey)) {
|
|
9730
|
+
return { ...base, value: layers.fileRaw[desc5.configKey], source: "file", editable: true };
|
|
9722
9731
|
}
|
|
9723
|
-
const value =
|
|
9732
|
+
const value = desc5.configKey ? layers.defaults[desc5.configKey] ?? null : desc5.defaultValue ?? null;
|
|
9724
9733
|
return { ...base, value, source: "default", editable: true };
|
|
9725
9734
|
}
|
|
9726
9735
|
function getSettings(ctx, projectId, io = {}) {
|
|
@@ -9736,78 +9745,75 @@ function getSettings(ctx, projectId, io = {}) {
|
|
|
9736
9745
|
discovery: discoverySnapshot(ctx)
|
|
9737
9746
|
};
|
|
9738
9747
|
}
|
|
9739
|
-
function orchestratorOn(ctx, projectId) {
|
|
9740
|
-
return projectOverrides(ctx, projectId).orchestrator === true;
|
|
9741
|
-
}
|
|
9742
9748
|
function updateSettings(ctx, input, io = {}) {
|
|
9743
9749
|
const env = io.env ?? process.env;
|
|
9744
9750
|
const configFile = io.configFile ?? configPath();
|
|
9745
9751
|
if (input.key.startsWith("stepModels.")) {
|
|
9746
9752
|
return updateStepModel(ctx, input, configFile, io);
|
|
9747
9753
|
}
|
|
9748
|
-
const
|
|
9749
|
-
if (!
|
|
9754
|
+
const desc5 = DESCRIPTORS.find((d) => d.key === input.key);
|
|
9755
|
+
if (!desc5)
|
|
9750
9756
|
throw new InvalidInputError(`unknown setting: ${input.key}`);
|
|
9751
|
-
if (
|
|
9752
|
-
const raw = env[
|
|
9757
|
+
if (desc5.envVar) {
|
|
9758
|
+
const raw = env[desc5.envVar];
|
|
9753
9759
|
if (raw !== undefined && raw !== "") {
|
|
9754
|
-
throw new InvalidInputError(`${
|
|
9760
|
+
throw new InvalidInputError(`${desc5.key} is set by environment variable ${desc5.envVar}; unset it to edit`);
|
|
9755
9761
|
}
|
|
9756
9762
|
}
|
|
9757
|
-
const toProject = input.projectId !== undefined &&
|
|
9763
|
+
const toProject = input.projectId !== undefined && desc5.projectColumn !== undefined;
|
|
9758
9764
|
if (input.value === null) {
|
|
9759
9765
|
if (!toProject)
|
|
9760
|
-
return clearGlobal(ctx,
|
|
9766
|
+
return clearGlobal(ctx, desc5, input, configFile, io);
|
|
9761
9767
|
const project = requireProjectById(ctx, input.projectId);
|
|
9762
|
-
ctx.db.update(projects).set({ [
|
|
9763
|
-
recordHuman(ctx, project.id,
|
|
9768
|
+
ctx.db.update(projects).set({ [desc5.projectColumn]: null }).where(eq11(projects.id, project.id)).run();
|
|
9769
|
+
recordHuman(ctx, project.id, desc5.key, null);
|
|
9764
9770
|
emitProject(ctx, project.id, {
|
|
9765
9771
|
type: "settings.updated",
|
|
9766
|
-
message: `${
|
|
9767
|
-
data: { key:
|
|
9772
|
+
message: `${desc5.key} override cleared`,
|
|
9773
|
+
data: { key: desc5.key, scope: "project", value: null }
|
|
9768
9774
|
});
|
|
9769
|
-
return field(getSettings(ctx, input.projectId, io),
|
|
9775
|
+
return field(getSettings(ctx, input.projectId, io), desc5.key);
|
|
9770
9776
|
}
|
|
9771
|
-
const parsed =
|
|
9777
|
+
const parsed = desc5.valueSchema.safeParse(input.value);
|
|
9772
9778
|
if (!parsed.success) {
|
|
9773
|
-
throw new InvalidInputError(`invalid value for ${
|
|
9779
|
+
throw new InvalidInputError(`invalid value for ${desc5.key}: ${parsed.error.issues[0]?.message}`);
|
|
9774
9780
|
}
|
|
9775
9781
|
const value = parsed.data;
|
|
9776
9782
|
if (toProject) {
|
|
9777
9783
|
const project = requireProjectById(ctx, input.projectId);
|
|
9778
|
-
ctx.db.update(projects).set({ [
|
|
9779
|
-
recordHuman(ctx, project.id,
|
|
9784
|
+
ctx.db.update(projects).set({ [desc5.projectColumn]: String(value) }).where(eq11(projects.id, project.id)).run();
|
|
9785
|
+
recordHuman(ctx, project.id, desc5.key, String(value));
|
|
9780
9786
|
emitProject(ctx, project.id, {
|
|
9781
9787
|
type: "settings.updated",
|
|
9782
|
-
message: `${
|
|
9783
|
-
data: { key:
|
|
9788
|
+
message: `${desc5.key} override set to ${String(value)}`,
|
|
9789
|
+
data: { key: desc5.key, scope: "project", value }
|
|
9784
9790
|
});
|
|
9785
|
-
return field(getSettings(ctx, input.projectId, io),
|
|
9791
|
+
return field(getSettings(ctx, input.projectId, io), desc5.key);
|
|
9786
9792
|
}
|
|
9787
|
-
if (!
|
|
9788
|
-
throw new InvalidInputError(`${
|
|
9793
|
+
if (!desc5.configKey) {
|
|
9794
|
+
throw new InvalidInputError(`${desc5.key} is a per-project setting; provide a projectId`);
|
|
9789
9795
|
}
|
|
9790
|
-
writeGlobal(configFile,
|
|
9791
|
-
ctx.config[
|
|
9796
|
+
writeGlobal(configFile, desc5.configKey, value);
|
|
9797
|
+
ctx.config[desc5.configKey] = value;
|
|
9792
9798
|
emitProject(ctx, GLOBAL_EVENT_KEY, {
|
|
9793
9799
|
type: "settings.updated",
|
|
9794
|
-
message: `${
|
|
9795
|
-
data: { key:
|
|
9800
|
+
message: `${desc5.key} set to ${describeValue(value)}`,
|
|
9801
|
+
data: { key: desc5.key, scope: "global", value }
|
|
9796
9802
|
});
|
|
9797
|
-
return field(getSettings(ctx, input.projectId, io),
|
|
9803
|
+
return field(getSettings(ctx, input.projectId, io), desc5.key);
|
|
9798
9804
|
}
|
|
9799
|
-
function clearGlobal(ctx,
|
|
9800
|
-
if (!
|
|
9801
|
-
throw new InvalidInputError(`${
|
|
9805
|
+
function clearGlobal(ctx, desc5, input, configFile, io) {
|
|
9806
|
+
if (!desc5.configKey || !GLOBAL_CLEARABLE_KEYS.has(desc5.key)) {
|
|
9807
|
+
throw new InvalidInputError(`${desc5.key} cannot be cleared`);
|
|
9802
9808
|
}
|
|
9803
|
-
removeGlobal(configFile,
|
|
9804
|
-
delete ctx.config[
|
|
9809
|
+
removeGlobal(configFile, desc5.configKey);
|
|
9810
|
+
delete ctx.config[desc5.configKey];
|
|
9805
9811
|
emitProject(ctx, GLOBAL_EVENT_KEY, {
|
|
9806
9812
|
type: "settings.updated",
|
|
9807
|
-
message: `${
|
|
9808
|
-
data: { key:
|
|
9813
|
+
message: `${desc5.key} cleared`,
|
|
9814
|
+
data: { key: desc5.key, scope: "global", value: null }
|
|
9809
9815
|
});
|
|
9810
|
-
return field(getSettings(ctx, input.projectId, io),
|
|
9816
|
+
return field(getSettings(ctx, input.projectId, io), desc5.key);
|
|
9811
9817
|
}
|
|
9812
9818
|
function describeValue(value) {
|
|
9813
9819
|
return typeof value === "object" && value !== null ? JSON.stringify(value) : String(value);
|
|
@@ -10069,14 +10075,6 @@ var init_settings = __esm(() => {
|
|
|
10069
10075
|
restartRequired: false,
|
|
10070
10076
|
valueSchema: z5.string().min(1),
|
|
10071
10077
|
parseEnv: idEnv
|
|
10072
|
-
},
|
|
10073
|
-
{
|
|
10074
|
-
key: "orchestrator",
|
|
10075
|
-
projectColumn: "orchestrator",
|
|
10076
|
-
restartRequired: false,
|
|
10077
|
-
valueSchema: z5.boolean(),
|
|
10078
|
-
parseEnv: idEnv,
|
|
10079
|
-
defaultValue: false
|
|
10080
10078
|
}
|
|
10081
10079
|
];
|
|
10082
10080
|
DEFAULTS = RuncastleConfig.parse({});
|
|
@@ -10323,11 +10321,11 @@ var require$1, __create, __defProp2, __getOwnPropDesc, __getOwnPropNames, __getP
|
|
|
10323
10321
|
}, __export2 = (target, all) => {
|
|
10324
10322
|
for (var name in all)
|
|
10325
10323
|
__defProp2(target, name, { get: all[name], enumerable: true });
|
|
10326
|
-
}, __copyProps = (to, from, except,
|
|
10324
|
+
}, __copyProps = (to, from, except, desc5) => {
|
|
10327
10325
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
10328
10326
|
for (let key of __getOwnPropNames(from))
|
|
10329
10327
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
10330
|
-
__defProp2(to, key, { get: () => from[key], enumerable: !(
|
|
10328
|
+
__defProp2(to, key, { get: () => from[key], enumerable: !(desc5 = __getOwnPropDesc(from, key)) || desc5.enumerable });
|
|
10331
10329
|
}
|
|
10332
10330
|
return to;
|
|
10333
10331
|
}, __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default")), __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp2(target, "default", { value: mod, enumerable: true }) : target, mod)), MAX_TAIL_CHARS, BoundedTail = class {
|
|
@@ -38304,8 +38302,8 @@ var init_chunk_DJRHWPEH = __esm(() => {
|
|
|
38304
38302
|
}
|
|
38305
38303
|
const keys3 = Reflect.ownKeys(O);
|
|
38306
38304
|
for (const key of keys3) {
|
|
38307
|
-
const
|
|
38308
|
-
if (
|
|
38305
|
+
const desc5 = Reflect.getOwnPropertyDescriptor(O, key);
|
|
38306
|
+
if (desc5?.enumerable) {
|
|
38309
38307
|
const typedKey = keyConverter(key, prefix, argument);
|
|
38310
38308
|
const typedValue = valueConverter(O[key], prefix, argument);
|
|
38311
38309
|
result[typedKey] = typedValue;
|
|
@@ -62657,7 +62655,7 @@ var init_registry2 = __esm(() => {
|
|
|
62657
62655
|
});
|
|
62658
62656
|
|
|
62659
62657
|
// src/workflows/runner.ts
|
|
62660
|
-
import { and as
|
|
62658
|
+
import { and as and9, eq as eq12 } from "drizzle-orm";
|
|
62661
62659
|
function workflowClaimsFeatureBranch(workflowId) {
|
|
62662
62660
|
return BRANCH_CLAIMING.has(workflowId);
|
|
62663
62661
|
}
|
|
@@ -62740,7 +62738,7 @@ async function startRun(ctx, featureId, workflowId, opts = {}) {
|
|
|
62740
62738
|
return { runId, done: done10 };
|
|
62741
62739
|
}
|
|
62742
62740
|
function runHaltHeadline(ctx, featureId, runId) {
|
|
62743
|
-
const halt = ctx.db.select({ message: events.message }).from(events).where(
|
|
62741
|
+
const halt = ctx.db.select({ message: events.message }).from(events).where(and9(eq12(events.featureId, featureId), eq12(events.runId, runId), eq12(events.type, "run.halted"))).get();
|
|
62744
62742
|
return halt?.message;
|
|
62745
62743
|
}
|
|
62746
62744
|
async function endRound(ctx, runId, featureId, runGates) {
|
|
@@ -62865,7 +62863,7 @@ var init_runner = __esm(() => {
|
|
|
62865
62863
|
});
|
|
62866
62864
|
|
|
62867
62865
|
// src/services/prep.ts
|
|
62868
|
-
import { and as
|
|
62866
|
+
import { and as and10, desc as desc5, eq as eq13, inArray as inArray7 } from "drizzle-orm";
|
|
62869
62867
|
import { readdir as readdir3 } from "fs/promises";
|
|
62870
62868
|
import { join as join26 } from "path";
|
|
62871
62869
|
function keysToPrepare(ctx, project3) {
|
|
@@ -62877,10 +62875,10 @@ function isPrepared(ctx, project3) {
|
|
|
62877
62875
|
return hasCompletedProjectSession(ctx, project3.id, "prepare");
|
|
62878
62876
|
}
|
|
62879
62877
|
function preparedAt(ctx, projectId) {
|
|
62880
|
-
const prepareSessions = new Set(ctx.db.select({ id: sessions.id }).from(sessions).where(
|
|
62878
|
+
const prepareSessions = new Set(ctx.db.select({ id: sessions.id }).from(sessions).where(and10(eq13(sessions.projectId, projectId), eq13(sessions.kind, "prepare"))).all().map((r) => r.id));
|
|
62881
62879
|
if (prepareSessions.size === 0)
|
|
62882
62880
|
return null;
|
|
62883
|
-
const ends = ctx.db.select({ ts: events.ts, data: events.data }).from(events).where(
|
|
62881
|
+
const ends = ctx.db.select({ ts: events.ts, data: events.data }).from(events).where(and10(eq13(events.projectId, projectId), inArray7(events.type, ["session.ended", "session.reconciled"]))).orderBy(desc5(events.ts)).all();
|
|
62884
62882
|
for (const end5 of ends) {
|
|
62885
62883
|
const sessionId = end5.data?.sessionId;
|
|
62886
62884
|
if (sessionId && prepareSessions.has(sessionId))
|
|
@@ -62919,7 +62917,7 @@ var init_prep = __esm(() => {
|
|
|
62919
62917
|
|
|
62920
62918
|
// src/services/project-notes.ts
|
|
62921
62919
|
import { existsSync as existsSync21, mkdirSync as mkdirSync14, rmSync as rmSync6, writeFileSync as writeFileSync11 } from "fs";
|
|
62922
|
-
import { and as
|
|
62920
|
+
import { and as and11, count as count3, desc as desc6, eq as eq14, inArray as inArray8 } from "drizzle-orm";
|
|
62923
62921
|
function rowToNote(row) {
|
|
62924
62922
|
return ProjectNote.parse({
|
|
62925
62923
|
...row,
|
|
@@ -62967,10 +62965,10 @@ function addNote(ctx, projectId, text3) {
|
|
|
62967
62965
|
return note2;
|
|
62968
62966
|
}
|
|
62969
62967
|
function listNotes(ctx, projectId) {
|
|
62970
|
-
return ctx.db.select().from(projectNotes).where(eq14(projectNotes.projectId, projectId)).orderBy(
|
|
62968
|
+
return ctx.db.select().from(projectNotes).where(eq14(projectNotes.projectId, projectId)).orderBy(desc6(projectNotes.createdAt), desc6(projectNotes.id)).all().map(rowToNote).sort((a, b) => Number(a.status === "triaged") - Number(b.status === "triaged"));
|
|
62971
62969
|
}
|
|
62972
62970
|
function openCount(ctx, projectId) {
|
|
62973
|
-
return ctx.db.select({ value: count3() }).from(projectNotes).where(
|
|
62971
|
+
return ctx.db.select({ value: count3() }).from(projectNotes).where(and11(eq14(projectNotes.projectId, projectId), eq14(projectNotes.status, "open"))).get()?.value ?? 0;
|
|
62974
62972
|
}
|
|
62975
62973
|
function editNote(ctx, noteId, text3) {
|
|
62976
62974
|
const note2 = getNote(ctx, noteId);
|
|
@@ -63111,7 +63109,7 @@ var init_orchestrator_delivery = __esm(() => {
|
|
|
63111
63109
|
});
|
|
63112
63110
|
|
|
63113
63111
|
// src/services/orchestrator-notify.ts
|
|
63114
|
-
import { and as
|
|
63112
|
+
import { and as and12, asc as asc5, desc as desc7, eq as eq15, gt as gt2, inArray as inArray9, isNotNull as isNotNull3, ne as ne2, sql as sql4 } from "drizzle-orm";
|
|
63115
63113
|
function record3(data) {
|
|
63116
63114
|
return data && typeof data === "object" && !Array.isArray(data) ? data : {};
|
|
63117
63115
|
}
|
|
@@ -63120,7 +63118,7 @@ function clip(text3, max3 = SUMMARY_MAX) {
|
|
|
63120
63118
|
return flat.length <= max3 ? flat : `${flat.slice(0, max3 - 1)}\u2026`;
|
|
63121
63119
|
}
|
|
63122
63120
|
function isHeadless(session) {
|
|
63123
|
-
return !!session && (session.headless === true || session.purpose === "self-plan"
|
|
63121
|
+
return !!session && (session.headless === true || session.purpose === "self-plan");
|
|
63124
63122
|
}
|
|
63125
63123
|
function describe(event, session, ticket, slug) {
|
|
63126
63124
|
const data = record3(event.data);
|
|
@@ -63128,14 +63126,31 @@ function describe(event, session, ticket, slug) {
|
|
|
63128
63126
|
const tickets2 = `Read it with list_tickets({ slug: "${slug}" }).`;
|
|
63129
63127
|
switch (event.type) {
|
|
63130
63128
|
case "session.self_plan_ended":
|
|
63131
|
-
return data.complete === true ? { kind: "end", text: `finished planning
|
|
63129
|
+
return data.complete === true ? { kind: "end", text: `finished planning. ${context5}` } : { kind: "end", text: `stopped before the plan was complete. ${context5}` };
|
|
63132
63130
|
case "session.run_ended":
|
|
63133
63131
|
return data.complete === true ? { kind: "end", text: `headless run finished. ${context5}` } : { kind: "end", text: `headless run stopped before it finished. ${context5}` };
|
|
63134
63132
|
case "run.finished": {
|
|
63135
63133
|
const summary = typeof data.summary === "string" ? clip(data.summary) : "";
|
|
63136
|
-
const verb = data.status === "succeeded" ? "
|
|
63134
|
+
const verb = data.status === "succeeded" ? "round finished" : "round failed";
|
|
63137
63135
|
return { kind: "end", text: `${verb}${summary ? ` (${summary})` : ""}. ${tickets2}` };
|
|
63138
63136
|
}
|
|
63137
|
+
case "feature.waiting": {
|
|
63138
|
+
if (typeof data.why !== "string" || data.why === READY_REASON)
|
|
63139
|
+
return null;
|
|
63140
|
+
return {
|
|
63141
|
+
kind: "end",
|
|
63142
|
+
text: `waits for you: ${clip(data.why, WHY_MAX)}. Read what it asked with read_feature_chat({ slug: "${slug}" }).`
|
|
63143
|
+
};
|
|
63144
|
+
}
|
|
63145
|
+
case "feature.ready_declared": {
|
|
63146
|
+
const scenario = typeof data.scenario === "string" ? clip(data.scenario) : "";
|
|
63147
|
+
return { kind: "end", text: `declared ready to test${scenario ? ` (drive: ${scenario})` : ""}. ${context5}` };
|
|
63148
|
+
}
|
|
63149
|
+
case "lead.wake_failed":
|
|
63150
|
+
return {
|
|
63151
|
+
kind: "end",
|
|
63152
|
+
text: `could not wake its agent, so messages sent to it have not reached it yet (${clip(event.message)}). ${context5}`
|
|
63153
|
+
};
|
|
63139
63154
|
case "ticket.done":
|
|
63140
63155
|
case "ticket.failed": {
|
|
63141
63156
|
if (ticket?.kind !== "review")
|
|
@@ -63158,9 +63173,6 @@ function describe(event, session, ticket, slug) {
|
|
|
63158
63173
|
case "session.launched":
|
|
63159
63174
|
if (!session || session.purpose === "lead-wake" || session.kind === "side")
|
|
63160
63175
|
return null;
|
|
63161
|
-
if (session.purpose === "start-lap") {
|
|
63162
|
-
return { kind: "act", text: `the human started the next lap in a chat; it is busy until that session ends.` };
|
|
63163
|
-
}
|
|
63164
63176
|
if (isHeadless(session))
|
|
63165
63177
|
return { kind: "act", text: `the human had it plan itself. Wait for the run to end.` };
|
|
63166
63178
|
return { kind: "act", text: `the human opened a ${session.kind} session on it; it is busy until that session ends.` };
|
|
@@ -63168,9 +63180,6 @@ function describe(event, session, ticket, slug) {
|
|
|
63168
63180
|
if (!session || isHeadless(session) || session.kind === "side")
|
|
63169
63181
|
return null;
|
|
63170
63182
|
return { kind: "act", text: `the human's ${session.kind} session on it ended; it is free again. ${context5}` };
|
|
63171
|
-
case "burn.started":
|
|
63172
|
-
case "burn.restarted":
|
|
63173
|
-
return { kind: "act", text: `the human started a burn. Wait for it to finish.` };
|
|
63174
63183
|
case "testdrive.started":
|
|
63175
63184
|
if (data.purpose === "review")
|
|
63176
63185
|
return null;
|
|
@@ -63183,14 +63192,8 @@ function describe(event, session, ticket, slug) {
|
|
|
63183
63192
|
return null;
|
|
63184
63193
|
}
|
|
63185
63194
|
}
|
|
63186
|
-
function isOrchestratorTurnedOff(event) {
|
|
63187
|
-
const data = record3(event.data);
|
|
63188
|
-
return event.type === "settings.updated" && data.key === "orchestrator" && data.scope === "project" && (data.value === false || data.value === null);
|
|
63189
|
-
}
|
|
63190
63195
|
function orchestratorLineFor(event, state) {
|
|
63191
|
-
if (!event.featureId)
|
|
63192
|
-
return isOrchestratorTurnedOff(event) ? ORCHESTRATOR_STOP_LINE : null;
|
|
63193
|
-
if (!state.feature?.orchestrated)
|
|
63196
|
+
if (!event.featureId || !state.feature?.followed)
|
|
63194
63197
|
return null;
|
|
63195
63198
|
const described = describe(event, state.session, state.ticket, state.feature.slug);
|
|
63196
63199
|
if (!described)
|
|
@@ -63200,15 +63203,16 @@ function orchestratorLineFor(event, state) {
|
|
|
63200
63203
|
}
|
|
63201
63204
|
return `${PREFIX} ${state.feature.slug} ${described.text}`;
|
|
63202
63205
|
}
|
|
63203
|
-
function
|
|
63204
|
-
const
|
|
63205
|
-
return
|
|
63206
|
+
function followedFeatures(ctx, where) {
|
|
63207
|
+
const orchestratorActed = sql4`exists (select 1 from ${events} where ${events.featureId} = ${features.id} and json_extract(${events.data}, '$.actor') = 'orchestrator')`;
|
|
63208
|
+
return ctx.db.select({ id: features.id, slug: features.slug }).from(features).where(and12(where, ne2(features.status, "shipped"), orchestratorActed)).all();
|
|
63206
63209
|
}
|
|
63207
63210
|
function stateFor(ctx, event) {
|
|
63208
63211
|
const state = {};
|
|
63209
63212
|
if (event.featureId) {
|
|
63210
63213
|
const feature = getFeatureRow(ctx, event.featureId);
|
|
63211
|
-
|
|
63214
|
+
const followed2 = followedFeatures(ctx, eq15(features.id, feature.id)).length > 0;
|
|
63215
|
+
state.feature = { slug: feature.slug, followed: followed2 };
|
|
63212
63216
|
}
|
|
63213
63217
|
const sessionId = record3(event.data).sessionId;
|
|
63214
63218
|
if (typeof sessionId === "string") {
|
|
@@ -63229,7 +63233,7 @@ function ticketFor(ctx, event) {
|
|
|
63229
63233
|
}
|
|
63230
63234
|
function notifyOrchestrator(ctx, projectId, eventId) {
|
|
63231
63235
|
try {
|
|
63232
|
-
const orchestrator =
|
|
63236
|
+
const orchestrator = activeProjectSession(ctx, projectId, "project");
|
|
63233
63237
|
if (!orchestrator)
|
|
63234
63238
|
return;
|
|
63235
63239
|
const event = getEventRow(ctx, eventId);
|
|
@@ -63245,14 +63249,14 @@ function notifyOrchestrator(ctx, projectId, eventId) {
|
|
|
63245
63249
|
}
|
|
63246
63250
|
}
|
|
63247
63251
|
function orchestratorCatchUp(ctx, project3) {
|
|
63248
|
-
const last2 = ctx.db.select({ endedAt: sessions.endedAt }).from(sessions).where(
|
|
63252
|
+
const last2 = ctx.db.select({ endedAt: sessions.endedAt }).from(sessions).where(and12(eq15(sessions.projectId, project3.id), eq15(sessions.kind, "project"), isNotNull3(sessions.endedAt))).orderBy(desc7(sessions.endedAt)).limit(1).get();
|
|
63249
63253
|
if (!last2?.endedAt)
|
|
63250
63254
|
return [];
|
|
63251
|
-
const
|
|
63252
|
-
if (
|
|
63255
|
+
const followed2 = followedFeatures(ctx, eq15(features.projectId, project3.id));
|
|
63256
|
+
if (followed2.length === 0)
|
|
63253
63257
|
return [];
|
|
63254
|
-
const slugs = new Map(
|
|
63255
|
-
const ids3 = ctx.db.select({ id: events.id }).from(events).where(
|
|
63258
|
+
const slugs = new Map(followed2.map((f) => [f.id, f.slug]));
|
|
63259
|
+
const ids3 = ctx.db.select({ id: events.id }).from(events).where(and12(inArray9(events.featureId, [...slugs.keys()]), inArray9(events.type, END_TYPES), gt2(events.ts, last2.endedAt))).orderBy(asc5(events.id)).all();
|
|
63256
63260
|
const lines2 = [];
|
|
63257
63261
|
for (const { id: id3 } of ids3) {
|
|
63258
63262
|
const event = getEventRow(ctx, id3);
|
|
@@ -63276,7 +63280,7 @@ function startOrchestratorNotifier(ctx) {
|
|
|
63276
63280
|
notifyOrchestrator(ctx, signal.projectId, signal.eventId);
|
|
63277
63281
|
});
|
|
63278
63282
|
}
|
|
63279
|
-
var PREFIX = "[runcastle]", SUMMARY_MAX = 80, CATCH_UP_MAX = 50,
|
|
63283
|
+
var PREFIX = "[runcastle]", SUMMARY_MAX = 80, CATCH_UP_MAX = 50, READY_REASON = "ready to test", WHY_MAX = 200, END_TYPES, ANSWERED_END_TYPES;
|
|
63280
63284
|
var init_orchestrator_notify = __esm(() => {
|
|
63281
63285
|
init_schema();
|
|
63282
63286
|
init_sessions();
|
|
@@ -63286,11 +63290,13 @@ var init_orchestrator_notify = __esm(() => {
|
|
|
63286
63290
|
init_orchestrator_messages();
|
|
63287
63291
|
init_repo();
|
|
63288
63292
|
init_tickets();
|
|
63289
|
-
ORCHESTRATOR_STOP_LINE = `${PREFIX} The orchestrator mode was turned off for this project. Start nothing new; runs already going finish on their own. Tell the human what is still in flight.`;
|
|
63290
63293
|
END_TYPES = [
|
|
63291
63294
|
"session.self_plan_ended",
|
|
63292
63295
|
"session.run_ended",
|
|
63293
63296
|
"run.finished",
|
|
63297
|
+
"feature.waiting",
|
|
63298
|
+
"feature.ready_declared",
|
|
63299
|
+
"lead.wake_failed",
|
|
63294
63300
|
"ticket.done",
|
|
63295
63301
|
"ticket.failed",
|
|
63296
63302
|
"feature.shipped",
|
|
@@ -63723,7 +63729,7 @@ var init_lockfiles = __esm(() => {
|
|
|
63723
63729
|
});
|
|
63724
63730
|
|
|
63725
63731
|
// src/pty/end-session.ts
|
|
63726
|
-
function endSession(ctx, sessionId) {
|
|
63732
|
+
function endSession(ctx, sessionId, message = "session ended by user") {
|
|
63727
63733
|
const registry = ptyRegistry();
|
|
63728
63734
|
noteRequestedEnd(sessionId);
|
|
63729
63735
|
const killed = registry.kill(sessionId);
|
|
@@ -63734,7 +63740,7 @@ function endSession(ctx, sessionId) {
|
|
|
63734
63740
|
stopDesignServerForSession(ctx, session);
|
|
63735
63741
|
emitForSession(ctx, session, {
|
|
63736
63742
|
type: "session.ended",
|
|
63737
|
-
message
|
|
63743
|
+
message,
|
|
63738
63744
|
data: { sessionId, killed }
|
|
63739
63745
|
});
|
|
63740
63746
|
} else {
|
|
@@ -63751,10 +63757,65 @@ var init_end_session = __esm(() => {
|
|
|
63751
63757
|
init_registry();
|
|
63752
63758
|
});
|
|
63753
63759
|
|
|
63760
|
+
// src/launcher/untouched.ts
|
|
63761
|
+
function markAutoResumed(ctx, sessionId) {
|
|
63762
|
+
unsubscribe ??= ptyRegistry().onSinksChanged(sinksChanged);
|
|
63763
|
+
untouched.set(sessionId, { ctx });
|
|
63764
|
+
sinksChanged(sessionId, ptyRegistry().get(sessionId)?.sinks.size ?? 0);
|
|
63765
|
+
}
|
|
63766
|
+
function forgetUntouched(sessionId) {
|
|
63767
|
+
const entry = untouched.get(sessionId);
|
|
63768
|
+
if (!entry)
|
|
63769
|
+
return;
|
|
63770
|
+
if (entry.timer)
|
|
63771
|
+
clearTimeout(entry.timer);
|
|
63772
|
+
untouched.delete(sessionId);
|
|
63773
|
+
}
|
|
63774
|
+
function sinksChanged(sessionId, attached) {
|
|
63775
|
+
const entry = untouched.get(sessionId);
|
|
63776
|
+
if (!entry)
|
|
63777
|
+
return;
|
|
63778
|
+
if (attached > 0) {
|
|
63779
|
+
if (entry.timer)
|
|
63780
|
+
clearTimeout(entry.timer);
|
|
63781
|
+
entry.timer = undefined;
|
|
63782
|
+
return;
|
|
63783
|
+
}
|
|
63784
|
+
if (entry.timer)
|
|
63785
|
+
return;
|
|
63786
|
+
entry.timer = setTimeout(() => leave(sessionId), graceMs);
|
|
63787
|
+
entry.timer.unref?.();
|
|
63788
|
+
}
|
|
63789
|
+
function leave(sessionId) {
|
|
63790
|
+
const entry = untouched.get(sessionId);
|
|
63791
|
+
if (!entry)
|
|
63792
|
+
return;
|
|
63793
|
+
entry.timer = undefined;
|
|
63794
|
+
if ((ptyRegistry().get(sessionId)?.sinks.size ?? 0) > 0)
|
|
63795
|
+
return;
|
|
63796
|
+
untouched.delete(sessionId);
|
|
63797
|
+
try {
|
|
63798
|
+
const row = getSessionRow(entry.ctx, sessionId);
|
|
63799
|
+
if (row && row.status !== "ended") {
|
|
63800
|
+
endSession(entry.ctx, sessionId, "chat closed \u2014 it resumed by itself and was left unused");
|
|
63801
|
+
}
|
|
63802
|
+
} catch (err) {
|
|
63803
|
+
console.error(`[untouched] ending ${sessionId} failed`, err);
|
|
63804
|
+
}
|
|
63805
|
+
}
|
|
63806
|
+
var UNTOUCHED_LEAVE_GRACE_MS = 1e4, untouched, graceMs, unsubscribe;
|
|
63807
|
+
var init_untouched = __esm(() => {
|
|
63808
|
+
init_end_session();
|
|
63809
|
+
init_registry();
|
|
63810
|
+
init_sessions();
|
|
63811
|
+
untouched = new Map;
|
|
63812
|
+
graceMs = UNTOUCHED_LEAVE_GRACE_MS;
|
|
63813
|
+
});
|
|
63814
|
+
|
|
63754
63815
|
// src/launcher/launcher.ts
|
|
63755
63816
|
import { existsSync as existsSync22 } from "fs";
|
|
63756
63817
|
import { join as join28 } from "path";
|
|
63757
|
-
import { and as
|
|
63818
|
+
import { and as and13, eq as eq16 } from "drizzle-orm";
|
|
63758
63819
|
function ptyExitMessage(exitCode) {
|
|
63759
63820
|
const label = typeof exitCode === "number" ? String(exitCode) : "unknown";
|
|
63760
63821
|
return `terminal exited (code ${label})`;
|
|
@@ -63784,7 +63845,7 @@ function renderCommand(runtime5, spec) {
|
|
|
63784
63845
|
return [...env, runtime5.binary, ...spec.argv].join(" ");
|
|
63785
63846
|
}
|
|
63786
63847
|
function activeRunFor(ctx, featureId) {
|
|
63787
|
-
const row = ctx.db.select().from(runs).where(
|
|
63848
|
+
const row = ctx.db.select().from(runs).where(and13(eq16(runs.featureId, featureId), eq16(runs.status, "running"))).limit(1).get();
|
|
63788
63849
|
return row ? rowToRun(row) : null;
|
|
63789
63850
|
}
|
|
63790
63851
|
function assertSpawnable(ctx, feature, excludeSessionId) {
|
|
@@ -64002,11 +64063,12 @@ async function launchSession(ctx, input, opts = {}) {
|
|
|
64002
64063
|
data: { sessionId: session.id, kind: input.kind, ...capped }
|
|
64003
64064
|
});
|
|
64004
64065
|
}
|
|
64005
|
-
|
|
64066
|
+
const quietResume = input.kind === "chat" && !!resumeSessionId && !input.purpose && !plan.explicit;
|
|
64067
|
+
if (input.kind === "chat" && !quietResume) {
|
|
64006
64068
|
const header = chatKickoffHeader(ctx, feature, runtime5.id, selfPlan ? selfPlanKickoffFor(runtime5.id) : relay ? relayKickoffFor(runtime5.id, input.message) : leadWake ? leadWakeKickoff(runtime5.id, input.message, !!resumeSessionId) : chatKickoffFor(runtime5.id));
|
|
64007
64069
|
plan.line = plan.line ? `${plan.line} ${header}` : header;
|
|
64008
64070
|
}
|
|
64009
|
-
const kickoffLine = resumeSessionId && input.kind !== "chat" ? undefined : input.kind === "chat" ? tagRuncastlePrompt(kickoffLineFor(input.kind, plan.line, runtime5.id)) : kickoffLineFor(input.kind, plan.line, runtime5.id);
|
|
64071
|
+
const kickoffLine = quietResume || resumeSessionId && input.kind !== "chat" ? undefined : input.kind === "chat" ? tagRuncastlePrompt(kickoffLineFor(input.kind, plan.line, runtime5.id)) : kickoffLineFor(input.kind, plan.line, runtime5.id);
|
|
64010
64072
|
emit(ctx, feature.id, {
|
|
64011
64073
|
type: "session.launching",
|
|
64012
64074
|
message: selfPlan ? "launching a self-planning run" : relay ? "relaying the orchestrator's message to the planning conversation" : leadWake ? "waking the agent headless \u2014 no terminal of it was open" : `launching ${input.kind} session`,
|
|
@@ -64053,6 +64115,8 @@ async function launchSession(ctx, input, opts = {}) {
|
|
|
64053
64115
|
...headless ? { headless: true } : {},
|
|
64054
64116
|
...runtime5.id === "claude-code" && isUnguardedLead(input.kind) ? { permissionMode: "bypassPermissions" } : {}
|
|
64055
64117
|
});
|
|
64118
|
+
if (quietResume)
|
|
64119
|
+
markAwaitingInput(ctx, session.id);
|
|
64056
64120
|
if (opts.spawn === false) {
|
|
64057
64121
|
emit(ctx, feature.id, {
|
|
64058
64122
|
type: "session.launched",
|
|
@@ -64073,6 +64137,57 @@ async function launchSession(ctx, input, opts = {}) {
|
|
|
64073
64137
|
}
|
|
64074
64138
|
return { sessionId: session.id, resumed: !!resumeSessionId };
|
|
64075
64139
|
}
|
|
64140
|
+
function skippedOnGate(guard) {
|
|
64141
|
+
try {
|
|
64142
|
+
guard();
|
|
64143
|
+
return null;
|
|
64144
|
+
} catch (e) {
|
|
64145
|
+
if (e instanceof GateError)
|
|
64146
|
+
return { autoSkipped: e.message };
|
|
64147
|
+
throw e;
|
|
64148
|
+
}
|
|
64149
|
+
}
|
|
64150
|
+
function autoResumeRuntimeRefusal(model, opts) {
|
|
64151
|
+
if (model.runtime === "codex")
|
|
64152
|
+
return { autoSkipped: "the Codex runtime cannot resume a conversation on open" };
|
|
64153
|
+
return skippedOnGate(() => assertRuntimeReady(runtimeAdapterFor(model.runtime), opts));
|
|
64154
|
+
}
|
|
64155
|
+
function watchAutoResumed(ctx, result, opts) {
|
|
64156
|
+
if (opts.spawn === false || !result.resumed)
|
|
64157
|
+
return;
|
|
64158
|
+
if (getSessionRow(ctx, result.sessionId)?.status === "ended")
|
|
64159
|
+
return;
|
|
64160
|
+
markAutoResumed(ctx, result.sessionId);
|
|
64161
|
+
}
|
|
64162
|
+
async function autoResumeChat(ctx, input, opts = {}) {
|
|
64163
|
+
if (input.kind !== "chat" || input.purpose || input.purposeData || input.kickoffLine || input.message || input.fresh) {
|
|
64164
|
+
throw new GateError("an automatic resume opens the feature chat alone, with no errand of its own");
|
|
64165
|
+
}
|
|
64166
|
+
const feature = getFeatureRow(ctx, input.featureId);
|
|
64167
|
+
if (feature.status !== "active")
|
|
64168
|
+
return { autoSkipped: `${feature.slug} is not in progress` };
|
|
64169
|
+
const liveChat = activeSessionsForFeature(ctx, feature.id).find((s) => s.kind === "chat");
|
|
64170
|
+
if (liveChat?.headless)
|
|
64171
|
+
return { autoSkipped: headlessRunningError(feature, liveChat).message };
|
|
64172
|
+
if (liveChat)
|
|
64173
|
+
return { sessionId: liveChat.id };
|
|
64174
|
+
const held = skippedOnGate(() => assertSpawnable(ctx, feature));
|
|
64175
|
+
if (held)
|
|
64176
|
+
return held;
|
|
64177
|
+
const project3 = projectForFeature(ctx, feature);
|
|
64178
|
+
const refused = autoResumeRuntimeRefusal(resolveModelEntry("chat", rosterConfig(ctx), project3), opts);
|
|
64179
|
+
if (refused)
|
|
64180
|
+
return refused;
|
|
64181
|
+
const prior = mostRecentResumableSession(ctx, feature.id, "chat");
|
|
64182
|
+
if (!prior?.ccSessionId)
|
|
64183
|
+
return { autoSkipped: "no resumable conversation is on record" };
|
|
64184
|
+
const gone = resumeGoneReason(prior, worktreeDir(project3.id, feature.slug));
|
|
64185
|
+
if (gone)
|
|
64186
|
+
return { autoSkipped: `the conversation can't be resumed \u2014 ${gone}` };
|
|
64187
|
+
const launched = await launchSession(ctx, { featureId: feature.id, kind: "chat" }, opts);
|
|
64188
|
+
watchAutoResumed(ctx, launched, opts);
|
|
64189
|
+
return launched;
|
|
64190
|
+
}
|
|
64076
64191
|
function sideKickoff(ctx, feature, sessionId, runtime5 = DEFAULT_RUNTIME) {
|
|
64077
64192
|
return mainChatContext(ctx, feature, sessionId, chatKickoffHeader(ctx, feature, runtime5, kickoffLineFor("side", undefined, runtime5)));
|
|
64078
64193
|
}
|
|
@@ -64442,13 +64557,11 @@ async function launchProjectSession(ctx, input, opts = {}) {
|
|
|
64442
64557
|
type: "repo.head_healed",
|
|
64443
64558
|
message: "created runcastle: initial commit so branches can be cut"
|
|
64444
64559
|
}));
|
|
64445
|
-
const orchestrate2 = input.purpose !== "triage" && orchestratorOn(ctx, project3.id);
|
|
64446
64560
|
const session = createSessionRow(ctx, {
|
|
64447
64561
|
projectId: project3.id,
|
|
64448
64562
|
kind: "project",
|
|
64449
64563
|
worktreePath,
|
|
64450
|
-
model
|
|
64451
|
-
...orchestrate2 ? { purpose: "orchestrate" } : {}
|
|
64564
|
+
model
|
|
64452
64565
|
});
|
|
64453
64566
|
const resumeRowId = input.fresh || input.purpose ? undefined : input.resumeSessionId;
|
|
64454
64567
|
const resumedFrom = resumeRowId ? getSessionRow(ctx, resumeRowId) ?? undefined : undefined;
|
|
@@ -64504,7 +64617,7 @@ async function launchProjectSession(ctx, input, opts = {}) {
|
|
|
64504
64617
|
worktreePath,
|
|
64505
64618
|
base,
|
|
64506
64619
|
openNotes: openCount(ctx, project3.id),
|
|
64507
|
-
|
|
64620
|
+
catchUp: orchestratorCatchUp(ctx, project3)
|
|
64508
64621
|
},
|
|
64509
64622
|
worktreePath,
|
|
64510
64623
|
serverUrl: serverUrlFor(ctx.config),
|
|
@@ -64512,15 +64625,17 @@ async function launchProjectSession(ctx, input, opts = {}) {
|
|
|
64512
64625
|
resumeSessionId,
|
|
64513
64626
|
resumeSourceSessionId: resumedFrom?.id,
|
|
64514
64627
|
kickoffLine,
|
|
64515
|
-
permissionMode:
|
|
64628
|
+
permissionMode: "bypassPermissions"
|
|
64516
64629
|
});
|
|
64630
|
+
if (!kickoffLine)
|
|
64631
|
+
markAwaitingInput(ctx, session.id);
|
|
64517
64632
|
if (opts.spawn === false) {
|
|
64518
64633
|
emitProject(ctx, project3.id, {
|
|
64519
64634
|
type: "session.launched",
|
|
64520
64635
|
message: "project session prepared (terminal spawn skipped)",
|
|
64521
64636
|
data: { sessionId: session.id, command: renderCommand(runtime5, spec), spawned: false }
|
|
64522
64637
|
});
|
|
64523
|
-
return { sessionId: session.id };
|
|
64638
|
+
return { sessionId: session.id, resumed: !!resumeSessionId };
|
|
64524
64639
|
}
|
|
64525
64640
|
const spawned = spawnEmbeddedPty(ctx, undefined, session, worktreePath, runtime5, spec, {
|
|
64526
64641
|
resumeSessionId
|
|
@@ -64532,7 +64647,29 @@ async function launchProjectSession(ctx, input, opts = {}) {
|
|
|
64532
64647
|
data: { sessionId: session.id, kind: "project", line: kickoffLine, mechanism: "argv" }
|
|
64533
64648
|
});
|
|
64534
64649
|
}
|
|
64535
|
-
return { sessionId: session.id };
|
|
64650
|
+
return { sessionId: session.id, resumed: !!resumeSessionId };
|
|
64651
|
+
}
|
|
64652
|
+
async function autoResumeProjectChat(ctx, input, opts = {}) {
|
|
64653
|
+
if (!input.resumeSessionId || input.fresh || input.purpose) {
|
|
64654
|
+
throw new GateError("an automatic resume picks up the conversation it names, and nothing else");
|
|
64655
|
+
}
|
|
64656
|
+
const project3 = requireProjectById(ctx, input.projectId);
|
|
64657
|
+
if (activeProjectSession(ctx, project3.id, "project")) {
|
|
64658
|
+
return { autoSkipped: `a project session is already open for ${project3.name}` };
|
|
64659
|
+
}
|
|
64660
|
+
const refused = autoResumeRuntimeRefusal(resolveModelEntry("project", rosterConfig(ctx), project3), opts);
|
|
64661
|
+
if (refused)
|
|
64662
|
+
return refused;
|
|
64663
|
+
const prior = getSessionRow(ctx, input.resumeSessionId);
|
|
64664
|
+
if (prior?.projectId !== project3.id || prior.kind !== "project" || !prior.ccSessionId) {
|
|
64665
|
+
return { autoSkipped: "that conversation has nothing to resume" };
|
|
64666
|
+
}
|
|
64667
|
+
const capped = applyResumeCap(ctx, prior.ccSessionId, prior, { projectId: project3.id, kind: "project" });
|
|
64668
|
+
if (capped)
|
|
64669
|
+
return { autoSkipped: capped.detail };
|
|
64670
|
+
const launched = await launchProjectSession(ctx, { projectId: project3.id, resumeSessionId: prior.id }, opts);
|
|
64671
|
+
watchAutoResumed(ctx, launched, opts);
|
|
64672
|
+
return launched;
|
|
64536
64673
|
}
|
|
64537
64674
|
async function buildPrepareBrief(ctx, project3) {
|
|
64538
64675
|
const findings = await listFindings(ctx, project3);
|
|
@@ -64562,6 +64699,7 @@ function probePrepareHost(repoPath) {
|
|
|
64562
64699
|
function handlePtyExit(ctx, feature, session, meta, exitCode, output) {
|
|
64563
64700
|
const diedBeforeLive = getSessionRow(ctx, session.id)?.status === "launching";
|
|
64564
64701
|
markSessionEnded(ctx, session.id);
|
|
64702
|
+
forgetUntouched(session.id);
|
|
64565
64703
|
landProjectSession(ctx, session);
|
|
64566
64704
|
stopDesignServerForSession(ctx, session);
|
|
64567
64705
|
if (session.runtime === "codex" && feature) {
|
|
@@ -64679,7 +64817,6 @@ var init_launcher = __esm(() => {
|
|
|
64679
64817
|
init_prep();
|
|
64680
64818
|
init_project_notes();
|
|
64681
64819
|
init_orchestrator_notify();
|
|
64682
|
-
init_settings();
|
|
64683
64820
|
init_scenarios();
|
|
64684
64821
|
init_resolved_merge();
|
|
64685
64822
|
init_main_chat_excerpt();
|
|
@@ -64690,6 +64827,7 @@ var init_launcher = __esm(() => {
|
|
|
64690
64827
|
init_skills();
|
|
64691
64828
|
init_lockfiles();
|
|
64692
64829
|
init_sessions();
|
|
64830
|
+
init_untouched();
|
|
64693
64831
|
init_end_session();
|
|
64694
64832
|
terminalQueues = new Map;
|
|
64695
64833
|
COMPOSE_FILES = [
|
|
@@ -64955,7 +65093,7 @@ var init_lead_delivery = __esm(() => {
|
|
|
64955
65093
|
});
|
|
64956
65094
|
|
|
64957
65095
|
// src/services/features.ts
|
|
64958
|
-
import { asc as asc6, desc as
|
|
65096
|
+
import { asc as asc6, desc as desc8, eq as eq17, or } from "drizzle-orm";
|
|
64959
65097
|
import { rmSync as rmSync7 } from "fs";
|
|
64960
65098
|
async function createFeature(ctx, input) {
|
|
64961
65099
|
const project3 = requireProjectById(ctx, input.projectId);
|
|
@@ -65100,7 +65238,7 @@ function getFeatureFull(ctx, id3) {
|
|
|
65100
65238
|
};
|
|
65101
65239
|
}
|
|
65102
65240
|
function list(ctx, projectId) {
|
|
65103
|
-
const rows = ctx.db.select().from(features).where(eq17(features.projectId, projectId)).orderBy(
|
|
65241
|
+
const rows = ctx.db.select().from(features).where(eq17(features.projectId, projectId)).orderBy(desc8(features.createdAt)).all();
|
|
65104
65242
|
const lastActivity = latestTsByFeature(ctx, projectId);
|
|
65105
65243
|
const projectFeatures = rows.map(rowToFeature);
|
|
65106
65244
|
const deps = dependencyMaps(ctx, projectId, projectFeatures);
|
|
@@ -65846,44 +65984,29 @@ var init_merge = __esm(() => {
|
|
|
65846
65984
|
// src/mcp/orchestrator-tools.ts
|
|
65847
65985
|
import { existsSync as existsSync24, mkdirSync as mkdirSync18, readFileSync as readFileSync16, writeFileSync as writeFileSync15 } from "fs";
|
|
65848
65986
|
import { dirname as dirname15 } from "path";
|
|
65849
|
-
import { and as
|
|
65987
|
+
import { and as and14, eq as eq19 } from "drizzle-orm";
|
|
65850
65988
|
import * as z6 from "zod";
|
|
65851
|
-
function requireOrchestratorSession(
|
|
65852
|
-
if (session.kind !== "project" ||
|
|
65853
|
-
throw new GateError("only the orchestrator may do this: the project session
|
|
65989
|
+
function requireOrchestratorSession(session) {
|
|
65990
|
+
if (session.kind !== "project" || !session.projectId) {
|
|
65991
|
+
throw new GateError("only the orchestrator may do this: the project session.");
|
|
65854
65992
|
}
|
|
65855
65993
|
if (session.status === "ended") {
|
|
65856
65994
|
throw new GateError("this orchestrator session has ended; a new one must be opened to act.");
|
|
65857
65995
|
}
|
|
65858
|
-
if (!orchestratorOn(ctx, session.projectId)) {
|
|
65859
|
-
throw new GateError("the project's Orchestrator setting is off, so the orchestrator starts nothing new. " + "Runs already going finish on their own.");
|
|
65860
|
-
}
|
|
65861
65996
|
return session.projectId;
|
|
65862
65997
|
}
|
|
65863
65998
|
function featureBySlug(ctx, projectId, slug) {
|
|
65864
|
-
const row = ctx.db.select().from(features).where(
|
|
65999
|
+
const row = ctx.db.select().from(features).where(and14(eq19(features.projectId, projectId), eq19(features.slug, slug))).get();
|
|
65865
66000
|
if (!row)
|
|
65866
66001
|
throw new GateError(`there is no feature \`${slug}\` in this project.`);
|
|
65867
66002
|
return rowToFeature(row);
|
|
65868
66003
|
}
|
|
65869
|
-
function
|
|
65870
|
-
|
|
65871
|
-
const feature = featureBySlug(ctx, projectId, slug);
|
|
65872
|
-
if (!feature.orchestrated) {
|
|
65873
|
-
throw new GateError(`\`${slug}\` is the human's feature, not yours: you act only on the features you created ` + "and the ones the human hands you. Tell the human if it bears on your work.");
|
|
65874
|
-
}
|
|
65875
|
-
return feature;
|
|
66004
|
+
function requireProjectFeature(ctx, session, slug) {
|
|
66005
|
+
return featureBySlug(ctx, requireOrchestratorSession(session), slug);
|
|
65876
66006
|
}
|
|
65877
66007
|
function orchestratorTool(handler) {
|
|
65878
66008
|
return (...args2) => runAsActor("orchestrator", () => handler(...args2));
|
|
65879
66009
|
}
|
|
65880
|
-
function toolTakeOverFeature(ctx, session, input) {
|
|
65881
|
-
const projectId = requireOrchestratorSession(ctx, session);
|
|
65882
|
-
const feature = featureBySlug(ctx, projectId, input.slug);
|
|
65883
|
-
if (!feature.orchestrated)
|
|
65884
|
-
markOrchestrated(ctx, feature.id, "taken over");
|
|
65885
|
-
return { slug: feature.slug, orchestrated: true };
|
|
65886
|
-
}
|
|
65887
66010
|
function withBriefSection(prior, text3) {
|
|
65888
66011
|
const date = new Date().toISOString().slice(0, 10);
|
|
65889
66012
|
const section = `## Added by the orchestrator, ${date}
|
|
@@ -65909,20 +66032,6 @@ function noSession() {
|
|
|
65909
66032
|
};
|
|
65910
66033
|
}
|
|
65911
66034
|
function registerOrchestratorTools(server, wants, resolve11) {
|
|
65912
|
-
if (wants("take_over_feature")) {
|
|
65913
|
-
server.registerTool("take_over_feature", {
|
|
65914
|
-
title: "Take over a feature",
|
|
65915
|
-
description: "Make a feature the human handed you yours, so your tools act on it and you hear when " + "something ends on it. Only when the human has handed it to you by name. Orchestrator only.",
|
|
65916
|
-
inputSchema: {
|
|
65917
|
-
slug: z6.string().describe("The slug of the feature the human handed you.")
|
|
65918
|
-
}
|
|
65919
|
-
}, orchestratorTool(async (args2, extra) => {
|
|
65920
|
-
const rs = await resolve11(extra);
|
|
65921
|
-
if (!rs)
|
|
65922
|
-
return noSession();
|
|
65923
|
-
return ok(toolTakeOverFeature(rs.ctx, rs.session, args2));
|
|
65924
|
-
}));
|
|
65925
|
-
}
|
|
65926
66035
|
function acting(name, config2, tool) {
|
|
65927
66036
|
if (!wants(name))
|
|
65928
66037
|
return;
|
|
@@ -65935,7 +66044,7 @@ function registerOrchestratorTools(server, wants, resolve11) {
|
|
|
65935
66044
|
}
|
|
65936
66045
|
acting("start_feature", {
|
|
65937
66046
|
title: "Start a draft",
|
|
65938
|
-
description: "Start a draft
|
|
66047
|
+
description: "Start a draft, as Start does: cut its branch and scaffold its brief. Refused while it " + "waits on an unmerged dependency, or once started. Launches nothing \u2014 call plan_feature " + "or send_to_feature when it is ready to work.",
|
|
65939
66048
|
inputSchema: {
|
|
65940
66049
|
slug: SlugInput,
|
|
65941
66050
|
baseBranch: z6.string().min(1).optional().describe("The branch to cut it from. Omit for the checkout\u2019s current branch.")
|
|
@@ -65943,46 +66052,37 @@ function registerOrchestratorTools(server, wants, resolve11) {
|
|
|
65943
66052
|
}, toolStartFeature);
|
|
65944
66053
|
acting("plan_feature", {
|
|
65945
66054
|
title: "Have a feature plan itself",
|
|
65946
|
-
description: "Launch a feature\u2019s self-planning run, as Plan it yourself does: headless, no terminal. " + "Refused
|
|
66055
|
+
description: "Launch a feature\u2019s self-planning run, as Plan it yourself does: headless, no terminal. " + "Refused on a draft or a shipped feature, or while another session is up on it. You are " + "told in your terminal when it ends.",
|
|
65947
66056
|
inputSchema: { slug: SlugInput }
|
|
65948
66057
|
}, toolPlanFeature);
|
|
65949
66058
|
acting("stop_feature_run", {
|
|
65950
66059
|
title: "Stop a feature\u2019s headless run",
|
|
65951
|
-
description: "End the headless run going on a feature (self-planning or a relay), as " + "its Stop button does.
|
|
66060
|
+
description: "End the headless run going on a feature (self-planning or a relay), as " + "its Stop button does.",
|
|
65952
66061
|
inputSchema: { slug: SlugInput }
|
|
65953
66062
|
}, toolStopFeatureRun);
|
|
65954
66063
|
acting("drive", {
|
|
65955
66064
|
title: "Start or stop a test drive",
|
|
65956
|
-
description: "Start or stop a test drive: of a feature\u2019s branch with `slug`, of the project\u2019s base " + "checkout without one. One drive runs on the machine at a time \u2014 a refusal comes back as " + "`deniedReason`.
|
|
66065
|
+
description: "Start or stop a test drive: of a feature\u2019s branch with `slug`, of the project\u2019s base " + "checkout without one. One drive runs on the machine at a time \u2014 a refusal comes back as " + "`deniedReason`.",
|
|
65957
66066
|
inputSchema: {
|
|
65958
66067
|
slug: SlugInput.optional().describe("The feature whose branch to drive. Omit to drive the base branch."),
|
|
65959
66068
|
action: z6.enum(["start", "stop"]).describe("Start the drive, or stop it.")
|
|
65960
66069
|
}
|
|
65961
66070
|
}, toolDrive);
|
|
65962
|
-
acting("record_test_note", {
|
|
65963
|
-
title: "Record a test note",
|
|
65964
|
-
description: "File a test note on a feature, as the human does from a drive. Say who observed it. " + "Notes are for the feature\u2019s agent: tell it what you found with send_to_feature. Orchestrator only.",
|
|
65965
|
-
inputSchema: {
|
|
65966
|
-
slug: SlugInput,
|
|
65967
|
-
text: z6.string().min(1).describe("What was observed, in a sentence or two."),
|
|
65968
|
-
observedBy: z6.enum(["human", "orchestrator"]).describe("`human` when the human told you this; `orchestrator` when you saw it.")
|
|
65969
|
-
}
|
|
65970
|
-
}, toolRecordTestNote);
|
|
65971
66071
|
acting("merge_feature", {
|
|
65972
66072
|
title: "Merge a feature",
|
|
65973
|
-
description: "Merge a feature into its base through runcastle, as Merge does: the outcome doc, the " + "shipped state and the worktree clean-up. On a conflict it returns `conflict` and the " + "`files`; resolve them in the feature\u2019s worktree (never while a session is live on it) " + "and merge again.
|
|
66073
|
+
description: "Merge a feature into its base through runcastle, as Merge does: the outcome doc, the " + "shipped state and the worktree clean-up. On a conflict it returns `conflict` and the " + "`files`; resolve them in the feature\u2019s worktree (never while a session is live on it) " + "and merge again.",
|
|
65974
66074
|
inputSchema: { slug: SlugInput }
|
|
65975
66075
|
}, toolMergeFeature);
|
|
65976
66076
|
acting("add_to_brief", {
|
|
65977
66077
|
title: "Add to a feature\u2019s brief",
|
|
65978
|
-
description: "Append what the human told you later about a feature to its brief.md, as a dated " + "section, and commit it; on a draft it is added to the parked brief that Start turns " + "into brief.md. The brief is where the human\u2019s intent for the feature lives.
|
|
66078
|
+
description: "Append what the human told you later about a feature to its brief.md, as a dated " + "section, and commit it; on a draft it is added to the parked brief that Start turns " + "into brief.md. The brief is where the human\u2019s intent for the feature lives.",
|
|
65979
66079
|
inputSchema: {
|
|
65980
66080
|
slug: SlugInput,
|
|
65981
66081
|
text: z6.string().min(1).describe("What the human said, in your words, as markdown.")
|
|
65982
66082
|
}
|
|
65983
66083
|
}, toolAddToBrief);
|
|
65984
66084
|
}
|
|
65985
|
-
var toolStartFeature, toolPlanFeature, toolStopFeatureRun, toolDrive,
|
|
66085
|
+
var toolStartFeature, toolPlanFeature, toolStopFeatureRun, toolDrive, toolMergeFeature, toolAddToBrief, SlugInput;
|
|
65986
66086
|
var init_orchestrator_tools = __esm(() => {
|
|
65987
66087
|
init_src();
|
|
65988
66088
|
init_paths();
|
|
@@ -65997,20 +66097,18 @@ var init_orchestrator_tools = __esm(() => {
|
|
|
65997
66097
|
init_git();
|
|
65998
66098
|
init_merge();
|
|
65999
66099
|
init_repo();
|
|
66000
|
-
init_settings();
|
|
66001
|
-
init_test_notes();
|
|
66002
66100
|
toolStartFeature = orchestratorTool(async (ctx, session, input) => {
|
|
66003
|
-
const feature =
|
|
66101
|
+
const feature = requireProjectFeature(ctx, session, input.slug);
|
|
66004
66102
|
const started = await startDraft(ctx, feature.id, { baseBranch: input.baseBranch });
|
|
66005
66103
|
return { slug: started.slug, status: started.status, baseBranch: started.baseBranch };
|
|
66006
66104
|
});
|
|
66007
66105
|
toolPlanFeature = orchestratorTool(async (ctx, session, input, launch3 = {}) => {
|
|
66008
|
-
const feature =
|
|
66106
|
+
const feature = requireProjectFeature(ctx, session, input.slug);
|
|
66009
66107
|
const { sessionId } = await launchSession(ctx, { featureId: feature.id, kind: "chat", purpose: "self-plan" }, launch3);
|
|
66010
66108
|
return { slug: feature.slug, sessionId };
|
|
66011
66109
|
});
|
|
66012
66110
|
toolStopFeatureRun = orchestratorTool((ctx, session, input) => {
|
|
66013
|
-
const feature =
|
|
66111
|
+
const feature = requireProjectFeature(ctx, session, input.slug);
|
|
66014
66112
|
const run7 = activeSessionsForFeature(ctx, feature.id).find(isStoppableHeadlessRun);
|
|
66015
66113
|
if (!run7)
|
|
66016
66114
|
throw new GateError(`\`${feature.slug}\` has no headless run going, so there is nothing to stop.`);
|
|
@@ -66018,23 +66116,19 @@ var init_orchestrator_tools = __esm(() => {
|
|
|
66018
66116
|
});
|
|
66019
66117
|
toolDrive = orchestratorTool(async (ctx, session, input) => {
|
|
66020
66118
|
if (input.slug === undefined) {
|
|
66021
|
-
const project3 = requireProjectById(ctx, requireOrchestratorSession(
|
|
66119
|
+
const project3 = requireProjectById(ctx, requireOrchestratorSession(session));
|
|
66022
66120
|
return projectDrive(ctx, project3, input.action);
|
|
66023
66121
|
}
|
|
66024
|
-
const feature =
|
|
66122
|
+
const feature = requireProjectFeature(ctx, session, input.slug);
|
|
66025
66123
|
requireNotDraft(feature);
|
|
66026
66124
|
return testDrive(ctx, projectForFeature(ctx, feature), feature, input.action);
|
|
66027
66125
|
});
|
|
66028
|
-
toolRecordTestNote = orchestratorTool((ctx, session, input) => {
|
|
66029
|
-
const feature = requireOrchestratedFeature(ctx, session, input.slug);
|
|
66030
|
-
return addNote2(ctx, feature.id, input.text, input.observedBy === "human" ? "human" : "agent");
|
|
66031
|
-
});
|
|
66032
66126
|
toolMergeFeature = orchestratorTool(async (ctx, session, input) => {
|
|
66033
|
-
const feature =
|
|
66127
|
+
const feature = requireProjectFeature(ctx, session, input.slug);
|
|
66034
66128
|
return mergeFeatureFlow(ctx, feature.id);
|
|
66035
66129
|
});
|
|
66036
66130
|
toolAddToBrief = orchestratorTool(async (ctx, session, input) => {
|
|
66037
|
-
const feature =
|
|
66131
|
+
const feature = requireProjectFeature(ctx, session, input.slug);
|
|
66038
66132
|
const text3 = input.text.trim();
|
|
66039
66133
|
if (!text3)
|
|
66040
66134
|
throw new InvalidInputError("there is nothing to add: the text is empty.");
|
|
@@ -66069,18 +66163,32 @@ var init_orchestrator_tools = __esm(() => {
|
|
|
66069
66163
|
// src/mcp/orchestrator-run-tools.ts
|
|
66070
66164
|
import * as z7 from "zod";
|
|
66071
66165
|
async function toolSendToFeature(ctx, session, input) {
|
|
66072
|
-
const feature =
|
|
66073
|
-
|
|
66166
|
+
const feature = requireProjectFeature(ctx, session, input.slug);
|
|
66167
|
+
const from = input.relaysHuman ? RELAYED_HUMAN_LINE : FROM_ORCHESTRATOR_LINE;
|
|
66168
|
+
await deliverToLead(ctx, feature, { text: `${from}
|
|
66169
|
+
|
|
66170
|
+
${input.message}`, source: "human" });
|
|
66074
66171
|
return { deliveredToLead: true };
|
|
66075
66172
|
}
|
|
66173
|
+
function toolReadFeatureChat(ctx, session, input) {
|
|
66174
|
+
const feature = requireProjectFeature(ctx, session, input.slug);
|
|
66175
|
+
const { quote, transcriptPath } = mainChatExcerpt(ctx, feature);
|
|
66176
|
+
return {
|
|
66177
|
+
slug: feature.slug,
|
|
66178
|
+
quote,
|
|
66179
|
+
transcriptPath,
|
|
66180
|
+
...quote === null ? { reason: "no chat on disk yet" } : {}
|
|
66181
|
+
};
|
|
66182
|
+
}
|
|
66076
66183
|
function registerOrchestratorRunTools(server, wants, resolve11) {
|
|
66077
66184
|
if (wants("send_to_feature")) {
|
|
66078
66185
|
server.registerTool("send_to_feature", {
|
|
66079
66186
|
title: "Send a feature a message",
|
|
66080
|
-
description: "Send a message to
|
|
66187
|
+
description: "Send a message to a feature's agent. It arrives marked as from the project's " + "orchestrator, not the human, and still counts as a human message: it lets the agent " + "start a round again after a Stop. Set relaysHuman only when the message is the " + "human's own answer, passed on as they gave it; then it arrives marked as theirs, relayed.",
|
|
66081
66188
|
inputSchema: {
|
|
66082
66189
|
slug: z7.string().describe("The slug of the feature."),
|
|
66083
|
-
message: z7.string().min(1).describe("What to change and why, or what you found.
|
|
66190
|
+
message: z7.string().min(1).describe("What to change and why, or what you found."),
|
|
66191
|
+
relaysHuman: z7.boolean().optional().describe("True when the message is the human's own answer, relayed.")
|
|
66084
66192
|
}
|
|
66085
66193
|
}, orchestratorTool(async (args2, extra) => {
|
|
66086
66194
|
const rs = await resolve11(extra);
|
|
@@ -66089,9 +66197,23 @@ function registerOrchestratorRunTools(server, wants, resolve11) {
|
|
|
66089
66197
|
return ok(await toolSendToFeature(rs.ctx, rs.session, args2));
|
|
66090
66198
|
}));
|
|
66091
66199
|
}
|
|
66200
|
+
if (wants("read_feature_chat")) {
|
|
66201
|
+
server.registerTool("read_feature_chat", {
|
|
66202
|
+
title: "Read a feature\u2019s chat",
|
|
66203
|
+
description: "The last turns of a feature's chat, for reading what its agent said or asked. " + "Returns the quote and the transcript path; the quote is null, with a reason, when " + "the feature has no chat on disk yet.",
|
|
66204
|
+
inputSchema: { slug: z7.string().describe("The slug of the feature.") }
|
|
66205
|
+
}, orchestratorTool(async (args2, extra) => {
|
|
66206
|
+
const rs = await resolve11(extra);
|
|
66207
|
+
if (!rs)
|
|
66208
|
+
return noSession();
|
|
66209
|
+
return ok(toolReadFeatureChat(rs.ctx, rs.session, args2));
|
|
66210
|
+
}));
|
|
66211
|
+
}
|
|
66092
66212
|
}
|
|
66213
|
+
var FROM_ORCHESTRATOR_LINE = "From the project's orchestrator, not the human:", RELAYED_HUMAN_LINE = "The human's own answer, relayed by the project's orchestrator:";
|
|
66093
66214
|
var init_orchestrator_run_tools = __esm(() => {
|
|
66094
66215
|
init_lead_delivery();
|
|
66216
|
+
init_main_chat_excerpt();
|
|
66095
66217
|
init_orchestrator_tools();
|
|
66096
66218
|
});
|
|
66097
66219
|
|
|
@@ -66234,6 +66356,7 @@ function featureContext(ctx, reader, scenarios) {
|
|
|
66234
66356
|
driveActive: activeTestDriveFeatureId(project3.id) === feature.id,
|
|
66235
66357
|
stopped: feature.stoppedAt !== null,
|
|
66236
66358
|
waitingReason: feature.waitingReason,
|
|
66359
|
+
humanTypedAt: humanTypedAt(ctx, feature.id),
|
|
66237
66360
|
dependsOn: dependsOnOf(ctx, feature.id).map(dependencyMark),
|
|
66238
66361
|
blocks: blocksOf(ctx, feature.id).map((b) => ({ slug: b.slug, upNext: b.upNext })),
|
|
66239
66362
|
upNext: feature.upNext,
|
|
@@ -66425,7 +66548,8 @@ function requireChatFeatureId(session) {
|
|
|
66425
66548
|
return featureId;
|
|
66426
66549
|
}
|
|
66427
66550
|
function toolStartDesignServer(ctx, session, input) {
|
|
66428
|
-
|
|
66551
|
+
const featureId = requireChatFeatureId(session);
|
|
66552
|
+
return startDesignServer(ctx, featureId, input.command, session.headless ? { sessionId: session.id } : {});
|
|
66429
66553
|
}
|
|
66430
66554
|
function emitPanelShow(ctx, featureId, input) {
|
|
66431
66555
|
const width = input.view === "design" ? input.width : undefined;
|
|
@@ -66721,6 +66845,9 @@ function createFeatureProject(ctx, session, input) {
|
|
|
66721
66845
|
return projectForFeature(ctx, getFeatureRow(ctx, requireFeatureId(session)));
|
|
66722
66846
|
}
|
|
66723
66847
|
async function toolCreateFeature(ctx, session, input) {
|
|
66848
|
+
return session.kind === "project" ? runAsActor("orchestrator", () => createFeatureFor(ctx, session, input)) : createFeatureFor(ctx, session, input);
|
|
66849
|
+
}
|
|
66850
|
+
async function createFeatureFor(ctx, session, input) {
|
|
66724
66851
|
const project3 = createFeatureProject(ctx, session, input);
|
|
66725
66852
|
const dependsOn = input.dependsOn ?? [];
|
|
66726
66853
|
if (input.upNext) {
|
|
@@ -66748,8 +66875,6 @@ async function toolCreateFeature(ctx, session, input) {
|
|
|
66748
66875
|
brief: input.brief,
|
|
66749
66876
|
draft: input.draft
|
|
66750
66877
|
});
|
|
66751
|
-
if (session.purpose === "orchestrate")
|
|
66752
|
-
markOrchestrated(ctx, feature.id, "created");
|
|
66753
66878
|
if (dependencyIds.length === 0) {
|
|
66754
66879
|
return { id: feature.id, slug: feature.slug, branch: feature.branch, status: feature.status };
|
|
66755
66880
|
}
|
|
@@ -66861,7 +66986,6 @@ function toolReadAdr(ctx, session, input) {
|
|
|
66861
66986
|
}
|
|
66862
66987
|
function featureIndexLine(feature) {
|
|
66863
66988
|
const edges = feature.dependsOn.length ? ` \xB7 waits on: ${feature.dependsOn.map((d) => d.satisfied ? `${d.slug} \u2713` : d.slug).join(", ")}` : "";
|
|
66864
|
-
const owner = feature.orchestrated ? " \xB7 orchestrated" : "";
|
|
66865
66989
|
if (feature.status !== "active") {
|
|
66866
66990
|
const mark = {
|
|
66867
66991
|
upNext: ", up next",
|
|
@@ -66870,7 +66994,7 @@ function featureIndexLine(feature) {
|
|
|
66870
66994
|
readyToStart: "",
|
|
66871
66995
|
backlog: ""
|
|
66872
66996
|
}[unstartedState(feature, feature.dependsOn) ?? "backlog"];
|
|
66873
|
-
return `${feature.slug} \u2014 ${feature.title} [${feature.status}${mark}]${edges}
|
|
66997
|
+
return `${feature.slug} \u2014 ${feature.title} [${feature.status}${mark}]${edges}`;
|
|
66874
66998
|
}
|
|
66875
66999
|
const state = [feature.activeRun ? "in flight: round running" : "in flight"];
|
|
66876
67000
|
const counts = feature.ticketCounts;
|
|
@@ -66878,7 +67002,7 @@ function featureIndexLine(feature) {
|
|
|
66878
67002
|
state.push(`${counts.pending} pending`);
|
|
66879
67003
|
if (counts.burning > 0)
|
|
66880
67004
|
state.push(`${counts.burning} burning`);
|
|
66881
|
-
return `${feature.slug} \u2014 ${feature.title} [${state.join(", ")}]${edges}
|
|
67005
|
+
return `${feature.slug} \u2014 ${feature.title} [${state.join(", ")}]${edges}`;
|
|
66882
67006
|
}
|
|
66883
67007
|
function toolReadFeatureBrief(ctx, session, input) {
|
|
66884
67008
|
const project3 = requireProject(ctx, session);
|
|
@@ -67284,7 +67408,7 @@ function buildMcpServer(audience) {
|
|
|
67284
67408
|
if (wants("get_feature_context")) {
|
|
67285
67409
|
const config2 = {
|
|
67286
67410
|
title: "Get feature context",
|
|
67287
|
-
description: "Everything true of the current feature, as a summary that never grows past what you " + "can see. It opens with a header: the feature row, `rounds` (every round so " + "far, numbered from 1, with its tickets, landed/failed seqs and gate result), " + "`latestReady` (your newest ready declaration), `driveActive` (the human is " + "test-driving this feature \u2014 no round starts), `stopped` (the human pressed Stop and has " + "not replied \u2014 no round starts), `waitingReason`, `dependsOn` (the " + "features it waits on to merge, each `satisfied` once shipped) and `blocks` (the " + "drafts waiting on this feature, each with its `upNext` mark) and `upNext` (this " + "feature\u2019s own mark: it starts by itself when its dependencies ship), `annotatedModels` (the " + "models the operator described a use case for \u2014 the only ones `emit_tickets` may " + "assign; empty when they annotated none), `burnConcurrency` (how many tickets this " + "project burns at once \u2014 budget a batch\u2019s blocking edges against it), `latestRun` (how " + "the newest burn went; absent only on a feature that has never burned), `scenarios` " + "(`setUp`: the base branch has .runcastle/scenarios/ and a runner is recorded \u2014 only then " + "does a ticket ship a scenario for state the person driving would otherwise build by " + "hand; plus `runnerConfigured`, the `scenarios` at this branch and the `_` `fixtures` " + "to reuse), " + "`reviewEvidence` (where the latest review pass left its DIGEST.md, screenshots and " + "walkthrough \u2014 read them before planning more work; null until a review has run). Then " + "every ticket as a row " + "with its goal; a ticket\u2019s context, acceptance criteria and the burner\u2019s digest are in " + "`get_ticket({ seq })`. On a feature too large for that, a row marked " + "`goalNotInlined` and the settled tickets named in `ticketsNotInlined` were moved out " + "whole: fetch them (`get_ticket`, `list_tickets`) before acting on those tickets. " + "Then the to-do in full: `openDefects` (every defect no fix or resolution has closed) " + "and `findings` (the rest of what the latest review reported). Last, " + "the canonical docs (brief, decisions, spec) in full while they fit; any doc in " + "`notInlined` was too large to include and MUST be read before acting " + "(`read_feature_doc` or its absPath). `moreDocs` indexes every other doc in " + "docs/features/<slug>/."
|
|
67411
|
+
description: "Everything true of the current feature, as a summary that never grows past what you " + "can see. It opens with a header: the feature row, `rounds` (every round so " + "far, numbered from 1, with its tickets, landed/failed seqs and gate result), " + "`latestReady` (your newest ready declaration), `driveActive` (the human is " + "test-driving this feature \u2014 no round starts), `stopped` (the human pressed Stop and has " + "not replied \u2014 no round starts), `waitingReason`, `humanTypedAt` (when the human last typed in this feature\u2019s chat, " + "ms since the epoch, or null), `dependsOn` (the " + "features it waits on to merge, each `satisfied` once shipped) and `blocks` (the " + "drafts waiting on this feature, each with its `upNext` mark) and `upNext` (this " + "feature\u2019s own mark: it starts by itself when its dependencies ship), `annotatedModels` (the " + "models the operator described a use case for \u2014 the only ones `emit_tickets` may " + "assign; empty when they annotated none), `burnConcurrency` (how many tickets this " + "project burns at once \u2014 budget a batch\u2019s blocking edges against it), `latestRun` (how " + "the newest burn went; absent only on a feature that has never burned), `scenarios` " + "(`setUp`: the base branch has .runcastle/scenarios/ and a runner is recorded \u2014 only then " + "does a ticket ship a scenario for state the person driving would otherwise build by " + "hand; plus `runnerConfigured`, the `scenarios` at this branch and the `_` `fixtures` " + "to reuse), " + "`reviewEvidence` (where the latest review pass left its DIGEST.md, screenshots and " + "walkthrough \u2014 read them before planning more work; null until a review has run). Then " + "every ticket as a row " + "with its goal; a ticket\u2019s context, acceptance criteria and the burner\u2019s digest are in " + "`get_ticket({ seq })`. On a feature too large for that, a row marked " + "`goalNotInlined` and the settled tickets named in `ticketsNotInlined` were moved out " + "whole: fetch them (`get_ticket`, `list_tickets`) before acting on those tickets. " + "Then the to-do in full: `openDefects` (every defect no fix or resolution has closed) " + "and `findings` (the rest of what the latest review reported). Last, " + "the canonical docs (brief, decisions, spec) in full while they fit; any doc in " + "`notInlined` was too large to include and MUST be read before acting " + "(`read_feature_doc` or its absPath). `moreDocs` indexes every other doc in " + "docs/features/<slug>/."
|
|
67288
67412
|
};
|
|
67289
67413
|
if (projectReads) {
|
|
67290
67414
|
server.registerTool("get_feature_context", {
|
|
@@ -67523,7 +67647,7 @@ function buildMcpServer(audience) {
|
|
|
67523
67647
|
if (wants("start_design_server")) {
|
|
67524
67648
|
server.registerTool("start_design_server", {
|
|
67525
67649
|
title: "Start the design server",
|
|
67526
|
-
description: "Run the project\u2019s own frontend dev server in your worktree so the human can see a " + "preview page live. Runcastle picks a free port and passes it as the env var " + "RUNCASTLE_DESIGN_PORT: put it in your command, pinned (for Vite: `bunx vite --port " + "$RUNCASTLE_DESIGN_PORT --strictPort`). Write the `$RUNCASTLE_DESIGN_PORT` form on every " + "OS, Windows included: runcastle substitutes the port before running, and records your " + "command as written. It returns { url, port, command } once the " + "server prints its localhost URL, or fails with the output tail after a minute without " + "one. The command is recorded in `.runcastle/design.json` \u2014 commit that file with your " + "design \u2014 so later calls may omit it. One server per feature: calling again returns " + "the running one; a different command restarts it. It stops when this session ends, " + "when the feature ships, or after 30 minutes with the design view closed.",
|
|
67650
|
+
description: "Run the project\u2019s own frontend dev server in your worktree so the human can see a " + "preview page live. Runcastle picks a free port and passes it as the env var " + "RUNCASTLE_DESIGN_PORT: put it in your command, pinned (for Vite: `bunx vite --port " + "$RUNCASTLE_DESIGN_PORT --strictPort`). Write the `$RUNCASTLE_DESIGN_PORT` form on every " + "OS, Windows included: runcastle substitutes the port before running, and records your " + "command as written. It returns { url, port, command } once the " + "server prints its localhost URL, or fails with the output tail after a minute without " + "one. The command is recorded in `.runcastle/design.json` \u2014 commit that file with your " + "design \u2014 so later calls may omit it. One server per feature: calling again returns " + "the running one; a different command restarts it. It stops when this session ends " + "(a headless run: when the run ends, unless the human has opened it since), " + "when the feature ships, or after 30 minutes with the design view closed.",
|
|
67527
67651
|
inputSchema: {
|
|
67528
67652
|
command: z8.string().min(1).optional().describe("The shell command that serves the preview pages on $RUNCASTLE_DESIGN_PORT. Omit it " + "to run the command recorded in `.runcastle/design.json`.")
|
|
67529
67653
|
}
|
|
@@ -67595,6 +67719,7 @@ var init_server = __esm(() => {
|
|
|
67595
67719
|
init_sessions();
|
|
67596
67720
|
init_features();
|
|
67597
67721
|
init_chat_branch();
|
|
67722
|
+
init_lead_status();
|
|
67598
67723
|
init_dependencies();
|
|
67599
67724
|
init_design();
|
|
67600
67725
|
init_events();
|
|
@@ -67677,13 +67802,12 @@ var init_server = __esm(() => {
|
|
|
67677
67802
|
report_finding: ["run"],
|
|
67678
67803
|
add_test_note: [],
|
|
67679
67804
|
record_event: [...FEATURE_KINDS, ...PROJECT_KINDS, "scenario"],
|
|
67680
|
-
take_over_feature: ["project"],
|
|
67681
67805
|
start_feature: ["project"],
|
|
67682
67806
|
plan_feature: ["project"],
|
|
67683
67807
|
send_to_feature: ["project"],
|
|
67808
|
+
read_feature_chat: ["project"],
|
|
67684
67809
|
stop_feature_run: ["project"],
|
|
67685
67810
|
drive: ["project"],
|
|
67686
|
-
record_test_note: ["project"],
|
|
67687
67811
|
merge_feature: ["project"],
|
|
67688
67812
|
add_to_brief: ["project"]
|
|
67689
67813
|
};
|
|
@@ -67964,7 +68088,7 @@ var init_lead_guard = __esm(() => {
|
|
|
67964
68088
|
// src/routes/hooks.ts
|
|
67965
68089
|
import { Hono as Hono2 } from "hono";
|
|
67966
68090
|
function waiterKeyOf(session) {
|
|
67967
|
-
if (session.
|
|
68091
|
+
if (session.kind === "project")
|
|
67968
68092
|
return session.id;
|
|
67969
68093
|
if (isLiveLeadSession(session) && session.featureId)
|
|
67970
68094
|
return leadMessageKey(session.featureId);
|
|
@@ -68110,7 +68234,7 @@ function handleLeadPrompt(ctx, feature, payload) {
|
|
|
68110
68234
|
releaseOrchestratorWaiter(leadMessageKey(feature.id), "superseded");
|
|
68111
68235
|
const prompt = typeof payload?.prompt === "string" ? payload.prompt : undefined;
|
|
68112
68236
|
if (prompt !== undefined && !isRuncastlePrompt(prompt))
|
|
68113
|
-
markHumanMessage(ctx, feature.id);
|
|
68237
|
+
markHumanMessage(ctx, feature.id, { typed: true });
|
|
68114
68238
|
}
|
|
68115
68239
|
function kickLeadAtStart(ctx, sessionId) {
|
|
68116
68240
|
const session = getSessionRow(ctx, sessionId);
|
|
@@ -68187,11 +68311,13 @@ function sessionStartContext(ctx, feature) {
|
|
|
68187
68311
|
return acc;
|
|
68188
68312
|
}, {});
|
|
68189
68313
|
const ticketSummary = tickets2.length === 0 ? "none yet" : `${tickets2.length} (${Object.entries(byStatus).map(([s, n]) => `${n} ${s}`).join(", ")})`;
|
|
68314
|
+
const featureRuns = listRunsByFeature(ctx, feature.id);
|
|
68190
68315
|
return [
|
|
68191
68316
|
`[runcastle] ${feature.title} \u2014 ${feature.oneLiner}`,
|
|
68192
68317
|
`status: ${featureStateLine(ctx, feature)}; branch: ${feature.branch}`,
|
|
68193
68318
|
`docs: docs/features/${feature.slug}/ (brief.md, decisions.md, spec.md)`,
|
|
68194
68319
|
`tickets: ${ticketSummary}`,
|
|
68320
|
+
`rounds run: ${featureRuns.length}; latest run: ${featureRuns[0]?.status ?? "none"}`,
|
|
68195
68321
|
"Call the runcastle MCP tool `get_feature_context` for the full docs + tickets."
|
|
68196
68322
|
].join(`
|
|
68197
68323
|
`);
|
|
@@ -68204,6 +68330,7 @@ var init_hooks = __esm(() => {
|
|
|
68204
68330
|
init_install_guard();
|
|
68205
68331
|
init_lead_guard();
|
|
68206
68332
|
init_runtime();
|
|
68333
|
+
init_untouched();
|
|
68207
68334
|
init_sessions();
|
|
68208
68335
|
init_events();
|
|
68209
68336
|
init_git();
|
|
@@ -68262,8 +68389,10 @@ var init_hooks = __esm(() => {
|
|
|
68262
68389
|
return c.json({});
|
|
68263
68390
|
if (event === "user-prompt" || event === "stop")
|
|
68264
68391
|
checkInFromTurn(ctx, session, body.payload);
|
|
68265
|
-
if (event === "user-prompt")
|
|
68392
|
+
if (event === "user-prompt") {
|
|
68266
68393
|
markAgentWorking(ctx, sessionId);
|
|
68394
|
+
forgetUntouched(sessionId);
|
|
68395
|
+
}
|
|
68267
68396
|
if (event === "stop") {
|
|
68268
68397
|
markAwaitingInput(ctx, sessionId);
|
|
68269
68398
|
const feature2 = session.featureId ? tryGetFeature(ctx, session.featureId) : null;
|
|
@@ -68533,7 +68662,7 @@ var init_stream = __esm(() => {
|
|
|
68533
68662
|
const stop3 = () => {
|
|
68534
68663
|
done10 = true;
|
|
68535
68664
|
};
|
|
68536
|
-
const
|
|
68665
|
+
const unsubscribe2 = subscribeLive((signal) => {
|
|
68537
68666
|
pending3.set(dedupeKey(signal), signal);
|
|
68538
68667
|
});
|
|
68539
68668
|
sse.onAbort(stop3);
|
|
@@ -68561,7 +68690,7 @@ var init_stream = __esm(() => {
|
|
|
68561
68690
|
}
|
|
68562
68691
|
}
|
|
68563
68692
|
} finally {
|
|
68564
|
-
|
|
68693
|
+
unsubscribe2();
|
|
68565
68694
|
c.req.raw.signal.removeEventListener("abort", stop3);
|
|
68566
68695
|
}
|
|
68567
68696
|
}));
|
|
@@ -68983,8 +69112,9 @@ var init_feature = __esm(() => {
|
|
|
68983
69112
|
kind: SessionKind,
|
|
68984
69113
|
kickoffLine: z12.string().min(1).optional(),
|
|
68985
69114
|
purpose: SessionPurpose.optional(),
|
|
68986
|
-
purposeData: MergeBranchPair.optional()
|
|
68987
|
-
|
|
69115
|
+
purposeData: MergeBranchPair.optional(),
|
|
69116
|
+
auto: z12.boolean().optional()
|
|
69117
|
+
})).mutation(({ ctx, input: { auto, ...input } }) => auto ? autoResumeChat(ctx, input) : launchSession(ctx, input)),
|
|
68988
69118
|
fixDrive: publicProcedure.input(z12.object({ featureId: z12.string() })).mutation(({ ctx, input }) => launchDriveFixSession(ctx, { featureId: input.featureId })),
|
|
68989
69119
|
openSideSession: publicProcedure.input(z12.object({ featureId: z12.string(), reopenSessionId: z12.string().optional() })).mutation(async ({ ctx, input }) => {
|
|
68990
69120
|
const { sessionId } = await launchSideSession(ctx, input);
|
|
@@ -69332,8 +69462,9 @@ var init_project = __esm(() => {
|
|
|
69332
69462
|
projectId: z13.string(),
|
|
69333
69463
|
fresh: z13.boolean().optional(),
|
|
69334
69464
|
resumeSessionId: z13.string().optional(),
|
|
69335
|
-
purpose: z13.literal("triage").optional()
|
|
69336
|
-
|
|
69465
|
+
purpose: z13.literal("triage").optional(),
|
|
69466
|
+
auto: z13.boolean().optional()
|
|
69467
|
+
})).mutation(({ ctx, input: { auto, ...input } }) => auto ? autoResumeProjectChat(ctx, input) : launchProjectSession(ctx, input)),
|
|
69337
69468
|
projectSession: publicProcedure.input(z13.object({ projectId: z13.string() })).query(({ ctx, input }) => activeProjectSession(ctx, input.projectId, "project")),
|
|
69338
69469
|
conversations: publicProcedure.input(z13.object({ projectId: z13.string() })).query(({ ctx, input }) => listProjectConversations(ctx, input.projectId)),
|
|
69339
69470
|
conversationTranscript: publicProcedure.input(z13.object({ sessionId: z13.string() })).query(({ ctx, input }) => conversationTranscript(ctx, input.sessionId)),
|