@devrouter/cli 0.0.62 → 0.0.64

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/devrouter.js CHANGED
@@ -671,7 +671,9 @@ function tryReclaimStaleLock(lockPath, staleLinkPath, livenessCache) {
671
671
  function acquireFileLock(lockPath, options) {
672
672
  const processBirth = processBirthIdentity(process.pid);
673
673
  if (!processBirth) {
674
- throw new Error(`could not determine process identity for ${options.activity} lock`);
674
+ throw new Error(
675
+ `could not determine process identity for ${options.activity} lock: process inspection may be unavailable or sandbox-denied; run the canonical command in a permitted host context`
676
+ );
675
677
  }
676
678
  const ownerId = (0, import_node_crypto2.randomUUID)();
677
679
  const owner = `${process.pid}:${Buffer.from(processBirth).toString("base64url")}:${ownerId}`;
@@ -2450,7 +2452,7 @@ function loadRepoConfig(repoPath, read = (file) => import_node_fs6.default.readF
2450
2452
  const config = parseConfig(parsed ?? {}, configPath);
2451
2453
  const requiredVersion = config.devrouter?.version;
2452
2454
  if (requiredVersion && !hasWarnedVersionMismatch) {
2453
- const cliVersion = true ? "0.0.62" : "0.0.0-dev";
2455
+ const cliVersion = true ? "0.0.64" : "0.0.0-dev";
2454
2456
  if (cliVersion !== "0.0.0-dev" && compareSemver(requiredVersion, cliVersion) > 0) {
2455
2457
  hasWarnedVersionMismatch = true;
2456
2458
  process.stderr.write(
@@ -4634,6 +4636,33 @@ function inspectManagedStopWorkspaceIds(endpoint, composeDirectory) {
4634
4636
  assertUniqueContainerIds(ids);
4635
4637
  return ids;
4636
4638
  }
4639
+ function hasExactManagedStopMissingObjectError(stderr, id3) {
4640
+ if (typeof stderr !== "string") return false;
4641
+ const line = stderr.endsWith("\r\n") ? stderr.slice(0, -2) : stderr.endsWith("\n") ? stderr.slice(0, -1) : stderr;
4642
+ return line.toLowerCase() === `error: no such object: ${id3}`;
4643
+ }
4644
+ function assertManagedStopContainersAbsent(endpoint, ids) {
4645
+ assertManagedStopEndpoint(endpoint);
4646
+ if (!Array.isArray(ids) || ids.length === 0 || ids.length > 256) {
4647
+ throw new Error("Managed stop absence proof requires between 1 and 256 container ids.");
4648
+ }
4649
+ ids.forEach(assertFullContainerId);
4650
+ assertUniqueContainerIds(ids);
4651
+ const env = { ...process.env };
4652
+ delete env.DOCKER_CONTEXT;
4653
+ delete env.DOCKER_HOST;
4654
+ for (const id3 of ids) {
4655
+ const result = (0, import_node_child_process6.spawnSync)("docker", ["--host", endpoint, "inspect", "--format", "{{.Id}}", id3], {
4656
+ env,
4657
+ encoding: "utf-8",
4658
+ timeout: MANAGED_STOP_DOCKER_TIMEOUT_MS,
4659
+ maxBuffer: MANAGED_STOP_DOCKER_MAX_BUFFER
4660
+ });
4661
+ if (result.status !== 1 || result.error || result.signal || result.stdout !== "" && result.stdout !== "\n" && result.stdout !== "\r\n" || !hasExactManagedStopMissingObjectError(result.stderr, id3)) {
4662
+ throw new Error("Managed stop Docker inspection did not prove container absence.");
4663
+ }
4664
+ }
4665
+ }
4637
4666
  function stopPinnedManagedContainer(endpoint, containerId) {
4638
4667
  assertFullContainerId(containerId);
4639
4668
  runManagedStopDocker(["stop", containerId], endpoint, 3e4);
@@ -7142,6 +7171,7 @@ function buildOnboardingPrompt(options = {}) {
7142
7171
  "- TLS: namespaced hosts (`web.<ws>.localhost`) are not covered by the `*.localhost` wildcard; devrouter auto-extends the mkcert cert SANs for active hosts when TLS is enabled.",
7143
7172
  "- Lifecycle: after one-time setup, use `devrouter ensure .` for both primary and linked checkouts; never branch on checkout kind or use live verify as startup. Managed consumer images contain no devrouter package/helper: ensure delivers its matching helper at runtime and invokes an exact captured snapshot of the repository-owned post-start adapter. Keep `.devrouter.yml` as the only consumer-side version pin. Use `devrouter stop .` for a non-destructive pause, `devrouter stop . --delete` only for explicit exact-owner cleanup without removing the checkout, and `devrouter exec . -- <command...>` for container commands. Never substitute raw DevPod/Devsy mutations; they bypass the machine-global ownership lock, which serializes provider mutations in a fair arrival-order queue and lets contenders wait up to thirty minutes with throttled stderr progress before failing with the queue position or holder PID and true durations. `workspace up` creates linked worktrees; destructive worktree removal and GC remain ledger-scoped.",
7144
7173
  "- After a drained interrupted ensure, manual exec can use an exactly proven retained Devsy runtime with the plain local Docker command. Revalidate identity for each tooling command until later preparation reconciles startup; preserve configuration, data and interrupted history. This does not repair absent stop baselines or provide OOM protection.",
7174
+ "- Busy ensure and exec wait up to thirty minutes for a positively identified active lifecycle worker on the same checkout, with stderr progress. Do not stop healthy work to make room. Commands stay serial; timeout or cancellation before admission leaves prior work untouched. A lifecycle fence change cancels admission, and uncertain execution is never replayed.",
7145
7175
  "- Ensure reconciles interrupted or never-dispatched ensure after positive worker drainage. Unknown arbitrary exec never replays; use explicit stop to reconcile that uncertainty. Preserve journals and generated configuration if evidence is unavailable. This does not prevent OOM or activate capacity-managed recovery.",
7146
7176
  "- Devsy agent readiness: run `devrouter setup --yes --workspace-runtime devsy` once so Devrouter acquires and verifies the pinned agent in its own machine cache. `doctor` reports `ready`, `missing`, `stale`, or `invalid` without network access, and `ensure` fails before the provider queue when readiness is not `ready`. An explicit `DEVSY_AGENT_BINARY` remains authoritative and must match a pinned official asset.",
7147
7177
  "- A managed adapter plus `postCreateCommand` requires `waitFor` exactly `postCreateCommand` or `postStartCommand`; managed selective config preserves lifecycle fields and changes only `runServices`.",
@@ -8725,6 +8755,9 @@ var init_devcontainer_diagnostics = __esm({
8725
8755
  });
8726
8756
 
8727
8757
  // src/core/route-state.ts
8758
+ function listRoutesForWorktreePath(worktreePath, routes = listHostRouteState()) {
8759
+ return routes.filter((route) => sameWorkspacePath(route.repoPath, worktreePath));
8760
+ }
8728
8761
  function listRoutesForWorktreePaths(worktreePaths) {
8729
8762
  const routes = listHostRouteState();
8730
8763
  const byWorktreePath = /* @__PURE__ */ new Map();
@@ -9584,11 +9617,13 @@ var init_status = __esm({
9584
9617
  });
9585
9618
 
9586
9619
  // src/core/devpod-registry.ts
9587
- function listDevpodWorkspacesRaw() {
9620
+ function listDevpodWorkspacesRaw(options = {}) {
9588
9621
  const result = (0, import_node_child_process12.spawnSync)("devpod", ["list", "--output", "json", "--skip-pro"], {
9589
9622
  encoding: "utf-8"
9590
9623
  });
9591
- if (result.status !== 0) {
9624
+ if (options.allowMissingExecutable && result.error?.code === "ENOENT")
9625
+ return [];
9626
+ if (result.error || result.status !== 0) {
9592
9627
  const details = [result.error?.message, result.stdout, result.stderr].filter(Boolean).join("\n").trim();
9593
9628
  throw new Error(`devpod list failed: ${details || "devpod is not installed or unavailable"}`);
9594
9629
  }
@@ -10954,7 +10989,56 @@ function proveRetainedManagedStop(state) {
10954
10989
  }
10955
10990
  return result;
10956
10991
  }
10992
+ function absentRegistrationIdentity(state) {
10993
+ resetWorkspaceRuntimeCaches();
10994
+ if (resolveWorkspaceRuntimeOrDefault(state.repoPath) !== "devsy")
10995
+ throw new Error("Stop provider changed.");
10996
+ if (!state.workspace || !isLinkedWorktree(state.repoPath))
10997
+ throw new Error("Absent stop requires a linked workspace.");
10998
+ const record4 = readWorkspaceOwnership(state.repoPath, state.workspace);
10999
+ if (!record4 || record4.workspace !== state.workspace || record4.devpodId !== state.devpodId || !sameWorkspacePath(record4.worktreePath, state.repoPath) || resolveWorktreeWorkspace(state.repoPath) !== state.workspace || inspectWorkspaceOwnership(record4, listGitWorktrees(state.repoPath), void 0).ownerStatus !== "present")
11000
+ throw new Error("Absent stop workspace ownership changed.");
11001
+ for (const entries of [
11002
+ listDevsyWorkspaces(),
11003
+ listDevpodWorkspacesRaw({ allowMissingExecutable: true })
11004
+ ]) {
11005
+ if (entries.some(
11006
+ (entry) => entry.id === state.devpodId || sameWorkspacePath(entry.source.localFolder, state.repoPath)
11007
+ ))
11008
+ throw new Error("Absent stop requires both provider registrations to remain absent.");
11009
+ }
11010
+ return { record: record4, gitCommonDir: resolveGitCommonDir(state.repoPath) };
11011
+ }
11012
+ function proveManagedStop(state) {
11013
+ const baseline = validateManagedStopBaseline(state.stopBaseline, state);
11014
+ const owner = inspectDevsyWorkspaceOwnership(
11015
+ listDevsyWorkspaces(),
11016
+ state.devpodId,
11017
+ state.repoPath
11018
+ );
11019
+ if (owner.status !== "absent")
11020
+ return { status: "retained", containers: proveRetainedManagedStop(state) };
11021
+ if (baseline.sourceContainer) throw new Error("Absent stop cannot prove a source container.");
11022
+ const before = absentRegistrationIdentity(state);
11023
+ if (!(0, import_node_util2.isDeepStrictEqual)(readManagedRuntimeState(state.repoPath, state.workspace), state))
11024
+ throw new Error("Retained stop generation changed.");
11025
+ if (inspectManagedStopDaemon(baseline.endpoint) !== baseline.daemonId)
11026
+ throw new Error("Stop daemon changed.");
11027
+ assertManagedStopContainersAbsent(
11028
+ baseline.endpoint,
11029
+ baseline.containers.map((container) => container.id)
11030
+ );
11031
+ const uidBytes = Buffer.byteLength(baseline.uid);
11032
+ const runnerId = uidBytes === 16 || uidBytes === 40 ? baseline.uid : baseline.providerId;
11033
+ if (inspectManagedStopContainers(baseline.project, baseline.endpoint).length !== 0 || inspectManagedStopWorkspaceIds(baseline.endpoint, baseline.composeDirectory).length !== 0 || inspectProviderRunnerContainers(baseline.endpoint, runnerId).length !== 0)
11034
+ throw new Error("Absent stop observed a remaining workspace population.");
11035
+ if (inspectManagedStopDaemon(baseline.endpoint) !== baseline.daemonId || !(0, import_node_util2.isDeepStrictEqual)(absentRegistrationIdentity(state), before) || !(0, import_node_util2.isDeepStrictEqual)(readManagedRuntimeState(state.repoPath, state.workspace), state))
11036
+ throw new Error("Absent stop authority changed during inspection.");
11037
+ return { status: "proven-absent", containers: [] };
11038
+ }
10957
11039
  function stopFromManagedBaseline(state) {
11040
+ const proof = proveManagedStop(state);
11041
+ if (proof.status === "proven-absent") return proof.status;
10958
11042
  const baseline = validateManagedStopBaseline(state.stopBaseline, state);
10959
11043
  const stopped = /* @__PURE__ */ new Set();
10960
11044
  for (const initial of proveRetainedManagedStop(state))
@@ -10977,6 +11061,12 @@ function stopFromManagedBaseline(state) {
10977
11061
  }
10978
11062
  if (inspect().some((container) => container.state.Running))
10979
11063
  throw new Error("Retained workloads remain running.");
11064
+ return "retained";
11065
+ }
11066
+ function managedStopRouteReferences(state) {
11067
+ return state.desired.apps.flatMap(
11068
+ (name) => ["http", "tcp"].map((protocol) => ({ repoPath: state.repoPath, name, protocol }))
11069
+ );
10980
11070
  }
10981
11071
  var import_node_os2, import_node_path31, import_node_util2;
10982
11072
  var init_managed_stop_recovery = __esm({
@@ -10986,6 +11076,7 @@ var init_managed_stop_recovery = __esm({
10986
11076
  import_node_path31 = __toESM(require("path"));
10987
11077
  import_node_util2 = require("util");
10988
11078
  init_devpod_environment();
11079
+ init_devpod_registry();
10989
11080
  init_devsy_workspaces();
10990
11081
  init_managed_compose_population();
10991
11082
  init_managed_runtime_state();
@@ -11095,8 +11186,7 @@ function stopRetainedManagedDevsyWorkspace(options) {
11095
11186
  throw new Error("Managed stop requires the exact retained workspace identity.");
11096
11187
  }
11097
11188
  if (state.stopBaseline) {
11098
- stopFromManagedBaseline(state);
11099
- return true;
11189
+ return stopFromManagedBaseline(state) === "proven-absent" ? "proven-absent" : true;
11100
11190
  }
11101
11191
  const workspaceEnv = workspace ? { token: workspace, gitCommonDir: resolveGitCommonDir(repoPath) } : void 0;
11102
11192
  function registration2() {
@@ -11280,12 +11370,14 @@ function inspectExactOwnership(devsyId, worktreePath) {
11280
11370
  }
11281
11371
  function mutateOwnedDevsyWorkspace(action2, devsyId, worktreePath) {
11282
11372
  return withMutationLock(`Devsy ${action2}`, worktreePath, () => {
11283
- if (action2 === "stop" && stopRetainedManagedDevsyWorkspace({
11284
- repoPath: worktreePath,
11285
- devsyId,
11286
- stopProvider: () => runDevsyAction("stop", devsyId)
11287
- }))
11288
- return { status: "changed" };
11373
+ if (action2 === "stop") {
11374
+ const stopped = stopRetainedManagedDevsyWorkspace({
11375
+ repoPath: worktreePath,
11376
+ devsyId,
11377
+ stopProvider: () => runDevsyAction("stop", devsyId)
11378
+ });
11379
+ if (stopped) return { status: stopped === "proven-absent" ? "proven-absent" : "changed" };
11380
+ }
11289
11381
  const before = inspectExactOwnership(devsyId, worktreePath);
11290
11382
  if (before.status === "absent") return { status: "absent" };
11291
11383
  runDevsyAction(action2, devsyId);
@@ -12068,7 +12160,7 @@ async function buildDoctorReport(options = {}) {
12068
12160
  const config = runtimeConfig.config;
12069
12161
  loadedConfig = config;
12070
12162
  loadedWorkspace = runtimeConfig.workspace;
12071
- const cliVersion = true ? "0.0.62" : "0.0.0-dev";
12163
+ const cliVersion = true ? "0.0.64" : "0.0.0-dev";
12072
12164
  const configVersion = config.devrouter?.version;
12073
12165
  if (configVersion && cliVersion !== "0.0.0-dev" && compareSemver(configVersion, cliVersion) > 0) {
12074
12166
  addCheck(checks, {
@@ -13362,7 +13454,7 @@ function applyReliabilityEvent(state, event, nowMs) {
13362
13454
  case "not-started":
13363
13455
  if (next.executionPolicy !== "manual" || next.operation?.kind !== "exec")
13364
13456
  return unchanged(next, "blocked");
13365
- if (next.operation.id !== event.operationId || next.operation.status !== "RUNNING")
13457
+ if (next.operation.id !== event.operationId || !["RUNNING", "DISPATCH_RECORDED"].includes(next.operation.status))
13366
13458
  return unchanged(next, "stale");
13367
13459
  next.operation = { ...next.operation, status: "NOT_LAUNCHED", exitCode: null };
13368
13460
  return transition(next, "accepted");
@@ -13417,6 +13509,11 @@ function sameFence2(record4, fence) {
13417
13509
  ([key, value]) => value === fence[key]
13418
13510
  );
13419
13511
  }
13512
+ function hasDuplicateOperation(record4, requestId, operationId) {
13513
+ return record4.state.operationHistory.some(
13514
+ (operation) => operation.key === requestId || operation.id === operationId
13515
+ );
13516
+ }
13420
13517
  function workerGroupAbsent(pid) {
13421
13518
  try {
13422
13519
  process.kill(-pid, 0);
@@ -13433,7 +13530,7 @@ function signalOwnedGroup(child, signal) {
13433
13530
  if (error.code !== "ESRCH") throw error;
13434
13531
  }
13435
13532
  }
13436
- async function runLifecycleWorker(request) {
13533
+ async function runLifecycleWorker(request, beforeAdmission) {
13437
13534
  if (process.platform === "win32")
13438
13535
  throw new Error("Lifecycle workers require POSIX process-group ownership.");
13439
13536
  const workerPath = import_node_path38.default.join(__dirname, "devrouter-lifecycle-worker.js");
@@ -13445,13 +13542,18 @@ async function runLifecycleWorker(request) {
13445
13542
  execArgv: []
13446
13543
  });
13447
13544
  let ready = false;
13545
+ let closed = false;
13546
+ let registered = false;
13547
+ let dispatchAcknowledged = false;
13548
+ let sendAttempted = false;
13549
+ let readinessTimer;
13448
13550
  let result;
13449
13551
  let cancelled = false;
13450
13552
  let forceTimer;
13451
13553
  let monitor;
13452
13554
  let failure;
13453
13555
  const cancel = () => {
13454
- if (cancelled) return;
13556
+ if (cancelled || closed) return;
13455
13557
  cancelled = true;
13456
13558
  try {
13457
13559
  signalOwnedGroup(child, "SIGTERM");
@@ -13474,20 +13576,56 @@ async function runLifecycleWorker(request) {
13474
13576
  child.once("error", (error) => {
13475
13577
  failure = error;
13476
13578
  });
13477
- child.on("message", (message) => {
13579
+ readinessTimer = setTimeout(() => {
13580
+ failure = new Error("Lifecycle worker readiness timed out before dispatch.");
13581
+ cancel();
13582
+ }, 3e4);
13583
+ child.on("message", async (message) => {
13478
13584
  if (!message || typeof message !== "object") return;
13479
13585
  if ("ready" in message && message.ready === true && !ready) {
13480
13586
  ready = true;
13587
+ if (readinessTimer) clearTimeout(readinessTimer);
13481
13588
  try {
13589
+ await new Promise((resolve2) => setImmediate(resolve2));
13590
+ if (closed) return;
13482
13591
  if (cancelled || !child.pid)
13483
13592
  throw new Error("Lifecycle invocation was cancelled before dispatch.");
13484
13593
  if (request.kind !== "stop") {
13485
13594
  const birth = processBirthIdentity(child.pid);
13486
13595
  if (!birth) throw new Error("Could not prove lifecycle worker incarnation.");
13487
13596
  updateReliabilityOperation(request.identity, (record4) => {
13488
- if (!sameFence2(record4, request.fence) || record4.worker || record4.state.operation?.id !== request.operationId) {
13597
+ beforeAdmission?.();
13598
+ if (!request.admission)
13599
+ throw new Error("Lifecycle operation admission is required.");
13600
+ if (hasDuplicateOperation(record4, request.requestId, request.operationId))
13601
+ throw new Error(
13602
+ "Lifecycle operation request conflicts with an existing operation."
13603
+ );
13604
+ if (!sameFence2(record4, request.fence))
13489
13605
  throw new Error("Lifecycle intent changed before dispatch.");
13606
+ if (record4.worker || record4.revision !== request.admission.expectedRevision) {
13607
+ throw new LifecycleWorkerAdmissionBusyError();
13490
13608
  }
13609
+ const admitted = stepReliability(
13610
+ record4.state,
13611
+ {
13612
+ ...request.fence,
13613
+ type: "operation-request",
13614
+ kind: request.kind === "exec" ? "exec" : "ensure",
13615
+ key: request.requestId,
13616
+ operationId: request.operationId,
13617
+ profile: request.admission.profile,
13618
+ consumer: request.admission.consumer,
13619
+ runtimeRunning: request.admission.runtimeRunning,
13620
+ ...request.admission.recoverInterruptedEnsure ? { recoverInterruptedEnsure: true } : {}
13621
+ },
13622
+ Date.now()
13623
+ );
13624
+ if (admitted.outcome !== "accepted")
13625
+ throw new Error(`Lifecycle admission is ${admitted.outcome}.`);
13626
+ record4.state = admitted.state;
13627
+ record4.outcome = null;
13628
+ request.fence = reliabilityFence(record4.state);
13491
13629
  record4.worker = {
13492
13630
  id: request.workerId,
13493
13631
  operationId: request.operationId,
@@ -13503,6 +13641,7 @@ async function runLifecycleWorker(request) {
13503
13641
  throw new Error("Lifecycle dispatch is blocked.");
13504
13642
  record4.state = transition2.state;
13505
13643
  });
13644
+ registered = true;
13506
13645
  updateReliabilityOperation(request.identity, (record4) => {
13507
13646
  const transition2 = stepReliability(
13508
13647
  record4.state,
@@ -13513,12 +13652,18 @@ async function runLifecycleWorker(request) {
13513
13652
  },
13514
13653
  Date.now()
13515
13654
  );
13516
- record4.state = transition2.state;
13517
13655
  if (!transition2.effects.some((effect2) => effect2.kind === "launch")) {
13518
13656
  throw new Error("Lifecycle dispatch persistence was not acknowledged.");
13519
13657
  }
13658
+ record4.state = transition2.state;
13520
13659
  });
13660
+ dispatchAcknowledged = true;
13521
13661
  }
13662
+ await new Promise((resolve2) => setImmediate(resolve2));
13663
+ if (closed) return;
13664
+ beforeAdmission?.();
13665
+ if (cancelled) throw new Error("Lifecycle invocation was cancelled before dispatch.");
13666
+ sendAttempted = true;
13522
13667
  child.send({ request }, (error) => {
13523
13668
  if (error) {
13524
13669
  failure = error;
@@ -13543,8 +13688,9 @@ async function runLifecycleWorker(request) {
13543
13688
  }
13544
13689
  });
13545
13690
  child.once("close", () => {
13691
+ closed = true;
13546
13692
  try {
13547
- if (request.kind !== "stop") {
13693
+ if (request.kind !== "stop" && registered) {
13548
13694
  updateReliabilityOperation(request.identity, (record4) => {
13549
13695
  if (record4.worker?.id !== request.workerId || record4.worker.pid !== child.pid) return;
13550
13696
  if (!result) {
@@ -13552,7 +13698,7 @@ async function runLifecycleWorker(request) {
13552
13698
  record4.state,
13553
13699
  {
13554
13700
  ...reliabilityFence(record4.state),
13555
- type: "interrupted",
13701
+ type: request.kind === "exec" && dispatchAcknowledged && !sendAttempted ? "not-started" : "interrupted",
13556
13702
  operationId: request.operationId
13557
13703
  },
13558
13704
  Date.now()
@@ -13586,6 +13732,7 @@ async function runLifecycleWorker(request) {
13586
13732
  });
13587
13733
  });
13588
13734
  } finally {
13735
+ if (readinessTimer) clearTimeout(readinessTimer);
13589
13736
  if (monitor) clearInterval(monitor);
13590
13737
  if (forceTimer) clearTimeout(forceTimer);
13591
13738
  process.removeListener("SIGINT", onSignal);
@@ -13595,7 +13742,7 @@ async function runLifecycleWorker(request) {
13595
13742
  function newLifecycleIds() {
13596
13743
  return { requestId: (0, import_node_crypto16.randomUUID)(), operationId: (0, import_node_crypto16.randomUUID)(), workerId: (0, import_node_crypto16.randomUUID)() };
13597
13744
  }
13598
- var import_node_child_process21, import_node_crypto16, import_node_fs31, import_node_path38;
13745
+ var import_node_child_process21, import_node_crypto16, import_node_fs31, import_node_path38, LifecycleWorkerAdmissionBusyError;
13599
13746
  var init_reliability_worker = __esm({
13600
13747
  "src/core/reliability-worker.ts"() {
13601
13748
  "use strict";
@@ -13607,17 +13754,12 @@ var init_reliability_worker = __esm({
13607
13754
  init_reliability_contract();
13608
13755
  init_reliability_model();
13609
13756
  init_reliability_operation_store();
13610
- }
13611
- });
13612
-
13613
- // src/core/managed-host-preparation.ts
13614
- var import_node_child_process22, import_node_fs32;
13615
- var init_managed_host_preparation = __esm({
13616
- "src/core/managed-host-preparation.ts"() {
13617
- "use strict";
13618
- import_node_child_process22 = require("child_process");
13619
- import_node_fs32 = __toESM(require("fs"));
13620
- init_repo_config();
13757
+ LifecycleWorkerAdmissionBusyError = class extends Error {
13758
+ constructor() {
13759
+ super("Lifecycle admission lost to another operation.");
13760
+ this.name = "LifecycleWorkerAdmissionBusyError";
13761
+ }
13762
+ };
13621
13763
  }
13622
13764
  });
13623
13765
 
@@ -13626,7 +13768,7 @@ function sleep(ms) {
13626
13768
  return new Promise((resolve) => setTimeout(resolve, ms));
13627
13769
  }
13628
13770
  function inspectRouterApi(protocol) {
13629
- const result = (0, import_node_child_process23.spawnSync)(
13771
+ const result = (0, import_node_child_process22.spawnSync)(
13630
13772
  "curl",
13631
13773
  [
13632
13774
  "--silent",
@@ -13653,6 +13795,11 @@ function inspectRouterApi(protocol) {
13653
13795
  if (!Array.isArray(value)) {
13654
13796
  return { ok: false, details: `${protocol.toUpperCase()} router API returned a non-array` };
13655
13797
  }
13798
+ if (value.some((item) => !isRecord4(item) || typeof item.name !== "string" || !item.name))
13799
+ return {
13800
+ ok: false,
13801
+ details: `${protocol.toUpperCase()} router API returned malformed entries`
13802
+ };
13656
13803
  return {
13657
13804
  ok: true,
13658
13805
  reachedPageLimit: value.length >= ROUTER_API_PAGE_SIZE,
@@ -13665,6 +13812,9 @@ function inspectRouterApi(protocol) {
13665
13812
  )
13666
13813
  };
13667
13814
  }
13815
+ function isRecord4(value) {
13816
+ return value !== null && typeof value === "object" && !Array.isArray(value);
13817
+ }
13668
13818
  function inspectExpectedRoutes(routes, expectation) {
13669
13819
  const mismatched = [];
13670
13820
  const failures = [];
@@ -13725,7 +13875,7 @@ async function ensureTraefikRouteExpectation(routes, expectation, options) {
13725
13875
  throw new Error(
13726
13876
  "Traefik routes did not reload; shared router restart is disabled for this operation."
13727
13877
  );
13728
- import_node_fs33.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
13878
+ import_node_fs32.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
13729
13879
  return withFileLock(
13730
13880
  ROUTER_RELOAD_LOCK_FILE,
13731
13881
  {
@@ -13756,12 +13906,12 @@ async function ensureTraefikRouteExpectation(routes, expectation, options) {
13756
13906
  async function ensureTraefikRoutesRemoved(routes, options = {}) {
13757
13907
  return ensureTraefikRouteExpectation(routes, "removed", options);
13758
13908
  }
13759
- var import_node_child_process23, import_node_fs33, import_node_path39, ROUTER_API_BASE, ROUTER_RELOAD_LOCK_FILE, ROUTER_API_REQUEST_TIMEOUT_SECONDS, DEFAULT_INITIAL_TIMEOUT_MS, DEFAULT_RECOVERY_TIMEOUT_MS, DEFAULT_POLL_INTERVAL_MS, ROUTER_API_PAGE_SIZE;
13909
+ var import_node_child_process22, import_node_fs32, import_node_path39, ROUTER_API_BASE, ROUTER_RELOAD_LOCK_FILE, ROUTER_API_REQUEST_TIMEOUT_SECONDS, DEFAULT_INITIAL_TIMEOUT_MS, DEFAULT_RECOVERY_TIMEOUT_MS, DEFAULT_POLL_INTERVAL_MS, ROUTER_API_PAGE_SIZE;
13760
13910
  var init_traefik_route_health = __esm({
13761
13911
  "src/core/traefik-route-health.ts"() {
13762
13912
  "use strict";
13763
- import_node_child_process23 = require("child_process");
13764
- import_node_fs33 = __toESM(require("fs"));
13913
+ import_node_child_process22 = require("child_process");
13914
+ import_node_fs32 = __toESM(require("fs"));
13765
13915
  import_node_path39 = __toESM(require("path"));
13766
13916
  init_file_lock();
13767
13917
  init_host_routes();
@@ -13777,6 +13927,17 @@ var init_traefik_route_health = __esm({
13777
13927
  }
13778
13928
  });
13779
13929
 
13930
+ // src/core/managed-host-preparation.ts
13931
+ var import_node_child_process23, import_node_fs33;
13932
+ var init_managed_host_preparation = __esm({
13933
+ "src/core/managed-host-preparation.ts"() {
13934
+ "use strict";
13935
+ import_node_child_process23 = require("child_process");
13936
+ import_node_fs33 = __toESM(require("fs"));
13937
+ init_repo_config();
13938
+ }
13939
+ });
13940
+
13780
13941
  // src/core/workspace-ensure.ts
13781
13942
  function resolveLinkedTarget(repoPath) {
13782
13943
  const snapshots = getWorkspaceRegistrySnapshots();
@@ -13839,6 +14000,11 @@ var init_workspace_ensure = __esm({
13839
14000
  });
13840
14001
 
13841
14002
  // src/core/reliability-lifecycle.ts
14003
+ function matchesFence(record4, fence) {
14004
+ return Object.entries(reliabilityFence(record4.state)).every(
14005
+ ([key, value]) => value === fence[key]
14006
+ );
14007
+ }
13842
14008
  async function withLifecycleOperationLock(repoPath, operation) {
13843
14009
  if (activeWorker && lockHeld) {
13844
14010
  if (!sameWorkspacePath(repoPath, activeWorker.repoPath))
@@ -13886,6 +14052,23 @@ function reconcileDrained(record4) {
13886
14052
  record4.worker = null;
13887
14053
  }
13888
14054
  }
14055
+ function reportBusyLifecycleWait(kind, operationId, waitedMs) {
14056
+ process.stderr.write(
14057
+ `Lifecycle ${kind} ${operationId} is waiting for the existing worker (${Math.ceil(waitedMs / 1e3)}s).
14058
+ `
14059
+ );
14060
+ }
14061
+ function installLifecycleWaitSignals(cancellation) {
14062
+ const onSignal = () => {
14063
+ cancellation.requested = true;
14064
+ };
14065
+ process.once("SIGINT", onSignal);
14066
+ process.once("SIGTERM", onSignal);
14067
+ return () => {
14068
+ process.removeListener("SIGINT", onSignal);
14069
+ process.removeListener("SIGTERM", onSignal);
14070
+ };
14071
+ }
13889
14072
  async function superviseLifecycle(kind, repoPath, options = {}, command) {
13890
14073
  repoPath = comparableWorkspacePath(repoPath);
13891
14074
  if (isLinkedWorktree(repoPath) && !readPersistedWorkspace(repoPath)) {
@@ -13898,58 +14081,115 @@ async function superviseLifecycle(kind, repoPath, options = {}, command) {
13898
14081
  provider: resolveWorkspaceRuntimeOrDefault(repoPath)
13899
14082
  };
13900
14083
  const ids = newLifecycleIds();
13901
- let fence;
13902
- const runtimeRunning = kind === "exec" ? Boolean(resolveRunningWorkspaceContainer(repoPath)) : false;
13903
- const previous = kind === "exec" && identity2.provider === "devsy" ? updateReliabilityOperation(identity2, (record4) => {
13904
- reconcileDrained(record4);
13905
- return record4;
13906
- }) : void 0;
13907
- let retainedExecProof;
13908
- if (previous && !previous.worker && canExecAfterInterruptedEnsure(previous.state)) {
13909
- retainedExecProof = withMutationLock(
13910
- "Prove retained exec",
13911
- repoPath,
13912
- () => captureDevsyExecProof(repoPath)
13913
- );
13914
- }
13915
- updateReliabilityOperation(identity2, (record4) => {
13916
- reconcileDrained(record4);
13917
- if (retainedExecProof && record4.state.operation?.id !== previous?.state.operation?.id)
13918
- throw new Error("Lifecycle operation changed during retained exec proof.");
13919
- if (kind === "stop") {
14084
+ const copiedOptions = { ...options };
14085
+ const copiedCommand = command ? [...command] : void 0;
14086
+ if (kind === "stop") {
14087
+ const fence = updateReliabilityOperation(identity2, (record4) => {
14088
+ reconcileDrained(record4);
13920
14089
  stepRecord(record4, { ...reliabilityFence(record4.state), type: "stop" });
13921
- } else {
13922
- if (record4.worker)
14090
+ return reliabilityFence(record4.state);
14091
+ });
14092
+ return runLifecycleWorker({ kind, repoPath, identity: identity2, ...ids, fence, options: copiedOptions });
14093
+ }
14094
+ const initial = readReliabilityOperation(identity2) ?? updateReliabilityOperation(identity2, (record4) => record4);
14095
+ const expectedFence = reliabilityFence(initial.state);
14096
+ const deadline = Date.now() + BUSY_LIFECYCLE_WAIT_MS;
14097
+ const cancellation = { requested: false };
14098
+ const removeSignals = installLifecycleWaitSignals(cancellation);
14099
+ let lastProgress = -Infinity;
14100
+ const assertWaiting = () => {
14101
+ if (cancellation.requested)
14102
+ throw new Error(`Lifecycle ${kind} ${ids.operationId} cancelled before dispatch.`);
14103
+ if (Date.now() >= deadline)
14104
+ throw new Error(
14105
+ `Lifecycle ${kind} ${ids.operationId} admission timed out; command was not launched.`
14106
+ );
14107
+ };
14108
+ try {
14109
+ for (; ; ) {
14110
+ assertWaiting();
14111
+ let previous = readReliabilityOperation(identity2);
14112
+ if (!previous || !matchesFence(previous, expectedFence))
13923
14113
  throw new Error(
13924
- "An earlier lifecycle worker may still be active; use explicit stop to reconcile it."
14114
+ "Lifecycle intent changed while awaiting admission; command was not launched."
13925
14115
  );
13926
- record4.outcome = null;
13927
- stepRecord(record4, {
13928
- ...reliabilityFence(record4.state),
13929
- type: "operation-request",
13930
- kind,
13931
- key: ids.requestId,
13932
- operationId: ids.operationId,
13933
- profile: options.profile ?? record4.state.profile ?? "full",
13934
- consumer: { id: "manual-cli", requiredCapabilities: [], pinned: false },
13935
- runtimeRunning,
13936
- ...retainedExecProof ? { recoverInterruptedEnsure: true } : {}
13937
- });
14116
+ if (hasDuplicateOperation(previous, ids.requestId, ids.operationId))
14117
+ throw new Error("Lifecycle request identity already exists; command was not replayed.");
14118
+ if (previous.worker) {
14119
+ const worker = previous.worker;
14120
+ const birth = processBirthIdentity(worker.pid);
14121
+ if (birth !== worker.birth && workerGroupAbsent(worker.pid)) {
14122
+ previous = updateReliabilityOperation(identity2, (record4) => {
14123
+ reconcileDrained(record4);
14124
+ return record4;
14125
+ });
14126
+ } else {
14127
+ if (!birth || birth !== worker.birth || previous.state.operation?.status === "COMPLETION_UNKNOWN")
14128
+ throw new Error(
14129
+ "Existing lifecycle worker identity or completion is uncertain; preserve its evidence."
14130
+ );
14131
+ const now = Date.now();
14132
+ if (now - lastProgress >= BUSY_LIFECYCLE_PROGRESS_MS) {
14133
+ reportBusyLifecycleWait(
14134
+ kind,
14135
+ ids.operationId,
14136
+ now - (deadline - BUSY_LIFECYCLE_WAIT_MS)
14137
+ );
14138
+ lastProgress = now;
14139
+ }
14140
+ await new Promise(
14141
+ (resolve) => setTimeout(resolve, Math.min(BUSY_LIFECYCLE_POLL_MS, deadline - now))
14142
+ );
14143
+ continue;
14144
+ }
14145
+ }
14146
+ if (!previous.worker && previous.state.operation?.status === "NOT_STARTED" && !previous.state.operation.drained)
14147
+ previous = updateReliabilityOperation(identity2, (record4) => {
14148
+ reconcileDrained(record4);
14149
+ return record4;
14150
+ });
14151
+ assertWaiting();
14152
+ const runtimeRunning = kind === "exec" && Boolean(resolveRunningWorkspaceContainer(repoPath));
14153
+ let retainedExecProof;
14154
+ if (kind === "exec" && identity2.provider === "devsy" && canExecAfterInterruptedEnsure(previous.state))
14155
+ retainedExecProof = withMutationLock(
14156
+ "Prove retained exec",
14157
+ repoPath,
14158
+ () => captureDevsyExecProof(repoPath)
14159
+ );
14160
+ assertWaiting();
14161
+ try {
14162
+ return await runLifecycleWorker(
14163
+ {
14164
+ kind,
14165
+ repoPath,
14166
+ identity: identity2,
14167
+ ...ids,
14168
+ fence: { ...expectedFence },
14169
+ options: copiedOptions,
14170
+ ...copiedCommand ? { command: copiedCommand } : {},
14171
+ ...retainedExecProof ? { retainedExecProof } : {},
14172
+ admission: {
14173
+ expectedRevision: previous.revision,
14174
+ profile: copiedOptions.profile ?? previous.state.profile ?? "full",
14175
+ consumer: { id: "manual-cli", requiredCapabilities: [], pinned: false },
14176
+ runtimeRunning,
14177
+ ...retainedExecProof ? { recoverInterruptedEnsure: true } : {}
14178
+ }
14179
+ },
14180
+ assertWaiting
14181
+ );
14182
+ } catch (error) {
14183
+ if (!(error instanceof LifecycleWorkerAdmissionBusyError)) throw error;
14184
+ assertWaiting();
14185
+ await new Promise((resolve) => setTimeout(resolve, BUSY_LIFECYCLE_POLL_MS));
14186
+ }
13938
14187
  }
13939
- fence = reliabilityFence(record4.state);
13940
- });
13941
- return runLifecycleWorker({
13942
- kind,
13943
- repoPath,
13944
- identity: identity2,
13945
- ...ids,
13946
- fence,
13947
- options,
13948
- ...command ? { command } : {},
13949
- ...retainedExecProof ? { retainedExecProof } : {}
13950
- });
14188
+ } finally {
14189
+ removeSignals();
14190
+ }
13951
14191
  }
13952
- var import_node_path41, activeWorker, lockHeld;
14192
+ var import_node_path41, activeWorker, lockHeld, BUSY_LIFECYCLE_WAIT_MS, BUSY_LIFECYCLE_POLL_MS, BUSY_LIFECYCLE_PROGRESS_MS;
13953
14193
  var init_reliability_lifecycle = __esm({
13954
14194
  "src/core/reliability-lifecycle.ts"() {
13955
14195
  "use strict";
@@ -13966,11 +14206,15 @@ var init_reliability_lifecycle = __esm({
13966
14206
  init_reliability_model();
13967
14207
  init_reliability_operation_store();
13968
14208
  init_reliability_worker();
14209
+ init_traefik_route_health();
13969
14210
  init_workspace();
13970
14211
  init_workspace_ensure();
13971
14212
  init_workspace_runtime();
13972
14213
  init_reliability_context();
13973
14214
  lockHeld = false;
14215
+ BUSY_LIFECYCLE_WAIT_MS = 30 * 60 * 1e3;
14216
+ BUSY_LIFECYCLE_POLL_MS = 250;
14217
+ BUSY_LIFECYCLE_PROGRESS_MS = 1e4;
13974
14218
  }
13975
14219
  });
13976
14220
 
@@ -15632,11 +15876,11 @@ var init_repo_devcontainer = __esm({
15632
15876
  function fail3(message) {
15633
15877
  throw new Error(message);
15634
15878
  }
15635
- function isRecord4(value) {
15879
+ function isRecord5(value) {
15636
15880
  return typeof value === "object" && value !== null && !Array.isArray(value);
15637
15881
  }
15638
15882
  function requireRecord(value, label) {
15639
- if (!isRecord4(value)) fail3(`${label} must be a mapping.`);
15883
+ if (!isRecord5(value)) fail3(`${label} must be a mapping.`);
15640
15884
  return value;
15641
15885
  }
15642
15886
  function requireExactKeys(value, allowed, label) {
@@ -17416,7 +17660,7 @@ var init_workspace_consumption = __esm({
17416
17660
  });
17417
17661
 
17418
17662
  // src/core/workspace-cleanup.ts
17419
- function isRecord5(value) {
17663
+ function isRecord6(value) {
17420
17664
  return value !== null && typeof value === "object" && !Array.isArray(value);
17421
17665
  }
17422
17666
  function isSha(value) {
@@ -17560,14 +17804,14 @@ function parseGitHubChanges(value, project, branch) {
17560
17804
  if (!Array.isArray(value)) return void 0;
17561
17805
  const changes = [];
17562
17806
  for (const item of value) {
17563
- if (!isRecord5(item)) return void 0;
17564
- const repository = isRecord5(item.repository) ? item.repository.nameWithOwner : void 0;
17565
- const headRepository = isRecord5(item.headRepository) ? item.headRepository.nameWithOwner : void 0;
17807
+ if (!isRecord6(item)) return void 0;
17808
+ const repository = isRecord6(item.repository) ? item.repository.nameWithOwner : void 0;
17809
+ const headRepository = isRecord6(item.headRepository) ? item.headRepository.nameWithOwner : void 0;
17566
17810
  if (repository !== project || headRepository !== project || item.headRefName !== branch || !isSha(item.headRefOid) || typeof item.baseRefName !== "string") {
17567
17811
  continue;
17568
17812
  }
17569
17813
  const merged = item.state === "MERGED" && validTimestamp(item.mergedAt);
17570
- const mergeCommit = isRecord5(item.mergeCommit) && isSha(item.mergeCommit.oid) ? item.mergeCommit.oid : void 0;
17814
+ const mergeCommit = isRecord6(item.mergeCommit) && isSha(item.mergeCommit.oid) ? item.mergeCommit.oid : void 0;
17571
17815
  const baseSha = isSha(item.baseRefOid) ? item.baseRefOid : void 0;
17572
17816
  changes.push({
17573
17817
  sourceBranch: branch,
@@ -17584,14 +17828,14 @@ function parseGitLabChanges(value, _project, branch) {
17584
17828
  if (!Array.isArray(value)) return void 0;
17585
17829
  const changes = [];
17586
17830
  for (const item of value) {
17587
- if (!isRecord5(item)) return void 0;
17831
+ if (!isRecord6(item)) return void 0;
17588
17832
  const sourceProjectId = item.source_project_id;
17589
17833
  const targetProjectId = item.target_project_id;
17590
17834
  const sameProject = typeof sourceProjectId === "number" && typeof targetProjectId === "number" && sourceProjectId === targetProjectId;
17591
17835
  if (!sameProject || item.source_branch !== branch || !isSha(item.sha) || typeof item.target_branch !== "string") {
17592
17836
  continue;
17593
17837
  }
17594
- const diffRefs = isRecord5(item.diff_refs) ? item.diff_refs : void 0;
17838
+ const diffRefs = isRecord6(item.diff_refs) ? item.diff_refs : void 0;
17595
17839
  const mergeCommitSha = isSha(item.merge_commit_sha) ? item.merge_commit_sha : void 0;
17596
17840
  const baseSha = diffRefs && isSha(diffRefs.base_sha) ? diffRefs.base_sha : void 0;
17597
17841
  changes.push({
@@ -18413,8 +18657,24 @@ async function mutateWorkspaceRuntime(action2, resolved, worktrees, quiet = fals
18413
18657
  "Retained managed runtime has no exact provider registration; routes were preserved."
18414
18658
  );
18415
18659
  }
18416
- const routes = removeWorkspaceRoutesForWorktree(resolved.workspace, resolved.worktreePath);
18417
- await ensureTraefikRoutesRemoved(routes);
18660
+ const retained = action2 === "stop" ? readManagedRuntimeState(resolved.worktreePath, resolved.workspace) : void 0;
18661
+ let absent = false;
18662
+ const remove = () => {
18663
+ if (retained?.stopBaseline) {
18664
+ absent = proveManagedStop(retained).status === "proven-absent";
18665
+ if (absent && listRoutesForWorktreePath(resolved.worktreePath).some(
18666
+ (route) => route.workspace !== resolved.workspace || !retained.desired.apps.includes(route.name)
18667
+ ))
18668
+ throw new Error("Absent stop contains routes outside the retained desired set.");
18669
+ }
18670
+ claimLifecycleEffect();
18671
+ return removeWorkspaceRoutesForWorktree(resolved.workspace, resolved.worktreePath);
18672
+ };
18673
+ const routes = retained?.stopBaseline ? withMutationLock("Revalidate stop routes", resolved.worktreePath, remove) : remove();
18674
+ await ensureTraefikRoutesRemoved([
18675
+ ...routes,
18676
+ ...absent && retained ? managedStopRouteReferences(retained) : []
18677
+ ]);
18418
18678
  if (!quiet) {
18419
18679
  process.stdout.write(
18420
18680
  `Freed ${routes.length} route(s) for workspace '${resolved.workspace}'.
@@ -18425,6 +18685,7 @@ async function mutateWorkspaceRuntime(action2, resolved, worktrees, quiet = fals
18425
18685
  ...mutation.status === "changed" ? { devpodId } : {},
18426
18686
  freedRoutes: routes.length,
18427
18687
  providerChanged: mutation.status === "changed",
18688
+ ...absent ? { runtimeAbsent: true } : {},
18428
18689
  workspace: resolved.workspace
18429
18690
  };
18430
18691
  }
@@ -18487,7 +18748,9 @@ var init_workspace_lifecycle = __esm({
18487
18748
  import_node_path49 = __toESM(require("path"));
18488
18749
  init_devpod_mutation();
18489
18750
  init_devpod_workspaces();
18751
+ init_devsy_mutation();
18490
18752
  init_managed_runtime_state();
18753
+ init_managed_stop_recovery();
18491
18754
  init_reliability_lifecycle();
18492
18755
  init_repo_config();
18493
18756
  init_route_state();
@@ -18643,7 +18906,7 @@ var init_version = __esm({
18643
18906
 
18644
18907
  // src/cli.ts
18645
18908
  var import_commander = require("commander");
18646
- var CLI_VERSION = true ? "0.0.62" : "0.0.0-dev";
18909
+ var CLI_VERSION = true ? "0.0.64" : "0.0.0-dev";
18647
18910
  var VERSION_FLAGS = /* @__PURE__ */ new Set(["-V", "--version"]);
18648
18911
  function withErrorHandling(action2) {
18649
18912
  return async (...args) => {