@schoolai/shipyard 3.19.0 → 3.20.0

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.
Files changed (32) hide show
  1. package/dist/capability-detector-worker.js +4 -4
  2. package/dist/{chunk-KMMF5R3M.js → chunk-FMHVDAJK.js} +3 -3
  3. package/dist/{chunk-FASB7KFF.js → chunk-LYUR7G6B.js} +2 -2
  4. package/dist/{chunk-NRXFTDBH.js → chunk-MD32Y6U4.js} +2 -2
  5. package/dist/{chunk-LVQLRQKF.js → chunk-OBYRPZRA.js} +40 -10
  6. package/dist/chunk-OBYRPZRA.js.map +1 -0
  7. package/dist/{chunk-6TK5KO76.js → chunk-OC75AUKJ.js} +33 -5
  8. package/dist/chunk-OC75AUKJ.js.map +1 -0
  9. package/dist/{chunk-4XU7QHYE.js → chunk-QNEFPC4Y.js} +2 -2
  10. package/dist/{chunk-JEIJ4LIM.js → chunk-SECZGRK2.js} +3 -3
  11. package/dist/{chunk-JEIJ4LIM.js.map → chunk-SECZGRK2.js.map} +1 -1
  12. package/dist/{chunk-HD66ZG4O.js → chunk-TJ4TMMMB.js} +3 -3
  13. package/dist/chunk-TJ4TMMMB.js.map +1 -0
  14. package/dist/cursor-runner.js +3 -3
  15. package/dist/electron-utility.js +2 -2
  16. package/dist/index.js +4 -4
  17. package/dist/{login-RU3BXPEI.js → login-VNZ6AQDV.js} +3 -3
  18. package/dist/{plan-backfill-CKSC7OK3.js → plan-backfill-ZNL2YQP6.js} +4 -4
  19. package/dist/{serve-AA4FCUY6.js → serve-WG7HQAYG.js} +653 -202
  20. package/dist/{serve-AA4FCUY6.js.map → serve-WG7HQAYG.js.map} +1 -1
  21. package/dist/{start-RNGMBSQN.js → start-5VG7QWUR.js} +6 -6
  22. package/package.json +1 -1
  23. package/dist/chunk-6TK5KO76.js.map +0 -1
  24. package/dist/chunk-HD66ZG4O.js.map +0 -1
  25. package/dist/chunk-LVQLRQKF.js.map +0 -1
  26. /package/dist/{chunk-KMMF5R3M.js.map → chunk-FMHVDAJK.js.map} +0 -0
  27. /package/dist/{chunk-FASB7KFF.js.map → chunk-LYUR7G6B.js.map} +0 -0
  28. /package/dist/{chunk-NRXFTDBH.js.map → chunk-MD32Y6U4.js.map} +0 -0
  29. /package/dist/{chunk-4XU7QHYE.js.map → chunk-QNEFPC4Y.js.map} +0 -0
  30. /package/dist/{login-RU3BXPEI.js.map → login-VNZ6AQDV.js.map} +0 -0
  31. /package/dist/{plan-backfill-CKSC7OK3.js.map → plan-backfill-ZNL2YQP6.js.map} +0 -0
  32. /package/dist/{start-RNGMBSQN.js.map → start-5VG7QWUR.js.map} +0 -0
@@ -12,7 +12,7 @@ import {
12
12
  guardedSubscribe,
13
13
  makeInitialAttemptState,
14
14
  shutdownFileWatcherGuard
15
- } from "./chunk-4XU7QHYE.js";
15
+ } from "./chunk-QNEFPC4Y.js";
16
16
  import {
17
17
  TRAILER_SCHEMA_VERSION,
18
18
  appendTrailerToMessage,
@@ -39,13 +39,13 @@ import {
39
39
  startPlanBackfillForDaemon,
40
40
  sync,
41
41
  validatePeerId
42
- } from "./chunk-JEIJ4LIM.js";
42
+ } from "./chunk-SECZGRK2.js";
43
43
  import {
44
44
  loadAuthToken
45
45
  } from "./chunk-TFWDJCUJ.js";
