@schoolai/shipyard 3.32.0 → 3.33.0-rc.20260923.1

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 (45) hide show
  1. package/dist/capability-detector-worker.js +8 -8
  2. package/dist/{chunk-PVN4AJAH.js → chunk-3PJULQHI.js} +2 -2
  3. package/dist/{chunk-NFVZCZPA.js → chunk-4WJOA7HF.js} +2 -2
  4. package/dist/{chunk-RBXWUYDC.js → chunk-DQQOVPVK.js} +2 -2
  5. package/dist/{chunk-ZDJEJI62.js → chunk-ELZLWLTO.js} +2 -2
  6. package/dist/{chunk-42SK2S4V.js → chunk-FYT3ETTP.js} +2 -2
  7. package/dist/{chunk-SGFCXJJJ.js → chunk-JYAI3JHY.js} +2 -2
  8. package/dist/{chunk-7IS6KGWY.js → chunk-M6XAVFHJ.js} +3 -3
  9. package/dist/{chunk-FARMJBC7.js → chunk-MV7VSD2T.js} +2 -2
  10. package/dist/{chunk-FARMJBC7.js.map → chunk-MV7VSD2T.js.map} +1 -1
  11. package/dist/{chunk-RR6V6SNM.js → chunk-VHPNDDB5.js} +100 -17
  12. package/dist/chunk-VHPNDDB5.js.map +1 -0
  13. package/dist/{chunk-5TF227G7.js → chunk-WKJAGQU7.js} +3 -3
  14. package/dist/{chunk-UIBOKFSG.js → chunk-XPOXRIHH.js} +9 -47
  15. package/dist/chunk-XPOXRIHH.js.map +1 -0
  16. package/dist/{chunk-MAA2DPW5.js → chunk-ZXYFMJAS.js} +3 -3
  17. package/dist/cursor-runner.js +3 -3
  18. package/dist/electron-utility.js +2 -2
  19. package/dist/{git-repo-M634UEX7.js → git-repo-K45YAY55.js} +3 -3
  20. package/dist/index.js +4 -4
  21. package/dist/{login-6YTDDUP4.js → login-SOAGMUHR.js} +3 -3
  22. package/dist/{mcp-servers-LTYMM2F2.js → mcp-servers-GHDSX3UE.js} +3 -3
  23. package/dist/{plan-backfill-52OBXJRO.js → plan-backfill-W4FLMVC6.js} +5 -5
  24. package/dist/{serve-JVTSDF4E.js → serve-SDRQ7JYB.js} +120 -83
  25. package/dist/{serve-JVTSDF4E.js.map → serve-SDRQ7JYB.js.map} +1 -1
  26. package/dist/{skills-BX2NZKPN.js → skills-RO2BW4XO.js} +3 -3
  27. package/dist/{start-TYBSF7PZ.js → start-DHYV7OQM.js} +6 -6
  28. package/package.json +2 -3
  29. package/dist/chunk-RR6V6SNM.js.map +0 -1
  30. package/dist/chunk-UIBOKFSG.js.map +0 -1
  31. /package/dist/{chunk-PVN4AJAH.js.map → chunk-3PJULQHI.js.map} +0 -0
  32. /package/dist/{chunk-NFVZCZPA.js.map → chunk-4WJOA7HF.js.map} +0 -0
  33. /package/dist/{chunk-RBXWUYDC.js.map → chunk-DQQOVPVK.js.map} +0 -0
  34. /package/dist/{chunk-ZDJEJI62.js.map → chunk-ELZLWLTO.js.map} +0 -0
  35. /package/dist/{chunk-42SK2S4V.js.map → chunk-FYT3ETTP.js.map} +0 -0
  36. /package/dist/{chunk-SGFCXJJJ.js.map → chunk-JYAI3JHY.js.map} +0 -0
  37. /package/dist/{chunk-7IS6KGWY.js.map → chunk-M6XAVFHJ.js.map} +0 -0
  38. /package/dist/{chunk-5TF227G7.js.map → chunk-WKJAGQU7.js.map} +0 -0
  39. /package/dist/{chunk-MAA2DPW5.js.map → chunk-ZXYFMJAS.js.map} +0 -0
  40. /package/dist/{git-repo-M634UEX7.js.map → git-repo-K45YAY55.js.map} +0 -0
  41. /package/dist/{login-6YTDDUP4.js.map → login-SOAGMUHR.js.map} +0 -0
  42. /package/dist/{mcp-servers-LTYMM2F2.js.map → mcp-servers-GHDSX3UE.js.map} +0 -0
  43. /package/dist/{plan-backfill-52OBXJRO.js.map → plan-backfill-W4FLMVC6.js.map} +0 -0
  44. /package/dist/{skills-BX2NZKPN.js.map → skills-RO2BW4XO.js.map} +0 -0
  45. /package/dist/{start-TYBSF7PZ.js.map → start-DHYV7OQM.js.map} +0 -0
@@ -3,7 +3,7 @@
3
3
  // src/shared/fs-utils.ts
4
4
  import { randomUUID } from "crypto";
5
5
  import { existsSync } from "fs";
6
- import { mkdir, readFile, rename, unlink, writeFile } from "fs/promises";
6
+ import { link, mkdir, readFile, rename, stat, unlink, writeFile } from "fs/promises";
7
7
  import { dirname, join, parse } from "path";
