runcastle 1.4.6 → 1.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/runcastle.js CHANGED
@@ -4640,6 +4640,63 @@ var init_git = __esm(() => {
4640
4640
  ];
4641
4641
  });
4642
4642
 
4643
+ // src/doctor/system-exec.ts
4644
+ import { spawn as spawn4 } from "child_process";
4645
+ function createSystemExec(opts = {}) {
4646
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_EXEC_TIMEOUT_MS;
4647
+ const containFn = opts.containFn ?? containHostProcess;
4648
+ return (command, args) => new Promise((resolve4) => {
4649
+ const { file, args: spawnArgs } = resolveSpawnTarget(command, args);
4650
+ let stdout = "";
4651
+ let stderr = "";
4652
+ let settled = false;
4653
+ const settle = (outcome) => {
4654
+ if (settled)
4655
+ return;
4656
+ settled = true;
4657
+ clearTimeout(timer);
4658
+ resolve4(outcome);
4659
+ };
4660
+ const child = spawn4(file, spawnArgs, { cwd: opts.cwd, windowsHide: true });
4661
+ const containment = child.pid === undefined ? null : containFn(child.pid);
4662
+ const timer = setTimeout(() => {
4663
+ if (containment)
4664
+ containment.kill();
4665
+ else if (child.pid !== undefined)
4666
+ killProcessTree(child.pid);
4667
+ const note = `${command} ${args.join(" ")} timed out after ${Math.round(timeoutMs / 1000)}s`;
4668
+ settle({
4669
+ ok: true,
4670
+ code: null,
4671
+ stdout,
4672
+ stderr: stderr ? `${stderr}
4673
+ ${note}` : note,
4674
+ timedOut: true
4675
+ });
4676
+ }, timeoutMs);
4677
+ child.stdout?.on("data", (d) => {
4678
+ stdout += d.toString();
4679
+ });
4680
+ child.stderr?.on("data", (d) => {
4681
+ stderr += d.toString();
4682
+ });
4683
+ child.on("error", (err) => {
4684
+ containment?.kill();
4685
+ settle({ ok: false, code: null, stdout, stderr: stderr || String(err) });
4686
+ });
4687
+ child.on("close", (code) => {
4688
+ containment?.kill();
4689
+ settle({ ok: true, code: code ?? 0, stdout, stderr });
4690
+ });
4691
+ });
4692
+ }
4693
+ var DEFAULT_EXEC_TIMEOUT_MS = 60000;
4694
+ var init_system_exec = __esm(() => {
4695
+ init_kill_tree();
4696
+ init_contain_host();
4697
+ init_resolve_executable();
4698
+ });
4699
+
4643
4700
  // src/launcher/edit-guard.ts
4644
4701
  import { isAbsolute, relative, resolve as resolve4 } from "path";
4645
4702
  function guardsEdits(kind) {
@@ -4759,6 +4816,64 @@ var init_install_guard = __esm(() => {
4759
4816
  HEREDOC_OPENER = /(?<!<)<<(?!<)(-?)[ \t]*(?:'([^'\n]+)'|"([^"\n]+)"|\\?([\w.-]+))/g;
4760
4817
  });
4761
4818
 
