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