8
8
  function isEnoent(err) {
9
9
  return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
@@ -26,8 +26,9 @@ var LOCK_ACQUIRE_TIMEOUT_MS = 5e3;
26
26
  var LOCK_STALE_MS = 3e4;
27
27
  var LOCK_BACKOFF_BASE_MS = 5;
28
28
  var LOCK_BACKOFF_CAP_MS = 50;
29
+ var UNPARSEABLE_LOCK_GRACE_MS = 5e3;
29
30
  function isLockStale(lock, nowMs, staleMs, isProcessAlive) {
30
- if (nowMs - lock.startedAt > staleMs) return true;
31
+ if (nowMs - (lock.refreshedAt ?? lock.startedAt) > staleMs) return true;
31
32
  if (!isProcessAlive(lock.pid)) return true;
32
33
  return false;
33
34
  }
@@ -55,21 +56,92 @@ async function acquireFileLock(lockPath, owner, settings) {
55
56
  const deadline = settings.now() + settings.timeoutMs;
56
57
  let attempt = 0;
57
58
  while (true) {
58
- try {
59
- await writeFile(lockPath, JSON.stringify(owner), { flag: "wx", mode: 384 });
59
+ if (await tryCreateLock(lockPath, owner)) return;
60
+ const stole = await tryStealStaleLock(
61
+ lockPath,
62
+ settings.now(),
63
+ settings.staleMs,
64
+ settings.isProcessAlive
65
+ );
66
+ if (settings.now() >= deadline) {
67
+ throw new Error(
68
+ `withFileLock: timed out acquiring ${lockPath} after ${settings.timeoutMs}ms`
69
+ );
70
+ }
71
+ if (stole) continue;
72
+ attempt += 1;
73
+ await settings.sleep(backoffDelay(attempt));
74
+ }
75
+ }
76
+ async function tryAcquireHeldFileLock(lockPath, options) {
77
+ const now = options.now ?? Date.now;
78
+ const isProcessAlive = options.isProcessAlive ?? defaultIsProcessAlive;
79
+ const startedAt = now();
80
+ const owner = {
81
+ pid: process.pid,
82
+ startedAt,
83
+ refreshedAt: startedAt,
84
+ id: randomUUID()
85
+ };
86
+ await mkdir(dirname(lockPath), { recursive: true });
87
+ if (!await tryCreateLock(lockPath, owner)) {
88
+ if (!await tryStealStaleLock(lockPath, now(), options.staleMs, isProcessAlive)) return null;
89
+ if (!await tryCreateLock(lockPath, owner)) return null;
90
+ }
91
+ let compromised = null;
92
+ let released = false;
93
+ const refresh = async () => {
94
+ const current = await readLockRecord(lockPath);
95
+ if (current?.id !== owner.id) {
96
+ compromised = new Error(
97
+ `Held lock compromised: ${lockPath} is no longer owned by this process`
98
+ );
99
+ clearInterval(timer);
60
100
  return;
61
- } catch (err) {
62
- if (!isEexist(err)) throw err;
63
- if (await tryStealStaleLock(lockPath, settings.now(), settings.staleMs, settings.isProcessAlive))
64
- continue;
65
- if (settings.now() >= deadline) {
66
- throw new Error(
67
- `withFileLock: timed out acquiring ${lockPath} after ${settings.timeoutMs}ms`
68
- );
69
- }
70
- attempt += 1;
71
- await settings.sleep(backoffDelay(attempt));
72
101
  }
102
+ await atomicWriteFile(lockPath, JSON.stringify({ ...owner, refreshedAt: now() }), {
103
+ mode: 384
104
+ });
105
+ };
106
+ const timer = setInterval(() => {
107
+ if (released || compromised) return;
108
+ refresh().catch((err) => {
109
+ compromised = err instanceof Error ? err : new Error(String(err));
110
+ clearInterval(timer);
111
+ });
112
+ }, options.refreshMs);
113
+ timer.unref();
114
+ return {
115
+ assertHealthy() {
116
+ if (compromised) throw compromised;
117
+ },
118
+ async release() {
119
+ if (released) return;
120
+ released = true;
121
+ clearInterval(timer);
122
+ await releaseLockIfOwner(lockPath, owner);
123
+ }
124
+ };
125
+ }
126
+ async function tryCreateLock(lockPath, owner) {
127
+ const tmpPath = `${lockPath}.${process.pid}.${owner.id}.tmp`;
128
+ await writeFile(tmpPath, JSON.stringify(owner), { mode: 384 });
129
+ try {
130
+ await link(tmpPath, lockPath);
131
+ return true;
132
+ } catch (err) {
133
+ if (isEexist(err)) return false;
134
+ throw err;
135
+ } finally {
136
+ await unlinkIfExists(tmpPath);
137
+ }
138
+ }
139
+ async function readLockRecord(lockPath) {
140
+ try {
141
+ return parseLockRecord(await readFile(lockPath, "utf-8"));
142
+ } catch (err) {
143
+ if (isEnoent(err)) return null;
144
+ throw err;
73
145
  }
74
146
  }
75
147
  function defaultSleep(ms) {
@@ -98,8 +170,9 @@ function parseLockRecord(raw) {
98
170
  const pid = Reflect.get(value, "pid");
99
171
  const startedAt = Reflect.get(value, "startedAt");
100
172
  const id = Reflect.get(value, "id");
173
+ const refreshedAt = Reflect.get(value, "refreshedAt");
101
174
  if (typeof pid === "number" && typeof startedAt === "number" && typeof id === "string") {
102
- return { pid, startedAt, id };
175
+ return typeof refreshedAt === "number" ? { pid, startedAt, id, refreshedAt } : { pid, startedAt, id };
103
176
  }
104
177
  return null;
105
178
  }
@@ -112,6 +185,7 @@ async function tryStealStaleLock(lockPath, nowMs, staleMs, isProcessAlive) {
112
185
  return false;
113
186
  }
114
187
  if (!existing) {
188
+ if (!await isOlderThan(lockPath, nowMs, UNPARSEABLE_LOCK_GRACE_MS)) return false;
115
189
  await unlinkIfExists(lockPath);
116
190
  return true;
117
191
  }
@@ -119,6 +193,14 @@ async function tryStealStaleLock(lockPath, nowMs, staleMs, isProcessAlive) {
119
193
  await unlinkMatching(lockPath, existing);
120
194
  return true;
121
195
  }
196
+ async function isOlderThan(path, nowMs, ageMs) {
197
+ try {
198
+ return nowMs - (await stat(path)).mtimeMs > ageMs;
199
+ } catch (err) {
200
+ if (isEnoent(err)) return true;
201
+ throw err;
202
+ }
203
+ }
122
204
  async function unlinkMatching(lockPath, expected) {
123
205
  try {
124
206
  const current = parseLockRecord(await readFile(lockPath, "utf-8"));
@@ -168,6 +250,7 @@ export {
168
250
  isCorruptionError,
169
251
  atomicWriteFile,
170
252
  withFileLock,
253
+ tryAcquireHeldFileLock,
171
254
  findProjectRoot
172
255
  };
173
- //# sourceMappingURL=chunk-RR6V6SNM.js.map
256
+ //# sourceMappingURL=chunk-VHPNDDB5.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/shared/fs-utils.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\nimport { existsSync } from 'node:fs';\nimport { link, mkdir, readFile, rename, stat, unlink, writeFile } from 'node:fs/promises';\nimport { dirname, join, parse } from 'node:path';\n\nexport function isEnoent(err: unknown): boolean {\n return typeof err === 'object' && err !== null && 'code' in err && err.code === 'ENOENT';\n}\n\nexport function isEexist(err: unknown): boolean {\n return typeof err === 'object' && err !== null && 'code' in err && err.code === 'EEXIST';\n}\n\n/**\n * Detect JSON parse errors or Zod validation failures that indicate\n * a corrupt store file. Shared across all file-backed stores.\n */\nexport function isCorruptionError(err: unknown): boolean {\n if (err instanceof SyntaxError) return true;\n if (typeof err === 'object' && err !== null && 'name' in err && err.name === 'ZodError')\n return true;\n return false;\n}\n\n/**\n * Write `contents` to `filePath` atomically by writing to a unique tmp\n * path, then `rename`-ing into place.\n *\n * The tmp path includes `pid` + `randomUUID()` so concurrent writers\n * (different daemon processes, parallel test workers) cannot truncate\n * each other's in-flight tmp file. A shared `${filePath}.tmp` is unsafe:\n * if writer A's `open(w)` truncates a tmp that writer B already wrote\n * but hasn't yet renamed, the next `rename(tmp, filePath)` slides a\n * partial 0-byte (or `{`) into the canonical path — exactly the\n * corruption we observed in `rate-limits.json`.\n *\n * `rename` within one filesystem is atomic per POSIX, so a reader either\n * sees the prior canonical file or the fully-written new one. fsync is\n * intentionally NOT called — power-loss durability is not the failure\n * mode we're fixing, and forcing fsync on every store write would slow\n * the daemon meaningfully without addressing the actual race.\n *\n * Pass `mode` to set permissions on the tmp file (used by credentials-\n * vault to preserve `0o600`). Callers are responsible for `mkdir`-ing\n * the parent dir.\n */\nexport async function atomicWriteFile(\n filePath: string,\n contents: string,\n options?: { mode?: number }\n): Promise<void> {\n const tmpPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;\n await writeFile(tmpPath, contents, { encoding: 'utf-8', mode: options?.mode });\n await rename(tmpPath, filePath);\n}\n\ninterface LockRecord {\n pid: number;\n startedAt: number;\n /** Held locks (`tryAcquireHeldFileLock`) rewrite this periodically; staleness is judged from it. */\n refreshedAt?: number;\n /**\n * Unique per acquisition — guards against a pid+startedAt collision (17\n * daemons can start within the same millisecond) unlinking a live peer's lock.\n */\n id: string;\n}\n\nexport interface WithFileLockOptions {\n /** Max time to wait to acquire before throwing. Default 5000ms. */\n timeoutMs?: number;\n /** Age past which a held lock is considered abandoned. Default 5000ms. */\n staleMs?: number;\n /** Injected in tests; defaults to `Date.now`. */\n now?: () => number;\n /** Injected in tests; defaults to a `process.kill(pid, 0)` liveness probe. */\n isProcessAlive?: (pid: number) => boolean;\n /** Injected in tests; defaults to a real `setTimeout` delay. */\n sleep?: (ms: number) => Promise<void>;\n}\n\nconst LOCK_ACQUIRE_TIMEOUT_MS = 5_000;\n/**\n * Stale-takeover age must be well ABOVE the acquire timeout so a live holder\n * whose critical section runs longer than a waiter's timeout is never declared\n * stale and stolen (which would run two `fn`s concurrently). A crashed holder\n * is still reclaimed immediately via the PID-liveness check; this age is only\n * the backstop for PID reuse.\n */\nconst LOCK_STALE_MS = 30_000;\nconst LOCK_BACKOFF_BASE_MS = 5;\nconst LOCK_BACKOFF_CAP_MS = 50;\nconst UNPARSEABLE_LOCK_GRACE_MS = 5_000;\n\n/**\n * Pure predicate: is an existing lock safe to steal? Stale when the owner PID\n * is no longer alive, or it has been held longer than `staleMs` (a backstop\n * against a crashed owner whose PID was later reused). Mirrors\n * `self-update-lock.ts:isStaleLock`; duplicated here because `fs-utils` is a\n * lower layer and must not import from `services/` (that would be a cycle).\n */\nexport function isLockStale(\n lock: { pid: number; startedAt: number; refreshedAt?: number },\n nowMs: number,\n staleMs: number,\n isProcessAlive: (pid: number) => boolean\n): boolean {\n if (nowMs - (lock.refreshedAt ?? lock.startedAt) > staleMs) return true;\n if (!isProcessAlive(lock.pid)) return true;\n return false;\n}\n\n/**\n * Run `fn` while holding a cross-process advisory lock at `lockPath`.\n *\n * Cross-process safe via an atomic `link` create (see `tryCreateLock`) —\n * exactly one process wins; losers inspect the existing lock and either wait (live owner) or\n * steal it (stale owner). Throws if the lock cannot be acquired within\n * `timeoutMs`: callers MUST treat a throw as \"did not run\". For credential /\n * record writes, failing the write (and retrying later) is strictly safer than\n * proceeding unlocked and clobbering another daemon's concurrent write.\n *\n * The lock is released in a `finally`, and only if we still own it — guarding\n * against unlinking a successor's lock when our `fn` overran `staleMs` and a\n * peer stole the lock mid-flight.\n */\nexport async function withFileLock<T>(\n lockPath: string,\n fn: () => Promise<T>,\n options?: WithFileLockOptions\n): Promise<T> {\n const settings = resolveLockSettings(options);\n const owner: LockRecord = { pid: process.pid, startedAt: settings.now(), id: randomUUID() };\n\n await mkdir(dirname(lockPath), { recursive: true });\n await acquireFileLock(lockPath, owner, settings);\n\n try {\n return await fn();\n } finally {\n await releaseLockIfOwner(lockPath, owner);\n }\n}\n\ninterface LockSettings {\n timeoutMs: number;\n staleMs: number;\n now: () => number;\n isProcessAlive: (pid: number) => boolean;\n sleep: (ms: number) => Promise<void>;\n}\n\nfunction resolveLockSettings(options?: WithFileLockOptions): LockSettings {\n return {\n timeoutMs: options?.timeoutMs ?? LOCK_ACQUIRE_TIMEOUT_MS,\n staleMs: options?.staleMs ?? LOCK_STALE_MS,\n now: options?.now ?? Date.now,\n isProcessAlive: options?.isProcessAlive ?? defaultIsProcessAlive,\n sleep: options?.sleep ?? defaultSleep,\n };\n}\n\n/**\n * Throw rather than return on timeout: a write that cannot take the lock must\n * NOT silently proceed unlocked, which would reintroduce the whole-file clobber.\n */\nasync function acquireFileLock(\n lockPath: string,\n owner: LockRecord,\n settings: LockSettings\n): Promise<void> {\n const deadline = settings.now() + settings.timeoutMs;\n let attempt = 0;\n\n while (true) {\n if (await tryCreateLock(lockPath, owner)) return;\n const stole = await tryStealStaleLock(\n lockPath,\n settings.now(),\n settings.staleMs,\n settings.isProcessAlive\n );\n /**\n * Deadline before the steal-retry: a path whose create keeps failing\n * EEXIST while every read says ENOENT (e.g. a dangling symlink) used to\n * `continue` here forever — a no-sleep, no-timeout event-loop spin.\n */\n if (settings.now() >= deadline) {\n throw new Error(\n `withFileLock: timed out acquiring ${lockPath} after ${settings.timeoutMs}ms`\n );\n }\n if (stole) continue;\n attempt += 1;\n await settings.sleep(backoffDelay(attempt));\n }\n}\n\nexport interface HeldFileLock {\n /** Throws once another process has taken the lock over (e.g. we stalled past `staleMs`). */\n assertHealthy(): void;\n release(): Promise<void>;\n}\n\nexport interface HeldFileLockOptions {\n /** A held lock not refreshed for this long (or whose owner PID is dead) may be stolen. */\n staleMs: number;\n /** How often the owner rewrites `refreshedAt`. Must be well under `staleMs`. */\n refreshMs: number;\n now?: () => number;\n isProcessAlive?: (pid: number) => boolean;\n}\n\n/**\n * Try once to take a long-held cross-process lock (held for minutes to\n * hours, unlike `withFileLock`'s short critical sections). Returns null when a\n * live, fresh owner holds it. The owner refreshes the record every\n * `refreshMs`; a crashed owner is reclaimable immediately (dead PID) or after\n * `staleMs` without refresh (PID reuse backstop).\n *\n * Replaces `proper-lockfile`, whose `signal-exit` dependency installs a\n * SIGTRAP listener: libuv then catches V8's OOM trap instruction, returns to\n * it, and the daemon spins at 99% CPU forever instead of crashing (the Sept\n * 2026 overnight \"wedges\").\n */\nexport async function tryAcquireHeldFileLock(\n lockPath: string,\n options: HeldFileLockOptions\n): Promise<HeldFileLock | null> {\n const now = options.now ?? Date.now;\n const isProcessAlive = options.isProcessAlive ?? defaultIsProcessAlive;\n const startedAt = now();\n const owner: LockRecord = {\n pid: process.pid,\n startedAt,\n refreshedAt: startedAt,\n id: randomUUID(),\n };\n\n await mkdir(dirname(lockPath), { recursive: true });\n if (!(await tryCreateLock(lockPath, owner))) {\n if (!(await tryStealStaleLock(lockPath, now(), options.staleMs, isProcessAlive))) return null;\n if (!(await tryCreateLock(lockPath, owner))) return null;\n }\n\n let compromised: Error | null = null;\n let released = false;\n const refresh = async () => {\n const current = await readLockRecord(lockPath);\n if (current?.id !== owner.id) {\n compromised = new Error(\n `Held lock compromised: ${lockPath} is no longer owned by this process`\n );\n clearInterval(timer);\n return;\n }\n await atomicWriteFile(lockPath, JSON.stringify({ ...owner, refreshedAt: now() }), {\n mode: 0o600,\n });\n };\n const timer = setInterval(() => {\n if (released || compromised) return;\n refresh().catch((err: unknown) => {\n compromised = err instanceof Error ? err : new Error(String(err));\n clearInterval(timer);\n });\n }, options.refreshMs);\n timer.unref();\n\n return {\n assertHealthy() {\n if (compromised) throw compromised;\n },\n async release() {\n if (released) return;\n released = true;\n clearInterval(timer);\n await releaseLockIfOwner(lockPath, owner);\n },\n };\n}\n\n/**\n * Create the lock with its content already in place: write a private temp\n * file, then hard-link it into place (`link` fails EEXIST atomically, like\n * O_EXCL). `writeFile(…, { flag: 'wx' })` instead creates the file BEFORE its\n * bytes land, so a waiter could read '' mid-create, treat it as corrupt, and\n * steal a live lock — two holders at once.\n */\nasync function tryCreateLock(lockPath: string, owner: LockRecord): Promise<boolean> {\n const tmpPath = `${lockPath}.${process.pid}.${owner.id}.tmp`;\n await writeFile(tmpPath, JSON.stringify(owner), { mode: 0o600 });\n try {\n await link(tmpPath, lockPath);\n return true;\n } catch (err) {\n if (isEexist(err)) return false;\n throw err;\n } finally {\n await unlinkIfExists(tmpPath);\n }\n}\n\nasync function readLockRecord(lockPath: string): Promise<LockRecord | null> {\n try {\n return parseLockRecord(await readFile(lockPath, 'utf-8'));\n } catch (err) {\n if (isEnoent(err)) return null;\n throw err;\n }\n}\n\nfunction defaultSleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction defaultIsProcessAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (err) {\n /** EPERM => the process exists but we lack permission to signal it. */\n return typeof err === 'object' && err !== null && 'code' in err && err.code === 'EPERM';\n }\n}\n\nfunction backoffDelay(attempt: number): number {\n const base = Math.min(LOCK_BACKOFF_BASE_MS * 2 ** (attempt - 1), LOCK_BACKOFF_CAP_MS);\n return base + Math.random() * LOCK_BACKOFF_BASE_MS;\n}\n\nfunction parseLockRecord(raw: string): LockRecord | null {\n let value: unknown;\n try {\n value = JSON.parse(raw);\n } catch {\n return null;\n }\n if (typeof value !== 'object' || value === null) return null;\n const pid = Reflect.get(value, 'pid');\n const startedAt = Reflect.get(value, 'startedAt');\n const id = Reflect.get(value, 'id');\n const refreshedAt = Reflect.get(value, 'refreshedAt');\n if (typeof pid === 'number' && typeof startedAt === 'number' && typeof id === 'string') {\n return typeof refreshedAt === 'number'\n ? { pid, startedAt, id, refreshedAt }\n : { pid, startedAt, id };\n }\n return null;\n}\n\nasync function tryStealStaleLock(\n lockPath: string,\n nowMs: number,\n staleMs: number,\n isProcessAlive: (pid: number) => boolean\n): Promise<boolean> {\n let existing: LockRecord | null;\n try {\n existing = parseLockRecord(await readFile(lockPath, 'utf-8'));\n } catch (err) {\n if (isEnoent(err)) return true;\n return false;\n }\n if (!existing) {\n /**\n * Unparseable: either corrupt (reclaim) or a pre-upgrade peer mid-create\n * via `writeFile(wx)` (still being written — never steal). Only a lock\n * that has sat unparseable past the grace window is treated as corrupt.\n */\n if (!(await isOlderThan(lockPath, nowMs, UNPARSEABLE_LOCK_GRACE_MS))) return false;\n await unlinkIfExists(lockPath);\n return true;\n }\n if (!isLockStale(existing, nowMs, staleMs, isProcessAlive)) return false;\n await unlinkMatching(lockPath, existing);\n return true;\n}\n\nasync function isOlderThan(path: string, nowMs: number, ageMs: number): Promise<boolean> {\n try {\n return nowMs - (await stat(path)).mtimeMs > ageMs;\n } catch (err) {\n if (isEnoent(err)) return true;\n throw err;\n }\n}\n\nasync function unlinkMatching(lockPath: string, expected: LockRecord): Promise<void> {\n try {\n const current = parseLockRecord(await readFile(lockPath, 'utf-8'));\n if (current && current.id !== expected.id) {\n return;\n }\n } catch (err) {\n if (isEnoent(err)) return;\n }\n await unlinkIfExists(lockPath);\n}\n\nasync function releaseLockIfOwner(lockPath: string, owner: LockRecord): Promise<void> {\n try {\n const current = parseLockRecord(await readFile(lockPath, 'utf-8'));\n if (current && current.id !== owner.id) {\n return;\n }\n } catch (err) {\n if (isEnoent(err)) return;\n }\n await unlinkIfExists(lockPath);\n}\n\nasync function unlinkIfExists(path: string): Promise<void> {\n try {\n await unlink(path);\n } catch (err) {\n if (!isEnoent(err)) throw err;\n }\n}\n\n/**\n * Walk up the directory tree from `startDir` to find the project root.\n *\n * Looks for `.git` (directory or file — worktrees use a `.git` file) or\n * `pnpm-workspace.yaml` as root markers.\n *\n * Returns a discriminated result so callers can distinguish \"marker found\"\n * (genuinely scoped, even if the marker is in startDir itself) from\n * \"no marker anywhere\" (genuinely unscoped). The bare-string return shape\n * conflated these, since both cases produced `startDir` — breaking the\n * unscoped detection in `serve.ts` whenever the daemon was launched from\n * a project's own root directory.\n *\n * In dev: `startDir` is typically `apps/daemon/` (pnpm runs from package dir),\n * so this walks up to find the monorepo root. In prod install: `startDir` is\n * the user's project directory where the daemon is launched, and `.git` is\n * found immediately. Called once at daemon startup — the result is used as\n * the workspace root for file-io, LSP, and file watching.\n */\nexport function findProjectRoot(startDir: string): { root: string; foundMarker: boolean } {\n let dir = startDir;\n while (true) {\n if (existsSync(join(dir, 'pnpm-workspace.yaml')) || existsSync(join(dir, '.git'))) {\n return { root: dir, foundMarker: true };\n }\n const parent = dirname(dir);\n if (parent === dir || parent === parse(dir).root) {\n return { root: startDir, foundMarker: false };\n }\n dir = parent;\n }\n}\n"],"mappings":";;;AAAA,SAAS,kBAAkB;AAC3B,SAAS,kBAAkB;AAC3B,SAAS,MAAM,OAAO,UAAU,QAAQ,MAAM,QAAQ,iBAAiB;AACvE,SAAS,SAAS,MAAM,aAAa;AAE9B,SAAS,SAAS,KAAuB;AAC9C,SAAO,OAAO,QAAQ,YAAY,QAAQ,QAAQ,UAAU,OAAO,IAAI,SAAS;AAClF;AAEO,SAAS,SAAS,KAAuB;AAC9C,SAAO,OAAO,QAAQ,YAAY,QAAQ,QAAQ,UAAU,OAAO,IAAI,SAAS;AAClF;AAMO,SAAS,kBAAkB,KAAuB;AACvD,MAAI,eAAe,YAAa,QAAO;AACvC,MAAI,OAAO,QAAQ,YAAY,QAAQ,QAAQ,UAAU,OAAO,IAAI,SAAS;AAC3E,WAAO;AACT,SAAO;AACT;AAwBA,eAAsB,gBACpB,UACA,UACA,SACe;AACf,QAAM,UAAU,GAAG,QAAQ,IAAI,QAAQ,GAAG,IAAI,WAAW,CAAC;AAC1D,QAAM,UAAU,SAAS,UAAU,EAAE,UAAU,SAAS,MAAM,SAAS,KAAK,CAAC;AAC7E,QAAM,OAAO,SAAS,QAAQ;AAChC;AA2BA,IAAM,0BAA0B;AAQhC,IAAM,gBAAgB;AACtB,IAAM,uBAAuB;AAC7B,IAAM,sBAAsB;AAC5B,IAAM,4BAA4B;AAS3B,SAAS,YACd,MACA,OACA,SACA,gBACS;AACT,MAAI,SAAS,KAAK,eAAe,KAAK,aAAa,QAAS,QAAO;AACnE,MAAI,CAAC,eAAe,KAAK,GAAG,EAAG,QAAO;AACtC,SAAO;AACT;AAgBA,eAAsB,aACpB,UACA,IACA,SACY;AACZ,QAAM,WAAW,oBAAoB,OAAO;AAC5C,QAAM,QAAoB,EAAE,KAAK,QAAQ,KAAK,WAAW,SAAS,IAAI,GAAG,IAAI,WAAW,EAAE;AAE1F,QAAM,MAAM,QAAQ,QAAQ,GAAG,EAAE,WAAW,KAAK,CAAC;AAClD,QAAM,gBAAgB,UAAU,OAAO,QAAQ;AAE/C,MAAI;AACF,WAAO,MAAM,GAAG;AAAA,EAClB,UAAE;AACA,UAAM,mBAAmB,UAAU,KAAK;AAAA,EAC1C;AACF;AAUA,SAAS,oBAAoB,SAA6C;AACxE,SAAO;AAAA,IACL,WAAW,SAAS,aAAa;AAAA,IACjC,SAAS,SAAS,WAAW;AAAA,IAC7B,KAAK,SAAS,OAAO,KAAK;AAAA,IAC1B,gBAAgB,SAAS,kBAAkB;AAAA,IAC3C,OAAO,SAAS,SAAS;AAAA,EAC3B;AACF;AAMA,eAAe,gBACb,UACA,OACA,UACe;AACf,QAAM,WAAW,SAAS,IAAI,IAAI,SAAS;AAC3C,MAAI,UAAU;AAEd,SAAO,MAAM;AACX,QAAI,MAAM,cAAc,UAAU,KAAK,EAAG;AAC1C,UAAM,QAAQ,MAAM;AAAA,MAClB;AAAA,MACA,SAAS,IAAI;AAAA,MACb,SAAS;AAAA,MACT,SAAS;AAAA,IACX;AAMA,QAAI,SAAS,IAAI,KAAK,UAAU;AAC9B,YAAM,IAAI;AAAA,QACR,qCAAqC,QAAQ,UAAU,SAAS,SAAS;AAAA,MAC3E;AAAA,IACF;AACA,QAAI,MAAO;AACX,eAAW;AACX,UAAM,SAAS,MAAM,aAAa,OAAO,CAAC;AAAA,EAC5C;AACF;AA6BA,eAAsB,uBACpB,UACA,SAC8B;AAC9B,QAAM,MAAM,QAAQ,OAAO,KAAK;AAChC,QAAM,iBAAiB,QAAQ,kBAAkB;AACjD,QAAM,YAAY,IAAI;AACtB,QAAM,QAAoB;AAAA,IACxB,KAAK,QAAQ;AAAA,IACb;AAAA,IACA,aAAa;AAAA,IACb,IAAI,WAAW;AAAA,EACjB;AAEA,QAAM,MAAM,QAAQ,QAAQ,GAAG,EAAE,WAAW,KAAK,CAAC;AAClD,MAAI,CAAE,MAAM,cAAc,UAAU,KAAK,GAAI;AAC3C,QAAI,CAAE,MAAM,kBAAkB,UAAU,IAAI,GAAG,QAAQ,SAAS,cAAc,EAAI,QAAO;AACzF,QAAI,CAAE,MAAM,cAAc,UAAU,KAAK,EAAI,QAAO;AAAA,EACtD;AAEA,MAAI,cAA4B;AAChC,MAAI,WAAW;AACf,QAAM,UAAU,YAAY;AAC1B,UAAM,UAAU,MAAM,eAAe,QAAQ;AAC7C,QAAI,SAAS,OAAO,MAAM,IAAI;AAC5B,oBAAc,IAAI;AAAA,QAChB,0BAA0B,QAAQ;AAAA,MACpC;AACA,oBAAc,KAAK;AACnB;AAAA,IACF;AACA,UAAM,gBAAgB,UAAU,KAAK,UAAU,EAAE,GAAG,OAAO,aAAa,IAAI,EAAE,CAAC,GAAG;AAAA,MAChF,MAAM;AAAA,IACR,CAAC;AAAA,EACH;AACA,QAAM,QAAQ,YAAY,MAAM;AAC9B,QAAI,YAAY,YAAa;AAC7B,YAAQ,EAAE,MAAM,CAAC,QAAiB;AAChC,oBAAc,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC;AAChE,oBAAc,KAAK;AAAA,IACrB,CAAC;AAAA,EACH,GAAG,QAAQ,SAAS;AACpB,QAAM,MAAM;AAEZ,SAAO;AAAA,IACL,gBAAgB;AACd,UAAI,YAAa,OAAM;AAAA,IACzB;AAAA,IACA,MAAM,UAAU;AACd,UAAI,SAAU;AACd,iBAAW;AACX,oBAAc,KAAK;AACnB,YAAM,mBAAmB,UAAU,KAAK;AAAA,IAC1C;AAAA,EACF;AACF;AASA,eAAe,cAAc,UAAkB,OAAqC;AAClF,QAAM,UAAU,GAAG,QAAQ,IAAI,QAAQ,GAAG,IAAI,MAAM,EAAE;AACtD,QAAM,UAAU,SAAS,KAAK,UAAU,KAAK,GAAG,EAAE,MAAM,IAAM,CAAC;AAC/D,MAAI;AACF,UAAM,KAAK,SAAS,QAAQ;AAC5B,WAAO;AAAA,EACT,SAAS,KAAK;AACZ,QAAI,SAAS,GAAG,EAAG,QAAO;AAC1B,UAAM;AAAA,EACR,UAAE;AACA,UAAM,eAAe,OAAO;AAAA,EAC9B;AACF;AAEA,eAAe,eAAe,UAA8C;AAC1E,MAAI;AACF,WAAO,gBAAgB,MAAM,SAAS,UAAU,OAAO,CAAC;AAAA,EAC1D,SAAS,KAAK;AACZ,QAAI,SAAS,GAAG,EAAG,QAAO;AAC1B,UAAM;AAAA,EACR;AACF;AAEA,SAAS,aAAa,IAA2B;AAC/C,SAAO,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,EAAE,CAAC;AACzD;AAEA,SAAS,sBAAsB,KAAsB;AACnD,MAAI;AACF,YAAQ,KAAK,KAAK,CAAC;AACnB,WAAO;AAAA,EACT,SAAS,KAAK;AAEZ,WAAO,OAAO,QAAQ,YAAY,QAAQ,QAAQ,UAAU,OAAO,IAAI,SAAS;AAAA,EAClF;AACF;AAEA,SAAS,aAAa,SAAyB;AAC7C,QAAM,OAAO,KAAK,IAAI,uBAAuB,MAAM,UAAU,IAAI,mBAAmB;AACpF,SAAO,OAAO,KAAK,OAAO,IAAI;AAChC;AAEA,SAAS,gBAAgB,KAAgC;AACvD,MAAI;AACJ,MAAI;AACF,YAAQ,KAAK,MAAM,GAAG;AAAA,EACxB,QAAQ;AACN,WAAO;AAAA,EACT;AACA,MAAI,OAAO,UAAU,YAAY,UAAU,KAAM,QAAO;AACxD,QAAM,MAAM,QAAQ,IAAI,OAAO,KAAK;AACpC,QAAM,YAAY,QAAQ,IAAI,OAAO,WAAW;AAChD,QAAM,KAAK,QAAQ,IAAI,OAAO,IAAI;AAClC,QAAM,cAAc,QAAQ,IAAI,OAAO,aAAa;AACpD,MAAI,OAAO,QAAQ,YAAY,OAAO,cAAc,YAAY,OAAO,OAAO,UAAU;AACtF,WAAO,OAAO,gBAAgB,WAC1B,EAAE,KAAK,WAAW,IAAI,YAAY,IAClC,EAAE,KAAK,WAAW,GAAG;AAAA,EAC3B;AACA,SAAO;AACT;AAEA,eAAe,kBACb,UACA,OACA,SACA,gBACkB;AAClB,MAAI;AACJ,MAAI;AACF,eAAW,gBAAgB,MAAM,SAAS,UAAU,OAAO,CAAC;AAAA,EAC9D,SAAS,KAAK;AACZ,QAAI,SAAS,GAAG,EAAG,QAAO;AAC1B,WAAO;AAAA,EACT;AACA,MAAI,CAAC,UAAU;AAMb,QAAI,CAAE,MAAM,YAAY,UAAU,OAAO,yBAAyB,EAAI,QAAO;AAC7E,UAAM,eAAe,QAAQ;AAC7B,WAAO;AAAA,EACT;AACA,MAAI,CAAC,YAAY,UAAU,OAAO,SAAS,cAAc,EAAG,QAAO;AACnE,QAAM,eAAe,UAAU,QAAQ;AACvC,SAAO;AACT;AAEA,eAAe,YAAY,MAAc,OAAe,OAAiC;AACvF,MAAI;AACF,WAAO,SAAS,MAAM,KAAK,IAAI,GAAG,UAAU;AAAA,EAC9C,SAAS,KAAK;AACZ,QAAI,SAAS,GAAG,EAAG,QAAO;AAC1B,UAAM;AAAA,EACR;AACF;AAEA,eAAe,eAAe,UAAkB,UAAqC;AACnF,MAAI;AACF,UAAM,UAAU,gBAAgB,MAAM,SAAS,UAAU,OAAO,CAAC;AACjE,QAAI,WAAW,QAAQ,OAAO,SAAS,IAAI;AACzC;AAAA,IACF;AAAA,EACF,SAAS,KAAK;AACZ,QAAI,SAAS,GAAG,EAAG;AAAA,EACrB;AACA,QAAM,eAAe,QAAQ;AAC/B;AAEA,eAAe,mBAAmB,UAAkB,OAAkC;AACpF,MAAI;AACF,UAAM,UAAU,gBAAgB,MAAM,SAAS,UAAU,OAAO,CAAC;AACjE,QAAI,WAAW,QAAQ,OAAO,MAAM,IAAI;AACtC;AAAA,IACF;AAAA,EACF,SAAS,KAAK;AACZ,QAAI,SAAS,GAAG,EAAG;AAAA,EACrB;AACA,QAAM,eAAe,QAAQ;AAC/B;AAEA,eAAe,eAAe,MAA6B;AACzD,MAAI;AACF,UAAM,OAAO,IAAI;AAAA,EACnB,SAAS,KAAK;AACZ,QAAI,CAAC,SAAS,GAAG,EAAG,OAAM;AAAA,EAC5B;AACF;AAqBO,SAAS,gBAAgB,UAA0D;AACxF,MAAI,MAAM;AACV,SAAO,MAAM;AACX,QAAI,WAAW,KAAK,KAAK,qBAAqB,CAAC,KAAK,WAAW,KAAK,KAAK,MAAM,CAAC,GAAG;AACjF,aAAO,EAAE,MAAM,KAAK,aAAa,KAAK;AAAA,IACxC;AACA,UAAM,SAAS,QAAQ,GAAG;AAC1B,QAAI,WAAW,OAAO,WAAW,MAAM,GAAG,EAAE,MAAM;AAChD,aAAO,EAAE,MAAM,UAAU,aAAa,MAAM;AAAA,IAC9C;AACA,UAAM;AAAA,EACR;AACF;","names":[]}
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-4T2OQAVL.js";
5
5
  import {
6
6
  isEnoent
7
- } from "./chunk-RR6V6SNM.js";
7
+ } from "./chunk-VHPNDDB5.js";
8
8
  import {
9
9
  logger
10
10
  } from "./chunk-FIE2FRIW.js";
@@ -261,7 +261,7 @@ async function refreshRepoPathsInBackground(log) {
261
261
  const startGen = cacheGeneration;
262
262
  const start = Date.now();
263
263
  try {
264
- const { findGitRepos: findGitRepos2, discoverWorktrees: discoverWorktrees2 } = await import("./git-repo-M634UEX7.js");
264
+ const { findGitRepos: findGitRepos2, discoverWorktrees: discoverWorktrees2 } = await import("./git-repo-K45YAY55.js");
265
265
  const { homedir: homedir2 } = await import("os");
266
266
  const repos = await findGitRepos2(homedir2());
267
267
  const worktrees = await discoverWorktrees2(repos);
@@ -796,4 +796,4 @@ export {
796
796
  discoverWorktrees,
797
797
  _testing
798
798
  };
799
- //# sourceMappingURL=chunk-5TF227G7.js.map
799
+ //# sourceMappingURL=chunk-WKJAGQU7.js.map
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  detectSkills
4
- } from "./chunk-ZDJEJI62.js";
4
+ } from "./chunk-ELZLWLTO.js";
5
5
  import {
6
6
  AUTH_STATUS_TIMEOUT_MS,
7
7
  BUFFER_OVERFLOW_MSG,
@@ -13,7 +13,7 @@ import {
13
13
  run,
14
14
  runWithTimeout,
15
15
  truncateDiff
16
- } from "./chunk-5TF227G7.js";
16
+ } from "./chunk-WKJAGQU7.js";
17
17
  import {
18
18
  gitExecSafe
19
19
  } from "./chunk-4T2OQAVL.js";
@@ -23,10 +23,10 @@ import {
23
23
  CURSOR_RUNTIME_ID,
24
24
  DEVIN_RUNTIME_ID,
25
25
  detectMCPServers
26
- } from "./chunk-42SK2S4V.js";
26
+ } from "./chunk-FYT3ETTP.js";
27
27
  import {
28
28
  isEnoent
29
- } from "./chunk-RR6V6SNM.js";
29
+ } from "./chunk-VHPNDDB5.js";
30
30
  import {
31
31
  createOneShotPool
32
32
  } from "./chunk-ZFKJAYAN.js";
@@ -39,7 +39,7 @@ import {
39
39
  formatTaskFeedback,
40
40
  toRecord,
41
41
  toSdkContent
42
- } from "./chunk-PVN4AJAH.js";
42
+ } from "./chunk-3PJULQHI.js";
43
43
  import {
44
44
  AGENT_IDS,
45
45
  AGENT_SYSTEM_CLAUDE_CODE,
@@ -55,7 +55,7 @@ import {
55
55
  claudeDisplayPricing,
56
56
  inflateToolResultContent,
57
57
  isImageMimeType
58
- } from "./chunk-RBXWUYDC.js";
58
+ } from "./chunk-DQQOVPVK.js";
59
59
  import {
60
60
  AnthropicAuthStatusSchema,
61
61
  CodexAuthStatusSchema,
@@ -63,7 +63,7 @@ import {
63
63
  DevinAuthStatusSchema,
64
64
  PermissionModeSchema,
65
65
  TESTED_CODEX_VERSION
66
- } from "./chunk-FARMJBC7.js";
66
+ } from "./chunk-MV7VSD2T.js";
67
67
  import {
68
68
  logger
69
69
  } from "./chunk-FIE2FRIW.js";
@@ -3219,7 +3219,7 @@ function displayCostFor(modelId) {
3219
3219
  return {
3220
3220
  costInputUsd: seed.inputUsdPerMtok,
3221
3221
  costOutputUsd: seed.outputUsdPerMtok,
3222
- pricingNote: `API-equivalent rates, not subscription charges. Cached input: ${seed.cachedInputUsdPerMtok}/M. Prompts above 272K: 2\xD7 input/cache, 1.5\xD7 output.${modelId === "gpt-5.6-sol" ? " Sol promotion guaranteed at least through November 21, 2026." : ""}`
3222
+ pricingNote: `API-equivalent rates, not subscription charges. Cached input: ${seed.cachedInputUsdPerMtok}/M. Prompts above 272K: 2\xD7 input/cache, 1.5\xD7 output.`
3223
3223
  };
3224
3224
  }
3225
3225
  var CODEX_MODEL_CATALOG = withCapabilityTiers([
@@ -3280,25 +3280,6 @@ var CODEX_MODEL_CATALOG = withCapabilityTiers([
3280
3280
  vision: true
3281
3281
  }
3282
3282
  },
3283
- {
3284
- id: "gpt-5.6-sol",
3285
- nativeName: "gpt-5.6-sol",
3286
- displayName: "GPT-5.6 Sol",
3287
- description: "Reliable agentic workhorse for everyday tasks.",
3288
- contextWindow: 272e3,
3289
- supportedEfforts: ["low", "medium", "high", "xhigh", "max", "ultra"],
3290
- defaultEffort: "low",
3291
- pricing: pricingFor("gpt-5.6-sol"),
3292
- tagline: "Reliable coding, research and everyday work",
3293
- speedTier: "standard",
3294
- ...displayCostFor("gpt-5.6-sol"),
3295
- capabilities: {
3296
- reasoning: true,
3297
- adaptiveThinking: false,
3298
- webSearch: false,
3299
- vision: true
3300
- }
3301
- },
3302
3283
  {
3303
3284
  id: "gpt-5.6-terra",
3304
3285
  nativeName: "gpt-5.6-terra",
@@ -3317,25 +3298,6 @@ var CODEX_MODEL_CATALOG = withCapabilityTiers([
3317
3298
  webSearch: false,
3318
3299
  vision: true
3319
3300
  }
3320
- },
3321
- {
3322
- id: "gpt-5.6-luna",
3323
- nativeName: "gpt-5.6-luna",
3324
- displayName: "GPT-5.6 Luna",
3325
- description: "Fast and affordable agentic coding model.",
3326
- contextWindow: 272e3,
3327
- supportedEfforts: ["low", "medium", "high", "xhigh", "max"],
3328
- defaultEffort: "medium",
3329
- pricing: pricingFor("gpt-5.6-luna"),
3330
- tagline: "Fast, affordable OpenAI Codex",
3331
- speedTier: "fast",
3332
- ...displayCostFor("gpt-5.6-luna"),
3333
- capabilities: {
3334
- reasoning: true,
3335
- adaptiveThinking: false,
3336
- webSearch: false,
3337
- vision: true
3338
- }
3339
3301
  }
3340
3302
  ]);
3341
3303
 
@@ -8933,4 +8895,4 @@ export {
8933
8895
  refreshInstalledAgents,
8934
8896
  refreshCodexAuthSlice
8935
8897
  };
8936
- //# sourceMappingURL=chunk-UIBOKFSG.js.map
8898
+ //# sourceMappingURL=chunk-XPOXRIHH.js.map