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