4819
+ // src/launcher/skills-root.ts
4820
+ import { existsSync as existsSync7 } from "fs";
4821
+ import { dirname as dirname3, join as join7, resolve as resolve6 } from "path";
4822
+ import { fileURLToPath as fileURLToPath3 } from "url";
4823
+ function resolveSkillsRoot(fromDir) {
4824
+ const override = process.env[SKILLS_DIR_ENV];
4825
+ if (override) {
4826
+ const root = resolve6(override);
4827
+ if (existsSync7(join7(root, SKILLS_MARKER)))
4828
+ return root;
4829
+ throw new Error(`${SKILLS_DIR_ENV}=${override} is not a runcastle skills root (missing ${SKILLS_MARKER})`);
4830
+ }
4831
+ const searched = [];
4832
+ let dir = fromDir;
4833
+ for (let i = 0;i < 8; i += 1) {
4834
+ const candidate = join7(dir, "packages", "skills");
4835
+ searched.push(candidate);
4836
+ if (existsSync7(join7(candidate, SKILLS_MARKER)))
4837
+ return candidate;
4838
+ const parent = dirname3(dir);
4839
+ if (parent === dir)
4840
+ break;
4841
+ dir = parent;
4842
+ }
4843
+ throw new Error(`runcastle skills root not found; searched:
4844
+ ${searched.join(`
4845
+ `)}`);
4846
+ }
4847
+ function resolvePluginDir(fromDir = dirname3(fileURLToPath3(import.meta.url))) {
4848
+ const rel = join7("packages", "skills", "packs", "runcastle");
4849
+ const override = process.env[SKILLS_DIR_ENV];
4850
+ if (override) {
4851
+ const dir2 = join7(resolve6(override), "packs", "runcastle");
4852
+ if (existsSync7(dir2))
4853
+ return dir2;
4854
+ throw new Error(`${SKILLS_DIR_ENV}=${override} has no plugin dir at ${dir2}`);
4855
+ }
4856
+ const searched = [];
4857
+ let dir = fromDir;
4858
+ for (let i = 0;i < 8; i += 1) {
4859
+ const candidate = join7(dir, rel);
4860
+ searched.push(candidate);
4861
+ if (existsSync7(candidate))
4862
+ return candidate;
4863
+ const parent = dirname3(dir);
4864
+ if (parent === dir)
4865
+ break;
4866
+ dir = parent;
4867
+ }
4868
+ throw new Error(`runcastle plugin dir (${rel}) not found; searched:
4869
+ ${searched.join(`
4870
+ `)}`);
4871
+ }
4872
+ var SKILLS_DIR_ENV = "RUNCASTLE_SKILLS_DIR", SKILLS_MARKER;
4873
+ var init_skills_root = __esm(() => {
4874
+ SKILLS_MARKER = join7("packs", "runcastle");
4875
+ });
4876
+
4762
4877
  // src/launcher/runtimes/skills.ts