46
46
  import {
47
47
  getDaemonVersion
48
- } from "./chunk-KMMF5R3M.js";
48
+ } from "./chunk-FMHVDAJK.js";
49
49
  import {
50
50
  collectPriorCrashDetail,
51
51
  getRecentWasmPanicMessages
@@ -174,7 +174,7 @@ import {
174
174
  unresolveReviewThread,
175
175
  withLabelAsync,
176
176
  writeCursorApiKeyToVault
177
- } from "./chunk-LVQLRQKF.js";
177
+ } from "./chunk-OBYRPZRA.js";
178
178
  import {
179
179
  buildNodeSpawnEnv,
180
180
  nodeSpawnEnvOverlay,
@@ -235,7 +235,7 @@ import {
235
235
  createAssetResolver,
236
236
  toSdkContent,
237
237
  toSdkPermissionMode
238
- } from "./chunk-FASB7KFF.js";
238
+ } from "./chunk-LYUR7G6B.js";
239
239
  import {
240
240
  AGENT_IDS,
241
241
  AGENT_SYSTEM_CLAUDE_CODE,
@@ -306,6 +306,7 @@ import {
306
306
  MERMAID_SECURITY_LEVEL,
307
307
  MessageSchema,
308
308
  OBSOLETE_PREFIXES,
309
+ OPEN_FILE_TOOL,
309
310
  PLAN_EPOCH,
310
311
  PLUGIN_READ_ME_TOOL,
311
312
  PRESENT_TOOL,
@@ -420,7 +421,7 @@ import {
420
421
  unsafeAssertSourceId,
421
422
  vizFileExtension,
422
423
  withPreferredRuntimeAuthMethod
423
- } from "./chunk-6TK5KO76.js";
424
+ } from "./chunk-OC75AUKJ.js";
424
425
  import {
425
426
  AuthGitHubCallbackRequestSchema,
426
427
  AuthGitHubCallbackResponseSchema,
@@ -468,7 +469,7 @@ import {
468
469
  VaultKeyPutResponseSchema,
469
470
  classifyClaudeCodeCompatibility,
470
471
  classifyCodexCompatibility
471
- } from "./chunk-HD66ZG4O.js";
472
+ } from "./chunk-TJ4TMMMB.js";
472
473
  import {
473
474
  external_exports as external_exports2
474
475
  } from "./chunk-EHQITHQX.js";
@@ -4857,28 +4858,133 @@ function getErrorCode(err3) {
4857
4858
 
4858
4859
  // src/services/bootstrap/heartbeat.ts
4859
4860
  import { randomUUID as randomUUID4 } from "crypto";
4861
+
4862
+ // src/services/bootstrap/pid-identity.ts
4863
+ import { execFileSync } from "child_process";
4864
+ function decideProcessIdentity(recorded, live) {
4865
+ if (recorded === null) return "unrecorded";
4866
+ if (live === null) return "unverifiable";
4867
+ return recorded === live ? "ours" : "not-ours";
4868
+ }
4869
+ function readProcessStartTime(pid, exec = defaultExec) {
4870
+ try {
4871
+ const out = exec("ps", ["-p", String(pid), "-o", "lstart="]).trim();
4872
+ return out.length > 0 ? out : null;
4873
+ } catch {
4874
+ return null;
4875
+ }
4876
+ }
4877
+ function defaultExec(command, args) {
4878
+ return execFileSync(command, args, { encoding: "utf8" });
4879
+ }
4880
+
4881
+ // src/services/bootstrap/pid-liveness.ts
4882
+ function probePid(pid, kill = process.kill.bind(process)) {
4883
+ try {
4884
+ kill(pid, 0);
4885
+ return { outcome: "signaled" };
4886
+ } catch (err3) {
4887
+ const code = errorCode(err3);
4888
+ if (code === "ESRCH") return { outcome: "no-process" };
4889
+ if (code === "EPERM") return { outcome: "foreign-owner" };
4890
+ return { outcome: "probe-error", code };
4891
+ }
4892
+ }
4893
+ function errorCode(err3) {
4894
+ if (err3 instanceof Error && "code" in err3) {
4895
+ const { code } = err3;
4896
+ return typeof code === "string" ? code : void 0;
4897
+ }
4898
+ return void 0;
4899
+ }
4900
+ function classifyPidLiveness(probe) {
4901
+ switch (probe.outcome) {
4902
+ case "signaled":
4903
+ return "alive-same-user";
4904
+ case "no-process":
4905
+ return "stale";
4906
+ case "foreign-owner":
4907
+ return "foreign";
4908
+ case "probe-error":
4909
+ return "indeterminate";
4910
+ default:
4911
+ return assertNever(probe);
4912
+ }
4913
+ }
4914
+ function decidePidAcquisition(reading, ctx) {
4915
+ switch (reading.kind) {
4916
+ case "absent":
4917
+ return { action: "reclaim", reason: "no-file" };
4918
+ case "corrupt":
4919
+ return { action: "reclaim", reason: "corrupt" };
4920
+ case "present":
4921
+ return decideForLiveness(reading.liveness, reading.identity, ctx);
4922
+ default:
4923
+ return assertNever(reading);
4924
+ }
4925
+ }
4926
+ function decideForLiveness(liveness, identity, ctx) {
4927
+ switch (liveness) {
4928
+ case "stale":
4929
+ return { action: "reclaim", reason: "stale" };
4930
+ case "foreign":
4931
+ return { action: "reclaim", reason: "foreign" };
4932
+ case "alive-same-user":
4933
+ return decideForSameUser(identity, ctx);
4934
+ case "indeterminate":
4935
+ return { action: "refuse", reason: "indeterminate" };
4936
+ default:
4937
+ return assertNever(liveness);
4938
+ }
4939
+ }
4940
+ function decideForSameUser(identity, ctx) {
4941
+ switch (identity) {
4942
+ case "ours":
4943
+ return { action: "refuse", reason: "live-same-user" };
4944
+ case "not-ours":
4945
+ return { action: "reclaim", reason: "recycled-pid" };
4946
+ case "unverifiable":
4947
+ return { action: "refuse", reason: "identity-unverifiable" };
4948
+ case "unrecorded":
4949
+ return ctx.selfTokenAvailable ? { action: "reclaim", reason: "unrecorded-stale" } : { action: "refuse", reason: "live-same-user" };
4950
+ default:
4951
+ return assertNever(identity);
4952
+ }
4953
+ }
4954
+
4955
+ // src/services/bootstrap/heartbeat.ts
4860
4956
  var DEFAULT_INTERVAL_MS2 = 15e3;
4861
4957
  var DEFAULT_STALE_THRESHOLD_MS = 5 * 6e4;
4862
4958
  function isHeartbeatPayload(value) {
4863
4959
  if (typeof value !== "object" || value === null) return false;
4864
4960
  if (!("pid" in value) || !("timestamp" in value)) return false;
4865
4961
  const { pid, timestamp } = value;
4866
- return typeof pid === "number" && typeof timestamp === "number";
4962
+ if (typeof pid !== "number" || typeof timestamp !== "number") return false;
4963
+ if ("startTime" in value) {
4964
+ const { startTime } = value;
4965
+ if (typeof startTime !== "string") return false;
4966
+ }
4967
+ return true;
4867
4968
  }
4868
4969
  function getErrorCode2(err3) {
4869
4970
  if (!(err3 instanceof Error) || !("code" in err3)) return void 0;
4870
4971
  const { code } = err3;
4871
4972
  return typeof code === "string" ? code : void 0;
4872
4973
  }
4873
- function isProcessRunning(pid) {
4874
- try {
4875
- process.kill(pid, 0);
4876
- return true;
4877
- } catch (err3) {
4878
- if (getErrorCode2(err3) === "ESRCH") {
4974
+ function isPriorDaemonRunning(payload, readStartTime) {
4975
+ const liveness = classifyPidLiveness(probePid(payload.pid));
4976
+ switch (liveness) {
4977
+ case "stale":
4879
4978
  return false;
4979
+ case "foreign":
4980
+ case "indeterminate":
4981
+ return true;
4982
+ case "alive-same-user": {
4983
+ const identity = decideProcessIdentity(payload.startTime ?? null, readStartTime(payload.pid));
4984
+ return identity !== "not-ours";
4880
4985
  }
4881
- return true;
4986
+ default:
4987
+ return assertNever(liveness);
4882
4988
  }
4883
4989
  }
4884
4990
  async function writeHeartbeatAtomic(fs3, pidFilePath, payload) {
@@ -4888,11 +4994,14 @@ async function writeHeartbeatAtomic(fs3, pidFilePath, payload) {
4888
4994
  }
4889
4995
  function startHeartbeat(deps) {
4890
4996
  const intervalMs = deps.intervalMs ?? DEFAULT_INTERVAL_MS2;
4997
+ const readStartTime = deps.readProcessStartTime ?? readProcessStartTime;
4998
+ const ownStartTime = readStartTime(process.pid);
4891
4999
  const writeOnce = async () => {
4892
5000
  try {
4893
5001
  await writeHeartbeatAtomic(deps.fs, deps.pidFilePath, {
4894
5002
  pid: process.pid,
4895
- timestamp: deps.now()
5003
+ timestamp: deps.now(),
5004
+ ...ownStartTime !== null ? { startTime: ownStartTime } : {}
4896
5005
  });
4897
5006
  } catch (err3) {
4898
5007
  deps.log({ event: "daemon_heartbeat_write_failed", err: String(err3) });
@@ -4923,6 +5032,7 @@ function startHeartbeat(deps) {
4923
5032
  }
4924
5033
  async function detectPriorAbnormalExit(deps) {
4925
5034
  const staleThresholdMs = deps.staleThresholdMs ?? DEFAULT_STALE_THRESHOLD_MS;
5035
+ const readStartTime = deps.readProcessStartTime ?? readProcessStartTime;
4926
5036
  let raw;
4927
5037
  try {
4928
5038
  raw = await deps.fs.readFile(deps.pidFilePath, "utf-8");
@@ -4950,7 +5060,7 @@ async function detectPriorAbnormalExit(deps) {
4950
5060
  if (ageMs > staleThresholdMs) {
4951
5061
  return { prior: "unknown" };
4952
5062
  }
4953
- if (isProcessRunning(parsed.pid)) {
5063
+ if (isPriorDaemonRunning(parsed, readStartTime)) {
4954
5064
  return { prior: "clean" };
4955
5065
  }
4956
5066
  return {
@@ -5200,7 +5310,7 @@ function forwardAbnormalExitMetric(report, restartDetail, metrics) {
5200
5310
  }
5201
5311
 
5202
5312
  // src/services/bootstrap/lifecycle.ts
5203
- import { unlinkSync } from "fs";
5313
+ import { readFileSync, unlinkSync } from "fs";
5204
5314
  import { readFile as readFile3, unlink as unlink2, writeFile as writeFile4 } from "fs/promises";
5205
5315
  import { join as join9 } from "path";
5206
5316
 
@@ -5252,67 +5362,14 @@ function getLoroCrdtVersion() {
5252
5362
  return cachedVersion;
5253
5363
  }
5254
5364
 
5255
- // src/services/bootstrap/pid-liveness.ts
5256
- function probePid(pid, kill = process.kill.bind(process)) {
5257
- try {
5258
- kill(pid, 0);
5259
- return { outcome: "signaled" };
5260
- } catch (err3) {
5261
- const code = errorCode(err3);
5262
- if (code === "ESRCH") return { outcome: "no-process" };
5263
- if (code === "EPERM") return { outcome: "foreign-owner" };
5264
- return { outcome: "probe-error", code };
5265
- }
5266
- }
5267
- function errorCode(err3) {
5268
- if (err3 instanceof Error && "code" in err3) {
5269
- const { code } = err3;
5270
- return typeof code === "string" ? code : void 0;
5271
- }
5272
- return void 0;
5273
- }
5274
- function classifyPidLiveness(probe) {
5275
- switch (probe.outcome) {
5276
- case "signaled":
5277
- return "alive-same-user";
5278
- case "no-process":
5279
- return "stale";
5280
- case "foreign-owner":
5281
- return "foreign";
5282
- case "probe-error":
5283
- return "indeterminate";
5284
- default:
5285
- return assertNever(probe);
5286
- }
5287
- }
5288
- function decidePidAcquisition(reading) {
5289
- switch (reading.kind) {
5290
- case "absent":
5291
- return { action: "reclaim", reason: "no-file" };
5292
- case "corrupt":
5293
- return { action: "reclaim", reason: "corrupt" };
5294
- case "present":
5295
- return decideForLiveness(reading.liveness);
5296
- default:
5297
- return assertNever(reading);
5298
- }
5299
- }
5300
- function decideForLiveness(liveness) {
5301
- switch (liveness) {
5302
- case "stale":
5303
- return { action: "reclaim", reason: "stale" };
5304
- case "foreign":
5305
- return { action: "reclaim", reason: "foreign" };
5306
- case "alive-same-user":
5307
- return { action: "refuse", reason: "live-same-user" };
5308
- case "indeterminate":
5309
- return { action: "refuse", reason: "indeterminate" };
5310
- default:
5311
- return assertNever(liveness);
5312
- }
5313
- }
5314
-
5315
5365
  // src/services/bootstrap/lifecycle.ts
5366
+ var PID_META_SUFFIX = ".meta";
5367
+ function isPidMetaPayload(value) {
5368
+ if (typeof value !== "object" || value === null) return false;
5369
+ if (!("pid" in value) || !("startTime" in value)) return false;
5370
+ const { pid, startTime } = value;
5371
+ return typeof pid === "number" && typeof startTime === "string";
5372
+ }
5316
5373
  function extractWasmPanicContext(error) {
5317
5374
  const stack = error instanceof Error && typeof error.stack === "string" ? error.stack : "";
5318
5375
  const frames = stack.split("\n").map((f2) => f2.trim());
@@ -5328,16 +5385,19 @@ var LifecycleManager = class {
5328
5385
  #isShuttingDown = false;
5329
5386
  #cleanupFns = [];
5330
5387
  #pidFilePath = null;
5388
+ #sidecarFilePath = null;
5331
5389
  #log;
5332
5390
  #flushLogger;
5333
5391
  #deps;
5334
5392
  #probePid;
5393
+ #readProcessStartTime;
5335
5394
  #started = false;
5336
5395
  constructor(log, deps, flushLogger2) {
5337
5396
  this.#log = log;
5338
5397
  this.#deps = deps;
5339
5398
  this.#flushLogger = flushLogger2 ?? (() => Promise.resolve());
5340
5399
  this.#probePid = deps.probePid ?? ((pid) => probePid(pid));
5400
+ this.#readProcessStartTime = deps.readProcessStartTime ?? ((pid) => readProcessStartTime(pid));
5341
5401
  }
5342
5402
  start() {
5343
5403
  if (this.#started) return;
@@ -5414,15 +5474,19 @@ var LifecycleManager = class {
5414
5474
  }
5415
5475
  async acquirePidFile(shipyardHome) {
5416
5476
  const pidFilePath = join9(shipyardHome, "daemon.pid");
5417
- const reading = await this.#readPidFile(pidFilePath);
5418
- const decision = decidePidAcquisition(reading);
5477
+ const sidecarPath = pidFilePath + PID_META_SUFFIX;
5478
+ const ownStartTime = this.#readProcessStartTime(process.pid);
5479
+ const reading = await this.#readPidFile(pidFilePath, sidecarPath);
5480
+ const decision = decidePidAcquisition(reading, { selfTokenAvailable: ownStartTime !== null });
5419
5481
  this.#applyPidDecision(decision, reading, pidFilePath);
5482
+ await this.#writeSidecar(sidecarPath, ownStartTime);
5483
+ this.#sidecarFilePath = sidecarPath;
5420
5484
  await writeFile4(pidFilePath, String(process.pid), { mode: 420 });
5421
5485
  this.#pidFilePath = pidFilePath;
5422
5486
  this.#log.info({ pid: process.pid, pidFile: pidFilePath }, "PID file acquired");
5423
5487
  }
5424
5488
  /** GATHER: read + normalize the existing PID file into a `PidFileReading`. */
5425
- async #readPidFile(pidFilePath) {
5489
+ async #readPidFile(pidFilePath, sidecarPath) {
5426
5490
  let existing;
5427
5491
  try {
5428
5492
  existing = await readFile3(pidFilePath, "utf-8");
@@ -5433,27 +5497,63 @@ var LifecycleManager = class {
5433
5497
  const raw = existing.trim();
5434
5498
  const pid = Number.parseInt(raw, 10);
5435
5499
  if (!Number.isInteger(pid) || pid <= 0) return { kind: "corrupt", raw };
5436
- return { kind: "present", pid, liveness: classifyPidLiveness(this.#probePid(pid)) };
5500
+ const liveness = classifyPidLiveness(this.#probePid(pid));
5501
+ const identity = liveness === "alive-same-user" ? this.#readIdentity(pid, sidecarPath) : "unrecorded";
5502
+ return { kind: "present", pid, liveness, identity };
5503
+ }
5504
+ /** GATHER + DECIDE: compare the recorded token against the live start time. */
5505
+ #readIdentity(pid, sidecarPath) {
5506
+ const recorded = this.#readRecordedStartTime(pid, sidecarPath);
5507
+ const live = this.#readProcessStartTime(pid);
5508
+ return decideProcessIdentity(recorded, live);
5509
+ }
5510
+ /**
5511
+ * Read the start-time token recorded for `pid` from the sidecar. Returns
5512
+ * `null` when the sidecar is absent, unparseable, or was written for a
5513
+ * different PID (a stale sidecar must not be trusted as this PID's identity).
5514
+ */
5515
+ #readRecordedStartTime(pid, sidecarPath) {
5516
+ let raw;
5517
+ try {
5518
+ raw = readFileSync(sidecarPath, "utf-8");
5519
+ } catch {
5520
+ return null;
5521
+ }
5522
+ let parsed;
5523
+ try {
5524
+ parsed = JSON.parse(raw);
5525
+ } catch {
5526
+ return null;
5527
+ }
5528
+ if (!isPidMetaPayload(parsed) || parsed.pid !== pid) return null;
5529
+ return parsed.startTime;
5530
+ }
5531
+ /**
5532
+ * Persist the identity token alongside `daemon.pid`. When we could not read
5533
+ * our own start time (`null`), remove any stale sidecar rather than writing a
5534
+ * bogus one — a future reader then sees `unrecorded` and picks a safe default.
5535
+ */
5536
+ async #writeSidecar(sidecarPath, startTime) {
5537
+ if (startTime === null) {
5538
+ try {
5539
+ await unlink2(sidecarPath);
5540
+ } catch {
5541
+ }
5542
+ return;
5543
+ }
5544
+ const payload = { pid: process.pid, startTime };
5545
+ await writeFile4(sidecarPath, JSON.stringify(payload), { mode: 420 });
5437
5546
  }
5438
5547
  /** EXECUTE: log the decision and refuse (exit) when mutual exclusion applies. */
5439
5548
  #applyPidDecision(decision, reading, pidFilePath) {
5440
5549
  if (decision.action === "refuse") {
5441
5550
  const pid = reading.kind === "present" ? reading.pid : null;
5442
- if (decision.reason === "live-same-user") {
5443
- this.#log.error(
5444
- { event: "pid_live_daemon_refused", pid, pidFile: pidFilePath },
5445
- "Another daemon is already running. Stop it first or remove the stale PID file."
5446
- );
5447
- } else {
5448
- this.#log.error(
5449
- { event: "pid_probe_indeterminate_refused", pid, pidFile: pidFilePath },
5450
- "Cannot determine if existing daemon is alive \u2014 refusing to overwrite PID file."
5451
- );
5452
- }
5551
+ this.#logRefusal(decision.reason, pid, pidFilePath);
5453
5552
  process.exit(1);
5454
5553
  }
5455
5554
  const reclaimedPid = reading.kind === "present" ? reading.pid : null;
5456
- switch (decision.reason) {
5555
+ const reason = decision.reason;
5556
+ switch (reason) {
5457
5557
  case "no-file":
5458
5558
  return;
5459
5559
  case "corrupt":
@@ -5478,23 +5578,80 @@ var LifecycleManager = class {
5478
5578
  "PID file points at a process owned by another user \u2014 not our daemon, reclaiming"
5479
5579
  );
5480
5580
  return;
5581
+ case "recycled-pid":
5582
+ this.#log.info(
5583
+ { event: "pid_reclaimed_recycled", stalePid: reclaimedPid, pidFile: pidFilePath },
5584
+ "PID file points at a recycled PID (start time differs) \u2014 not our daemon, reclaiming"
5585
+ );
5586
+ return;
5587
+ case "unrecorded-stale":
5588
+ this.#log.info(
5589
+ { event: "pid_reclaimed_unrecorded", stalePid: reclaimedPid, pidFile: pidFilePath },
5590
+ "PID file has no identity sidecar (pre-fix leftover) \u2014 treating as stale and reclaiming"
5591
+ );
5592
+ return;
5593
+ default:
5594
+ assertNever(reason);
5481
5595
  }
5482
5596
  }
5597
+ #logRefusal(reason, pid, pidFilePath) {
5598
+ switch (reason) {
5599
+ case "live-same-user":
5600
+ this.#log.error(
5601
+ { event: "pid_live_daemon_refused", pid, pidFile: pidFilePath },
5602
+ "Another daemon is already running. Stop it first or remove the stale PID file."
5603
+ );
5604
+ return;
5605
+ case "identity-unverifiable":
5606
+ this.#log.error(
5607
+ { event: "pid_identity_unverifiable_refused", pid, pidFile: pidFilePath },
5608
+ "Existing PID is alive but its identity could not be verified \u2014 refusing to overwrite PID file."
5609
+ );
5610
+ return;
5611
+ case "indeterminate":
5612
+ this.#log.error(
5613
+ { event: "pid_probe_indeterminate_refused", pid, pidFile: pidFilePath },
5614
+ "Cannot determine if existing daemon is alive \u2014 refusing to overwrite PID file."
5615
+ );
5616
+ return;
5617
+ default:
5618
+ assertNever(reason);
5619
+ }
5620
+ }
5621
+ /**
5622
+ * Remove `daemon.pid` BEFORE the sidecar (mirror of the acquire ordering):
5623
+ * once `daemon.pid` is gone a reader treats the slot as free regardless of
5624
+ * the sidecar, so we never expose "pid present, sidecar gone" during teardown.
5625
+ */
5483
5626
  #removePidFileSync() {
5484
- if (!this.#pidFilePath) return;
5627
+ if (this.#pidFilePath) {
5628
+ try {
5629
+ unlinkSync(this.#pidFilePath);
5630
+ } catch {
5631
+ }
5632
+ this.#pidFilePath = null;
5633
+ }
5634
+ if (!this.#sidecarFilePath) return;
5485
5635
  try {
5486
- unlinkSync(this.#pidFilePath);
5636
+ unlinkSync(this.#sidecarFilePath);
5487
5637
  } catch {
5488
5638
  }
5489
- this.#pidFilePath = null;
5639
+ this.#sidecarFilePath = null;
5490
5640
  }
5491
5641
  async #removePidFile() {
5492
- if (!this.#pidFilePath) return;
5642
+ if (this.#pidFilePath) {
5643
+ try {
5644
+ await unlink2(this.#pidFilePath);
5645
+ } catch {
5646
+ }
5647
+ this.#pidFilePath = null;
5648
+ }
5649
+ if (!this.#sidecarFilePath) return;
5493
5650
  try {
5494
- await unlink2(this.#pidFilePath);
5651
+ await unlink2(this.#sidecarFilePath);
5495
5652
  } catch {
5496
5653
  }
5497
- this.#pidFilePath = null;
5654
+ this.#sidecarFilePath = null;
5498
5655
  }
5499
5656
  /**
5500
5657
  * Wasm-specific recovery path. Runs when a panic originates inside
@@ -5828,7 +5985,7 @@ function buildReloadDocsFromStorage(repoRef, log) {
5828
5985
  }
5829
5986
 
5830
5987
  // src/services/bootstrap/signaling.ts
5831
- import { mkdirSync, readFileSync, writeFileSync } from "fs";
5988
+ import { mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
5832
5989
  import { hostname } from "os";
5833
5990
  import { dirname as dirname5, join as join12 } from "path";
5834
5991
 
@@ -5864,7 +6021,7 @@ function nanoid(size = 21) {
5864
6021
  }
5865
6022
 
5866
6023
  // src/services/bootstrap/signaling.ts
5867
- var DAEMON_NPM_VERSION = true ? "3.19.0" : "unknown";
6024
+ var DAEMON_NPM_VERSION = true ? "3.20.0" : "unknown";
5868
6025
  function createDaemonSignaling(config) {
5869
6026
  const agentId = config.agentId ?? nanoid();
5870
6027
  function send(msg) {
@@ -5895,7 +6052,7 @@ function createDaemonSignaling(config) {
5895
6052
  function getOrCreateAgentId(shipyardHome) {
5896
6053
  const filePath = join12(shipyardHome, "agent-id");
5897
6054
  try {
5898
- const existing = readFileSync(filePath, "utf-8").trim();
6055
+ const existing = readFileSync2(filePath, "utf-8").trim();
5899
6056
  if (existing) return existing;
5900
6057
  } catch {
5901
6058
  }
@@ -7583,10 +7740,21 @@ function runOnCloseCleanup(isLive, cleanup) {
7583
7740
  cleanup.liveOnly.deleteEpochEntry();
7584
7741
  }
7585
7742
  function runTaskSearch(args) {
7586
- const { daemon, controlHandler, logAdapter, requestId, query: query3, limit, includeReviews, projectId } = args;
7743
+ const {
7744
+ daemon,
7745
+ controlHandler,
7746
+ logAdapter,
7747
+ requestId,
7748
+ query: query3,
7749
+ limit,
7750
+ includeReviews,
7751
+ includeArchived,
7752
+ projectId
7753
+ } = args;
7587
7754
  daemon.taskStateStore.searchTasks(query3, {
7588
7755
  limit: limit ?? 100,
7589
7756
  includeReviews,
7757
+ includeArchived,
7590
7758
  ...projectId !== void 0 && { projectId }
7591
7759
  }).then(({ tasks, truncated }) => {
7592
7760
  controlHandler.sendControl({
@@ -10624,7 +10792,7 @@ import { basename as basename5, join as join19 } from "path";
10624
10792
 
10625
10793
  // src/services/publish/published-artifact-store.ts
10626
10794
  import { createHash, randomUUID as randomUUID7 } from "crypto";
10627
- import { mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync2 } from "fs";
10795
+ import { mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
10628
10796
  import { mkdir as mkdir7, readFile as readFile9, rename as rename6, unlink as unlink5, writeFile as writeFile9 } from "fs/promises";
10629
10797
  import { dirname as dirname9, join as join17, relative as relative3 } from "path";
10630
10798
  import { isDeepStrictEqual } from "util";
@@ -10647,7 +10815,7 @@ async function atomicWrite(filePath, content) {
10647
10815
  function parseRecord(filePath, log) {
10648
10816
  let raw;
10649
10817
  try {
10650
- raw = readFileSync2(filePath, "utf-8");
10818
+ raw = readFileSync3(filePath, "utf-8");
10651
10819
  } catch {
10652
10820
  return null;
10653
10821
  }
@@ -14326,6 +14494,13 @@ function filterOutboundForCollab(msg, collabTaskId, getTaskSync) {
14326
14494
  */
14327
14495
  case "auth_refresh_failed":
14328
14496
  case "open_canvas":
14497
+ /**
14498
+ * open_file: owner-local UI navigation, suppressed for the same reason as
14499
+ * open_canvas. Auto-hijacking a viewer's code panel to a file the owner's
14500
+ * agent opened is intrusive, and the path is owner-cwd-relative so it may
14501
+ * not even resolve on a viewer's tree. Viewers navigate their own file view.
14502
+ */
14503
+ case "open_file":
14329
14504
  case "plugin_data_response":
14330
14505
  case "plugin_auth_status":
14331
14506
  case "plugin_panels_updated":
@@ -15500,6 +15675,7 @@ function routeMessage(msg, callbacks, sendControl, log) {
15500
15675
  query: msg.query,
15501
15676
  limit: msg.limit,
15502
15677
  includeReviews: msg.includeReviews ?? false,
15678
+ includeArchived: msg.includeArchived ?? false,
15503
15679
  projectId: msg.projectId
15504
15680
  });
15505
15681
  break;
@@ -19316,7 +19492,7 @@ function decideCodexAuthFromAccountUpdated(authMode) {
19316
19492
  }
19317
19493
 
19318
19494
  // src/services/serve-factory-helpers.ts
19319
- import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
19495
+ import { existsSync as existsSync3, readFileSync as readFileSync5 } from "fs";
19320
19496
  import { join as join28 } from "path";
19321
19497
 
19322
19498
  // src/shared/capabilities/runtime/rate-limit-account-key.ts
@@ -19713,7 +19889,7 @@ function isSafeStorageReply(msg) {
19713
19889
  }
19714
19890
 
19715
19891
  // src/services/harness/lsp-agent-server.ts
19716
- import { readFileSync as readFileSync3 } from "fs";
19892
+ import { readFileSync as readFileSync4 } from "fs";
19717
19893
  import { basename as basename6, relative as relative4, resolve as resolve6 } from "path";
19718
19894
  import { fileURLToPath, pathToFileURL } from "url";
19719
19895
  var LspPositionSchema = external_exports.object({ line: external_exports.number(), character: external_exports.number() });
@@ -19808,7 +19984,7 @@ function isSecretFile(absolutePath) {
19808
19984
  }
19809
19985
  function readSnippet(absolutePath, line0, context = 2) {
19810
19986
  try {
19811
- const lines = readFileSync3(absolutePath, "utf8").split("\n");
19987
+ const lines = readFileSync4(absolutePath, "utf8").split("\n");
19812
19988
  const start = Math.max(0, line0 - context);
19813
19989
  const end = Math.min(lines.length - 1, line0 + context);
19814
19990
  return lines.slice(start, end + 1).map((l, i) => ` ${start + i + 1} ${l}`).join("\n");
@@ -20203,6 +20379,9 @@ var REGISTER_DELIVERABLE_PROMPT_DOC = [
20203
20379
  "",
20204
20380
  "**Required fields:** `kind` (one of `screenshot`, `video`, `log`, `markdown`, `html`, `json`, `ax-tree`, `dom-snapshot`, `other`), `title`, and exactly one of `assetId` or `filePath`."
20205
20381
  ].join("\n");
20382
+ var OPEN_FILE_PROMPT_DOC = [
20383
+ "`open_file` opens a file in the code-editor side panel \u2014 pass `path` (absolute or workspace-relative) and an optional 1-based `line` to scroll to. Reach for it to surface a file the reviewer should see: right after you edit it, when your reply references specific code, or when pointing them at what to review next. Not a substitute for a diff comment \u2014 use `add_comment` to explain a change; use `open_file` to put the file in front of them."
20384
+ ].join("\n");
20206
20385
  var SKILLS_PROMPT_DOC = [
20207
20386
  "Shipyard curates a set of skills for this task \u2014 reusable, expert instructions for specific workflows. **Before starting domain-specific work, check skills first:** call `list_skills` to see what's available, then `load_skill({ name })` to pull a skill's full instructions (and a directory path for any sibling files it references) before you dive in. A relevant skill saves you re-deriving conventions the team already wrote down. Some skills are also reachable through your own `/name` mentions; for anything in `list_skills`, `load_skill` always works."
20208
20387
  ].join("\n");
@@ -20317,7 +20496,9 @@ var HARNESS_TOOL_REGISTRY = [
20317
20496
  appliesTo: "all",
20318
20497
  modeGate: "task",
20319
20498
  promptDoc: `\`${LSP_RENAME_SYMBOL_TOOL}\` \u2014 ${LSP_RENAME_SYMBOL_DESCRIPTION}`
20320
- }
20499
+ },
20500
+ /** File viewer — gate: task. */
20501
+ { name: OPEN_FILE_TOOL, appliesTo: "all", modeGate: "task", promptDoc: OPEN_FILE_PROMPT_DOC }
20321
20502
  ];
20322
20503
 
20323
20504
  // src/services/harness/system-prompt.ts
@@ -20348,7 +20529,8 @@ var TOOL_SECTIONS = [
20348
20529
  {
20349
20530
  heading: "### Code Navigation (LSP)",
20350
20531
  tools: ["lsp_go_to_definition", "lsp_find_references", "lsp_rename_symbol"]
20351
- }
20532
+ },
20533
+ { heading: "### File Viewer", tools: ["open_file"] }
20352
20534
  ];
20353
20535
  var TOOL_SECTION_HEADINGS = TOOL_SECTIONS.map((s2) => s2.heading);
20354
20536
  function buildToolsBlock(agent, intro) {
@@ -20368,14 +20550,10 @@ function buildToolsBlock(agent, intro) {
20368
20550
  return parts.join("\n\n");
20369
20551
  }
20370
20552
  var CLAUDE_TOOLS = buildToolsBlock("claude-code", null);
20371
- var CODEX_TOOLS = buildToolsBlock(
20372
- "codex",
20373
- "You have access to a Shipyard MCP server. The tools below are MCP tools registered on that server \u2014 call them by their unqualified names (e.g. `add_comment`, `visualize`, `present`). For shell access use the native `shell` tool; for file edits use `apply_patch`; for the live task list use the native `update_plan` tool."
20374
- );
20375
- var CURSOR_TOOLS = buildToolsBlock(
20376
- "cursor",
20377
- "You have access to a Shipyard MCP server. The tools below are MCP tools registered on that server \u2014 call them by their unqualified names (e.g. `add_comment`, `visualize`, `present`). For shell access use the native `Shell` tool; for file edits use `Write` / `Edit`; for the live task list use the `shipyard_update_plan` MCP tool."
20378
- );
20553
+ var CODEX_TOOLS_PREAMBLE = "You have access to a Shipyard MCP server. The tools below are MCP tools registered on that server \u2014 call them by their unqualified names (e.g. `add_comment`, `visualize`, `present`). For shell access use the native `shell` tool; for file edits use `apply_patch`; for the live task list use the native `update_plan` tool; for spawning subagents use the native `spawn_agent` tool. Those native tools are always available even though they are NOT in the MCP tool list below \u2014 do not go looking for them there.";
20554
+ var CURSOR_TOOLS_PREAMBLE = "You have access to a Shipyard MCP server. The tools below are MCP tools registered on that server \u2014 call them by their unqualified names (e.g. `add_comment`, `visualize`, `present`). For shell access use the native `Shell` tool; for file edits use `Write` / `Edit`; for spawning subagents use the native `task` tool; for the live task list use the `shipyard_update_plan` MCP tool. Those native tools are always available even though they are NOT in the MCP tool list below. Careful: the list below also registers a Shipyard MCP tool named `task` \u2014 that one manages sibling tasks (send/cancel/snapshot) and does NOT spawn subagents; only your native `task` tool does.";
20555
+ var CODEX_TOOLS = buildToolsBlock("codex", CODEX_TOOLS_PREAMBLE);
20556
+ var CURSOR_TOOLS = buildToolsBlock("cursor", CURSOR_TOOLS_PREAMBLE);
20379
20557
  function sharedIntro(shellWord) {
20380
20558
  return `# Shipyard
20381
20559
 
@@ -20615,7 +20793,7 @@ var CODEX_NOTES = `## Codex-specific Notes
20615
20793
  - **Plan mode** \u2014 Publish plans by wrapping them in \`<proposed_plan>...</proposed_plan>\` blocks (Codex's native publish marker, parsed out of your streamed text and rendered as a structured plan on the Shipyard canvas). There is no \`ExitPlanMode\` tool. To revise, emit a fresh \`<proposed_plan>\` block in the next turn \u2014 each block is a complete replacement of the prior one. See the \`<plan>\` resource section above for full semantics (sandbox shape, reviewer feedback flow, approval signal).
20616
20794
  - **\`request_user_input\`** \u2014 Codex's tool for asking the user multiple-choice questions during plan refinement. Routes to the same UX as Claude's \`AskUserQuestion\`. Use it whenever you need a decision from the user that materially changes the plan. Options-only \u2014 freeform input (\`options: null\`) and secret input (\`isSecret: true\`) are not yet supported by Shipyard and will return an error suggesting you re-ask with multiple-choice options.
20617
20795
  - **Todo list** \u2014 Use the \`update_plan\` tool to keep the user-visible task list in sync **in non-plan modes only**. Codex blocks \`update_plan\` while you are in plan-collaboration mode and will return an error \u2014 \`update_plan\` is a checklist/progress tool, distinct from the \`<proposed_plan>\` publishing flow. In normal mode: when starting, include any items already visible in \`<task-list>\` using their exact subjects, then add your own steps below them. Don't paraphrase template subjects or you'll create duplicate sidebar entries. Update statuses (pending \u2192 in_progress \u2192 completed) as you go; call \`update_plan\` whenever you start, finish, or reorder a step.
20618
- - **Subagents** \u2014 Use the \`spawn_agent\` tool to launch a sub-thread for a self-contained task. Codex runs sub-agents in separate threads; results land on the parent turn via \`wait\`. \`spawn_agent\` has no \`run_in_background\` field \u2014 every call is foreground from your perspective.
20796
+ - **Subagents** \u2014 Use your native \`spawn_agent\` tool to launch a sub-thread for a self-contained task. It is a built-in Codex tool, NOT a Shipyard MCP tool, so it will not appear in the MCP tool list \u2014 call it directly rather than searching the MCP surface for it. Codex runs sub-agents in separate threads; results land on the parent turn via \`wait\`. \`spawn_agent\` has no \`run_in_background\` field \u2014 every call is foreground from your perspective.
20619
20797
  - **Edits** \u2014 Use the \`apply_patch\` tool for all file edits. Codex does not have separate Read/Edit/Write tools; \`apply_patch\` is the single edit verb.
20620
20798
  - **Patching** \u2014 \`apply_patch\` does NOT enforce string uniqueness the way Claude's \`Edit\` tool does. When editing a repeated pattern (e.g. \`return null;\` appearing in many functions), include enough surrounding context in the hunk to make the match unambiguous, or you will silently edit the wrong occurrence. Concretely: prefer a 3+ line context window above and below the change, and include the enclosing function header in the hunk when in doubt.
20621
20799
  - **Permission failures \u2014 do not retry escalated** \u2014 this runtime's approval policy is \`never\` and sandbox escalation requests will be rejected. If a \`shell\` command fails on permissions or sandboxing, report the failure to the user and stop; do NOT retry with elevated permissions, do NOT loop on \`with_escalated_permissions\`. The \`openai-docs\` skill's "immediately retry with escalated permissions" advice does not apply here \u2014 it would just burn turns hitting the same rejection.
@@ -20678,7 +20856,7 @@ var CURSOR_NOTES = `## Cursor-specific Notes
20678
20856
  - **Ignore \`run_in_background: true\`** \u2014 that is a Claude-Code-only flag on \`Bash\`/Task. Cursor's \`Shell\` and \`task\` do not accept it. AGENTS.md says "Always use \`run_in_background: true\` on all Task tool calls" \u2014 that rule does not apply here.
20679
20857
  - **Background execution** \u2014 Cursor does not support background execution at all. Long-running \`Shell\` commands block the current turn; keep them short or break work into smaller steps so the user sees progress. For genuinely long work, ask the user to split it into multiple turns.
20680
20858
  - **Todo list** \u2014 Use the \`shipyard_update_plan\` MCP tool to keep the user-visible task list in sync. Pass the FULL list of steps with their statuses on every call \u2014 partial updates are not supported. When starting, include any items already visible in \`<${XML_TAGS.TASK_LIST}>\` with their visible IDs (for example, use \`id: "1"\` for \`#1\`). Keep that same ID if you rename a template item, and omit \`id\` only for brand-new steps. Do NOT use Cursor's native \`task\` subagent tool for todo tracking \u2014 \`task\` is for spawning subagents, not the user-facing plan.
20681
- - **Subagents** \u2014 Use the \`task\` tool to launch a sub-agent for a self-contained piece of work. \`task\` has no \`run_in_background\` field (every call is foreground from your perspective) and no cancel API.
20859
+ - **Subagents** \u2014 Use your native \`task\` tool to launch a sub-agent for a self-contained piece of work. It is a built-in Cursor tool, NOT the same-named Shipyard \`task\` MCP tool (that one manages sibling tasks \u2014 see below); call the native one directly rather than searching the MCP surface. The native \`task\` has no \`run_in_background\` field (every call is foreground from your perspective) and no cancel API.
20682
20860
  - **Edits** \u2014 Use \`Write\` (new files) and \`Edit\` (modify existing files) for all file changes.
20683
20861
  - **Permission failures \u2014 do not retry** \u2014 when a tool call is blocked (plan mode, or a denied permission prompt), it fails with a clear message. Report the failure to the user and stop; do NOT loop retrying the same blocked call.
20684
20862
  - **Skills** \u2014 Invoke a skill by typing \`/skill-name\` in a message. If a skill fails to load, read its \`SKILL.md\` directly via \`Shell\` (e.g. \`cat ~/.claude/skills/<name>/SKILL.md\`). The Shipyard repo's domain docs (\`docs/architecture.md\`, \`docs/engineering-standards.md\`, \`docs/development.md\`, and \`AGENTS.md\`) are always available via \`Shell\` regardless of skill resolution.
@@ -21196,6 +21374,22 @@ function matchCollabQueueCorrelationIds(messages, queueEntries) {
21196
21374
  return claimedIds;
21197
21375
  }
21198
21376
 
21377
+ // src/services/session/claude-tool-guard.ts
21378
+ var SHIPYARD_DENIED_CLAUDE_TOOLS = ["Artifact"];
21379
+ function detectDeniedToolsPresent(initTools, denied = SHIPYARD_DENIED_CLAUDE_TOOLS) {
21380
+ const present = new Set(initTools);
21381
+ return denied.filter((name) => present.has(name));
21382
+ }
21383
+ function partitionInitTools(initTools) {
21384
+ const builtin = [];
21385
+ const mcp = [];
21386
+ for (const name of initTools) {
21387
+ if (name.startsWith("mcp__")) mcp.push(name);
21388
+ else builtin.push(name);
21389
+ }
21390
+ return { builtin, mcp };
21391
+ }
21392
+
21199
21393
  // src/services/session/codex-home-isolation.ts
21200
21394
  import { join as join26 } from "path";
21201
21395
  var ISOLATED_CODEX_HOME_DIR = "codex-isolated";
@@ -21807,9 +22001,26 @@ var AnthropicAgentSubprocess = class _AnthropicAgentSubprocess {
21807
22001
  ...options.env,
21808
22002
  DISABLE_AUTO_COMPACT: "1",
21809
22003
  CLAUDE_CODE_ENTRYPOINT: "shipyard",
22004
+ /**
22005
+ * The `shipyard` entrypoint above opts Shipyard OUT of the runtime's
22006
+ * SDK-session default that hides claude.ai Artifacts, so the built-in
22007
+ * `Artifact` tool would otherwise be live and compete with Shipyard's
22008
+ * own `visualize` / `present` / `register_deliverable` surface (#4992).
22009
+ * This is the runtime's dedicated kill switch: it disables the tool,
22010
+ * its prompt guidance, and the coupled claude.ai publish flow. It is
22011
+ * the belt to the `disallowedTools` suspenders below. The runtime does
22012
+ * a raw truthiness check — set `'1'`, never `'0'` to "disable".
22013
+ */
22014
+ CLAUDE_CODE_DISABLE_ARTIFACT: "1",
21810
22015
  ...buildAgentSubprocessCodexHomeEnv()
21811
22016
  },
21812
- disallowedTools: options.disallowedTools,
22017
+ /**
22018
+ * Deny Shipyard-suppressed built-in tools unconditionally at this
22019
+ * chokepoint (not via the caller's option, which `Thread.setMode` may
22020
+ * clear to `undefined`). Union with any caller-supplied denies so this
22021
+ * never clobbers them. See `claude-tool-guard.ts`.
22022
+ */
22023
+ disallowedTools: [...options.disallowedTools ?? [], ...SHIPYARD_DENIED_CLAUDE_TOOLS],
21813
22024
  skills: options.skills,
21814
22025
  // eslint-disable-next-line no-restricted-syntax -- SpawnOptions.betas is string[] for loose protocol compat; SDK expects SdkBeta[] which is a branded subset
21815
22026
  betas: options.betas,
@@ -22074,6 +22285,34 @@ var AnthropicAgentSubprocess = class _AnthropicAgentSubprocess {
22074
22285
  })
22075
22286
  });
22076
22287
  }
22288
+ /**
22289
+ * Record the built-in tool surface the SDK reported at init, and alarm if a
22290
+ * Shipyard-denied tool leaked through despite the spawn-time deny config
22291
+ * (`claude-tool-guard.ts`). `agent_tool_surface` is emitted once per spawn so
22292
+ * fleet telemetry can spot a silently-added opt-in tool across an SDK bump
22293
+ * without a hardcoded baseline; `agent_denied_tool_leaked` is the regression
22294
+ * canary for our own disable (e.g. the runtime renamed `Artifact`).
22295
+ */
22296
+ #reportToolSurface(event, log) {
22297
+ if (!log) return;
22298
+ const tools = event.metadata?.tools ?? [];
22299
+ const { builtin, mcp } = partitionInitTools(tools);
22300
+ log({
22301
+ event: "agent_tool_surface",
22302
+ builtinCount: builtin.length,
22303
+ mcpCount: mcp.length,
22304
+ builtinTools: builtin,
22305
+ agentRuntimeVersion: event.metadata?.agentRuntimeVersion
22306
+ });
22307
+ const leaked = detectDeniedToolsPresent(tools);
22308
+ if (leaked.length > 0) {
22309
+ log({
22310
+ event: "agent_denied_tool_leaked",
22311
+ leaked,
22312
+ agentRuntimeVersion: event.metadata?.agentRuntimeVersion
22313
+ });
22314
+ }
22315
+ }
22077
22316
  async #runMessageLoop(log) {
22078
22317
  const taskFilter = createSubagentTaskFilter();
22079
22318
  try {
@@ -22083,6 +22322,7 @@ var AnthropicAgentSubprocess = class _AnthropicAgentSubprocess {
22083
22322
  if (event) {
22084
22323
  if (event.type === "init_received") {
22085
22324
  await this.#awaitMcpSettleForInit(message, log);
22325
+ this.#reportToolSurface(event, log);
22086
22326
  }
22087
22327
  this.#onEvent(event);
22088
22328
  }
@@ -22482,7 +22722,7 @@ async function runFastSliceAgentBroadcast(deps) {
22482
22722
  if (deps.isDisposed?.()) return;
22483
22723
  const current = deps.capabilitiesRef.current;
22484
22724
  if (!current) return;
22485
- deps.capabilitiesRef.current = {
22725
+ deps.commit({
22486
22726
  ...current,
22487
22727
  installedAgents: agents,
22488
22728
  installedRuntimes: agents,
@@ -22494,7 +22734,7 @@ async function runFastSliceAgentBroadcast(deps) {
22494
22734
  * must not also assume other slices are populated until the second broadcast.
22495
22735
  */
22496
22736
  detectionComplete: true
22497
- };
22737
+ });
22498
22738
  deps.broadcast();
22499
22739
  }
22500
22740
  function decidePreferredAuthBackfill(args) {
@@ -22635,7 +22875,7 @@ function rehydrateVizRegistry(registry, vizWatcher, resolvedTaskId, vizDir, log)
22635
22875
  const registryPath = join28(vizDir, resolvedTaskId, "registry.json");
22636
22876
  if (!existsSync3(registryPath)) return Promise.resolve();
22637
22877
  try {
22638
- const raw = readFileSync4(registryPath, "utf-8");
22878
+ const raw = readFileSync5(registryPath, "utf-8");
22639
22879
  const parsed = PersistedVisualizationRegistrySchema.safeParse(JSON.parse(raw));
22640
22880
  if (parsed.success) {
22641
22881
  const effects = registry.rehydrate(resolvedTaskId, parsed.data);
@@ -23924,6 +24164,7 @@ import { randomUUID as randomUUID9 } from "crypto";
23924
24164
  var NOTIFICATIONS_PER_TASK_CAP = 50;
23925
24165
  var CREATE_THEN_RESOLVE_WINDOW_MS = 500;
23926
24166
  var DEFAULT_GC_HORIZON_MS = 24 * 60 * 60 * 1e3;
24167
+ var EVENT_AGE_OUT_HORIZON_MS = 7 * 24 * 60 * 60 * 1e3;
23927
24168
  var STATEFUL_NOTIFICATION_KINDS = /* @__PURE__ */ new Set([
23928
24169
  "permission",
23929
24170
  "plan_review",
@@ -24128,19 +24369,28 @@ var NotificationsHandler = class {
24128
24369
  * the dedup window) are NOT replayed — they aren't durable yet.
24129
24370
  *
24130
24371
  * `viewerUserId` is accepted for symmetry with the handler signature
24131
- * documented in the plan, but per-user `targetUserIds` filtering is
24132
- * applied browser-side. Forwarding the entire map keeps the daemon
24133
- * simple and matches the `notification_snapshot` schema.
24372
+ * documented in the plan, but per-user `targetUserIds`/dismiss filtering is
24373
+ * applied browser-side. Aged pure events are dropped here (viewer-independent)
24374
+ * so this per-task path agrees with `buildInboxSnapshot`'s read-path age
24375
+ * filter — otherwise opening the active task on a long-uptime daemon would
24376
+ * re-surface events the cross-task `inbox_snapshot` just aged out.
24134
24377
  */
24135
24378
  replayNotificationsToPeer(send, _viewerUserId) {
24136
24379
  this.#runWhenReady(() => {
24137
24380
  const record = this.#deps.taskStateStore.getTaskSync(this.#deps.taskId);
24138
24381
  if (record?.archivedAt != null) return;
24139
- if (Object.keys(this.#notifications).length === 0) return;
24382
+ const now = Date.now();
24383
+ const notifications = {};
24384
+ for (const [id, n] of Object.entries(this.#notifications)) {
24385
+ if (n.resolvedAt == null && isPureEventPastHorizon(n, now, EVENT_AGE_OUT_HORIZON_MS))
24386
+ continue;
24387
+ notifications[id] = n;
24388
+ }
24389
+ if (Object.keys(notifications).length === 0) return;
24140
24390
  send({
24141
24391
  type: "notification_snapshot",
24142
24392
  taskId: this.#deps.taskId,
24143
- notifications: { ...this.#notifications }
24393
+ notifications
24144
24394
  });
24145
24395
  });
24146
24396
  }
@@ -24416,6 +24666,19 @@ function passReplaceByKind(next, bootTimestamp) {
24416
24666
  }
24417
24667
  return retired;
24418
24668
  }
24669
+ function passRetireAgedEvents(next, bootTimestamp, eventHorizonMs) {
24670
+ let retired = 0;
24671
+ for (const [id, n] of Object.entries(next)) {
24672
+ if (n.resolvedAt != null) continue;
24673
+ if (!isPureEventPastHorizon(n, bootTimestamp, eventHorizonMs)) continue;
24674
+ next[id] = { ...n, resolvedAt: bootTimestamp, resolvedBy: "aged_out" };
24675
+ retired++;
24676
+ }
24677
+ return retired;
24678
+ }
24679
+ function isPureEventPastHorizon(n, now, horizonMs) {
24680
+ return notificationDedupKey(n) === null && now - n.createdAt >= horizonMs;
24681
+ }
24419
24682
  function passPruneResolved(next, bootTimestamp, pruneHorizonMs) {
24420
24683
  const pruneCutoff = bootTimestamp - pruneHorizonMs;
24421
24684
  const after = {};
@@ -24429,31 +24692,46 @@ function passPruneResolved(next, bootTimestamp, pruneHorizonMs) {
24429
24692
  }
24430
24693
  return { after, pruned };
24431
24694
  }
24432
- function sweepNotificationsForTask(notifications, bootTimestamp, pruneHorizonMs = DEFAULT_GC_HORIZON_MS) {
24695
+ function sweepNotificationsForTask(notifications, bootTimestamp, pruneHorizonMs = DEFAULT_GC_HORIZON_MS, eventHorizonMs = EVENT_AGE_OUT_HORIZON_MS) {
24433
24696
  const working = { ...notifications };
24434
24697
  const retired1 = passRetireStatefulOrphans(working, bootTimestamp);
24435
24698
  const retired2 = passReplaceByKind(working, bootTimestamp);
24699
+ const retired3 = passRetireAgedEvents(working, bootTimestamp, eventHorizonMs);
24436
24700
  const { after, pruned } = passPruneResolved(working, bootTimestamp, pruneHorizonMs);
24437
- const retired = retired1 + retired2;
24701
+ const retired = retired1 + retired2 + retired3;
24438
24702
  const changed = retired > 0 || pruned > 0;
24439
24703
  return { next: changed ? after : null, retired, pruned };
24440
24704
  }
24441
- function buildInboxSnapshot(tasks) {
24705
+ function isInboxVisible(n, ownerUserId, acknowledgedAt, now) {
24706
+ if (n.resolvedAt != null) return false;
24707
+ if (n.ackByUser[ownerUserId]?.dismissedAt != null) return false;
24708
+ if (acknowledgedAt != null && acknowledgedAt >= n.createdAt) return false;
24709
+ if (isPureEventPastHorizon(n, now, EVENT_AGE_OUT_HORIZON_MS)) return false;
24710
+ return true;
24711
+ }
24712
+ function selectInboxNotificationsForTask(record, ownerUserId, now) {
24713
+ if (record.archivedAt != null) return null;
24714
+ const notifications = record.notifications;
24715
+ if (!notifications) return null;
24716
+ const acknowledgedAt = record.acknowledgedAt ?? null;
24717
+ let visible = null;
24718
+ for (const [id, n] of Object.entries(notifications)) {
24719
+ if (!isInboxVisible(n, ownerUserId, acknowledgedAt, now)) continue;
24720
+ if (visible === null) visible = {};
24721
+ visible[id] = n;
24722
+ }
24723
+ return visible;
24724
+ }
24725
+ function buildInboxSnapshot(tasks, ownerUserId) {
24726
+ const now = Date.now();
24442
24727
  const out = {};
24443
24728
  for (const [taskId, record] of Object.entries(tasks)) {
24444
- const notifications = record.notifications;
24445
- if (!notifications) continue;
24446
- let unresolved = null;
24447
- for (const [id, n] of Object.entries(notifications)) {
24448
- if (n.resolvedAt != null) continue;
24449
- if (unresolved === null) unresolved = {};
24450
- unresolved[id] = n;
24451
- }
24452
- if (unresolved !== null) out[taskId] = unresolved;
24729
+ const visible = selectInboxNotificationsForTask(record, ownerUserId, now);
24730
+ if (visible !== null) out[taskId] = visible;
24453
24731
  }
24454
24732
  return out;
24455
24733
  }
24456
- async function sweepOrphanedNotifications(taskStateStore, log) {
24734
+ async function sweepOrphanedNotifications(taskStateStore, log, eventHorizonMs = EVENT_AGE_OUT_HORIZON_MS) {
24457
24735
  const bootTimestamp = Date.now();
24458
24736
  let tasks;
24459
24737
  try {
@@ -24471,7 +24749,12 @@ async function sweepOrphanedNotifications(taskStateStore, log) {
24471
24749
  for (const [taskId, record] of Object.entries(tasks)) {
24472
24750
  const notifications = record.notifications;
24473
24751
  if (!notifications || Object.keys(notifications).length === 0) continue;
24474
- const { next, retired, pruned } = sweepNotificationsForTask(notifications, bootTimestamp);
24752
+ const { next, retired, pruned } = sweepNotificationsForTask(
24753
+ notifications,
24754
+ bootTimestamp,
24755
+ DEFAULT_GC_HORIZON_MS,
24756
+ eventHorizonMs
24757
+ );
24475
24758
  if (!next) continue;
24476
24759
  sweptTasks++;
24477
24760
  sweptNotifications += retired;
@@ -25034,7 +25317,7 @@ function buildTaskCallbacks(deps) {
25034
25317
  daemon.taskStateStore.listTasks().then((all) => {
25035
25318
  getControlHandler().sendControl({
25036
25319
  type: "inbox_snapshot",
25037
- notificationsByTask: buildInboxSnapshot(all)
25320
+ notificationsByTask: buildInboxSnapshot(all, daemon.userId)
25038
25321
  });
25039
25322
  }).catch((err3) => {
25040
25323
  logAdapter({
@@ -30383,7 +30666,7 @@ function isNodeBuildReleaseDir(dir) {
30383
30666
  }
30384
30667
 
30385
30668
  // src/services/port-detection.ts
30386
- import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
30669
+ import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
30387
30670
  import { dirname as dirname19, join as join35 } from "path";
30388
30671
  var LSOF_TIMEOUT_MS = 8e3;
30389
30672
  var LSOF_TTL_MS = 5e3;
@@ -30570,7 +30853,7 @@ function resolveProjectRoot(cwd) {
30570
30853
  if (existsSync6(candidate)) {
30571
30854
  let packageName;
30572
30855
  try {
30573
- const raw = readFileSync5(candidate, "utf8");
30856
+ const raw = readFileSync6(candidate, "utf8");
30574
30857
  const parsed = JSON.parse(raw);
30575
30858
  if (parsed !== null && typeof parsed === "object" && hasStringName(parsed)) {
30576
30859
  packageName = parsed.name;
@@ -30685,7 +30968,7 @@ import { randomUUID as randomUUID10 } from "crypto";
30685
30968
  import {
30686
30969
  mkdirSync as mkdirSync3,
30687
30970
  readdirSync as readdirSync4,
30688
- readFileSync as readFileSync6,
30971
+ readFileSync as readFileSync7,
30689
30972
  renameSync,
30690
30973
  unlinkSync as unlinkSync2,
30691
30974
  writeFileSync as writeFileSync2
@@ -30705,7 +30988,7 @@ function recordFilePath2(rootDir, taskId, elementId) {
30705
30988
  function parseRecord2(filePath, logger2) {
30706
30989
  let raw;
30707
30990
  try {
30708
- raw = readFileSync6(filePath, "utf-8");
30991
+ raw = readFileSync7(filePath, "utf-8");
30709
30992
  } catch {
30710
30993
  return null;
30711
30994
  }
@@ -37899,10 +38182,10 @@ function isWakeableSession(state) {
37899
38182
  }
37900
38183
  }
37901
38184
  function buildWakeMessage(prNumber, ciState) {
37902
- if (ciState === "failure") {
37903
- return `[Shipyard] CI checks failed on your open pull request #${prNumber}. Review the failing checks in the <branch> resource, fix them, and push to the same PR.`;
37904
- }
37905
- return `[Shipyard] CI checks passed on your open pull request #${prNumber}. Review the <branch> resource and continue your task \u2014 merge it if it is ready, or address any remaining work.`;
38185
+ const body = ciState === "failure" ? `CI checks failed on your open pull request #${prNumber}. Review the failing checks in the <branch> resource, fix them, and push to the same PR.` : `CI checks passed on your open pull request #${prNumber}. Review the <branch> resource and continue your task \u2014 merge it if it is ready, or address any remaining work.`;
38186
+ return `<system-reminder>
38187
+ ${body}
38188
+ </system-reminder>`;
37906
38189
  }
37907
38190
  function maybeWakeAgentOnPrCheck(deps, taskId, prev, next) {
37908
38191
  const decision = decidePrCheckWake({
@@ -37925,7 +38208,17 @@ function buildPrCheckWakeDeps(taskManager, log) {
37925
38208
  return {
37926
38209
  getSessionState: (taskId) => taskManager.getTaskState(taskId)?.sessionState ?? null,
37927
38210
  wake: (taskId, message) => {
37928
- void taskManager.handleUserMessage(taskId, [{ type: "text", text: message }]).catch((err3) => {
38211
+ void taskManager.handleUserMessage(
38212
+ taskId,
38213
+ [{ type: "text", text: message }],
38214
+ void 0,
38215
+ void 0,
38216
+ void 0,
38217
+ void 0,
38218
+ void 0,
38219
+ "human",
38220
+ true
38221
+ ).catch((err3) => {
37929
38222
  log({
37930
38223
  event: "pr_check_wake_failed",
37931
38224
  taskId,
@@ -39580,6 +39873,56 @@ function createDeliverableTools(ctx) {
39580
39873
  ];
39581
39874
  }
39582
39875
 
39876
+ // src/services/harness/open-file-server.ts
39877
+ var OPEN_FILE_DESCRIPTION = [
39878
+ "Open a file in Shipyard's code-editor side panel.",
39879
+ "",
39880
+ "Use it to surface a file in the UI at the moment it matters \u2014 right after you edit it, when your reply references specific code, or when you point the reviewer at what to look at next. Opens the panel if closed, selects the file, and (with `line`) scrolls to that line.",
39881
+ "",
39882
+ "WHEN: after an edit the reviewer should see, when your answer references a file, or when directing attention to a specific spot.",
39883
+ 'WHEN NOT: for a file the reviewer is already looking at, or as a substitute for explaining a change \u2014 use `add_comment` for the "why" on a diff hunk.',
39884
+ "",
39885
+ "Params:",
39886
+ " path \u2014 the file to open. Absolute or workspace-relative; the browser resolves it against the task working directory.",
39887
+ " line \u2014 optional 1-based line to scroll to and place the cursor on.",
39888
+ "",
39889
+ "EXAMPLES:",
39890
+ ' {"path": "src/db.ts"}',
39891
+ ' {"path": "src/index.ts", "line": 42}'
39892
+ ].join("\n");
39893
+ var OpenFileInput = {
39894
+ path: external_exports.string().trim().min(1, "path cannot be empty").describe(
39895
+ "File to open, absolute or workspace-relative. Resolved against the task working directory in the browser."
39896
+ ),
39897
+ line: external_exports.number().int().positive().optional().describe("Optional 1-based line number to scroll to and place the cursor on.")
39898
+ };
39899
+ var OpenFileSchema = external_exports.object(OpenFileInput);
39900
+ function createOpenFileTools(ctx) {
39901
+ return [
39902
+ {
39903
+ name: OPEN_FILE_TOOL,
39904
+ description: OPEN_FILE_DESCRIPTION,
39905
+ inputSchema: OpenFileInput,
39906
+ handler: async (input) => {
39907
+ const parsed = OpenFileSchema.safeParse(input);
39908
+ if (!parsed.success) {
39909
+ const reason = parsed.error.issues.map((i) => i.message).join("; ");
39910
+ return {
39911
+ content: [{ type: "text", text: `Invalid open_file input: ${reason}` }],
39912
+ isError: true
39913
+ };
39914
+ }
39915
+ const { path: path5, line } = parsed.data;
39916
+ ctx.broadcastControl({ type: "open_file", taskId: ctx.taskId.current, path: path5, line });
39917
+ const where = line !== void 0 ? ` at line ${line}` : "";
39918
+ return {
39919
+ content: [{ type: "text", text: `Opened ${path5}${where} in the file viewer.` }]
39920
+ };
39921
+ }
39922
+ }
39923
+ ];
39924
+ }
39925
+
39583
39926
  // src/services/harness/plugin-server.ts
39584
39927
  import { mkdir as mkdir19, writeFile as writeFile19 } from "fs/promises";
39585
39928
  import { join as join52 } from "path";
@@ -40433,6 +40776,12 @@ var CreateTaskInputSchema = external_exports.object({
40433
40776
  "Must be a model id registered with this daemon (see the `models` capability snapshot). The daemon validates the id against the registered profile catalog at handler time and refuses spawn if the model is unknown.",
40434
40777
  "The spawned task always runs on the agent that owns this model \u2014 pre-warm adoption respects the target agent."
40435
40778
  ].join("\n")
40779
+ ),
40780
+ mcpServers: external_exports.array(external_exports.string()).optional().describe(
40781
+ [
40782
+ "ALLOWLIST of MCP server names the spawned task may use. Omit to inherit the parent task's MCP set (the default). When set, ONLY the named servers stay enabled for the spawned task \u2014 all other configured servers are filtered out, so a spawned sub-task does not inherit the full workspace roster.",
40783
+ "Pass [] to spawn with no MCP servers at all. Names must match the daemon's configured MCP set (see the `mcpServers` capability snapshot); unknown names are reported and the spawn is refused so you can correct."
40784
+ ].join("\n")
40436
40785
  )
40437
40786
  });
40438
40787
  var TASK_TOOL_DESCRIPTION = [
@@ -40518,6 +40867,18 @@ function isTerminalTaskStatus(status) {
40518
40867
  return TERMINAL_TASK_STATUS_SET.has(status);
40519
40868
  }
40520
40869
  var TASK_SNAPSHOT_URI_PREFIX = "shipyard://task-snapshot/";
40870
+ function planMcpAllowlist(allowlist, knownNames) {
40871
+ if (allowlist === void 0) return { kind: "inherit" };
40872
+ const known = knownNames ? [...knownNames] : [];
40873
+ const knownSet = new Set(known);
40874
+ const unknown = [...new Set(allowlist.filter((name) => !knownSet.has(name)))];
40875
+ if (unknown.length > 0) return { kind: "unknown", unknown, known };
40876
+ return { kind: "filter", mcpServerSnapshot: [...new Set(allowlist)], disabledMcpServers: [] };
40877
+ }
40878
+ function unknownMcpServersMessage(unknown, known) {
40879
+ const suggestion = known.length === 0 ? "This daemon has no MCP servers configured \u2014 omit `mcpServers` to inherit the parent set, or pass [] to spawn with none." : `Configured MCP servers: ${[...known].sort((a, b2) => a.localeCompare(b2)).join(", ")}.`;
40880
+ return `Unknown MCP server(s) in the mcpServers allowlist: ${unknown.join(", ")}. ${suggestion}`;
40881
+ }
40521
40882
  async function handleCreateTask(input, ctx) {
40522
40883
  const parsed = CreateTaskInputSchema.safeParse(input);
40523
40884
  if (!parsed.success) {
@@ -40527,9 +40888,17 @@ async function handleCreateTask(input, ctx) {
40527
40888
  const callingTaskId = ctx.callingTaskId.current;
40528
40889
  const inputError = validateCreateTaskInputs(args, ctx);
40529
40890
  if (inputError !== null) return inputError;
40891
+ const mcpPlan = planMcpAllowlist(
40892
+ args.mcpServers,
40893
+ args.mcpServers === void 0 ? void 0 : ctx.getKnownMcpServerNames()
40894
+ );
40895
+ if (mcpPlan.kind === "unknown") {
40896
+ return errorResult(unknownMcpServersMessage(mcpPlan.unknown, mcpPlan.known));
40897
+ }
40530
40898
  const inheritedDefaults = await resolveInheritedTaskDefaults(
40531
40899
  args.permissionMode,
40532
40900
  args.model,
40901
+ mcpPlan,
40533
40902
  callingTaskId,
40534
40903
  ctx
40535
40904
  );
@@ -40836,7 +41205,7 @@ async function spawnWithFreshWorktree(args, callingTaskId, ctx, inheritedDefault
40836
41205
  }
40837
41206
  return successResult(taskId, args.title, branchName, "create");
40838
41207
  }
40839
- async function resolveInheritedTaskDefaults(permissionModeOverride, modelOverride, callingTaskId, ctx) {
41208
+ async function resolveInheritedTaskDefaults(permissionModeOverride, modelOverride, mcpPlan, callingTaskId, ctx) {
40840
41209
  const parent = await ctx.taskStateStore.getTask(callingTaskId);
40841
41210
  const parentSettings = parent?.composerSettings ?? {};
40842
41211
  const merged = { ...parentSettings };
@@ -40846,6 +41215,10 @@ async function resolveInheritedTaskDefaults(permissionModeOverride, modelOverrid
40846
41215
  if (modelOverride !== void 0) {
40847
41216
  merged.model = modelOverride;
40848
41217
  }
41218
+ if (mcpPlan.kind === "filter") {
41219
+ merged.disabledMcpServers = mcpPlan.disabledMcpServers;
41220
+ merged.mcpServerSnapshot = mcpPlan.mcpServerSnapshot;
41221
+ }
40849
41222
  return {
40850
41223
  initialComposerSettings: Object.keys(merged).length === 0 ? void 0 : merged,
40851
41224
  projectId: parent?.projectId ?? null
@@ -50252,6 +50625,7 @@ var LSP_TOOL_NAMES = [
50252
50625
  "lsp_find_references",
50253
50626
  "lsp_rename_symbol"
50254
50627
  ];
50628
+ var OPEN_FILE_TOOL_NAMES = ["open_file"];
50255
50629
  function buildHarnessTools(ctx) {
50256
50630
  const taskIdRef = { current: ctx.taskId };
50257
50631
  const epochRef = { current: null };
@@ -50305,6 +50679,7 @@ function buildHarnessTools(ctx) {
50305
50679
  defaultRepoPath: ctx.projectRoot,
50306
50680
  isKnownModel: ctx.isKnownModel,
50307
50681
  getModels: ctx.getModels,
50682
+ getKnownMcpServerNames: ctx.getKnownMcpServerNames,
50308
50683
  resolveSnapshot: ctx.resolveSnapshot
50309
50684
  };
50310
50685
  const lspAgentCtx = {
@@ -50312,6 +50687,10 @@ function buildHarnessTools(ctx) {
50312
50687
  log: ctx.log,
50313
50688
  getClient: ctx.getLspClient
50314
50689
  };
50690
+ const openFileCtx = {
50691
+ taskId: taskIdRef,
50692
+ broadcastControl: ctx.broadcastControl
50693
+ };
50315
50694
  const skillsCtx = {
50316
50695
  taskId: taskIdRef,
50317
50696
  agentSystem: ctx.agentSystem,
@@ -50335,6 +50714,7 @@ function buildHarnessTools(ctx) {
50335
50714
  registerFactory(TASK_CONTROL_TOOL_NAMES, () => createTaskControlTools(taskControlCtx));
50336
50715
  registerFactory(DELIVERABLE_TOOL_NAMES, () => createDeliverableTools(deliverableCtx));
50337
50716
  registerFactory(LSP_TOOL_NAMES, () => createLspAgentTools(lspAgentCtx));
50717
+ registerFactory(OPEN_FILE_TOOL_NAMES, () => createOpenFileTools(openFileCtx));
50338
50718
  const tools = [];
50339
50719
  for (const spec of HARNESS_TOOL_REGISTRY) {
50340
50720
  if (!passes(spec)) continue;
@@ -50398,12 +50778,12 @@ function createHarnessMcpServer(ctx) {
50398
50778
 
50399
50779
  // src/services/harness/lsp-agent-client.ts
50400
50780
  import { spawn as spawn10 } from "child_process";
50401
- import { readFileSync as readFileSync8 } from "fs";
50781
+ import { readFileSync as readFileSync9 } from "fs";
50402
50782
  import { extname as extname3 } from "path";
50403
50783
  import { pathToFileURL as pathToFileURL3 } from "url";
50404
50784
 
50405
50785
  // src/services/lsp/language-server-registry.ts
50406
- import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
50786
+ import { existsSync as existsSync7, readFileSync as readFileSync8 } from "fs";
50407
50787
  import { createRequire as createRequire3 } from "module";
50408
50788
  import { dirname as dirname22, isAbsolute as isAbsolute4, join as join53 } from "path";
50409
50789
  function decideTypescriptServer(d) {
@@ -50532,7 +50912,7 @@ function createRegistryDeps(requireFrom) {
50532
50912
  fileExists: (path5) => existsSync7(path5),
50533
50913
  readJsonFile: (path5) => {
50534
50914
  try {
50535
- return parse2(readFileSync7(path5, "utf-8"));
50915
+ return parse2(readFileSync8(path5, "utf-8"));
50536
50916
  } catch {
50537
50917
  return void 0;
50538
50918
  }
@@ -50560,7 +50940,7 @@ function resolveTsgoBinaryPath(req) {
50560
50940
  }
50561
50941
  function resolveBinViaPackageJson(pkg, binName, req) {
50562
50942
  const pkgJsonPath = req.resolve(`${pkg}/package.json`);
50563
- const manifest = parse2(readFileSync7(pkgJsonPath, "utf-8"));
50943
+ const manifest = parse2(readFileSync8(pkgJsonPath, "utf-8"));
50564
50944
  const binField = isRecord7(manifest) ? manifest.bin : void 0;
50565
50945
  const named = isRecord7(binField) ? binField[binName] : void 0;
50566
50946
  const bin = typeof binField === "string" ? binField : typeof named === "string" ? named : void 0;
@@ -50716,7 +51096,7 @@ var LspAgentClient = class {
50716
51096
  if (this.#disposed) return;
50717
51097
  this.#armIdleTimer();
50718
51098
  const uri = pathToFileURL3(absolutePath).href;
50719
- const text = readFileSync8(absolutePath, "utf8");
51099
+ const text = readFileSync9(absolutePath, "utf8");
50720
51100
  const prevVersion = this.#openedVersions.get(uri);
50721
51101
  if (prevVersion === void 0) {
50722
51102
  this.#notify("textDocument/didOpen", {
@@ -59983,6 +60363,13 @@ var ScheduleEvaluator = class {
59983
60363
  }
59984
60364
  };
59985
60365
 
60366
+ // src/services/serve-factory/capability-writer.ts
60367
+ function createCapabilityWriter(capabilitiesRef) {
60368
+ return (next) => {
60369
+ capabilitiesRef.current = next;
60370
+ };
60371
+ }
60372
+
59986
60373
  // ../../packages/session/src/codex-app-server/shared-protocol-types.ts
59987
60374
  var ReasoningEffortSchema = external_exports2.string();
59988
60375
  var TurnStatusSchema = external_exports2.enum(["completed", "interrupted", "failed", "inProgress"]);
@@ -63107,6 +63494,29 @@ function buildCodexPlanUpdateHandler(deps) {
63107
63494
  };
63108
63495
  }
63109
63496
 
63497
+ // src/services/session/codex-resume-classifier.ts
63498
+ function isCodexArchiveThreadNotFound(err3) {
63499
+ const message = err3 instanceof Error ? err3.message : String(err3);
63500
+ return err3 instanceof JsonRpcRequestError && err3.method === "thread/archive" && /(?:no rollout found for thread id|ThreadNotFound|thread not found|no rollout|rollout.*not.*found)/i.test(
63501
+ message
63502
+ );
63503
+ }
63504
+ function isCodexResumeThreadNotFound(err3) {
63505
+ const message = err3 instanceof Error ? err3.message : String(err3);
63506
+ return err3 instanceof JsonRpcRequestError && err3.method === "thread/resume" && /(?:no rollout found for thread id|ThreadNotFound|thread not found|no rollout|rollout.*not.*found)/i.test(
63507
+ message
63508
+ );
63509
+ }
63510
+ function isCodexResumeThreadArchived(err3) {
63511
+ const message = err3 instanceof Error ? err3.message : String(err3);
63512
+ return err3 instanceof JsonRpcRequestError && err3.method === "thread/resume" && /archived/i.test(message);
63513
+ }
63514
+ function classifyResumeFallback(err3) {
63515
+ if (isCodexResumeThreadArchived(err3)) return { reason: "archived" };
63516
+ if (isCodexResumeThreadNotFound(err3)) return { reason: "no_rollout" };
63517
+ return null;
63518
+ }
63519
+
63110
63520
  // src/services/session/codex-turn-event-dispatch.ts
63111
63521
  function maybeEmitBlockBoundary(nextType, cell, onEvent) {
63112
63522
  if (!cell) return;
@@ -63123,6 +63533,7 @@ function maybeEmitBlockBoundary(nextType, cell, onEvent) {
63123
63533
  cell.current = nextType;
63124
63534
  }
63125
63535
  function dispatchCodexTurnEvent(frame, deps) {
63536
+ deps.onLiveness();
63126
63537
  switch (frame.method) {
63127
63538
  case CodexNotifications.itemStarted:
63128
63539
  case CodexNotifications.itemCompleted:
@@ -64259,8 +64670,13 @@ var CodexAgentSubprocess = class _CodexAgentSubprocess {
64259
64670
  try {
64260
64671
  return await client.resumeThread(opts.resume, threadParams);
64261
64672
  } catch (err3) {
64262
- if (!isCodexResumeThreadNotFound(err3)) throw err3;
64263
- opts.log({ event: "codex_resume_no_rollout_fallback", threadId: opts.resume });
64673
+ const fallback = classifyResumeFallback(err3);
64674
+ if (fallback === null) throw err3;
64675
+ opts.log({
64676
+ event: "codex_resume_no_rollout_fallback",
64677
+ threadId: opts.resume,
64678
+ reason: fallback.reason
64679
+ });
64264
64680
  if (opts.onResumeNoRollout !== void 0) {
64265
64681
  try {
64266
64682
  await opts.onResumeNoRollout(opts.resume);
@@ -64429,8 +64845,13 @@ var CodexAgentSubprocess = class _CodexAgentSubprocess {
64429
64845
  try {
64430
64846
  startResponse = await client.resumeThread(priorThreadId, startParams);
64431
64847
  } catch (err3) {
64432
- if (!isCodexResumeThreadNotFound(err3)) throw err3;
64433
- this.#log({ event: "codex_engine_resume_no_rollout_fallback", threadId: priorThreadId });
64848
+ const fallback = classifyResumeFallback(err3);
64849
+ if (fallback === null) throw err3;
64850
+ this.#log({
64851
+ event: "codex_engine_resume_no_rollout_fallback",
64852
+ threadId: priorThreadId,
64853
+ reason: fallback.reason
64854
+ });
64434
64855
  startResponse = await client.startThread(startParams);
64435
64856
  }
64436
64857
  } else {
@@ -64628,6 +65049,13 @@ var CodexAgentSubprocess = class _CodexAgentSubprocess {
64628
65049
  dispatchCodexTurnEvent(event, {
64629
65050
  translatorCtx: this.#translatorCtx,
64630
65051
  onEvent: this.#onEvent,
65052
+ /**
65053
+ * Stall-watchdog liveness (#4921): every turn frame is proof of life.
65054
+ * `stream_activity` resets the (agent-neutral) session-manager watchdog;
65055
+ * the manager throttles high-rate deltas and no-ops when the watchdog is
65056
+ * disabled, so this is safe to fire per frame.
65057
+ */
65058
+ onLiveness: () => this.#onEvent({ type: "stream_activity" }),
64631
65059
  onTokenUsage: (payload) => {
64632
65060
  this.#lastTurnTokenUsage = payload.tokenUsage;
64633
65061
  },
@@ -64919,7 +65347,7 @@ var CodexAgentSubprocess = class _CodexAgentSubprocess {
64919
65347
  */
64920
65348
  async rollback(numTurns) {
64921
65349
  const threadId = this.#threadId;
64922
- if (!threadId || this.#closed) return;
65350
+ if (!threadId || this.#closed || numTurns < 1) return;
64923
65351
  await this.#client.rollback(threadId, { numTurns });
64924
65352
  }
64925
65353
  /**
@@ -65211,18 +65639,6 @@ var CodexAgentSubprocess = class _CodexAgentSubprocess {
65211
65639
  function toCodexWireEffort(effort) {
65212
65640
  return effort;
65213
65641
  }
65214
- function isCodexArchiveThreadNotFound(err3) {
65215
- const message = err3 instanceof Error ? err3.message : String(err3);
65216
- return err3 instanceof JsonRpcRequestError && err3.method === "thread/archive" && /(?:no rollout found for thread id|ThreadNotFound|thread not found|no rollout|rollout.*not.*found)/i.test(
65217
- message
65218
- );
65219
- }
65220
- function isCodexResumeThreadNotFound(err3) {
65221
- const message = err3 instanceof Error ? err3.message : String(err3);
65222
- return err3 instanceof JsonRpcRequestError && err3.method === "thread/resume" && /(?:no rollout found for thread id|ThreadNotFound|thread not found|no rollout|rollout.*not.*found)/i.test(
65223
- message
65224
- );
65225
- }
65226
65642
 
65227
65643
  // src/services/session/codex-subprocess-facade.ts
65228
65644
  function detectedCodexVersionForLog(input) {
@@ -72353,13 +72769,13 @@ async function defaultFileExists2(path5) {
72353
72769
  return false;
72354
72770
  }
72355
72771
  }
72356
- function defaultExec(command, args, cwd) {
72772
+ function defaultExec2(command, args, cwd) {
72357
72773
  return runWithTimeout(command, args, cwd, STACK_TIMEOUT_MS);
72358
72774
  }
72359
72775
  async function detectStack(cwd, currentBranch, deps = {}) {
72360
72776
  const isCommandAvailable = deps.isCommandAvailable ?? defaultIsCommandAvailable;
72361
72777
  const fileExists2 = deps.fileExists ?? defaultFileExists2;
72362
- const exec = deps.exec ?? defaultExec;
72778
+ const exec = deps.exec ?? defaultExec2;
72363
72779
  const getDefaultBranch2 = deps.getDefaultBranch ?? getRepoDefaultBranch;
72364
72780
  const graphite = await detectGraphite(
72365
72781
  cwd,
@@ -77233,7 +77649,7 @@ function handleSpawning(snapshot, event) {
77233
77649
  b2.log("spawning", "cold_idle", event.type);
77234
77650
  b2.taskStatus("input_required");
77235
77651
  b2.emitError(
77236
- "Cursor timed out waiting for the first response. Send a message to try again.",
77652
+ "The agent timed out waiting for the first response. Send a message to try again.",
77237
77653
  "sdk_error",
77238
77654
  "run_stall_timeout"
77239
77655
  );
@@ -77256,7 +77672,7 @@ function runningStallTimeout(sessionId, event) {
77256
77672
  if (!event.recoveryArmed) {
77257
77673
  b2.taskStatus("input_required");
77258
77674
  b2.emitError(
77259
- "Cursor stopped responding mid-run. Send a message to try again.",
77675
+ "The agent stopped responding mid-run. Send a message to try again.",
77260
77676
  "sdk_error",
77261
77677
  "run_stall_timeout"
77262
77678
  );
@@ -81163,6 +81579,15 @@ function createPlanFileWatcher(deps) {
81163
81579
  }
81164
81580
 
81165
81581
  // src/services/plan/plan-handler-helpers.ts
81582
+ function narrowPrePlanMode(mode) {
81583
+ if (mode === void 0 || mode === "plan") return void 0;
81584
+ return mode;
81585
+ }
81586
+ function decidePostApprovalMode(incoming, prePlanMode) {
81587
+ if (incoming !== void 0 && incoming !== "default") return incoming;
81588
+ if (prePlanMode !== void 0) return prePlanMode;
81589
+ return incoming;
81590
+ }
81166
81591
  function parseAllowedPrompts(input) {
81167
81592
  if (input === null || input === void 0 || typeof input !== "object" || Array.isArray(input))
81168
81593
  return [];
@@ -82311,7 +82736,14 @@ var PlanHandler = class {
82311
82736
  }
82312
82737
  }
82313
82738
  async #tryPersistPreAttach(toolUseId, filePath, planDocId, allowedPrompts) {
82314
- const pendingDetection = { toolUseId, filePath, planDocId, allowedPrompts };
82739
+ const prePlanPermissionMode = narrowPrePlanMode(this.#deps.getLatestSettings().permissionMode);
82740
+ const pendingDetection = {
82741
+ toolUseId,
82742
+ filePath,
82743
+ planDocId,
82744
+ allowedPrompts,
82745
+ prePlanPermissionMode
82746
+ };
82315
82747
  try {
82316
82748
  await this.#persistDetection(pendingDetection);
82317
82749
  return true;
@@ -82409,8 +82841,12 @@ var PlanHandler = class {
82409
82841
  });
82410
82842
  }
82411
82843
  #applyPermissionMode(permissionMode) {
82412
- if (!permissionMode) return;
82413
- this.#deps.applyTaskSettings({ permissionMode });
82844
+ const target = decidePostApprovalMode(
82845
+ permissionMode,
82846
+ this.#lastPlanDetection?.prePlanPermissionMode
82847
+ );
82848
+ if (!target) return;
82849
+ this.#deps.applyTaskSettings({ permissionMode: target });
82414
82850
  }
82415
82851
  /**
82416
82852
  * Build the narrow Context for the review-resolution free functions. State
@@ -83849,18 +84285,27 @@ var RewindCheckpointHandler = class {
83849
84285
  return { kind: "fresh" };
83850
84286
  }
83851
84287
  /**
83852
- * Count the number of completed agent turns AFTER the checkpoint turn
83853
- * that corresponds to `targetSeqNo`. Used to compute `numTurns` for
83854
- * Codex's count-based `client.rollback` call.
84288
+ * Count the completed agent turns dropped by rewinding to `targetSeqNo`,
84289
+ * inclusive of the turn that STARTS at the target. Used to compute
84290
+ * `numTurns` for Codex's count-based `client.rollback` call and Cursor's
84291
+ * store truncation.
83855
84292
  *
83856
- * Each user-facing turn = 1 spawn + 1 end = 2 consecutive entries in
83857
- * `#allCheckpointTurnNos`. The number of turns after `targetTurnNo`
83858
- * is therefore floor(count of entries > targetTurnNo / 2).
84293
+ * A user-facing turn = 1 spawn + 1 end = 2 consecutive entries in
84294
+ * `#allCheckpointTurnNos` (`[spawn0, end0, spawn1, end1, ...]`). A rewind
84295
+ * target seqNo is a user message, which maps to that turn's SPAWN
84296
+ * checkpoint (even index) — and `rewind_files` restores the tree to that
84297
+ * spawn checkpoint, i.e. to BEFORE the turn ran. So the target's own turn
84298
+ * is being dropped and must be counted: the answer is
84299
+ * floor(count of entries `>= targetTurnNo` / 2), not `>`. Using `>` here
84300
+ * undercounts by one whenever the target lands on a spawn checkpoint,
84301
+ * yielding `numTurns: 0` for a rewind to the initial or most-recent turn —
84302
+ * an invalid Codex `thread/rollback` (#5041). (For an odd/end target the
84303
+ * two forms agree, so this only corrects the spawn-checkpoint case.)
83859
84304
  */
83860
84305
  #countTurnsAfter(targetSeqNo) {
83861
84306
  const targetTurnNo = this.#findCheckpointForSeqNo(targetSeqNo);
83862
84307
  if (targetTurnNo === null) return 0;
83863
- return Math.floor(this.#allCheckpointTurnNos.filter((t) => t > targetTurnNo).length / 2);
84308
+ return Math.floor(this.#allCheckpointTurnNos.filter((t) => t >= targetTurnNo).length / 2);
83864
84309
  }
83865
84310
  async rewind(targetSeqNo, mode) {
83866
84311
  if (this.#rewindInProgress) {
@@ -92960,6 +93405,7 @@ async function persistUserMessageAtChokepoint(args) {
92960
93405
  senderKind: args.senderKind ?? "human",
92961
93406
  content: stripBlobResourcesForStorage(args.content),
92962
93407
  timestamp: Date.now(),
93408
+ ...args.isSynthetic === true && { isSynthetic: true },
92963
93409
  ...persistedCorrelationId !== void 0 && { correlationId: persistedCorrelationId },
92964
93410
  model: effectiveSettings.model ?? null,
92965
93411
  reasoningEffort: effectiveSettings.reasoningEffort ?? null,
@@ -94449,7 +94895,7 @@ var TaskManager = class {
94449
94895
  });
94450
94896
  return task;
94451
94897
  }
94452
- async handleUserMessage(taskId, content, settings, cwd, participantId, senderDisplayName, correlationId, senderKind = "human") {
94898
+ async handleUserMessage(taskId, content, settings, cwd, participantId, senderDisplayName, correlationId, senderKind = "human", isSynthetic = false) {
94453
94899
  const task = await this.#ensureTaskForUserMessage(taskId);
94454
94900
  if (cwd && cwd !== task.cwd) {
94455
94901
  if (task.mode === "review" && !task.cwd) {
@@ -94480,7 +94926,8 @@ var TaskManager = class {
94480
94926
  latestSettings: task.orchestrator.latestSettings,
94481
94927
  participantId,
94482
94928
  senderKind,
94483
- correlationId
94929
+ correlationId,
94930
+ isSynthetic
94484
94931
  });
94485
94932
  task.orchestrator.handleUserMessage(
94486
94933
  content,
@@ -95761,6 +96208,7 @@ async function createDaemon(deps) {
95761
96208
  shipyardResolver.addSubResolver("plugin", pluginResourceResolver);
95762
96209
  resourceRegistry.register(shipyardResolver);
95763
96210
  const capabilitiesRef = { current: null };
96211
+ const commitCapabilities = createCapabilityWriter(capabilitiesRef);
95764
96212
  const rateLimitStoreRef = { current: null };
95765
96213
  const gitCheckpoint = createGitCheckpointService();
95766
96214
  const skillResolution = buildSkillResolutionService({
@@ -96266,6 +96714,7 @@ async function createDaemon(deps) {
96266
96714
  * fallback to Claude.
96267
96715
  */
96268
96716
  getModels: () => profileRegistry.list().flatMap((p2) => p2.models),
96717
+ getKnownMcpServerNames: () => mcpCoordinatorRef.current?.getKnownServerNames(),
96269
96718
  /**
96270
96719
  * Lazy: taskSnapshotResolver is assigned after this function is defined
96271
96720
  * (same pattern as `getTaskManager: () => taskManager`). The callback is
@@ -96403,6 +96852,7 @@ async function createDaemon(deps) {
96403
96852
  * fallback to Claude.
96404
96853
  */
96405
96854
  getModels: () => profileRegistry.list().flatMap((p2) => p2.models),
96855
+ getKnownMcpServerNames: () => mcpCoordinatorRef.current?.getKnownServerNames(),
96406
96856
  /**
96407
96857
  * Same lazy-resolver pattern as the Codex path above — taskSnapshotResolver
96408
96858
  * is assigned later in this function but always before any handler fires.
@@ -97424,14 +97874,14 @@ async function createDaemon(deps) {
97424
97874
  if (stallProfilerConfig.enabled) {
97425
97875
  stallProfiler.start();
97426
97876
  }
97427
- capabilitiesRef.current = await buildCapabilitiesStubWithDiskSeed();
97877
+ commitCapabilities(await buildCapabilitiesStubWithDiskSeed());
97428
97878
  function markDetectionFailed(err3) {
97429
97879
  deps.log({
97430
97880
  event: "background_capability_init_failed",
97431
97881
  error: err3 instanceof Error ? err3.message : String(err3)
97432
97882
  });
97433
97883
  if (disposed || !capabilitiesRef.current) return;
97434
- capabilitiesRef.current = { ...capabilitiesRef.current, detectionComplete: true };
97884
+ commitCapabilities({ ...capabilitiesRef.current, detectionComplete: true });
97435
97885
  broadcastRealCapabilities();
97436
97886
  }
97437
97887
  async function detectAndCommitCapabilities() {
@@ -97444,7 +97894,7 @@ async function createDaemon(deps) {
97444
97894
  const cursorDetect = profileRegistry.get("cursor")?.authDetector;
97445
97895
  next = await mergeCursorAuthFromVault(next, cursorDetect, deps.log);
97446
97896
  if (disposed) return null;
97447
- capabilitiesRef.current = next;
97897
+ commitCapabilities(next);
97448
97898
  void saveCapabilitiesCache({
97449
97899
  environments: next.environments,
97450
97900
  mcpServers: next.mcpServers
@@ -97455,6 +97905,7 @@ async function createDaemon(deps) {
97455
97905
  await runFastSliceAgentBroadcast({
97456
97906
  detectAgents: (lastKnown) => detectAgentProviders(lastKnown),
97457
97907
  capabilitiesRef,
97908
+ commit: commitCapabilities,
97458
97909
  broadcast: broadcastRealCapabilities,
97459
97910
  log: deps.log,
97460
97911
  isDisposed: () => disposed
@@ -97603,12 +98054,12 @@ async function createDaemon(deps) {
97603
98054
  function wireProxyAuthCallbacks(proxyServer) {
97604
98055
  proxyServer.setOnAuthRequired((serverName) => {
97605
98056
  if (capabilitiesRef.current?.mcpServers) {
97606
- capabilitiesRef.current = {
98057
+ commitCapabilities({
97607
98058
  ...capabilitiesRef.current,
97608
98059
  mcpServers: capabilitiesRef.current.mcpServers.map(
97609
98060
  (s2) => s2.name === serverName ? { ...s2, authStatus: "unauthenticated" } : s2
97610
98061
  )
97611
- };
98062
+ });
97612
98063
  }
97613
98064
  refreshCoordinatorFromCaps();
97614
98065
  });
@@ -97616,12 +98067,12 @@ async function createDaemon(deps) {
97616
98067
  function markConnectedProxyServersAuthed(proxyServer) {
97617
98068
  if (!capabilitiesRef.current?.mcpServers) return;
97618
98069
  const connectedNames = new Set(proxyServer.getServerNames());
97619
- capabilitiesRef.current = {
98070
+ commitCapabilities({
97620
98071
  ...capabilitiesRef.current,
97621
98072
  mcpServers: capabilitiesRef.current.mcpServers.map(
97622
98073
  (s2) => connectedNames.has(s2.name) ? { ...s2, authStatus: "authenticated" } : s2
97623
98074
  )
97624
- };
98075
+ });
97625
98076
  }
97626
98077
  function buildEnrichedSkillRows(skills) {
97627
98078
  try {
@@ -97913,7 +98364,7 @@ async function createDaemon(deps) {
97913
98364
  return capabilitiesRef.current;
97914
98365
  },
97915
98366
  set capabilities(c) {
97916
- capabilitiesRef.current = applyPluginSupersession(c);
98367
+ commitCapabilities(applyPluginSupersession(c));
97917
98368
  refreshCoordinatorFromCaps();
97918
98369
  taskManager.refreshAllTaskSkillsSnapshots();
97919
98370
  reBroadcastEnrichedSkillsAfterWarm();
@@ -101194,4 +101645,4 @@ export {
101194
101645
  decideWorkspaceScope,
101195
101646
  serve
101196
101647
  };
101197
- //# sourceMappingURL=serve-AA4FCUY6.js.map
101648
+ //# sourceMappingURL=serve-WG7HQAYG.js.map