4763
4878
  function skillRef(runtime, skill) {
4764
4879
  return SPELLINGS[runtime](skill);
@@ -4814,10 +4929,10 @@ var init_skills = __esm(() => {
4814
4929
 
4815
4930
  // src/launcher/artifacts.ts
4816
4931
  import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync2 } from "fs";
4817
- import { fileURLToPath as fileURLToPath3 } from "url";
4818
- import { join as join7 } from "path";
4932
+ import { fileURLToPath as fileURLToPath4 } from "url";
4933
+ import { join as join8 } from "path";
4819
4934
  function hookClientPath() {
4820
- return resolveAsset(ASSET_ENV.hookClient, fileURLToPath3(new URL("./hook-client.ts", import.meta.url)));
4935
+ return resolveAsset(ASSET_ENV.hookClient, fileURLToPath4(new URL("./hook-client.ts", import.meta.url)));
4821
4936
  }
4822
4937
  function serverUrlFor(config2) {
4823
4938
  return `http://localhost:${config2.serverPort}`;
@@ -5261,6 +5376,10 @@ function renderProjectPrompt(brief, runtime = DEFAULT_RUNTIME) {
5261
5376
  ].join(`
5262
5377
  `);
5263
5378
  }
5379
+ function skillPackReadRule(packDir) {
5380
+ const posix2 = packDir.replace(/\\/g, "/").replace(/^([A-Za-z]):/, (_, drive) => `/${drive.toLowerCase()}`).replace(/\/+$/, "");
5381
+ return `Read(/${posix2}/**)`;
5382
+ }
5264
5383
  function entrySkillDenyRules(kind) {
5265
5384
  const allowed = new Set(entrySkillsFor(kind));
5266
5385
  return ENTRY_SKILLS.filter((skill) => !allowed.has(skill)).flatMap((skill) => [
@@ -5279,7 +5398,7 @@ function sessionBashAllowRules(kind) {
5279
5398
  }
5280
5399
  return SESSION_BASH_ALLOW_RULES;
5281
5400
  }
5282
- function renderSettings(hookClient, kind) {
5401
+ function renderSettings(hookClient, kind, skillPackDir) {
5283
5402
  const cmd = (event) => ({
5284
5403
  type: "command",
5285
5404
  command: `bun run "${hookClient}" ${event}`,
@@ -5287,7 +5406,11 @@ function renderSettings(hookClient, kind) {
5287
5406
  });
5288
5407
  return {
5289
5408
  permissions: {
5290
- allow: [...RUNCASTLE_MCP_ALLOW_RULES, ...sessionBashAllowRules(kind)],
5409
+ allow: [
5410
+ ...RUNCASTLE_MCP_ALLOW_RULES,
5411
+ ...sessionBashAllowRules(kind),
5412
+ ...skillPackDir ? [skillPackReadRule(skillPackDir)] : []
5413
+ ],
5291
5414
  deny: entrySkillDenyRules(kind)
5292
5415
  },
5293
5416
  hooks: {
@@ -5348,15 +5471,15 @@ function renderSessionPrompt(input) {
5348
5471
  return renderProjectPrompt(projectBrief, runtime);
5349
5472
  throw new Error(`session ${session.id} has no feature and no project-session brief`);
5350
5473
  }
5351
- async function writeSessionArtifacts(input) {
5474
+ async function writeSessionArtifacts(input, pluginDir = resolvePluginDir()) {
5352
5475
  const { session, config: config2 } = input;
5353
5476
  const dir = sessionDir(session.id);
5354
5477
  mkdirSync3(dir, { recursive: true });
5355
- const systemPromptPath = join7(dir, "system-prompt.md");
5356
- const settingsPath = join7(dir, "settings.json");
5357
- const mcpConfigPath = join7(dir, "mcp.json");
5478
+ const systemPromptPath = join8(dir, "system-prompt.md");
5479
+ const settingsPath = join8(dir, "settings.json");
5480
+ const mcpConfigPath = join8(dir, "mcp.json");
5358
5481
  writeFileSync2(systemPromptPath, renderSessionPrompt(input), "utf8");
5359
- writeFileSync2(settingsPath, JSON.stringify(renderSettings(hookClientPath(), session.kind), null, 2), "utf8");
5482
+ writeFileSync2(settingsPath, JSON.stringify(renderSettings(hookClientPath(), session.kind, pluginDir), null, 2), "utf8");
5360
5483
  writeFileSync2(mcpConfigPath, JSON.stringify(renderMcpConfig(session, config2), null, 2), "utf8");
5361
5484
  return { systemPromptPath, settingsPath, mcpConfigPath };
5362
5485
  }
@@ -5368,6 +5491,7 @@ var init_artifacts = __esm(() => {
5368
5491
  init_asset_paths();
5369
5492
  init_edit_guard();
5370
5493
  init_install_guard();
5494
+ init_skills_root();
5371
5495
  init_skills();
5372
5496
  RUNCASTLE_MCP_ALLOW_RULES = [
5373
5497
  "mcp__runcastle__get_feature_context",
@@ -5416,64 +5540,6 @@ var init_artifacts = __esm(() => {
5416
5540
  SESSION_START_SOURCES = ["startup", "resume", "clear", "compact", "fork"];
5417
5541
  });
5418
5542
 
5419
- // src/launcher/skills-root.ts
5420
- import { existsSync as existsSync7 } from "fs";
5421
- import { dirname as dirname3, join as join8, resolve as resolve6 } from "path";
5422
- import { fileURLToPath as fileURLToPath4 } from "url";
5423
- function resolveSkillsRoot(fromDir) {
5424
- const override = process.env[SKILLS_DIR_ENV];
5425
- if (override) {
5426
- const root = resolve6(override);
5427
- if (existsSync7(join8(root, SKILLS_MARKER)))
5428
- return root;
5429
- throw new Error(`${SKILLS_DIR_ENV}=${override} is not a runcastle skills root (missing ${SKILLS_MARKER})`);
5430
- }
5431
- const searched = [];
5432
- let dir = fromDir;
5433
- for (let i = 0;i < 8; i += 1) {
5434
- const candidate = join8(dir, "packages", "skills");
5435
- searched.push(candidate);
5436
- if (existsSync7(join8(candidate, SKILLS_MARKER)))
5437
- return candidate;
5438
- const parent = dirname3(dir);
5439
- if (parent === dir)
5440
- break;
5441
- dir = parent;
5442
- }
5443
- throw new Error(`runcastle skills root not found; searched:
5444
- ${searched.join(`
5445
- `)}`);
5446
- }
5447
- function resolvePluginDir(fromDir = dirname3(fileURLToPath4(import.meta.url))) {
5448
- const rel = join8("packages", "skills", "packs", "runcastle");
5449
- const override = process.env[SKILLS_DIR_ENV];
5450
- if (override) {
5451
- const dir2 = join8(resolve6(override), "packs", "runcastle");
5452
- if (existsSync7(dir2))
5453
- return dir2;
5454
- throw new Error(`${SKILLS_DIR_ENV}=${override} has no plugin dir at ${dir2}`);
5455
- }
5456
- const searched = [];
5457
- let dir = fromDir;
5458
- for (let i = 0;i < 8; i += 1) {
5459
- const candidate = join8(dir, rel);
5460
- searched.push(candidate);
5461
- if (existsSync7(candidate))
5462
- return candidate;
5463
- const parent = dirname3(dir);
5464
- if (parent === dir)
5465
- break;
5466
- dir = parent;
5467
- }
5468
- throw new Error(`runcastle plugin dir (${rel}) not found; searched:
5469
- ${searched.join(`
5470
- `)}`);
5471
- }
5472
- var SKILLS_DIR_ENV = "RUNCASTLE_SKILLS_DIR", SKILLS_MARKER;
5473
- var init_skills_root = __esm(() => {
5474
- SKILLS_MARKER = join8("packs", "runcastle");
5475
- });
5476
-
5477
5543
  // src/launcher/runtimes/types.ts
5478
5544
  function assertHeadlessKickoff(headless, line) {
5479
5545
  if (headless && !line)
@@ -5511,8 +5577,27 @@ function buildClaudeArgs(input) {
5511
5577
  ...input.kickoffLine ? [input.kickoffLine] : []
5512
5578
  ];
5513
5579
  }
5514
- var CC_NESTING_ENV, KICKOFF_LINES, CLAUDE_BIN = "claude", claudeRuntime;
5580
+ function findBackgroundSessionIn(listing, ccSessionId) {
5581
+ if (!Array.isArray(listing))
5582
+ return;
5583
+ const held = listing.find((entry) => typeof entry === "object" && entry !== null && entry.sessionId === ccSessionId && entry.kind === "background");
5584
+ if (!held)
5585
+ return;
5586
+ return { sessionId: ccSessionId, shortId: typeof held.id === "string" ? held.id : ccSessionId.slice(0, 8) };
5587
+ }
5588
+ async function listClaudeAgents() {
5589
+ try {
5590
+ const out = await createSystemExec({ timeoutMs: AGENTS_LISTING_TIMEOUT_MS })(CLAUDE_BIN, ["agents", "--json"]);
5591
+ if (!out.ok || out.timedOut)
5592
+ return;
5593
+ return JSON.parse(out.stdout);
5594
+ } catch {
5595
+ return;
5596
+ }
5597
+ }
5598
+ var CC_NESTING_ENV, KICKOFF_LINES, AGENTS_LISTING_TIMEOUT_MS = 1e4, CLAUDE_BIN = "claude", claudeRuntime;
5515
5599
  var init_claude = __esm(() => {
5600
+ init_system_exec();
5516
5601
  init_resolve_executable();
5517
5602
  init_artifacts();
5518
5603
  init_skills_root();
@@ -5544,11 +5629,12 @@ var init_claude = __esm(() => {
5544
5629
  };
5545
5630
  },
5546
5631
  async writeArtifacts(input) {
5547
- const files = await writeSessionArtifacts({ ...input, runtime: this.id });
5632
+ const pluginDir = resolvePluginDir();
5633
+ const files = await writeSessionArtifacts({ ...input, runtime: this.id }, pluginDir);
5548
5634
  return {
5549
5635
  files: [files.systemPromptPath, files.settingsPath, files.mcpConfigPath],
5550
5636
  argv: buildClaudeArgs({
5551
- pluginDir: resolvePluginDir(),
5637
+ pluginDir,
5552
5638
  settingsPath: files.settingsPath,
5553
5639
  mcpConfigPath: files.mcpConfigPath,
5554
5640
  systemPromptPath: files.systemPromptPath,
@@ -5561,13 +5647,17 @@ var init_claude = __esm(() => {
5561
5647
  }),
5562
5648
  env: {
5563
5649
  RUNCASTLE_SESSION_ID: input.session.id,
5564
- RUNCASTLE_SERVER_URL: input.serverUrl
5650
+ RUNCASTLE_SERVER_URL: input.serverUrl,
5651
+ CLAUDE_CODE_DISABLE_AGENT_VIEW: "1"
5565
5652
  },
5566
5653
  envScrub: CC_NESTING_ENV
5567
5654
  };
5568
5655
  },
5569
5656
  kickoffLine(kind) {
5570
5657
  return KICKOFF_LINES[kind];
5658
+ },
5659
+ async findBackgroundSession(ccSessionId) {
5660
+ return findBackgroundSessionIn(await listClaudeAgents(), ccSessionId);
5571
5661
  }
5572
5662
  };
5573
5663
  });
@@ -8214,7 +8304,7 @@ var init_chat_branch = __esm(() => {
8214
8304
  });
8215
8305
 
8216
8306
  // src/workflows/kill-registry.ts
8217
- import { spawn as spawn4 } from "child_process";
8307
+ import { spawn as spawn5 } from "child_process";
8218
8308
  function log2(msg) {
8219
8309
  console.error(`[kill-registry] ${msg}`);
8220
8310
  }
@@ -8231,7 +8321,7 @@ function runDockerCommand(args) {
8231
8321
  const timer = setTimeout(() => settle(false), DOCKER_COMMAND_TIMEOUT_MS);
8232
8322
  timer.unref?.();
8233
8323
  try {
8234
- const child = spawn4("docker", args, { windowsHide: true, stdio: "ignore" });
8324
+ const child = spawn5("docker", args, { windowsHide: true, stdio: "ignore" });
8235
8325
  child.on("exit", (code) => settle(code === 0));
8236
8326
  child.on("close", (code) => settle(code === 0));
8237
8327
  child.on("error", () => settle(false));
@@ -8362,63 +8452,6 @@ var init_kill_registry = __esm(() => {
8362
8452
  REGISTRY_KEY2 = Symbol.for("runcastle.kill.registry");
8363
8453
  });
8364
8454
 
8365
- // src/doctor/system-exec.ts
8366
- import { spawn as spawn5 } from "child_process";
8367
- function createSystemExec(opts = {}) {
8368
- const timeoutMs = opts.timeoutMs ?? DEFAULT_EXEC_TIMEOUT_MS;
8369
- const containFn = opts.containFn ?? containHostProcess;
8370
- return (command, args) => new Promise((resolve9) => {
8371
- const { file, args: spawnArgs } = resolveSpawnTarget(command, args);
8372
- let stdout = "";
8373
- let stderr = "";
8374
- let settled = false;
8375
- const settle = (outcome) => {
8376
- if (settled)
8377
- return;
8378
- settled = true;
8379
- clearTimeout(timer);
8380
- resolve9(outcome);
8381
- };
8382
- const child = spawn5(file, spawnArgs, { cwd: opts.cwd, windowsHide: true });
8383
- const containment = child.pid === undefined ? null : containFn(child.pid);
8384
- const timer = setTimeout(() => {
8385
- if (containment)
8386
- containment.kill();
8387
- else if (child.pid !== undefined)
8388
- killProcessTree(child.pid);
8389
- const note = `${command} ${args.join(" ")} timed out after ${Math.round(timeoutMs / 1000)}s`;
8390
- settle({
8391
- ok: true,
8392
- code: null,
8393
- stdout,
8394
- stderr: stderr ? `${stderr}
8395
- ${note}` : note,
8396
- timedOut: true
8397
- });
8398
- }, timeoutMs);
8399
- child.stdout?.on("data", (d) => {
8400
- stdout += d.toString();
8401
- });
8402
- child.stderr?.on("data", (d) => {
8403
- stderr += d.toString();
8404
- });
8405
- child.on("error", (err) => {
8406
- containment?.kill();
8407
- settle({ ok: false, code: null, stdout, stderr: stderr || String(err) });
8408
- });
8409
- child.on("close", (code) => {
8410
- containment?.kill();
8411
- settle({ ok: true, code: code ?? 0, stdout, stderr });
8412
- });
8413
- });
8414
- }
8415
- var DEFAULT_EXEC_TIMEOUT_MS = 60000;
8416
- var init_system_exec = __esm(() => {
8417
- init_kill_tree();
8418
- init_contain_host();
8419
- init_resolve_executable();
8420
- });
8421
-
8422
8455
  // src/services/settings.ts
8423
8456
  import { existsSync as existsSync13, mkdirSync as mkdirSync7, readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "fs";
8424
8457
  import { dirname as dirname6 } from "path";
@@ -57724,7 +57757,8 @@ function runCommand2(bin, args2) {
57724
57757
  const timer = setTimeout(() => settle(false), COMMAND_TIMEOUT_MS);
57725
57758
  timer.unref?.();
57726
57759
  try {
57727
- const child = spawn8(bin, [...args2], { windowsHide: true, stdio: "ignore" });
57760
+ const target = spawnTargetFor(bin, [...args2]);
57761
+ const child = spawn8(target.file, target.args, { windowsHide: true, stdio: "ignore" });
57728
57762
  child.on("exit", (code) => settle(code === 0));
57729
57763
  child.on("close", (code) => settle(code === 0));
57730
57764
  child.on("error", () => settle(false));
@@ -57790,6 +57824,7 @@ async function reapRecorder(ticketId, options = {}) {
57790
57824
  var AGENT_BROWSER_BIN = "agent-browser", DEFAULT_TIMEOUT_MS2 = 1e4, POLL_INTERVAL_MS = 100, COMMAND_TIMEOUT_MS = 5000, REAL_DEPS2;
57791
57825
  var init_recorder_reap = __esm(() => {
57792
57826
  init_paths();
57827
+ init_resolve_executable();
57793
57828
  REAL_DEPS2 = {
57794
57829
  findBrowser: () => findOnPath(AGENT_BROWSER_BIN),
57795
57830
  fileExists: existsSync16,
@@ -58400,7 +58435,10 @@ function couldNotReview(ticket, reason, digest) {
58400
58435
  **${headline2}**` : `**${headline2}**`
58401
58436
  };
58402
58437
  }
58403
- var FFMPEG_BIN = "ffmpeg", spawnVersion = (path2) => spawnSync(path2, ["--version"], { timeout: 3000, stdio: "ignore" }), withheldDrives, DECLARATION, REVIEW_DIRECTORY_FS;
58438
+ var FFMPEG_BIN = "ffmpeg", spawnVersion = (path2) => {
58439
+ const { file, args: args2 } = spawnTargetFor(path2, ["--version"]);
58440
+ return spawnSync(file, args2, { timeout: 3000, stdio: "ignore", windowsHide: true });
58441
+ }, withheldDrives, DECLARATION, REVIEW_DIRECTORY_FS;
58404
58442
  var init_review_ticket = __esm(() => {
58405
58443
  init_src();
58406
58444
  init_paths();
@@ -58410,6 +58448,7 @@ var init_review_ticket = __esm(() => {
58410
58448
  init_agent_stream();
58411
58449
  init_git();
58412
58450
  init_review_findings();
58451
+ init_resolve_executable();
58413
58452
  init_kill_registry();
58414
58453
  init_recorder_reap();
58415
58454
  init_recorder_reap();
@@ -62837,6 +62876,24 @@ async function commitDocsCheckpoint(ctx, session, summary) {
62837
62876
  }
62838
62877
  await landChatCommits(ctx, project3, feature);
62839
62878
  }
62879
+ function checkpointDocsAfterReply(ctx, session, summary) {
62880
+ let queue = docsCheckpointQueues.get(session.worktreePath);
62881
+ if (!queue) {
62882
+ queue = createSerialQueue();
62883
+ docsCheckpointQueues.set(session.worktreePath, queue);
62884
+ }
62885
+ queue(() => commitDocsCheckpoint(ctx, session, summary)).catch((e) => {
62886
+ try {
62887
+ emit(ctx, requireFeatureId(session), {
62888
+ type: "git.commit_pending",
62889
+ message: `docs checkpoint failed: ${e instanceof Error ? e.message : String(e)}`,
62890
+ data: { summary }
62891
+ });
62892
+ } catch (err) {
62893
+ console.error(`docs checkpoint failed for session ${session.id}`, e, err);
62894
+ }
62895
+ });
62896
+ }
62840
62897
  function requireProject(ctx, session) {
62841
62898
  if (!session.projectId) {
62842
62899
  throw new GateError(`this tool belongs to a project-scoped session; this is a ${session.kind} session on a ` + "feature. Use get_feature_context and the pipeline tools for feature work.");
@@ -63577,7 +63634,7 @@ function buildMcpServer(audience) {
63577
63634
  if (!rs)
63578
63635
  return noSession();
63579
63636
  const result = toolEmitTickets(rs.ctx, rs.session, args2);
63580
- await commitDocsCheckpoint(rs.ctx, rs.session, `tickets emitted (${result.stored})`);
63637
+ checkpointDocsAfterReply(rs.ctx, rs.session, `tickets emitted (${result.stored})`);
63581
63638
  return ok(result);
63582
63639
  });
63583
63640
  }
@@ -63599,7 +63656,7 @@ function buildMcpServer(audience) {
63599
63656
  if (!rs)
63600
63657
  return noSession();
63601
63658
  const result = toolUpdateTicket(rs.ctx, rs.session, args2);
63602
- await commitDocsCheckpoint(rs.ctx, rs.session, `ticket ${result.ticket.seq} updated`);
63659
+ checkpointDocsAfterReply(rs.ctx, rs.session, `ticket ${result.ticket.seq} updated`);
63603
63660
  return ok(result);
63604
63661
  });
63605
63662
  }
@@ -63616,7 +63673,7 @@ function buildMcpServer(audience) {
63616
63673
  if (!rs)
63617
63674
  return noSession();
63618
63675
  const result = toolCancelTicket(rs.ctx, rs.session, args2);
63619
- await commitDocsCheckpoint(rs.ctx, rs.session, `ticket ${result.ticket.seq} cancelled`);
63676
+ checkpointDocsAfterReply(rs.ctx, rs.session, `ticket ${result.ticket.seq} cancelled`);
63620
63677
  return ok(result);
63621
63678
  });
63622
63679
  }
@@ -63660,7 +63717,7 @@ function buildMcpServer(audience) {
63660
63717
  return noSession();
63661
63718
  const result = toolCompletePhase(rs.ctx, rs.session, args2);
63662
63719
  if (result.ok) {
63663
- await commitDocsCheckpoint(rs.ctx, rs.session, `phase '${args2.phase}' complete`);
63720
+ checkpointDocsAfterReply(rs.ctx, rs.session, `phase '${args2.phase}' complete`);
63664
63721
  }
63665
63722
  return ok(result);
63666
63723
  });
@@ -63693,7 +63750,7 @@ async function readMcpBody(c) {
63693
63750
  return;
63694
63751
  }
63695
63752
  }
63696
- var DOCS_NOTE, TICKETS_NOTE, REVIEW_TITLE, ResolveFindingShape, ResolveFindingInput, DRAFTING_KINDS, ADRS_NOTE, FEATURE_INDEX_NOTE, FEATURE_INDEX_SHIPPED_CAP = 50, WORK_RECORD_DIGESTS_NOTE, FEATURE_KINDS, PROJECT_KINDS, ALL_AUDIENCES, TOOL_AUDIENCES, CompletablePhase, EventType, mcp, server_default;
63753
+ var DOCS_NOTE, TICKETS_NOTE, REVIEW_TITLE, ResolveFindingShape, ResolveFindingInput, docsCheckpointQueues, DRAFTING_KINDS, ADRS_NOTE, FEATURE_INDEX_NOTE, FEATURE_INDEX_SHIPPED_CAP = 50, WORK_RECORD_DIGESTS_NOTE, FEATURE_KINDS, PROJECT_KINDS, ALL_AUDIENCES, TOOL_AUDIENCES, CompletablePhase, EventType, mcp, server_default;
63697
63754
  var init_server = __esm(() => {
63698
63755
  init_src();
63699
63756
  init_paths();
@@ -63712,6 +63769,7 @@ var init_server = __esm(() => {
63712
63769
  init_review_findings();
63713
63770
  init_git();
63714
63771
  init_knowledge();
63772
+ init_landing_queue();
63715
63773
  init_planning();
63716
63774
  init_project_notes();
63717
63775
  init_repo();
@@ -63736,6 +63794,7 @@ var init_server = __esm(() => {
63736
63794
  });
63737
63795
  }
63738
63796
  });
63797
+ docsCheckpointQueues = new Map;
63739
63798
  DRAFTING_KINDS = ["chat"];
63740
63799
  ADRS_NOTE = "adrs[] is an index of the project\u2019s LIVE decisions (superseded ones are already omitted), not " + "their text. Read the ones your work touches with read_adr({ relPath }) \u2014 they bind you the " + "same either way. They are also plain files in this worktree at docs/adr/.";
63741
63800
  FEATURE_INDEX_NOTE = "featureIndex lists every in-flight feature, then every draft, then the most recently shipped, " + "one `slug \u2014 title [state]` line each. A trailing `\xB7 waits on: a, b \u2713` lists the features it " + "must wait to merge (\u2713 = already shipped), and `[draft, waiting]` marks a draft that cannot " + "start until every unmarked one ships. A shipped feature\u2019s docs live at docs/features/<slug>/ " + "in this worktree; an in-flight or draft feature\u2019s docs live on an unmerged branch. Any " + "feature\u2019s brief is read_feature_brief({ slug }); what one actually did is " + "get_work_record({ featureSlug }).";
@@ -65368,16 +65427,17 @@ function handlePtyExit(ctx, feature, session, meta, exitCode) {
65368
65427
  if (session.runtime === "codex" && feature) {
65369
65428
  noteResolvedMerge(ctx, session, feature).catch(() => {});
65370
65429
  }
65430
+ let resumeFailureReported;
65371
65431
  if (diedBeforeLive && meta.resumeSessionId) {
65372
- emitForSession(ctx, session, {
65373
- type: "session.resume_failed",
65374
- message: `resume failed for ${session.kind} \u2014 the session exited before starting (code ${exitCode ?? "unknown"}); the previous conversation is still resumable`,
65375
- data: {
65376
- sessionId: session.id,
65377
- resumeSessionId: meta.resumeSessionId,
65378
- exitCode: exitCode ?? null
65379
- }
65380
- });
65432
+ const resumeSessionId = meta.resumeSessionId;
65433
+ const report = (background) => emitResumeFailed(ctx, session, resumeSessionId, exitCode, background);
65434
+ if (meta.findBackgroundSession) {
65435
+ resumeFailureReported = meta.findBackgroundSession(resumeSessionId).catch(() => {
65436
+ return;
65437
+ }).then(report);
65438
+ } else {
65439
+ report(undefined);
65440
+ }
65381
65441
  }
65382
65442
  if (session.purpose === "self-plan" && feature) {
65383
65443
  const current2 = getFeatureRow(ctx, feature.id);
@@ -65393,19 +65453,36 @@ function handlePtyExit(ctx, feature, session, meta, exitCode) {
65393
65453
  message: ptyExitMessage(exitCode),
65394
65454
  data: { sessionId: session.id, exitCode: exitCode ?? null }
65395
65455
  });
65456
+ return resumeFailureReported;
65457
+ }
65458
+ function emitResumeFailed(ctx, session, resumeSessionId, exitCode, background) {
65459
+ const exited = `resume failed for ${session.kind} \u2014 the session exited before starting (code ${exitCode ?? "unknown"})`;
65460
+ const short = background?.shortId;
65461
+ emitForSession(ctx, session, {
65462
+ type: "session.resume_failed",
65463
+ message: short ? `${exited}: its conversation is running as a Claude Code background session (${short}), which cannot be resumed here. ` + `Run \`claude attach ${short}\` to look at it, then \`claude stop ${short}\` (stopping keeps the conversation), then relaunch.` : `${exited}; the previous conversation is still resumable`,
65464
+ data: {
65465
+ sessionId: session.id,
65466
+ resumeSessionId,
65467
+ exitCode: exitCode ?? null,
65468
+ ...short ? { backgroundSessionId: short } : {}
65469
+ }
65470
+ });
65396
65471
  }
65397
65472
  function spawnEmbeddedPty(ctx, feature, session, worktreePath, runtime5, spec, meta = {}) {
65398
65473
  const { file, args: args2 } = spawnTargetFor(runtime5.resolveBinary(), spec.argv);
65399
65474
  const env = { ...process.env, ...spec.env };
65400
65475
  for (const key of spec.envScrub)
65401
65476
  delete env[key];
65477
+ const findBackgroundSession = runtime5.findBackgroundSession?.bind(runtime5);
65478
+ const exitMeta = findBackgroundSession ? { ...meta, findBackgroundSession } : meta;
65402
65479
  try {
65403
65480
  const entry = ptyRegistry().create({
65404
65481
  sessionId: session.id,
65405
65482
  cmd: file,
65406
65483
  args: args2,
65407
65484
  opts: { cwd: worktreePath, env, cols: 80, rows: 24, useConpty: true },
65408
- onExit: ({ exitCode }) => handlePtyExit(ctx, feature, session, meta, exitCode)
65485
+ onExit: ({ exitCode }) => void handlePtyExit(ctx, feature, session, exitMeta, exitCode)
65409
65486
  });
65410
65487
  if (feature)
65411
65488
  startDocsWatch(ctx, feature);
@@ -65974,8 +66051,8 @@ var init_transcripts = __esm(() => {
65974
66051
 
65975
66052
  // src/services/conversations.ts
65976
66053
  function withoutKickoff(turns, kind, runtime5) {
65977
- const kickoff = kickoffLineFor(kind, undefined, runtime5);
65978
- return turns.filter((turn) => !(turn.role === "user" && promptMatchesKickoff(kickoff, turn.text)));
66054
+ const kickoffs = [kickoffLineFor(kind, undefined, runtime5), selfPlanKickoffFor(runtime5)];
66055
+ return turns.filter((turn) => !(turn.role === "user" && kickoffs.some((kickoff) => promptMatchesKickoff(kickoff, turn.text))));
65979
66056
  }
65980
66057
  function promptMatchesKickoff(line, prompt) {
65981
66058
  if (!prompt)
@@ -66069,6 +66146,7 @@ var TITLE_MAX = 60, MATCH_PREFIX = 40, INTERRUPTED = "[Request interrupted by us
66069
66146
  var init_conversations = __esm(() => {
66070
66147
  init_src();
66071
66148
  init_sessions();
66149
+ init_skills();
66072
66150
  init_transcripts();
66073
66151
  });
66074
66152