@devrouter/cli 0.0.72 → 0.0.74

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
@@ -31,7 +31,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
31
31
  ));
32
32
 
33
33
  // src/core/controller-probe.ts
34
- function runControllerProbe(command, args, signal, options = {}) {
34
+ function probe(command, args, signal, options) {
35
35
  return new Promise((resolve, reject) => {
36
36
  if (signal.aborted) {
37
37
  reject(new Error("Observation cancelled."));
@@ -85,12 +85,19 @@ function runControllerProbe(command, args, signal, options = {}) {
85
85
  if (finished) return;
86
86
  finished = true;
87
87
  cleanup();
88
- if (failed || code !== 0)
89
- reject(new Error("Observation process failed or exceeded its bound."));
90
- else resolve(Buffer.concat(chunks).toString("utf8"));
88
+ if (failed) reject(new Error("Observation process failed or exceeded its bound."));
89
+ else resolve({ output: Buffer.concat(chunks).toString("utf8"), code });
91
90
  });
92
91
  });
93
92
  }
93
+ async function runControllerProbe(command, args, signal, options = {}) {
94
+ const result = await probe(command, args, signal, options);
95
+ if (result.code !== 0) throw new Error("Observation process failed or exceeded its bound.");
96
+ return result.output;
97
+ }
98
+ function runControllerProbeStatus(command, args, signal, options = {}) {
99
+ return probe(command, args, signal, options);
100
+ }
94
101
  var import_node_child_process, ControllerProbeUnavailable;
95
102
  var init_controller_probe = __esm({
96
103
  "src/core/controller-probe.ts"() {
@@ -132,12 +139,12 @@ async function readCapacityHostSnapshot(signal, dependencies = {}) {
132
139
  try {
133
140
  if ((dependencies.platform ?? process.platform) !== "darwin" || signal.aborted)
134
141
  throw new Error("Host probe unavailable.");
135
- const probe = dependencies.probe ?? runControllerProbe;
142
+ const probe2 = dependencies.probe ?? runControllerProbe;
136
143
  const cancellation = new AbortController();
137
144
  const probeSignal = AbortSignal.any([signal, cancellation.signal]);
138
145
  const results = await Promise.allSettled(
139
146
  ["hw.memsize", "kern.memorystatus_vm_pressure_level"].map(
140
- (name) => Promise.resolve().then(() => probe("/usr/sbin/sysctl", ["-n", name], probeSignal)).catch((error) => {
147
+ (name) => Promise.resolve().then(() => probe2("/usr/sbin/sysctl", ["-n", name], probeSignal)).catch((error) => {
141
148
  cancellation.abort();
142
149
  throw error;
143
150
  })
@@ -1170,6 +1177,55 @@ function inspectManagedStopWorkspaceIds(endpoint, composeDirectory) {
1170
1177
  assertUniqueContainerIds(ids2);
1171
1178
  return ids2;
1172
1179
  }
1180
+ function assertManagedStopCheckoutAbsent(endpoint, repoPath) {
1181
+ const list2 = () => {
1182
+ const ids3 = parseDockerLines(
1183
+ runManagedStopDocker(["ps", "-a", "--no-trunc", "--format", "{{.ID}}"], endpoint)
1184
+ );
1185
+ if (ids3.length > 256) throw new Error("Managed stop population exceeds its bound.");
1186
+ ids3.forEach(assertFullContainerId);
1187
+ assertUniqueContainerIds(ids3);
1188
+ return ids3;
1189
+ };
1190
+ const ids2 = list2();
1191
+ const seen = [];
1192
+ const labels = [
1193
+ "com.docker.compose.project.working_dir",
1194
+ "devcontainer.local_folder",
1195
+ "vsch.local.folder"
1196
+ ];
1197
+ const template = '{"id":{{json .Id}},"labels":{"com.docker.compose.project.working_dir":{{json (index .Config.Labels "com.docker.compose.project.working_dir")}},"devcontainer.local_folder":{{json (index .Config.Labels "devcontainer.local_folder")}},"vsch.local.folder":{{json (index .Config.Labels "vsch.local.folder")}}},"mounts":[{{range $i, $m := .Mounts}}{{if $i}},{{end}}{"Type":{{json $m.Type}},"Source":{{json $m.Source}}}{{end}}]}';
1198
+ if (ids2.length) {
1199
+ for (const line of parseDockerLines(
1200
+ runManagedStopDocker(["inspect", "--format", template, ...ids2], endpoint)
1201
+ )) {
1202
+ const value = parseManagedStopJson(line);
1203
+ if (!isRecord(value) || !isRecord(value.labels) || !Array.isArray(value.mounts))
1204
+ throw new Error("Managed stop checkout inspection is malformed.");
1205
+ assertFullContainerId(value.id);
1206
+ seen.push(value.id);
1207
+ for (const label2 of labels) {
1208
+ const field = value.labels[label2];
1209
+ if (field !== null && typeof field !== "string")
1210
+ throw new Error("Managed stop checkout labels are incomplete.");
1211
+ if (typeof field === "string" && sameWorkspacePath(
1212
+ field,
1213
+ label2 === labels[0] ? import_node_path4.default.join(repoPath, ".devcontainer") : repoPath
1214
+ ))
1215
+ throw new Error("Managed stop observed a remaining checkout container.");
1216
+ }
1217
+ for (const mount of value.mounts) {
1218
+ if (!isRecord(mount) || typeof mount.Type !== "string" || typeof mount.Source !== "string")
1219
+ throw new Error("Managed stop checkout mounts are incomplete.");
1220
+ if (mount.Type === "bind" && sameWorkspacePath(mount.Source, repoPath))
1221
+ throw new Error("Managed stop observed a remaining checkout mount.");
1222
+ }
1223
+ }
1224
+ }
1225
+ assertUniqueContainerIds(seen);
1226
+ requireSameContainerPopulation(ids2, seen);
1227
+ requireSameContainerPopulation(ids2, list2());
1228
+ }
1173
1229
  function hasExactManagedStopMissingObjectError(stderr, id5) {
1174
1230
  if (typeof stderr !== "string") return false;
1175
1231
  const line = stderr.endsWith("\r\n") ? stderr.slice(0, -2) : stderr.endsWith("\n") ? stderr.slice(0, -1) : stderr;
@@ -1803,6 +1859,62 @@ function parseScheduling(value) {
1803
1859
  }
1804
1860
  return result;
1805
1861
  }
1862
+ function parseRecovery(value) {
1863
+ if (value === void 0) return { ...DEFAULT_CAPACITY_RECOVERY };
1864
+ const label2 = "recovery";
1865
+ const recovery = ensureObject(value, label2);
1866
+ ensureAllowedKeys(
1867
+ recovery,
1868
+ [
1869
+ "enabled",
1870
+ "maxProcessRestarts",
1871
+ "maxServiceRestarts",
1872
+ "maxCorrectiveActions",
1873
+ "windowSeconds",
1874
+ "observationSeconds",
1875
+ "resumeDwellSeconds"
1876
+ ],
1877
+ label2
1878
+ );
1879
+ if (typeof recovery.enabled !== "boolean") {
1880
+ throw new Error(`${label2}.enabled must be a boolean.`);
1881
+ }
1882
+ const result = {
1883
+ enabled: recovery.enabled,
1884
+ maxProcessRestarts: parseSafeInteger(
1885
+ recovery.maxProcessRestarts,
1886
+ `${label2}.maxProcessRestarts`,
1887
+ 0
1888
+ ),
1889
+ maxServiceRestarts: parseSafeInteger(
1890
+ recovery.maxServiceRestarts,
1891
+ `${label2}.maxServiceRestarts`,
1892
+ 0
1893
+ ),
1894
+ maxCorrectiveActions: parsePositiveInteger(
1895
+ recovery.maxCorrectiveActions,
1896
+ `${label2}.maxCorrectiveActions`
1897
+ ),
1898
+ windowSeconds: parsePositiveInteger(recovery.windowSeconds, `${label2}.windowSeconds`),
1899
+ observationSeconds: parsePositiveInteger(
1900
+ recovery.observationSeconds,
1901
+ `${label2}.observationSeconds`
1902
+ ),
1903
+ resumeDwellSeconds: parsePositiveInteger(
1904
+ recovery.resumeDwellSeconds,
1905
+ `${label2}.resumeDwellSeconds`
1906
+ )
1907
+ };
1908
+ if (result.maxCorrectiveActions < result.maxProcessRestarts + result.maxServiceRestarts) {
1909
+ throw new Error(
1910
+ `${label2}.maxCorrectiveActions must cover maxProcessRestarts and maxServiceRestarts.`
1911
+ );
1912
+ }
1913
+ if (result.maxProcessRestarts > 4 || result.maxServiceRestarts > 2 || result.maxCorrectiveActions > 8 || result.windowSeconds > 3600 || result.observationSeconds > 300 || result.resumeDwellSeconds > 3600) {
1914
+ throw new Error(`${label2} exceeds supported controller bounds.`);
1915
+ }
1916
+ return result;
1917
+ }
1806
1918
  function validateDomainReferences(domains) {
1807
1919
  const daemonIds = /* @__PURE__ */ new Map();
1808
1920
  const endpoints = /* @__PURE__ */ new Map();
@@ -1926,7 +2038,7 @@ function parseCapacityPolicy(value) {
1926
2038
  const root = ensureObject(value, "capacity policy");
1927
2039
  ensureAllowedKeys(
1928
2040
  root,
1929
- ["version", "revision", "admissions", "scheduling", "domains", "enrollments"],
2041
+ ["version", "revision", "admissions", "scheduling", "recovery", "domains", "enrollments"],
1930
2042
  "capacity policy"
1931
2043
  );
1932
2044
  const version = parsePositiveInteger(root.version, "capacity policy.version");
@@ -1939,6 +2051,7 @@ function parseCapacityPolicy(value) {
1939
2051
  throw new Error("capacity policy.admissions must be 'enabled' or 'paused'.");
1940
2052
  }
1941
2053
  const scheduling = parseScheduling(root.scheduling);
2054
+ const recovery = parseRecovery(root.recovery);
1942
2055
  const domainsValue = ensureObject(root.domains, "capacity policy.domains");
1943
2056
  const domainEntries = Object.entries(domainsValue);
1944
2057
  if (domainEntries.length > MAX_DOMAINS) {
@@ -1985,11 +2098,12 @@ function parseCapacityPolicy(value) {
1985
2098
  revision,
1986
2099
  admissions,
1987
2100
  scheduling,
2101
+ recovery,
1988
2102
  domains,
1989
2103
  enrollments
1990
2104
  };
1991
2105
  }
1992
- var import_node_fs4, import_node_path6, MAX_DOMAINS, MAX_ENROLLMENTS, MAX_PROFILES_PER_ENROLLMENT, MAX_STRING_LENGTH, MAX_PATH_LENGTH, DOMAIN_ID_RE, PROFILE_NAME_RE, WORKSPACE_TOKEN_RE, SHA256_RE;
2106
+ var import_node_fs4, import_node_path6, MAX_DOMAINS, MAX_ENROLLMENTS, MAX_PROFILES_PER_ENROLLMENT, MAX_STRING_LENGTH, MAX_PATH_LENGTH, DOMAIN_ID_RE, PROFILE_NAME_RE, WORKSPACE_TOKEN_RE, SHA256_RE, DEFAULT_CAPACITY_RECOVERY;
1993
2107
  var init_capacity_policy = __esm({
1994
2108
  "src/core/capacity-policy.ts"() {
1995
2109
  "use strict";
@@ -2004,6 +2118,15 @@ var init_capacity_policy = __esm({
2004
2118
  PROFILE_NAME_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
2005
2119
  WORKSPACE_TOKEN_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
2006
2120
  SHA256_RE = /^[0-9a-f]{64}$/;
2121
+ DEFAULT_CAPACITY_RECOVERY = {
2122
+ enabled: false,
2123
+ maxProcessRestarts: 2,
2124
+ maxServiceRestarts: 1,
2125
+ maxCorrectiveActions: 3,
2126
+ windowSeconds: 600,
2127
+ observationSeconds: 30,
2128
+ resumeDwellSeconds: 300
2129
+ };
2007
2130
  }
2008
2131
  });
2009
2132
 
@@ -4133,7 +4256,7 @@ function loadRepoConfig(repoPath, read2 = (file) => import_node_fs7.default.read
4133
4256
  const config = parseConfig(parsed ?? {}, configPath);
4134
4257
  const requiredVersion = config.devrouter?.version;
4135
4258
  if (requiredVersion && !hasWarnedVersionMismatch) {
4136
- const cliVersion = true ? "0.0.72" : "0.0.0-dev";
4259
+ const cliVersion = true ? "0.0.74" : "0.0.0-dev";
4137
4260
  if (cliVersion !== "0.0.0-dev" && compareSemver(requiredVersion, cliVersion) > 0) {
4138
4261
  hasWarnedVersionMismatch = true;
4139
4262
  process.stderr.write(
@@ -5386,7 +5509,7 @@ var init_reliability_contract = __esm({
5386
5509
 
5387
5510
  // src/core/reliability-operation-store.ts
5388
5511
  function installedCliVersion() {
5389
- return true ? "0.0.72" : "0.0.0-dev";
5512
+ return true ? "0.0.74" : "0.0.0-dev";
5390
5513
  }
5391
5514
  function compareCliVersions(left, right) {
5392
5515
  const parts = (value) => value.split(".").map((part) => Number.parseInt(part, 10) || 0);
@@ -6423,6 +6546,16 @@ async function readProviderGeneration(repoPath, providerId, signal) {
6423
6546
  throw new Error("Capacity provider generation is malformed.");
6424
6547
  return generation;
6425
6548
  }
6549
+ function ownershipPopulationEvidence(record5, state) {
6550
+ return {
6551
+ environmentId: record5.state.environmentId,
6552
+ executionPolicy: record5.state.executionPolicy,
6553
+ enrollment: record5.enrollment ?? null,
6554
+ composeProject: state?.composeProject ?? null,
6555
+ devpodId: state?.devpodId ?? null,
6556
+ stopBaseline: state?.stopBaseline ?? null
6557
+ };
6558
+ }
6426
6559
  async function resolveCapacityOwnership(policy, runtimeDomain, signal, dependencies = {
6427
6560
  resolve: resolveCapacityEnrollment,
6428
6561
  providerGeneration: readProviderGeneration,
@@ -6473,18 +6606,21 @@ async function resolveCapacityOwnership(policy, runtimeDomain, signal, dependenc
6473
6606
  check();
6474
6607
  if (!(0, import_node_util5.isDeepStrictEqual)(current, entry.binding) || !(0, import_node_util5.isDeepStrictEqual)(generation, entry.generation))
6475
6608
  throw new Error("Capacity provider ownership changed during collection.");
6476
- if (!entry.evidence || !(0, import_node_util5.isDeepStrictEqual)(entry.evidence, {
6477
- record: dependencies.journal({
6478
- repoPath: entry.enrollment.repoPath,
6479
- workspace: entry.enrollment.workspace || null,
6480
- provider: entry.enrollment.provider
6481
- }),
6482
- state: dependencies.managed(
6483
- entry.enrollment.repoPath,
6484
- entry.enrollment.workspace || void 0,
6485
- (file) => readControllerEvidence(file, 1048576)
6609
+ if (!entry.evidence || !(0, import_node_util5.isDeepStrictEqual)(
6610
+ entry.evidence,
6611
+ ownershipPopulationEvidence(
6612
+ dependencies.journal({
6613
+ repoPath: entry.enrollment.repoPath,
6614
+ workspace: entry.enrollment.workspace || null,
6615
+ provider: entry.enrollment.provider
6616
+ }),
6617
+ dependencies.managed(
6618
+ entry.enrollment.repoPath,
6619
+ entry.enrollment.workspace || void 0,
6620
+ (file) => readControllerEvidence(file, 1048576)
6621
+ )
6486
6622
  )
6487
- }))
6623
+ ))
6488
6624
  throw new Error("Capacity ownership records changed before publication.");
6489
6625
  }
6490
6626
  },
@@ -6533,7 +6669,7 @@ async function resolveCapacityOwnership(policy, runtimeDomain, signal, dependenc
6533
6669
  throw new Error("Capacity witnessed startup generation changed.");
6534
6670
  } else if (!baseline || baseline.provider !== enrollment.provider || baseline.context !== entry.generation.context || baseline.uid !== entry.generation.uid || baseline.sourceContainer !== entry.generation.sourceContainer || baseline.endpoint !== `unix://${domain.endpoint}` || baseline.daemonId !== domain.daemonId)
6535
6671
  throw new Error("Capacity population lacks an exact retained runtime baseline.");
6536
- const evidence = { record: record5, state };
6672
+ const evidence = ownershipPopulationEvidence(record5, state);
6537
6673
  if (entry.evidence && !(0, import_node_util5.isDeepStrictEqual)(entry.evidence, evidence))
6538
6674
  throw new Error("Capacity ownership generation changed during collection.");
6539
6675
  entry.evidence = structuredClone(evidence);
@@ -6569,7 +6705,13 @@ async function resolveCapacityOwnership(policy, runtimeDomain, signal, dependenc
6569
6705
  endpoint: `unix://${domain.endpoint}`
6570
6706
  });
6571
6707
  }
6572
- if (!(0, import_node_util5.isDeepStrictEqual)(state, readState()) || !(0, import_node_util5.isDeepStrictEqual)(record5, dependencies.journal(identity2)))
6708
+ if (!(0, import_node_util5.isDeepStrictEqual)(state, readState()) || !(0, import_node_util5.isDeepStrictEqual)(
6709
+ ownershipPopulationEvidence(record5, state),
6710
+ ownershipPopulationEvidence(
6711
+ dependencies.journal(identity2),
6712
+ state
6713
+ )
6714
+ ))
6573
6715
  throw new Error("Capacity ownership records changed during collection.");
6574
6716
  result.push({ environmentId: binding.environment.id, containers: containers2 });
6575
6717
  }
@@ -6623,8 +6765,8 @@ async function collectDeclaredRuntimeCapacity(domain, proveOwned, signal, depend
6623
6765
  const sampledAtMs = Date.now();
6624
6766
  const cancellation = new AbortController();
6625
6767
  const combined = AbortSignal.any([signal, cancellation.signal]);
6626
- const timer = setTimeout(() => cancellation.abort(), 3e3);
6627
- const deadline = performance.now() + 3e3;
6768
+ const timer = setTimeout(() => cancellation.abort(), RUNTIME_PROBE_BUDGET_MS);
6769
+ const deadline = performance.now() + RUNTIME_PROBE_BUDGET_MS;
6628
6770
  const check = () => {
6629
6771
  if (combined.aborted || performance.now() >= deadline)
6630
6772
  throw new Error("Runtime capacity collection expired.");
@@ -6717,13 +6859,14 @@ async function collectDeclaredRuntimeCapacity(domain, proveOwned, signal, depend
6717
6859
  clearTimeout(timer);
6718
6860
  }
6719
6861
  }
6720
- var import_node_util6;
6862
+ var import_node_util6, RUNTIME_PROBE_BUDGET_MS;
6721
6863
  var init_capacity_runtime_probe = __esm({
6722
6864
  "src/core/capacity-runtime-probe.ts"() {
6723
6865
  "use strict";
6724
6866
  import_node_util6 = require("util");
6725
6867
  init_capacity_docker_probe();
6726
6868
  init_capacity_host_probe();
6869
+ RUNTIME_PROBE_BUDGET_MS = 8e3;
6727
6870
  }
6728
6871
  });
6729
6872
 
@@ -7577,10 +7720,11 @@ function inspectDevsyRuntimeAbsence(devsyId) {
7577
7720
  encoding: "utf-8",
7578
7721
  timeout: 1e4
7579
7722
  });
7580
- if (!result.error && result.status === 0) {
7723
+ if (result.error || result.status === null) return false;
7724
+ if (result.status === 0) {
7581
7725
  return parseDevsyRuntimeStatus(result.stdout, devsyId) === "not-found";
7582
7726
  }
7583
- const output2 = [result.error?.message, result.stdout, result.stderr].filter(Boolean).join("\n");
7727
+ const output2 = [result.stdout, result.stderr].filter(Boolean).join("\n");
7584
7728
  return /workspace not found/i.test(output2);
7585
7729
  }
7586
7730
  function inspectDevsyWorkspaceOwnership(workspaces, devsyId, worktreePath) {
@@ -8423,8 +8567,10 @@ function listDevpodWorkspacesRaw(options = {}) {
8423
8567
  const result = (0, import_node_child_process9.spawnSync)("devpod", ["list", "--output", "json", "--skip-pro"], {
8424
8568
  encoding: "utf-8"
8425
8569
  });
8426
- if (options.allowMissingExecutable && result.error?.code === "ENOENT")
8427
- return [];
8570
+ if (result.error?.code === "ENOENT") {
8571
+ if (options.readLocalWhenMissing) return readLocalDevpodRegistry();
8572
+ if (options.allowMissingExecutable) return [];
8573
+ }
8428
8574
  if (result.error || result.status !== 0) {
8429
8575
  const details = [result.error?.message, result.stdout, result.stderr].filter(Boolean).join("\n").trim();
8430
8576
  throw new Error(`devpod list failed: ${details || "devpod is not installed or unavailable"}`);
@@ -8457,11 +8603,110 @@ function listDevpodWorkspacesRaw(options = {}) {
8457
8603
  return workspace;
8458
8604
  });
8459
8605
  }
8460
- var import_node_child_process9;
8606
+ function devpodRegistryRoot() {
8607
+ return import_node_path26.default.resolve(process.env.DEVPOD_HOME || import_node_path26.default.join(import_node_os3.default.homedir(), ".devpod"));
8608
+ }
8609
+ function readLocalDevpodRegistry() {
8610
+ const fail4 = () => new Error("Local DevPod registry evidence is incomplete, unreadable, or changed.");
8611
+ const stamps = /* @__PURE__ */ new Map();
8612
+ const stamp = (stat, identityOnly = false) => JSON.stringify([
8613
+ stat.dev,
8614
+ stat.ino,
8615
+ stat.mode,
8616
+ stat.uid,
8617
+ ...identityOnly ? [] : [stat.size, stat.mtimeMs, stat.ctimeMs]
8618
+ ]);
8619
+ const inspect = (file) => {
8620
+ try {
8621
+ return import_node_fs19.default.lstatSync(file);
8622
+ } catch (error) {
8623
+ if (error.code === "ENOENT") return void 0;
8624
+ throw fail4();
8625
+ }
8626
+ };
8627
+ const directory = (dir, optional = false, identityOnly = false) => {
8628
+ const stat = inspect(dir);
8629
+ if (!stat && !optional || stat && !stat.isDirectory()) throw fail4();
8630
+ stamps.set(dir, { value: stat && stamp(stat, identityOnly), identityOnly });
8631
+ return !!stat;
8632
+ };
8633
+ const entries = (dir) => {
8634
+ const names = import_node_fs19.default.readdirSync(dir).sort();
8635
+ if (names.length > 256) throw fail4();
8636
+ return names;
8637
+ };
8638
+ const result = [];
8639
+ try {
8640
+ const root = devpodRegistryRoot();
8641
+ let ancestor = import_node_path26.default.parse(root).root;
8642
+ let present = directory(ancestor, false, ancestor !== root);
8643
+ for (const part of root.slice(ancestor.length).split(import_node_path26.default.sep).filter(Boolean)) {
8644
+ if (!present) break;
8645
+ ancestor = import_node_path26.default.join(ancestor, part);
8646
+ present = directory(ancestor, true, ancestor !== root);
8647
+ }
8648
+ const contexts = import_node_path26.default.join(root, "contexts");
8649
+ if (present && directory(contexts, true)) {
8650
+ for (const context of entries(contexts)) {
8651
+ const contextDir = import_node_path26.default.join(contexts, context);
8652
+ directory(contextDir);
8653
+ const workspaces = import_node_path26.default.join(contextDir, "workspaces");
8654
+ if (!directory(workspaces, true)) continue;
8655
+ for (const id5 of entries(workspaces)) {
8656
+ if (result.length >= 256) throw fail4();
8657
+ const workspace = import_node_path26.default.join(workspaces, id5);
8658
+ directory(workspace);
8659
+ const file = import_node_path26.default.join(workspace, "workspace.json");
8660
+ const stat = inspect(file);
8661
+ if (!stat?.isFile() || stat.size > 1024 * 1024) throw fail4();
8662
+ stamps.set(file, { value: stamp(stat), identityOnly: false });
8663
+ const fd = import_node_fs19.default.openSync(file, import_node_fs19.default.constants.O_RDONLY | import_node_fs19.default.constants.O_NOFOLLOW);
8664
+ let raw;
8665
+ try {
8666
+ if (stamp(import_node_fs19.default.fstatSync(fd)) !== stamp(stat)) throw fail4();
8667
+ const buffer = Buffer.alloc(stat.size + 1);
8668
+ const bytes2 = import_node_fs19.default.readSync(fd, buffer, 0, buffer.length, 0);
8669
+ if (bytes2 !== stat.size || stamp(import_node_fs19.default.fstatSync(fd)) !== stamp(stat)) throw fail4();
8670
+ raw = JSON.parse(buffer.subarray(0, bytes2).toString("utf8"));
8671
+ } finally {
8672
+ import_node_fs19.default.closeSync(fd);
8673
+ }
8674
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw fail4();
8675
+ const value = raw;
8676
+ if (value.id !== id5 || !value.source || typeof value.source !== "object" || Array.isArray(value.source))
8677
+ throw fail4();
8678
+ const source = value.source;
8679
+ if (source.localFolder !== void 0 && (typeof source.localFolder !== "string" || source.localFolder !== "" && !import_node_path26.default.isAbsolute(source.localFolder)))
8680
+ throw fail4();
8681
+ const remote = ["gitRepository", "image", "container"];
8682
+ if (remote.some((key) => source[key] !== void 0 && typeof source[key] !== "string"))
8683
+ throw fail4();
8684
+ if (!source.localFolder && !remote.some((key) => typeof source[key] === "string" && source[key]))
8685
+ throw fail4();
8686
+ result.push({
8687
+ id: id5,
8688
+ source: { localFolder: source.localFolder ?? "" }
8689
+ });
8690
+ }
8691
+ }
8692
+ }
8693
+ for (const [file, expected] of stamps) {
8694
+ const actual = inspect(file);
8695
+ if ((actual && stamp(actual, expected.identityOnly)) !== expected.value) throw fail4();
8696
+ }
8697
+ return result;
8698
+ } catch {
8699
+ throw fail4();
8700
+ }
8701
+ }
8702
+ var import_node_child_process9, import_node_fs19, import_node_os3, import_node_path26;
8461
8703
  var init_devpod_registry = __esm({
8462
8704
  "src/core/devpod-registry.ts"() {
8463
8705
  "use strict";
8464
8706
  import_node_child_process9 = require("child_process");
8707
+ import_node_fs19 = __toESM(require("fs"));
8708
+ import_node_os3 = __toESM(require("os"));
8709
+ import_node_path26 = __toESM(require("path"));
8465
8710
  }
8466
8711
  });
8467
8712
 
@@ -8479,13 +8724,13 @@ function proveManagedComposePopulation(options) {
8479
8724
  }
8480
8725
  services.add(service);
8481
8726
  const files = (container.labels["com.docker.compose.project.config_files"] ?? "").split(",").map((file) => file.trim());
8482
- if (files.some((file) => !file || !import_node_path26.default.isAbsolute(file)) || new Set(files.map((file) => import_node_path26.default.resolve(file))).size !== files.length || files.length < plan.composeFiles.length || plan.composeFiles.some((file, index) => !sameWorkspacePath(file, files[index]))) {
8727
+ if (files.some((file) => !file || !import_node_path27.default.isAbsolute(file)) || new Set(files.map((file) => import_node_path27.default.resolve(file))).size !== files.length || files.length < plan.composeFiles.length || plan.composeFiles.some((file, index) => !sameWorkspacePath(file, files[index]))) {
8483
8728
  throw new Error("Managed stop Compose file identity changed.");
8484
8729
  }
8485
8730
  for (const file of files.slice(plan.composeFiles.length)) {
8486
- if (import_node_path26.default.dirname(file) !== featureDirectory || !/^docker-compose\.devcontainer\.containerFeatures-[a-zA-Z0-9_-]+\.yml$/.test(
8487
- import_node_path26.default.basename(file)
8488
- ) || import_node_fs19.default.realpathSync(file) !== import_node_path26.default.join(import_node_fs19.default.realpathSync(providerRoot), import_node_path26.default.relative(providerRoot, file))) {
8731
+ if (import_node_path27.default.dirname(file) !== featureDirectory || !/^docker-compose\.devcontainer\.containerFeatures-[a-zA-Z0-9_-]+\.yml$/.test(
8732
+ import_node_path27.default.basename(file)
8733
+ ) || import_node_fs20.default.realpathSync(file) !== import_node_path27.default.join(import_node_fs20.default.realpathSync(providerRoot), import_node_path27.default.relative(providerRoot, file))) {
8489
8734
  throw new Error("Managed stop refuses a foreign or escaped provider Compose file.");
8490
8735
  }
8491
8736
  }
@@ -8503,12 +8748,12 @@ function proveManagedComposePopulation(options) {
8503
8748
  }
8504
8749
  return primary;
8505
8750
  }
8506
- var import_node_fs19, import_node_path26;
8751
+ var import_node_fs20, import_node_path27;
8507
8752
  var init_managed_compose_population = __esm({
8508
8753
  "src/core/managed-compose-population.ts"() {
8509
8754
  "use strict";
8510
- import_node_fs19 = __toESM(require("fs"));
8511
- import_node_path26 = __toESM(require("path"));
8755
+ import_node_fs20 = __toESM(require("fs"));
8756
+ import_node_path27 = __toESM(require("path"));
8512
8757
  init_workspace();
8513
8758
  }
8514
8759
  });
@@ -8538,13 +8783,13 @@ function parseWorkspaceRuntime(value) {
8538
8783
  }
8539
8784
  function isRuntimeInstalled(runtime) {
8540
8785
  const probeArgs = runtime === "devsy" ? ["--version"] : ["version"];
8541
- const probe = (0, import_node_child_process10.spawnSync)(runtime, probeArgs, { encoding: "utf-8" });
8542
- return probe.status === 0 && !probe.error;
8786
+ const probe2 = (0, import_node_child_process10.spawnSync)(runtime, probeArgs, { encoding: "utf-8" });
8787
+ return probe2.status === 0 && !probe2.error;
8543
8788
  }
8544
8789
  function readWorkspaceRuntimeConfig() {
8545
8790
  let raw;
8546
8791
  try {
8547
- raw = JSON.parse(import_node_fs20.default.readFileSync(RUNTIME_CONFIG_FILE, "utf-8"));
8792
+ raw = JSON.parse(import_node_fs21.default.readFileSync(RUNTIME_CONFIG_FILE, "utf-8"));
8548
8793
  } catch {
8549
8794
  return {};
8550
8795
  }
@@ -8565,7 +8810,7 @@ function readWorkspaceRuntimeConfig() {
8565
8810
  function inspectWorkspaceRuntimeConfig() {
8566
8811
  let rawText;
8567
8812
  try {
8568
- rawText = import_node_fs20.default.readFileSync(RUNTIME_CONFIG_FILE, "utf-8");
8813
+ rawText = import_node_fs21.default.readFileSync(RUNTIME_CONFIG_FILE, "utf-8");
8569
8814
  } catch {
8570
8815
  return { exists: false, config: {}, problems: [] };
8571
8816
  }
@@ -8616,7 +8861,7 @@ function writeWorkspaceRuntimeConfig(config) {
8616
8861
  }
8617
8862
  next.devsyInactivityTimeout = timeout;
8618
8863
  }
8619
- import_node_fs20.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
8864
+ import_node_fs21.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
8620
8865
  writeFileAtomically(RUNTIME_CONFIG_FILE, `${JSON.stringify(next, null, 2)}
8621
8866
  `);
8622
8867
  }
@@ -8724,20 +8969,20 @@ function resetWorkspaceRuntimeCaches() {
8724
8969
  cachedRuntime = void 0;
8725
8970
  cachedSnapshots = void 0;
8726
8971
  }
8727
- var import_node_child_process10, import_node_fs20, import_node_path27, SUPPORTED_RUNTIMES2, RUNTIME_CONFIG_FILE, INACTIVITY_TIMEOUT_PATTERN, cachedRuntime, cachedSnapshots, UnsupportedWorkspaceRuntimeError, WorkspaceRuntimeOwnershipError;
8972
+ var import_node_child_process10, import_node_fs21, import_node_path28, SUPPORTED_RUNTIMES2, RUNTIME_CONFIG_FILE, INACTIVITY_TIMEOUT_PATTERN, cachedRuntime, cachedSnapshots, UnsupportedWorkspaceRuntimeError, WorkspaceRuntimeOwnershipError;
8728
8973
  var init_workspace_runtime = __esm({
8729
8974
  "src/core/workspace-runtime.ts"() {
8730
8975
  "use strict";
8731
8976
  import_node_child_process10 = require("child_process");
8732
- import_node_fs20 = __toESM(require("fs"));
8733
- import_node_path27 = __toESM(require("path"));
8977
+ import_node_fs21 = __toESM(require("fs"));
8978
+ import_node_path28 = __toESM(require("path"));
8734
8979
  init_atomic_file();
8735
8980
  init_devpod_registry();
8736
8981
  init_devsy_workspaces();
8737
8982
  init_router();
8738
8983
  init_workspace();
8739
8984
  SUPPORTED_RUNTIMES2 = ["devpod", "devsy"];
8740
- RUNTIME_CONFIG_FILE = import_node_path27.default.join(DEVROUTER_HOME, "workspace-runtime.json");
8985
+ RUNTIME_CONFIG_FILE = import_node_path28.default.join(DEVROUTER_HOME, "workspace-runtime.json");
8741
8986
  INACTIVITY_TIMEOUT_PATTERN = /^(?:\d+(?:ms|s|m|h))+$/;
8742
8987
  UnsupportedWorkspaceRuntimeError = class extends Error {
8743
8988
  };
@@ -8864,7 +9109,7 @@ function resolveGitCommonDir(repoPath) {
8864
9109
  if (result.status !== 0 || !output2) {
8865
9110
  throw commandError("Could not resolve the Git common directory", repoPath, result.stderr);
8866
9111
  }
8867
- return comparableWorkspacePath(import_node_path28.default.isAbsolute(output2) ? output2 : import_node_path28.default.resolve(repoPath, output2));
9112
+ return comparableWorkspacePath(import_node_path29.default.isAbsolute(output2) ? output2 : import_node_path29.default.resolve(repoPath, output2));
8868
9113
  }
8869
9114
  function resolveGitTopLevel(repoPath) {
8870
9115
  const result = (0, import_node_child_process12.spawnSync)("git", ["-C", repoPath, "rev-parse", "--show-toplevel"], {
@@ -8915,7 +9160,7 @@ function listGitWorktrees(repoPath) {
8915
9160
  return worktrees;
8916
9161
  }
8917
9162
  function ownershipDirectory(repoPath) {
8918
- return import_node_path28.default.join(resolveGitCommonDir(repoPath), OWNERSHIP_DIR);
9163
+ return import_node_path29.default.join(resolveGitCommonDir(repoPath), OWNERSHIP_DIR);
8919
9164
  }
8920
9165
  function validateWorkspace(value, label2) {
8921
9166
  if (typeof value !== "string" || wsFromBranch(value) !== value) {
@@ -8943,7 +9188,7 @@ function validateRecord(value, expectedWorkspace) {
8943
9188
  `workspace ownership file '${expectedWorkspace}' contains identity '${workspace}'`
8944
9189
  );
8945
9190
  }
8946
- if (typeof candidate.worktreePath !== "string" || !import_node_path28.default.isAbsolute(candidate.worktreePath)) {
9191
+ if (typeof candidate.worktreePath !== "string" || !import_node_path29.default.isAbsolute(candidate.worktreePath)) {
8947
9192
  throw new Error("invalid workspace ownership worktreePath");
8948
9193
  }
8949
9194
  if (candidate.branch !== null && typeof candidate.branch !== "string") {
@@ -8961,7 +9206,7 @@ function validateRecord(value, expectedWorkspace) {
8961
9206
  };
8962
9207
  }
8963
9208
  function recordPath(repoPath, workspace) {
8964
- return import_node_path28.default.join(
9209
+ return import_node_path29.default.join(
8965
9210
  ownershipDirectory(repoPath),
8966
9211
  `${validateWorkspace(workspace, "workspace")}.json`
8967
9212
  );
@@ -8969,7 +9214,7 @@ function recordPath(repoPath, workspace) {
8969
9214
  function readRecordFile(filePath, expectedWorkspace) {
8970
9215
  let parsed;
8971
9216
  try {
8972
- parsed = JSON.parse(import_node_fs21.default.readFileSync(filePath, "utf-8"));
9217
+ parsed = JSON.parse(import_node_fs22.default.readFileSync(filePath, "utf-8"));
8973
9218
  } catch (error) {
8974
9219
  if (error instanceof SyntaxError) {
8975
9220
  throw new Error(`invalid workspace ownership JSON at '${filePath}'`);
@@ -8994,7 +9239,7 @@ function listWorkspaceOwnership(repoPath) {
8994
9239
  function listWorkspaceOwnershipInDirectory(directory) {
8995
9240
  let entries;
8996
9241
  try {
8997
- entries = import_node_fs21.default.readdirSync(directory, { withFileTypes: true });
9242
+ entries = import_node_fs22.default.readdirSync(directory, { withFileTypes: true });
8998
9243
  } catch (error) {
8999
9244
  if (error.code === "ENOENT") return [];
9000
9245
  throw error;
@@ -9002,14 +9247,14 @@ function listWorkspaceOwnershipInDirectory(directory) {
9002
9247
  return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).sort((left, right) => left.name.localeCompare(right.name)).map((entry) => {
9003
9248
  const workspace = entry.name.slice(0, -".json".length);
9004
9249
  validateWorkspace(workspace, "filename");
9005
- return readRecordFile(import_node_path28.default.join(directory, entry.name), workspace);
9250
+ return readRecordFile(import_node_path29.default.join(directory, entry.name), workspace);
9006
9251
  });
9007
9252
  }
9008
9253
  function writeWorkspaceOwnershipInDirectory(directory, input2) {
9009
9254
  const workspace = validateWorkspace(input2.workspace, "workspace");
9010
9255
  const devpodId = validateWorkspace(input2.devpodId, "devpodId");
9011
9256
  const worktreePath = comparableWorkspacePath(input2.worktreePath);
9012
- const filePath = import_node_path28.default.join(directory, `${workspace}.json`);
9257
+ const filePath = import_node_path29.default.join(directory, `${workspace}.json`);
9013
9258
  const now = (/* @__PURE__ */ new Date()).toISOString();
9014
9259
  const records = listWorkspaceOwnershipInDirectory(directory);
9015
9260
  const existing = records.find((record6) => record6.workspace === workspace);
@@ -9045,9 +9290,9 @@ function writeWorkspaceOwnershipInDirectory(directory, input2) {
9045
9290
  return record5;
9046
9291
  }
9047
9292
  function removeWorkspaceOwnershipInDirectory(directory, workspace) {
9048
- const filePath = import_node_path28.default.join(directory, `${validateWorkspace(workspace, "workspace")}.json`);
9293
+ const filePath = import_node_path29.default.join(directory, `${validateWorkspace(workspace, "workspace")}.json`);
9049
9294
  try {
9050
- import_node_fs21.default.rmSync(filePath);
9295
+ import_node_fs22.default.rmSync(filePath);
9051
9296
  return true;
9052
9297
  } catch (error) {
9053
9298
  if (error.code === "ENOENT") return false;
@@ -9058,7 +9303,7 @@ function sameOwnershipRecord(left, right) {
9058
9303
  return left.version === right.version && left.workspace === right.workspace && sameWorkspacePath(left.worktreePath, right.worktreePath) && left.branch === right.branch && left.devpodId === right.devpodId && left.createdAt === right.createdAt && left.updatedAt === right.updatedAt;
9059
9304
  }
9060
9305
  function removeWorkspaceOwnershipIfMatchesInDirectory(directory, expected) {
9061
- const filePath = import_node_path28.default.join(
9306
+ const filePath = import_node_path29.default.join(
9062
9307
  directory,
9063
9308
  `${validateWorkspace(expected.workspace, "workspace")}.json`
9064
9309
  );
@@ -9070,14 +9315,14 @@ function removeWorkspaceOwnershipIfMatchesInDirectory(directory, expected) {
9070
9315
  throw error;
9071
9316
  }
9072
9317
  if (!sameOwnershipRecord(current, expected)) return "changed";
9073
- import_node_fs21.default.rmSync(filePath);
9318
+ import_node_fs22.default.rmSync(filePath);
9074
9319
  return "removed";
9075
9320
  }
9076
9321
  function withWorkspaceOwnershipTransaction(repoPath, operation, options = {}) {
9077
9322
  const directory = ownershipDirectory(repoPath);
9078
- import_node_fs21.default.mkdirSync(directory, { recursive: true });
9323
+ import_node_fs22.default.mkdirSync(directory, { recursive: true });
9079
9324
  return withFileLockSync(
9080
- import_node_path28.default.join(directory, ".lock"),
9325
+ import_node_path29.default.join(directory, ".lock"),
9081
9326
  {
9082
9327
  activity: "workspace ownership transaction",
9083
9328
  target: `'${repoPath}'`,
@@ -9110,7 +9355,7 @@ function persistedWorkspaceOwners(repoPath, worktreePath) {
9110
9355
  try {
9111
9356
  workspace = readPersistedWorkspace(worktree.path);
9112
9357
  } catch (error) {
9113
- if (!import_node_fs21.default.existsSync(worktree.path)) continue;
9358
+ if (!import_node_fs22.default.existsSync(worktree.path)) continue;
9114
9359
  throw error;
9115
9360
  }
9116
9361
  if (!workspace) continue;
@@ -9273,7 +9518,7 @@ function inspectWorkspaceOwnership(record5, worktrees, devpods) {
9273
9518
  if (worktree?.locked) {
9274
9519
  return { ownerStatus: "locked", devpodStatus, worktree };
9275
9520
  }
9276
- if ((!worktree || worktree.prunable) && import_node_fs21.default.existsSync(record5.worktreePath)) {
9521
+ if ((!worktree || worktree.prunable) && import_node_fs22.default.existsSync(record5.worktreePath)) {
9277
9522
  return { ownerStatus: "conflict", devpodStatus, worktree };
9278
9523
  }
9279
9524
  if (!worktree || worktree.prunable) {
@@ -9297,20 +9542,20 @@ function listMissingWorkspaceOwnership(repoPath) {
9297
9542
  (record5) => inspectWorkspaceOwnership(record5, worktrees, void 0).ownerStatus === "missing"
9298
9543
  );
9299
9544
  }
9300
- var import_node_child_process12, import_node_fs21, import_node_path28, READ_ONLY_GIT_ENV, OWNERSHIP_VERSION, OWNERSHIP_DIR;
9545
+ var import_node_child_process12, import_node_fs22, import_node_path29, READ_ONLY_GIT_ENV, OWNERSHIP_VERSION, OWNERSHIP_DIR;
9301
9546
  var init_workspace_ownership = __esm({
9302
9547
  "src/core/workspace-ownership.ts"() {
9303
9548
  "use strict";
9304
9549
  import_node_child_process12 = require("child_process");
9305
- import_node_fs21 = __toESM(require("fs"));
9306
- import_node_path28 = __toESM(require("path"));
9550
+ import_node_fs22 = __toESM(require("fs"));
9551
+ import_node_path29 = __toESM(require("path"));
9307
9552
  init_atomic_file();
9308
9553
  init_devpod_workspaces();
9309
9554
  init_file_lock();
9310
9555
  init_workspace();
9311
9556
  READ_ONLY_GIT_ENV = { ...process.env, GIT_OPTIONAL_LOCKS: "0" };
9312
9557
  OWNERSHIP_VERSION = 1;
9313
- OWNERSHIP_DIR = import_node_path28.default.join("devrouter", "workspaces");
9558
+ OWNERSHIP_DIR = import_node_path29.default.join("devrouter", "workspaces");
9314
9559
  }
9315
9560
  });
9316
9561
 
@@ -9559,12 +9804,12 @@ function managedStopRouteReferences(state) {
9559
9804
  (name) => ["http", "tcp"].map((protocol) => ({ repoPath: state.repoPath, name, protocol }))
9560
9805
  );
9561
9806
  }
9562
- var import_node_os3, import_node_path29, import_node_util8;
9807
+ var import_node_os4, import_node_path30, import_node_util8;
9563
9808
  var init_managed_stop_recovery = __esm({
9564
9809
  "src/core/managed-stop-recovery.ts"() {
9565
9810
  "use strict";
9566
- import_node_os3 = __toESM(require("os"));
9567
- import_node_path29 = __toESM(require("path"));
9811
+ import_node_os4 = __toESM(require("os"));
9812
+ import_node_path30 = __toESM(require("path"));
9568
9813
  import_node_util8 = require("util");
9569
9814
  init_devpod_environment();
9570
9815
  init_devpod_registry();
@@ -9711,8 +9956,8 @@ function tlsSetupCommand(repoPath) {
9711
9956
  }
9712
9957
  function getMkcertRootCAPath(options = {}) {
9713
9958
  ensureMkcert();
9714
- const rootCAPath = import_node_path30.default.join(runOrThrow("mkcert", ["-CAROOT"]), "rootCA.pem");
9715
- if (!import_node_fs22.default.existsSync(rootCAPath)) {
9959
+ const rootCAPath = import_node_path31.default.join(runOrThrow("mkcert", ["-CAROOT"]), "rootCA.pem");
9960
+ if (!import_node_fs23.default.existsSync(rootCAPath)) {
9716
9961
  throw new Error(
9717
9962
  `mkcert root CA was not found at '${rootCAPath}'. Run: ${tlsSetupCommand(options.repoPath)}`
9718
9963
  );
@@ -9813,11 +10058,11 @@ function compactTLSCertificateHosts(hosts) {
9813
10058
  );
9814
10059
  }
9815
10060
  function readCurrentCertificateHosts(options = {}) {
9816
- if (!import_node_fs22.default.existsSync(CERT_FILE)) {
10061
+ if (!import_node_fs23.default.existsSync(CERT_FILE)) {
9817
10062
  return [];
9818
10063
  }
9819
10064
  try {
9820
- const pem = import_node_fs22.default.readFileSync(CERT_FILE, "utf-8");
10065
+ const pem = import_node_fs23.default.readFileSync(CERT_FILE, "utf-8");
9821
10066
  return parseCertificateDnsHosts(pem);
9822
10067
  } catch (error) {
9823
10068
  if (options.replaceMalformed) {
@@ -9945,20 +10190,20 @@ Run: ${tlsSetupCommand(options.repoPath)}`
9945
10190
  );
9946
10191
  }
9947
10192
  }
9948
- var import_node_child_process14, import_node_crypto12, import_node_fs22, import_node_path30, DEFAULT_TLS_CERT_HOSTS, TLS_CERTIFICATE_LOCK_FILE, TLS_CERTIFICATE_LOCK_WAIT_MS, TLS_CERTIFICATE_WRITE_ATTEMPTS;
10193
+ var import_node_child_process14, import_node_crypto12, import_node_fs23, import_node_path31, DEFAULT_TLS_CERT_HOSTS, TLS_CERTIFICATE_LOCK_FILE, TLS_CERTIFICATE_LOCK_WAIT_MS, TLS_CERTIFICATE_WRITE_ATTEMPTS;
9949
10194
  var init_tls = __esm({
9950
10195
  "src/core/tls.ts"() {
9951
10196
  "use strict";
9952
10197
  import_node_child_process14 = require("child_process");
9953
10198
  import_node_crypto12 = require("crypto");
9954
- import_node_fs22 = __toESM(require("fs"));
9955
- import_node_path30 = __toESM(require("path"));
10199
+ import_node_fs23 = __toESM(require("fs"));
10200
+ import_node_path31 = __toESM(require("path"));
9956
10201
  init_docker();
9957
10202
  init_file_lock();
9958
10203
  init_host_routes();
9959
10204
  init_router();
9960
10205
  DEFAULT_TLS_CERT_HOSTS = ["localhost", "*.localhost"];
9961
- TLS_CERTIFICATE_LOCK_FILE = import_node_path30.default.join(DEVROUTER_HOME, "tls-certificate.lock");
10206
+ TLS_CERTIFICATE_LOCK_FILE = import_node_path31.default.join(DEVROUTER_HOME, "tls-certificate.lock");
9962
10207
  TLS_CERTIFICATE_LOCK_WAIT_MS = 6e4;
9963
10208
  TLS_CERTIFICATE_WRITE_ATTEMPTS = 2;
9964
10209
  }
@@ -10039,6 +10284,174 @@ var init_route_publication = __esm({
10039
10284
  }
10040
10285
  });
10041
10286
 
10287
+ // src/core/traefik-route-health.ts
10288
+ function sleep(ms) {
10289
+ return new Promise((resolve) => setTimeout(resolve, ms));
10290
+ }
10291
+ function inspectRouterApi(protocol) {
10292
+ const result = (0, import_node_child_process15.spawnSync)(
10293
+ "curl",
10294
+ [
10295
+ "--silent",
10296
+ "--show-error",
10297
+ "--fail",
10298
+ "--max-time",
10299
+ ROUTER_API_REQUEST_TIMEOUT_SECONDS,
10300
+ `${ROUTER_API_BASE}/${protocol}/routers?per_page=${ROUTER_API_PAGE_SIZE}`
10301
+ ],
10302
+ { encoding: "utf-8" }
10303
+ );
10304
+ if (result.status !== 0) {
10305
+ return {
10306
+ ok: false,
10307
+ details: result.stderr?.trim() || `curl exited with status ${result.status ?? "unknown"}`
10308
+ };
10309
+ }
10310
+ let value;
10311
+ try {
10312
+ value = JSON.parse(result.stdout);
10313
+ } catch {
10314
+ return { ok: false, details: `${protocol.toUpperCase()} router API returned invalid JSON` };
10315
+ }
10316
+ if (!Array.isArray(value)) {
10317
+ return { ok: false, details: `${protocol.toUpperCase()} router API returned a non-array` };
10318
+ }
10319
+ if (value.some((item) => !isRecord5(item) || typeof item.name !== "string" || !item.name))
10320
+ return {
10321
+ ok: false,
10322
+ details: `${protocol.toUpperCase()} router API returned malformed entries`
10323
+ };
10324
+ return {
10325
+ ok: true,
10326
+ reachedPageLimit: value.length >= ROUTER_API_PAGE_SIZE,
10327
+ names: new Set(
10328
+ value.flatMap((item) => {
10329
+ if (!item || typeof item !== "object") return [];
10330
+ const name = item.name;
10331
+ return typeof name === "string" ? [name] : [];
10332
+ })
10333
+ )
10334
+ };
10335
+ }
10336
+ function isRecord5(value) {
10337
+ return value !== null && typeof value === "object" && !Array.isArray(value);
10338
+ }
10339
+ function inspectExpectedRoutes(routes, expectation) {
10340
+ const mismatched = [];
10341
+ const failures = [];
10342
+ for (const protocol of ["http", "tcp"]) {
10343
+ const expected = routes.filter((route) => (route.protocol ?? "http") === protocol).map((route) => buildHostRouteRouterName(route.repoPath, route.name));
10344
+ if (expected.length === 0) continue;
10345
+ const result = inspectRouterApi(protocol);
10346
+ if (!result.ok) {
10347
+ failures.push(result.details);
10348
+ mismatched.push(...expected);
10349
+ continue;
10350
+ }
10351
+ const unexpected = expected.filter(
10352
+ (name) => expectation === "loaded" ? !result.names.has(name) : result.names.has(name)
10353
+ );
10354
+ const boundedAbsenceIsUncertain = result.reachedPageLimit && (expectation === "loaded" ? unexpected.length > 0 : unexpected.length === 0);
10355
+ if (boundedAbsenceIsUncertain) {
10356
+ failures.push(
10357
+ `${protocol.toUpperCase()} router API reached the ${ROUTER_API_PAGE_SIZE}-entry safety limit`
10358
+ );
10359
+ }
10360
+ mismatched.push(...unexpected);
10361
+ if (expectation === "removed" && unexpected.length === 0 && result.reachedPageLimit) {
10362
+ mismatched.push(...expected);
10363
+ }
10364
+ }
10365
+ return mismatched.length === 0 ? { ok: true } : {
10366
+ ok: false,
10367
+ routes: mismatched,
10368
+ details: failures.length > 0 ? failures.join("; ") : expectation === "loaded" ? "router names are absent" : "router names are still present"
10369
+ };
10370
+ }
10371
+ async function waitForExpectedRoutes(routes, expectation, timeoutMs, pollIntervalMs) {
10372
+ const deadline = Date.now() + timeoutMs;
10373
+ let result;
10374
+ do {
10375
+ result = inspectExpectedRoutes(routes, expectation);
10376
+ if (result.ok) return result;
10377
+ if (Date.now() < deadline) {
10378
+ await sleep(Math.min(pollIntervalMs, Math.max(1, deadline - Date.now())));
10379
+ }
10380
+ } while (Date.now() < deadline);
10381
+ return result;
10382
+ }
10383
+ async function ensureTraefikRouteExpectation(routes, expectation, options) {
10384
+ if (routes.length === 0) return { restarted: false };
10385
+ const initialTimeoutMs = options.initialTimeoutMs ?? DEFAULT_INITIAL_TIMEOUT_MS;
10386
+ const recoveryTimeoutMs = options.recoveryTimeoutMs ?? DEFAULT_RECOVERY_TIMEOUT_MS;
10387
+ const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
10388
+ const initial = await waitForExpectedRoutes(
10389
+ routes,
10390
+ expectation,
10391
+ initialTimeoutMs,
10392
+ pollIntervalMs
10393
+ );
10394
+ if (initial.ok) return { restarted: false };
10395
+ if (options.allowRestart === false)
10396
+ throw new Error(
10397
+ "Traefik routes did not reload; shared router restart is disabled for this operation."
10398
+ );
10399
+ import_node_fs24.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
10400
+ return withFileLock(
10401
+ ROUTER_RELOAD_LOCK_FILE,
10402
+ {
10403
+ activity: "Traefik route reload recovery",
10404
+ waitMs: initialTimeoutMs + recoveryTimeoutMs + 1e4,
10405
+ fair: true,
10406
+ onWait: createStderrWaitReporter("Traefik route reload recovery", "shared router")
10407
+ },
10408
+ async () => {
10409
+ const recheck = await waitForExpectedRoutes(routes, expectation, 0, pollIntervalMs);
10410
+ if (recheck.ok) return { restarted: false };
10411
+ claimLifecycleEffect();
10412
+ restartRouterStack();
10413
+ const recovered = await waitForExpectedRoutes(
10414
+ routes,
10415
+ expectation,
10416
+ recoveryTimeoutMs,
10417
+ pollIntervalMs
10418
+ );
10419
+ if (recovered.ok) return { restarted: true };
10420
+ const action2 = expectation === "loaded" ? "load" : "remove";
10421
+ throw new Error(
10422
+ `Traefik did not ${action2} file-provider routes after one restart: ${recovered.routes.join(", ")} (${recovered.details}). Inspect: devrouter logs --tail 100`
10423
+ );
10424
+ }
10425
+ );
10426
+ }
10427
+ async function ensureTraefikRoutesRemoved(routes, options = {}) {
10428
+ return ensureTraefikRouteExpectation(routes, "removed", options);
10429
+ }
10430
+ function assertTraefikRoutesRemoved(routes) {
10431
+ const result = inspectExpectedRoutes(routes, "removed");
10432
+ if (!result.ok) throw new Error(`Workspace route cessation is not proven: ${result.details}`);
10433
+ }
10434
+ var import_node_child_process15, import_node_fs24, import_node_path32, 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;
10435
+ var init_traefik_route_health = __esm({
10436
+ "src/core/traefik-route-health.ts"() {
10437
+ "use strict";
10438
+ import_node_child_process15 = require("child_process");
10439
+ import_node_fs24 = __toESM(require("fs"));
10440
+ import_node_path32 = __toESM(require("path"));
10441
+ init_file_lock();
10442
+ init_host_routes();
10443
+ init_reliability_context();
10444
+ init_router();
10445
+ ROUTER_API_BASE = "http://127.0.0.1:8080/api";
10446
+ ROUTER_RELOAD_LOCK_FILE = import_node_path32.default.join(DEVROUTER_HOME, "router-reload.lock");
10447
+ ROUTER_API_REQUEST_TIMEOUT_SECONDS = "2";
10448
+ DEFAULT_INITIAL_TIMEOUT_MS = 3e3;
10449
+ DEFAULT_RECOVERY_TIMEOUT_MS = 1e4;
10450
+ DEFAULT_POLL_INTERVAL_MS = 250;
10451
+ ROUTER_API_PAGE_SIZE = 1e3;
10452
+ }
10453
+ });
10454
+
10042
10455
  // src/core/managed-devsy-stop.ts
10043
10456
  function sameSet(left, right) {
10044
10457
  return JSON.stringify([...left].sort()) === JSON.stringify([...right].sort());
@@ -10051,12 +10464,75 @@ function containerIdentity(container) {
10051
10464
  mounts: container.mounts.map((mount) => JSON.stringify(mount)).sort()
10052
10465
  });
10053
10466
  }
10054
- function stopInitialManagedDevsyWorkspace(repoPath, devsyId) {
10055
- if (!import_node_fs23.default.existsSync(import_node_path31.default.join(repoPath, ".devrouter.yml"))) return false;
10056
- const linked = isLinkedWorktree(repoPath);
10057
- const workspace = linked ? resolveWorktreeWorkspace(repoPath) : void 0;
10058
- const registration2 = () => {
10059
- resetWorkspaceRuntimeCaches();
10467
+ function proveInitialManagedDevsyAbsence(repoPath, expectedId) {
10468
+ if (!isLinkedWorktree(repoPath)) return void 0;
10469
+ const workspace = resolveWorktreeWorkspace(repoPath);
10470
+ if (!workspace || readManagedRuntimeState(repoPath, workspace)) return void 0;
10471
+ if (!import_node_fs25.default.existsSync(import_node_path33.default.join(repoPath, ".devrouter.yml"))) return void 0;
10472
+ const runtime = loadRuntimeConfig(repoPath, workspace);
10473
+ if (!runtime.config.managedRuntime) return void 0;
10474
+ const routes = proxyAppsFromConfig(runtime.config).flatMap(
10475
+ (app) => ["http", "tcp"].map((protocol) => ({ repoPath, name: app.name, protocol }))
10476
+ );
10477
+ const record5 = readWorkspaceOwnership(repoPath, workspace);
10478
+ if (!record5 || expectedId !== void 0 && record5.devpodId !== expectedId || record5.workspace !== workspace || !sameWorkspacePath(record5.worktreePath, repoPath))
10479
+ throw new Error("Initial managed stop requires exact linked ownership.");
10480
+ const devsyId = record5.devpodId;
10481
+ const ownership = inspectDevsyWorkspaceOwnership(listDevsyWorkspaces(), devsyId, repoPath);
10482
+ if (ownership.status === "conflict") throw new Error(ownership.reason);
10483
+ if (ownership.status !== "absent") return void 0;
10484
+ const gitCommonDir = resolveGitCommonDir(repoPath);
10485
+ const endpoint = resolveManagedStopEndpoint();
10486
+ if (!supportsManagedStopBaseline(endpoint))
10487
+ throw new Error("Initial managed stop requires a local Docker endpoint.");
10488
+ const daemon = inspectManagedStopDaemon(endpoint);
10489
+ const legacyHome = devpodRegistryRoot();
10490
+ const readLegacy = () => listDevpodWorkspacesRaw({ readLocalWhenMissing: true }).map(({ id: id5, source }) => ({ id: id5, source: { localFolder: source.localFolder } })).sort(
10491
+ (a, b) => a.id.localeCompare(b.id) || a.source.localFolder.localeCompare(b.source.localFolder)
10492
+ );
10493
+ const legacyWorkspaces = readLegacy();
10494
+ const observe = () => {
10495
+ resetWorkspaceRuntimeCaches();
10496
+ if (resolveWorkspaceRuntimeOrDefault(repoPath) !== "devsy" || !isLinkedWorktree(repoPath) || resolveWorktreeWorkspace(repoPath) !== workspace || resolveGitCommonDir(repoPath) !== gitCommonDir || !(0, import_node_util9.isDeepStrictEqual)(readWorkspaceOwnership(repoPath, workspace), record5) || inspectWorkspaceOwnership(record5, listGitWorktrees(repoPath), void 0).ownerStatus !== "present" || readManagedRuntimeState(repoPath, workspace))
10497
+ throw new Error("Initial managed stop ownership or retained state changed.");
10498
+ if (devpodRegistryRoot() !== legacyHome || !(0, import_node_util9.isDeepStrictEqual)(readLegacy(), legacyWorkspaces))
10499
+ throw new Error("Initial managed stop legacy registry evidence changed.");
10500
+ if (inspectDevsyWorkspaceOwnership(listDevsyWorkspaces(), devsyId, repoPath).status !== "absent" || legacyWorkspaces.some(
10501
+ (entry) => entry.id === devsyId || entry.source.localFolder !== "" && sameWorkspacePath(entry.source.localFolder, repoPath)
10502
+ ))
10503
+ throw new Error("Initial managed stop requires both provider registrations absent.");
10504
+ if (!inspectDevsyRuntimeAbsence(devsyId))
10505
+ throw new Error("Initial managed stop requires positive runtime not-found.");
10506
+ if (resolveManagedStopEndpoint() !== endpoint || inspectManagedStopDaemon(endpoint) !== daemon)
10507
+ throw new Error("Initial managed stop Docker identity changed.");
10508
+ if (inspectProviderRunnerContainers(endpoint, devsyId).length)
10509
+ throw new Error("Initial managed stop observed a remaining provider runner.");
10510
+ assertManagedStopCheckoutAbsent(endpoint, repoPath);
10511
+ if (listHostRouteState().some((route) => sameWorkspacePath(route.repoPath, repoPath)))
10512
+ throw new Error("Initial managed stop observed remaining workspace routes.");
10513
+ assertTraefikRoutesRemoved(routes);
10514
+ if (resolveManagedStopEndpoint() !== endpoint || inspectManagedStopDaemon(endpoint) !== daemon)
10515
+ throw new Error("Initial managed stop Docker identity changed.");
10516
+ };
10517
+ observe();
10518
+ observe();
10519
+ return {
10520
+ workspace,
10521
+ record: record5,
10522
+ gitCommonDir,
10523
+ endpoint,
10524
+ daemon,
10525
+ routes,
10526
+ legacyHome,
10527
+ legacyWorkspaces
10528
+ };
10529
+ }
10530
+ function stopInitialManagedDevsyWorkspace(repoPath, devsyId) {
10531
+ if (!import_node_fs25.default.existsSync(import_node_path33.default.join(repoPath, ".devrouter.yml"))) return false;
10532
+ const linked = isLinkedWorktree(repoPath);
10533
+ const workspace = linked ? resolveWorktreeWorkspace(repoPath) : void 0;
10534
+ const registration2 = () => {
10535
+ resetWorkspaceRuntimeCaches();
10060
10536
  if (resolveWorkspaceRuntimeOrDefault(repoPath) !== "devsy")
10061
10537
  throw new Error("Initial managed stop provider selection changed.");
10062
10538
  const owner2 = inspectDevsyWorkspaceOwnership(listDevsyWorkspaces(), devsyId, repoPath);
@@ -10080,8 +10556,8 @@ function stopInitialManagedDevsyWorkspace(repoPath, devsyId) {
10080
10556
  profile: runtime.resolvedProfile,
10081
10557
  linked
10082
10558
  });
10083
- const providerRoot = import_node_path31.default.resolve(process.env.DEVSY_HOME || import_node_path31.default.join(import_node_os4.default.homedir(), ".devsy"));
10084
- const featureDirectory = import_node_path31.default.join(
10559
+ const providerRoot = import_node_path33.default.resolve(process.env.DEVSY_HOME || import_node_path33.default.join(import_node_os5.default.homedir(), ".devsy"));
10560
+ const featureDirectory = import_node_path33.default.join(
10085
10561
  providerRoot,
10086
10562
  "contexts",
10087
10563
  owner.context,
@@ -10152,7 +10628,10 @@ function stopRetainedManagedDevsyWorkspace(options) {
10152
10628
  const linked = isLinkedWorktree(repoPath);
10153
10629
  const workspace = linked ? resolveWorktreeWorkspace(repoPath) : void 0;
10154
10630
  const retainedState = readManagedRuntimeState(repoPath, workspace);
10155
- if (!retainedState) return stopInitialManagedDevsyWorkspace(repoPath, devsyId);
10631
+ if (!retainedState) {
10632
+ if (proveInitialManagedDevsyAbsence(repoPath, devsyId)) return "proven-absent";
10633
+ return stopInitialManagedDevsyWorkspace(repoPath, devsyId);
10634
+ }
10156
10635
  const state = retainedState;
10157
10636
  if (state.devpodId !== devsyId || linked && !workspace) {
10158
10637
  throw new Error("Managed stop requires the exact retained workspace identity.");
@@ -10213,7 +10692,7 @@ function stopRetainedManagedDevsyWorkspace(options) {
10213
10692
  const owned = inspectWorkspaceContainers().filter(
10214
10693
  (container) => sameWorkspacePath(
10215
10694
  container.labels["com.docker.compose.project.working_dir"] ?? "",
10216
- import_node_path31.default.join(repoPath, ".devcontainer")
10695
+ import_node_path33.default.join(repoPath, ".devcontainer")
10217
10696
  )
10218
10697
  );
10219
10698
  if (owned.length !== 0) {
@@ -10242,8 +10721,8 @@ function stopRetainedManagedDevsyWorkspace(options) {
10242
10721
  if (!context || !/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(context) || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(devsyId)) {
10243
10722
  throw new Error("Managed stop requires a valid exact Devsy context and workspace ID.");
10244
10723
  }
10245
- const devsyRoot = import_node_path31.default.resolve(process.env.DEVSY_HOME || import_node_path31.default.join(import_node_os4.default.homedir(), ".devsy"));
10246
- const featureDirectory = import_node_path31.default.join(
10724
+ const devsyRoot = import_node_path33.default.resolve(process.env.DEVSY_HOME || import_node_path33.default.join(import_node_os5.default.homedir(), ".devsy"));
10725
+ const featureDirectory = import_node_path33.default.join(
10247
10726
  devsyRoot,
10248
10727
  "contexts",
10249
10728
  context,
@@ -10347,23 +10826,25 @@ function stopRetainedManagedDevsyWorkspace(options) {
10347
10826
  if (providerFailed) throw providerError;
10348
10827
  return true;
10349
10828
  }
10350
- var import_node_fs23, import_node_os4, import_node_path31, import_node_util9;
10829
+ var import_node_fs25, import_node_os5, import_node_path33, import_node_util9;
10351
10830
  var init_managed_devsy_stop = __esm({
10352
10831
  "src/core/managed-devsy-stop.ts"() {
10353
10832
  "use strict";
10354
- import_node_fs23 = __toESM(require("fs"));
10355
- import_node_os4 = __toESM(require("os"));
10356
- import_node_path31 = __toESM(require("path"));
10833
+ import_node_fs25 = __toESM(require("fs"));
10834
+ import_node_os5 = __toESM(require("os"));
10835
+ import_node_path33 = __toESM(require("path"));
10357
10836
  import_node_util9 = require("util");
10358
10837
  init_devcontainer_profile();
10359
10838
  init_devpod_environment();
10360
10839
  init_devpod_registry();
10361
10840
  init_devsy_workspaces();
10841
+ init_host_routes();
10362
10842
  init_managed_compose_population();
10363
10843
  init_managed_runtime_state();
10364
10844
  init_managed_stop_recovery();
10365
10845
  init_repo_config();
10366
10846
  init_route_publication();
10847
+ init_traefik_route_health();
10367
10848
  init_workspace();
10368
10849
  init_workspace_ownership();
10369
10850
  init_workspace_runtime();
@@ -10909,7 +11390,7 @@ function hasControlCharacter2(value) {
10909
11390
  function readPolicyFile(filePath) {
10910
11391
  let initialStats;
10911
11392
  try {
10912
- initialStats = import_node_fs24.default.lstatSync(filePath);
11393
+ initialStats = import_node_fs26.default.lstatSync(filePath);
10913
11394
  } catch (error) {
10914
11395
  if (isErrno(error, "ENOENT")) return { status: "absent" };
10915
11396
  return { status: "invalid", error: "network policy could not be read" };
@@ -10922,16 +11403,16 @@ function readPolicyFile(filePath) {
10922
11403
  }
10923
11404
  let fileDescriptor;
10924
11405
  try {
10925
- fileDescriptor = import_node_fs24.default.openSync(
11406
+ fileDescriptor = import_node_fs26.default.openSync(
10926
11407
  filePath,
10927
- import_node_fs24.default.constants.O_RDONLY | import_node_fs24.default.constants.O_NONBLOCK | (import_node_fs24.default.constants.O_NOFOLLOW ?? 0)
11408
+ import_node_fs26.default.constants.O_RDONLY | import_node_fs26.default.constants.O_NONBLOCK | (import_node_fs26.default.constants.O_NOFOLLOW ?? 0)
10928
11409
  );
10929
11410
  } catch (error) {
10930
11411
  if (isErrno(error, "ENOENT")) return { status: "absent" };
10931
11412
  return { status: "invalid", error: "network policy could not be read" };
10932
11413
  }
10933
11414
  try {
10934
- const openedStats = import_node_fs24.default.fstatSync(fileDescriptor);
11415
+ const openedStats = import_node_fs26.default.fstatSync(fileDescriptor);
10935
11416
  if (!openedStats.isFile()) {
10936
11417
  return { status: "invalid", error: "network policy must be a regular file" };
10937
11418
  }
@@ -10944,12 +11425,12 @@ function readPolicyFile(filePath) {
10944
11425
  const buffer = Buffer.alloc(openedStats.size);
10945
11426
  let offset = 0;
10946
11427
  while (offset < buffer.length) {
10947
- const bytesRead = import_node_fs24.default.readSync(fileDescriptor, buffer, offset, buffer.length - offset, null);
11428
+ const bytesRead = import_node_fs26.default.readSync(fileDescriptor, buffer, offset, buffer.length - offset, null);
10948
11429
  if (bytesRead === 0)
10949
11430
  return { status: "invalid", error: "network policy changed during read" };
10950
11431
  offset += bytesRead;
10951
11432
  }
10952
- const finalStats = import_node_fs24.default.fstatSync(fileDescriptor);
11433
+ const finalStats = import_node_fs26.default.fstatSync(fileDescriptor);
10953
11434
  if (!finalStats.isFile() || finalStats.size !== openedStats.size) {
10954
11435
  return { status: "invalid", error: "network policy changed during read" };
10955
11436
  }
@@ -10958,7 +11439,7 @@ function readPolicyFile(filePath) {
10958
11439
  return { status: "invalid", error: "network policy could not be read" };
10959
11440
  } finally {
10960
11441
  try {
10961
- import_node_fs24.default.closeSync(fileDescriptor);
11442
+ import_node_fs26.default.closeSync(fileDescriptor);
10962
11443
  } catch {
10963
11444
  }
10964
11445
  }
@@ -10975,16 +11456,16 @@ function assertDisjoint(cidrs, message) {
10975
11456
  function isErrno(error, code) {
10976
11457
  return typeof error === "object" && error !== null && "code" in error && error.code === code;
10977
11458
  }
10978
- var import_node_fs24, import_node_path32, NETWORK_POLICY_VERSION, NETWORK_POLICY_FILE, DEFAULT_NETWORK_POLICY_ENDPOINT_RESERVE, NETWORK_POLICY_PREFIX_LENGTHS, MAX_NETWORK_POLICY_FILE_BYTES, MAX_NETWORK_POLICY_ARRAY_ENTRIES, MAX_NETWORK_POLICY_DAEMON_ID_LENGTH, NetworkPolicyValidationError, NETWORK_POLICY_KEYS, PRIVATE_IPV4_RANGES;
11459
+ var import_node_fs26, import_node_path34, NETWORK_POLICY_VERSION, NETWORK_POLICY_FILE, DEFAULT_NETWORK_POLICY_ENDPOINT_RESERVE, NETWORK_POLICY_PREFIX_LENGTHS, MAX_NETWORK_POLICY_FILE_BYTES, MAX_NETWORK_POLICY_ARRAY_ENTRIES, MAX_NETWORK_POLICY_DAEMON_ID_LENGTH, NetworkPolicyValidationError, NETWORK_POLICY_KEYS, PRIVATE_IPV4_RANGES;
10979
11460
  var init_network_policy = __esm({
10980
11461
  "src/core/network-policy.ts"() {
10981
11462
  "use strict";
10982
- import_node_fs24 = __toESM(require("fs"));
10983
- import_node_path32 = __toESM(require("path"));
11463
+ import_node_fs26 = __toESM(require("fs"));
11464
+ import_node_path34 = __toESM(require("path"));
10984
11465
  init_network_capacity();
10985
11466
  init_router();
10986
11467
  NETWORK_POLICY_VERSION = 1;
10987
- NETWORK_POLICY_FILE = import_node_path32.default.join(DEVROUTER_HOME, "network-policy.json");
11468
+ NETWORK_POLICY_FILE = import_node_path34.default.join(DEVROUTER_HOME, "network-policy.json");
10988
11469
  DEFAULT_NETWORK_POLICY_ENDPOINT_RESERVE = 8;
10989
11470
  NETWORK_POLICY_PREFIX_LENGTHS = [24, 25, 26];
10990
11471
  MAX_NETWORK_POLICY_FILE_BYTES = 64 * 1024;
@@ -11085,27 +11566,27 @@ function assertExactKeys(value, keys2) {
11085
11566
  function readDocument(store) {
11086
11567
  let descriptor;
11087
11568
  try {
11088
- descriptor = import_node_fs25.default.openSync(
11569
+ descriptor = import_node_fs27.default.openSync(
11089
11570
  store.filePath,
11090
- import_node_fs25.default.constants.O_RDONLY | import_node_fs25.default.constants.O_NONBLOCK | (import_node_fs25.default.constants.O_NOFOLLOW ?? 0)
11571
+ import_node_fs27.default.constants.O_RDONLY | import_node_fs27.default.constants.O_NONBLOCK | (import_node_fs27.default.constants.O_NOFOLLOW ?? 0)
11091
11572
  );
11092
11573
  } catch (error) {
11093
11574
  if (isErrno2(error, "ENOENT")) return void 0;
11094
11575
  throw claimError("invalid-state", "network claim state could not be read");
11095
11576
  }
11096
11577
  try {
11097
- const stat = import_node_fs25.default.fstatSync(descriptor);
11578
+ const stat = import_node_fs27.default.fstatSync(descriptor);
11098
11579
  if (!stat.isFile() || stat.size > NETWORK_CLAIMS_MAX_FILE_BYTES || (stat.mode & 63) !== 0) {
11099
11580
  throw claimError("invalid-state", "network claim state is not a bounded private file");
11100
11581
  }
11101
11582
  const buffer = Buffer.alloc(NETWORK_CLAIMS_MAX_FILE_BYTES + 1);
11102
11583
  let count = 0;
11103
11584
  while (count < buffer.length) {
11104
- const bytesRead = import_node_fs25.default.readSync(descriptor, buffer, count, buffer.length - count, null);
11585
+ const bytesRead = import_node_fs27.default.readSync(descriptor, buffer, count, buffer.length - count, null);
11105
11586
  if (bytesRead === 0) break;
11106
11587
  count += bytesRead;
11107
11588
  }
11108
- const finalStat = import_node_fs25.default.fstatSync(descriptor);
11589
+ const finalStat = import_node_fs27.default.fstatSync(descriptor);
11109
11590
  if (count > NETWORK_CLAIMS_MAX_FILE_BYTES || finalStat.size !== stat.size) {
11110
11591
  throw claimError("invalid-state", "network claim state changed during read");
11111
11592
  }
@@ -11117,7 +11598,7 @@ function readDocument(store) {
11117
11598
  }
11118
11599
  return validateDocument(parsed, store.daemonId);
11119
11600
  } finally {
11120
- import_node_fs25.default.closeSync(descriptor);
11601
+ import_node_fs27.default.closeSync(descriptor);
11121
11602
  }
11122
11603
  }
11123
11604
  function validateDocument(value, daemonId) {
@@ -11184,16 +11665,16 @@ function createStore(daemonId, options) {
11184
11665
  return {
11185
11666
  root,
11186
11667
  daemonId,
11187
- filePath: import_node_path33.default.join(root, `${key}.json`),
11188
- lockPath: import_node_path33.default.join(root, `${key}.lock`),
11668
+ filePath: import_node_path35.default.join(root, `${key}.json`),
11669
+ lockPath: import_node_path35.default.join(root, `${key}.lock`),
11189
11670
  writeAtomic: options.writeAtomic ?? writeFileAtomically
11190
11671
  };
11191
11672
  }
11192
11673
  function validateRoot(root) {
11193
- if (typeof root !== "string" || !import_node_path33.default.isAbsolute(root) || root.includes("\0")) {
11674
+ if (typeof root !== "string" || !import_node_path35.default.isAbsolute(root) || root.includes("\0")) {
11194
11675
  throw claimError("invalid-input", "network claim root is invalid");
11195
11676
  }
11196
- const resolved = import_node_path33.default.resolve(root);
11677
+ const resolved = import_node_path35.default.resolve(root);
11197
11678
  if (resolved !== root) throw claimError("invalid-input", "network claim root is invalid");
11198
11679
  return resolved;
11199
11680
  }
@@ -11221,20 +11702,20 @@ function isErrno2(error, code) {
11221
11702
  function claimError(code, message) {
11222
11703
  return new NetworkClaimError(code, message);
11223
11704
  }
11224
- var import_node_crypto13, import_node_fs25, import_node_path33, NETWORK_CLAIMS_VERSION, NETWORK_CLAIMS_ROOT, NETWORK_CLAIMS_MAX_RECORDS, NETWORK_CLAIMS_MAX_FILE_BYTES, NetworkClaimError, CLAIM_CORE_KEYS, CLAIM_KEYS, FENCE_KEYS, DOCUMENT_KEYS, MAX_CLAIM_TEXT, MAX_ENDPOINT_TEXT;
11705
+ var import_node_crypto13, import_node_fs27, import_node_path35, NETWORK_CLAIMS_VERSION, NETWORK_CLAIMS_ROOT, NETWORK_CLAIMS_MAX_RECORDS, NETWORK_CLAIMS_MAX_FILE_BYTES, NetworkClaimError, CLAIM_CORE_KEYS, CLAIM_KEYS, FENCE_KEYS, DOCUMENT_KEYS, MAX_CLAIM_TEXT, MAX_ENDPOINT_TEXT;
11225
11706
  var init_network_claims = __esm({
11226
11707
  "src/core/network-claims.ts"() {
11227
11708
  "use strict";
11228
11709
  import_node_crypto13 = require("crypto");
11229
- import_node_fs25 = __toESM(require("fs"));
11230
- import_node_path33 = __toESM(require("path"));
11710
+ import_node_fs27 = __toESM(require("fs"));
11711
+ import_node_path35 = __toESM(require("path"));
11231
11712
  init_atomic_file();
11232
11713
  init_file_lock();
11233
11714
  init_network_capacity();
11234
11715
  init_network_policy();
11235
11716
  init_router();
11236
11717
  NETWORK_CLAIMS_VERSION = 1;
11237
- NETWORK_CLAIMS_ROOT = import_node_path33.default.join(DEVROUTER_HOME, "networks");
11718
+ NETWORK_CLAIMS_ROOT = import_node_path35.default.join(DEVROUTER_HOME, "networks");
11238
11719
  NETWORK_CLAIMS_MAX_RECORDS = 256;
11239
11720
  NETWORK_CLAIMS_MAX_FILE_BYTES = 512 * 1024;
11240
11721
  NetworkClaimError = class extends Error {
@@ -11276,14 +11757,14 @@ var init_network_claims = __esm({
11276
11757
 
11277
11758
  // src/core/network-claim-lookup.ts
11278
11759
  function networkOwnerKey(repoPath) {
11279
- if (!import_node_path34.default.isAbsolute(repoPath) || import_node_path34.default.resolve(repoPath) !== repoPath)
11760
+ if (!import_node_path36.default.isAbsolute(repoPath) || import_node_path36.default.resolve(repoPath) !== repoPath)
11280
11761
  throw new Error("Invalid network owner path.");
11281
11762
  return (0, import_node_crypto14.createHash)("sha256").update(repoPath).digest("hex");
11282
11763
  }
11283
11764
  function findOwnedNetworkClaim(repoPath, root = NETWORK_CLAIMS_ROOT) {
11284
11765
  let names;
11285
11766
  try {
11286
- names = import_node_fs26.default.readdirSync(root);
11767
+ names = import_node_fs28.default.readdirSync(root);
11287
11768
  } catch (error) {
11288
11769
  if (error.code === "ENOENT") return void 0;
11289
11770
  throw new Error("Network claims cannot be inspected; repair is required.");
@@ -11292,21 +11773,21 @@ function findOwnedNetworkClaim(repoPath, root = NETWORK_CLAIMS_ROOT) {
11292
11773
  if (files.length > 128) throw new Error("Network daemon inventory exceeds its bound.");
11293
11774
  let found;
11294
11775
  for (const file of files) {
11295
- const full = import_node_path34.default.join(root, file);
11296
- const stat = import_node_fs26.default.lstatSync(full);
11776
+ const full = import_node_path36.default.join(root, file);
11777
+ const stat = import_node_fs28.default.lstatSync(full);
11297
11778
  if (!stat.isFile() || stat.size > NETWORK_CLAIMS_MAX_FILE_BYTES)
11298
11779
  throw new Error("Invalid retained network claim file.");
11299
11780
  let value;
11300
- const descriptor = import_node_fs26.default.openSync(
11781
+ const descriptor = import_node_fs28.default.openSync(
11301
11782
  full,
11302
- import_node_fs26.default.constants.O_RDONLY | import_node_fs26.default.constants.O_NONBLOCK | import_node_fs26.default.constants.O_NOFOLLOW
11783
+ import_node_fs28.default.constants.O_RDONLY | import_node_fs28.default.constants.O_NONBLOCK | import_node_fs28.default.constants.O_NOFOLLOW
11303
11784
  );
11304
11785
  try {
11305
- if (!import_node_fs26.default.fstatSync(descriptor).isFile()) throw new Error();
11786
+ if (!import_node_fs28.default.fstatSync(descriptor).isFile()) throw new Error();
11306
11787
  const bytes2 = Buffer.alloc(NETWORK_CLAIMS_MAX_FILE_BYTES + 1);
11307
11788
  let size = 0;
11308
11789
  while (size < bytes2.length) {
11309
- const count = import_node_fs26.default.readSync(descriptor, bytes2, size, bytes2.length - size, null);
11790
+ const count = import_node_fs28.default.readSync(descriptor, bytes2, size, bytes2.length - size, null);
11310
11791
  if (!count) break;
11311
11792
  size += count;
11312
11793
  }
@@ -11316,7 +11797,7 @@ function findOwnedNetworkClaim(repoPath, root = NETWORK_CLAIMS_ROOT) {
11316
11797
  } catch {
11317
11798
  throw new Error("Retained network claim could not be read safely.");
11318
11799
  } finally {
11319
- import_node_fs26.default.closeSync(descriptor);
11800
+ import_node_fs28.default.closeSync(descriptor);
11320
11801
  }
11321
11802
  if (typeof value.daemonId !== "string" || `${(0, import_node_crypto14.createHash)("sha256").update(value.daemonId).digest("hex")}.json` !== file)
11322
11803
  throw new Error("Retained network daemon identity is invalid.");
@@ -11332,13 +11813,13 @@ function findOwnedNetworkClaim(repoPath, root = NETWORK_CLAIMS_ROOT) {
11332
11813
  }
11333
11814
  return found;
11334
11815
  }
11335
- var import_node_crypto14, import_node_fs26, import_node_path34;
11816
+ var import_node_crypto14, import_node_fs28, import_node_path36;
11336
11817
  var init_network_claim_lookup = __esm({
11337
11818
  "src/core/network-claim-lookup.ts"() {
11338
11819
  "use strict";
11339
11820
  import_node_crypto14 = require("crypto");
11340
- import_node_fs26 = __toESM(require("fs"));
11341
- import_node_path34 = __toESM(require("path"));
11821
+ import_node_fs28 = __toESM(require("fs"));
11822
+ import_node_path36 = __toESM(require("path"));
11342
11823
  init_network_claims();
11343
11824
  }
11344
11825
  });
@@ -11366,7 +11847,7 @@ function qualifyNetworkProviderDefinition(value, provider) {
11366
11847
  return (0, import_node_crypto15.createHash)("sha256").update(JSON.stringify({ name: definition.name, agent, options, exec })).digest("hex");
11367
11848
  }
11368
11849
  function read(provider, args) {
11369
- const result = (0, import_node_child_process15.spawnSync)(provider, args, {
11850
+ const result = (0, import_node_child_process16.spawnSync)(provider, args, {
11370
11851
  encoding: "utf8",
11371
11852
  timeout: 5e3,
11372
11853
  maxBuffer: 2 * 1024 * 1024
@@ -11454,11 +11935,11 @@ function inspectNetworkProviderBinding(input2) {
11454
11935
  );
11455
11936
  }
11456
11937
  }
11457
- var import_node_child_process15, import_node_crypto15;
11938
+ var import_node_child_process16, import_node_crypto15;
11458
11939
  var init_network_provider_inspect = __esm({
11459
11940
  "src/core/network-provider-inspect.ts"() {
11460
11941
  "use strict";
11461
- import_node_child_process15 = require("child_process");
11942
+ import_node_child_process16 = require("child_process");
11462
11943
  import_node_crypto15 = require("crypto");
11463
11944
  init_devpod_environment();
11464
11945
  }
@@ -11497,7 +11978,7 @@ var init_network_provider_effect = __esm({
11497
11978
 
11498
11979
  // src/core/devsy-mutation.ts
11499
11980
  function withMutationLock(activity, target, operation) {
11500
- import_node_fs27.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
11981
+ import_node_fs29.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
11501
11982
  return withFileLockSync(
11502
11983
  DEVSY_MUTATION_LOCK_FILE,
11503
11984
  {
@@ -11517,7 +11998,7 @@ function runDevsyAction(action2, devsyId, force = false, repoPath) {
11517
11998
  const args = action2 === "delete" ? ["delete", devsyId, ...force ? ["--force"] : [], "--ignore-not-found"] : ["stop", devsyId];
11518
11999
  claimLifecycleEffect();
11519
12000
  const binding = repoPath ? networkProviderEffectOptions("devsy", devsyId, repoPath) : { args: [] };
11520
- const result = (0, import_node_child_process16.spawnSync)("devsy", ["workspace", ...args, ...binding.args], {
12001
+ const result = (0, import_node_child_process17.spawnSync)("devsy", ["workspace", ...args, ...binding.args], {
11521
12002
  encoding: "utf-8",
11522
12003
  ...binding.env ? { env: binding.env } : {}
11523
12004
  });
@@ -11578,13 +12059,13 @@ function stopOwnedDevsyWorkspace(devsyId, worktreePath) {
11578
12059
  function deleteOwnedDevsyWorkspace(devsyId, worktreePath) {
11579
12060
  return mutateOwnedDevsyWorkspace("delete", devsyId, worktreePath);
11580
12061
  }
11581
- var import_node_child_process16, import_node_fs27, import_node_path35, DEVSY_MUTATION_LOCK_FILE, DEVSY_MUTATION_WAIT_MS;
12062
+ var import_node_child_process17, import_node_fs29, import_node_path37, DEVSY_MUTATION_LOCK_FILE, DEVSY_MUTATION_WAIT_MS;
11582
12063
  var init_devsy_mutation = __esm({
11583
12064
  "src/core/devsy-mutation.ts"() {
11584
12065
  "use strict";
11585
- import_node_child_process16 = require("child_process");
11586
- import_node_fs27 = __toESM(require("fs"));
11587
- import_node_path35 = __toESM(require("path"));
12066
+ import_node_child_process17 = require("child_process");
12067
+ import_node_fs29 = __toESM(require("fs"));
12068
+ import_node_path37 = __toESM(require("path"));
11588
12069
  init_devsy_agent();
11589
12070
  init_devsy_workspaces();
11590
12071
  init_file_lock();
@@ -11593,7 +12074,7 @@ var init_devsy_mutation = __esm({
11593
12074
  init_network_provider_effect();
11594
12075
  init_reliability_context();
11595
12076
  init_router();
11596
- DEVSY_MUTATION_LOCK_FILE = import_node_path35.default.join(DEVROUTER_HOME, "devsy-mutation.lock");
12077
+ DEVSY_MUTATION_LOCK_FILE = import_node_path37.default.join(DEVROUTER_HOME, "devsy-mutation.lock");
11597
12078
  DEVSY_MUTATION_WAIT_MS = 18e5;
11598
12079
  }
11599
12080
  });
@@ -12138,14 +12619,16 @@ function handleRecover(state, event) {
12138
12619
  if (state.executionPolicy === "manual" || state.desired !== "running")
12139
12620
  return unchanged(state, "blocked");
12140
12621
  if (possibleDispatch(state.operation)) return unchanged(state, "blocked");
12622
+ const stranded = state.operation?.drained === true && ["NOT_STARTED", "NOT_LAUNCHED"].includes(state.operation.status);
12623
+ const operation = stranded ? null : state.operation;
12141
12624
  if (state.incident !== null && state.incident.id !== event.incidentId) {
12142
12625
  return unchanged(state, "conflict");
12143
12626
  }
12144
12627
  if (state.incident !== null) {
12145
- if (state.operation && pendingCommand(state.operation)) {
12146
- return state.operation.id === event.operationId && state.phase === "recovering" ? unchanged(state, "joined") : unchanged(state, "conflict");
12628
+ if (operation && pendingCommand(operation)) {
12629
+ return operation.id === event.operationId && state.phase === "recovering" ? unchanged(state, "joined") : unchanged(state, "conflict");
12147
12630
  }
12148
- if (state.operation?.id === event.operationId && state.operation.status === "COMPLETED") {
12631
+ if (operation?.id === event.operationId && operation.status === "COMPLETED") {
12149
12632
  return unchanged(state, "joined");
12150
12633
  }
12151
12634
  if (state.incident.correctiveActionsTaken >= state.incident.actionLimit) {
@@ -12153,17 +12636,20 @@ function handleRecover(state, event) {
12153
12636
  }
12154
12637
  }
12155
12638
  if (state.incident === null) {
12156
- if (state.operation?.status === "NOT_STARTED") return unchanged(state, "blocked");
12157
- if (state.operation?.id === event.operationId) return unchanged(state, "conflict");
12639
+ if (operation?.status === "NOT_STARTED") return unchanged(state, "blocked");
12640
+ if (operation?.id === event.operationId) return unchanged(state, "conflict");
12158
12641
  state.incident = {
12159
12642
  id: event.incidentId,
12160
12643
  correctiveActionsTaken: 0,
12161
12644
  actionLimit: event.actionLimit
12162
12645
  };
12163
12646
  }
12164
- if (!state.operation || state.operation.status === "COMPLETED" || state.operation.status === "INTERRUPTED") {
12647
+ if (!operation || operation.status === "COMPLETED" || operation.status === "INTERRUPTED") {
12165
12648
  const runtimeGeneration = advance(state.runtimeGeneration);
12166
12649
  if (runtimeGeneration === null) return unchanged(state, "blocked");
12650
+ const consumer = state.consumers[0];
12651
+ if (state.profile === null || consumer === void 0) return unchanged(state, "blocked");
12652
+ if (state.operationHistory.length >= RELIABILITY_MAX_ITEMS) return unchanged(state, "blocked");
12167
12653
  state.runtimeGeneration = runtimeGeneration;
12168
12654
  state.observations = [];
12169
12655
  state.operation = {
@@ -12173,6 +12659,12 @@ function handleRecover(state, event) {
12173
12659
  status: "NOT_STARTED",
12174
12660
  exitCode: null
12175
12661
  };
12662
+ state.operationHistory.push({
12663
+ ...state.operation,
12664
+ key: event.operationId,
12665
+ profile: state.profile,
12666
+ consumer: cloneConsumer(consumer)
12667
+ });
12176
12668
  }
12177
12669
  state.phase = "recovering";
12178
12670
  return transition(state, "accepted");
@@ -12265,6 +12757,49 @@ function stepReliability(state, event, nowMs) {
12265
12757
  }
12266
12758
  return result;
12267
12759
  }
12760
+ function requiredCapabilities(state) {
12761
+ const required = /* @__PURE__ */ new Set();
12762
+ for (const consumer of state.consumers) {
12763
+ for (const capability of consumer.requiredCapabilities) required.add(capability);
12764
+ }
12765
+ return [...required];
12766
+ }
12767
+ function failedCapability(state, required, nowMs) {
12768
+ return required.some((capability) => {
12769
+ const observation = state.observations.find((entry) => entry.capability === capability);
12770
+ return observation !== void 0 && observation.infrastructure === "failed" && observation.observedAtMs >= state.observationsAfterMs && observation.observedAtMs <= nowMs && nowMs - observation.observedAtMs < observation.validForMs;
12771
+ });
12772
+ }
12773
+ function decideRecovery(input2) {
12774
+ const { state, nowMs, actionLimit, pressureDwellSatisfied } = input2;
12775
+ assertReliabilityState(state);
12776
+ if (!isReliabilityCounter(nowMs) || nowMs < state.observationsAfterMs)
12777
+ throw new Error("Invalid reliability clock.");
12778
+ if (input2.failedCapabilities !== void 0 && (input2.failedCapabilities.length > RELIABILITY_MAX_ITEMS || input2.failedCapabilities.some((capability) => !isReliabilityId(capability))))
12779
+ throw new Error("Invalid recovery evidence.");
12780
+ if (state.executionPolicy === "manual") return { action: "none", reason: "unmanaged" };
12781
+ if (state.desired === "stopped-by-user") return { action: "none", reason: "user-stopped" };
12782
+ if (state.desired === "parked-for-capacity") {
12783
+ const resumeReady = pressureDwellSatisfied && state.stopProof.workloadsStopped && state.stopProof.routesRemoved && state.consumers.length > 0 && state.admission === "admitted" && state.profile !== null && (state.incident === null || state.incident.correctiveActionsTaken < state.incident.actionLimit) && state.operation !== null && ["NOT_STARTED", "COMPLETED", "INTERRUPTED"].includes(state.operation.status);
12784
+ return { action: resumeReady ? "resume" : "none", reason: "parked" };
12785
+ }
12786
+ if (!isReliabilityCounter(actionLimit) || actionLimit < 1)
12787
+ throw new Error("Invalid recovery action budget.");
12788
+ const failed = input2.failedCapabilities !== void 0 ? input2.failedCapabilities.length > 0 : failedCapability(state, requiredCapabilities(state), nowMs);
12789
+ if (!failed) return { action: "none", reason: "healthy" };
12790
+ if (possibleDispatch(state.operation)) return { action: "none", reason: "active-operation" };
12791
+ if (state.incident !== null) {
12792
+ if (state.incident.correctiveActionsTaken >= state.incident.actionLimit)
12793
+ return { action: "blocked", reason: "budget-exhausted" };
12794
+ return {
12795
+ action: "continue",
12796
+ reason: "recovery-eligible",
12797
+ incidentId: state.incident.id,
12798
+ actionLimit: state.incident.actionLimit
12799
+ };
12800
+ }
12801
+ return { action: "start", reason: "recovery-eligible", actionLimit };
12802
+ }
12268
12803
  var INFRASTRUCTURE_VALUES, APPLICATION_VALUES, ADMISSION_VALUES;
12269
12804
  var init_reliability_model = __esm({
12270
12805
  "src/core/reliability-model.ts"() {
@@ -12311,10 +12846,10 @@ async function runLifecycleWorker(request2, supervision, beforeAdmission) {
12311
12846
  throw new Error("Lifecycle invocation was cancelled before dispatch.");
12312
12847
  if (process.platform === "win32")
12313
12848
  throw new Error("Lifecycle workers require POSIX process-group ownership.");
12314
- const workerPath = import_node_path36.default.join(__dirname, "devrouter-lifecycle-worker.js");
12315
- if (!import_node_fs28.default.existsSync(workerPath))
12849
+ const workerPath = import_node_path38.default.join(__dirname, "devrouter-lifecycle-worker.js");
12850
+ if (!import_node_fs30.default.existsSync(workerPath))
12316
12851
  throw new Error("The packaged lifecycle worker is missing; rebuild the CLI.");
12317
- const child = (0, import_node_child_process17.fork)(workerPath, [], {
12852
+ const child = (0, import_node_child_process18.fork)(workerPath, [], {
12318
12853
  detached: true,
12319
12854
  stdio: supervision ? ["ignore", "pipe", "pipe", "ipc"] : ["inherit", "inherit", "inherit", "ipc"],
12320
12855
  execArgv: []
@@ -12541,14 +13076,14 @@ async function runLifecycleWorker(request2, supervision, beforeAdmission) {
12541
13076
  function newLifecycleIds() {
12542
13077
  return { requestId: (0, import_node_crypto16.randomUUID)(), operationId: (0, import_node_crypto16.randomUUID)(), workerId: (0, import_node_crypto16.randomUUID)() };
12543
13078
  }
12544
- var import_node_child_process17, import_node_crypto16, import_node_fs28, import_node_path36, OUTPUT_BUFFER_LIMIT, DEFAULT_OUTPUT_PAGE_BYTES, LifecycleOutput, LifecycleWorkerAdmissionBusyError;
13079
+ var import_node_child_process18, import_node_crypto16, import_node_fs30, import_node_path38, OUTPUT_BUFFER_LIMIT, DEFAULT_OUTPUT_PAGE_BYTES, LifecycleOutput, LifecycleWorkerAdmissionBusyError;
12545
13080
  var init_reliability_worker = __esm({
12546
13081
  "src/core/reliability-worker.ts"() {
12547
13082
  "use strict";
12548
- import_node_child_process17 = require("child_process");
13083
+ import_node_child_process18 = require("child_process");
12549
13084
  import_node_crypto16 = require("crypto");
12550
- import_node_fs28 = __toESM(require("fs"));
12551
- import_node_path36 = __toESM(require("path"));
13085
+ import_node_fs30 = __toESM(require("fs"));
13086
+ import_node_path38 = __toESM(require("path"));
12552
13087
  init_file_lock();
12553
13088
  init_reliability_contract();
12554
13089
  init_reliability_model();
@@ -12593,243 +13128,79 @@ var init_reliability_worker = __esm({
12593
13128
  throw new Error("Invalid output cursor.");
12594
13129
  const cursorChunk = this.chunks.find((chunk) => chunk.sequence === cursor.sequence);
12595
13130
  if (cursorChunk && cursor.offset > cursorChunk.data.byteLength)
12596
- throw new Error("Invalid output cursor.");
12597
- if (!Number.isSafeInteger(maxJsonBytes) || maxJsonBytes <= 0)
12598
- throw new Error("Invalid output page byte bound.");
12599
- const cursorHasPartialChunk = cursorChunk !== void 0 && this.partialSequences.has(cursor.sequence) && cursor.offset < cursorChunk.data.byteLength;
12600
- let page = {
12601
- encoding: "base64",
12602
- gap: cursor.sequence < this.droppedThrough || cursor.sequence > 0 && cursor.sequence <= this.droppedThrough && !cursorChunk || cursorHasPartialChunk,
12603
- sequence: cursor,
12604
- chunks: []
12605
- };
12606
- if (outputPageBytes(page) > maxJsonBytes)
12607
- throw new Error("Output page byte bound is too small.");
12608
- for (const chunk of this.chunks) {
12609
- if (chunk.sequence < cursor.sequence) continue;
12610
- let offset = chunk.sequence === cursor.sequence ? cursor.offset : 0;
12611
- if (offset >= chunk.data.byteLength) {
12612
- const nextPage = { ...page, sequence: { sequence: chunk.sequence, offset } };
12613
- if (outputPageBytes(nextPage) > maxJsonBytes) {
12614
- if (page.sequence.sequence === cursor.sequence && page.sequence.offset === cursor.offset)
12615
- throw new Error("Output page byte bound is too small.");
12616
- return page;
12617
- }
12618
- page = nextPage;
12619
- continue;
12620
- }
12621
- while (offset < chunk.data.byteLength) {
12622
- let low = offset + 1;
12623
- let high = chunk.data.byteLength;
12624
- let best;
12625
- while (low <= high) {
12626
- const midpoint = Math.floor((low + high) / 2);
12627
- const candidatePage = {
12628
- encoding: "base64",
12629
- gap: page.gap,
12630
- sequence: { sequence: chunk.sequence, offset: midpoint },
12631
- chunks: [
12632
- ...page.chunks,
12633
- {
12634
- stream: chunk.stream,
12635
- data: chunk.data.subarray(offset, midpoint).toString("base64"),
12636
- sequence: chunk.sequence
12637
- }
12638
- ]
12639
- };
12640
- if (outputPageBytes(candidatePage) <= maxJsonBytes) {
12641
- best = candidatePage;
12642
- low = midpoint + 1;
12643
- } else high = midpoint - 1;
12644
- }
12645
- if (!best) {
12646
- if (page.sequence.sequence === cursor.sequence && page.sequence.offset === cursor.offset)
12647
- throw new Error("Output page byte bound is too small.");
12648
- return page;
12649
- }
12650
- page = best;
12651
- offset = best.sequence.offset;
12652
- }
12653
- }
12654
- return page;
12655
- }
12656
- };
12657
- LifecycleWorkerAdmissionBusyError = class extends Error {
12658
- constructor() {
12659
- super("Lifecycle admission lost to another operation.");
12660
- this.name = "LifecycleWorkerAdmissionBusyError";
12661
- }
12662
- };
12663
- }
12664
- });
12665
-
12666
- // src/core/traefik-route-health.ts
12667
- function sleep(ms) {
12668
- return new Promise((resolve) => setTimeout(resolve, ms));
12669
- }
12670
- function inspectRouterApi(protocol) {
12671
- const result = (0, import_node_child_process18.spawnSync)(
12672
- "curl",
12673
- [
12674
- "--silent",
12675
- "--show-error",
12676
- "--fail",
12677
- "--max-time",
12678
- ROUTER_API_REQUEST_TIMEOUT_SECONDS,
12679
- `${ROUTER_API_BASE}/${protocol}/routers?per_page=${ROUTER_API_PAGE_SIZE}`
12680
- ],
12681
- { encoding: "utf-8" }
12682
- );
12683
- if (result.status !== 0) {
12684
- return {
12685
- ok: false,
12686
- details: result.stderr?.trim() || `curl exited with status ${result.status ?? "unknown"}`
12687
- };
12688
- }
12689
- let value;
12690
- try {
12691
- value = JSON.parse(result.stdout);
12692
- } catch {
12693
- return { ok: false, details: `${protocol.toUpperCase()} router API returned invalid JSON` };
12694
- }
12695
- if (!Array.isArray(value)) {
12696
- return { ok: false, details: `${protocol.toUpperCase()} router API returned a non-array` };
12697
- }
12698
- if (value.some((item) => !isRecord5(item) || typeof item.name !== "string" || !item.name))
12699
- return {
12700
- ok: false,
12701
- details: `${protocol.toUpperCase()} router API returned malformed entries`
12702
- };
12703
- return {
12704
- ok: true,
12705
- reachedPageLimit: value.length >= ROUTER_API_PAGE_SIZE,
12706
- names: new Set(
12707
- value.flatMap((item) => {
12708
- if (!item || typeof item !== "object") return [];
12709
- const name = item.name;
12710
- return typeof name === "string" ? [name] : [];
12711
- })
12712
- )
12713
- };
12714
- }
12715
- function isRecord5(value) {
12716
- return value !== null && typeof value === "object" && !Array.isArray(value);
12717
- }
12718
- function inspectExpectedRoutes(routes, expectation) {
12719
- const mismatched = [];
12720
- const failures = [];
12721
- for (const protocol of ["http", "tcp"]) {
12722
- const expected = routes.filter((route) => (route.protocol ?? "http") === protocol).map((route) => buildHostRouteRouterName(route.repoPath, route.name));
12723
- if (expected.length === 0) continue;
12724
- const result = inspectRouterApi(protocol);
12725
- if (!result.ok) {
12726
- failures.push(result.details);
12727
- mismatched.push(...expected);
12728
- continue;
12729
- }
12730
- const unexpected = expected.filter(
12731
- (name) => expectation === "loaded" ? !result.names.has(name) : result.names.has(name)
12732
- );
12733
- const boundedAbsenceIsUncertain = result.reachedPageLimit && (expectation === "loaded" ? unexpected.length > 0 : unexpected.length === 0);
12734
- if (boundedAbsenceIsUncertain) {
12735
- failures.push(
12736
- `${protocol.toUpperCase()} router API reached the ${ROUTER_API_PAGE_SIZE}-entry safety limit`
12737
- );
12738
- }
12739
- mismatched.push(...unexpected);
12740
- if (expectation === "removed" && unexpected.length === 0 && result.reachedPageLimit) {
12741
- mismatched.push(...expected);
12742
- }
12743
- }
12744
- return mismatched.length === 0 ? { ok: true } : {
12745
- ok: false,
12746
- routes: mismatched,
12747
- details: failures.length > 0 ? failures.join("; ") : expectation === "loaded" ? "router names are absent" : "router names are still present"
12748
- };
12749
- }
12750
- async function waitForExpectedRoutes(routes, expectation, timeoutMs, pollIntervalMs) {
12751
- const deadline = Date.now() + timeoutMs;
12752
- let result;
12753
- do {
12754
- result = inspectExpectedRoutes(routes, expectation);
12755
- if (result.ok) return result;
12756
- if (Date.now() < deadline) {
12757
- await sleep(Math.min(pollIntervalMs, Math.max(1, deadline - Date.now())));
12758
- }
12759
- } while (Date.now() < deadline);
12760
- return result;
12761
- }
12762
- async function ensureTraefikRouteExpectation(routes, expectation, options) {
12763
- if (routes.length === 0) return { restarted: false };
12764
- const initialTimeoutMs = options.initialTimeoutMs ?? DEFAULT_INITIAL_TIMEOUT_MS;
12765
- const recoveryTimeoutMs = options.recoveryTimeoutMs ?? DEFAULT_RECOVERY_TIMEOUT_MS;
12766
- const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
12767
- const initial = await waitForExpectedRoutes(
12768
- routes,
12769
- expectation,
12770
- initialTimeoutMs,
12771
- pollIntervalMs
12772
- );
12773
- if (initial.ok) return { restarted: false };
12774
- if (options.allowRestart === false)
12775
- throw new Error(
12776
- "Traefik routes did not reload; shared router restart is disabled for this operation."
12777
- );
12778
- import_node_fs29.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
12779
- return withFileLock(
12780
- ROUTER_RELOAD_LOCK_FILE,
12781
- {
12782
- activity: "Traefik route reload recovery",
12783
- waitMs: initialTimeoutMs + recoveryTimeoutMs + 1e4,
12784
- fair: true,
12785
- onWait: createStderrWaitReporter("Traefik route reload recovery", "shared router")
12786
- },
12787
- async () => {
12788
- const recheck = await waitForExpectedRoutes(routes, expectation, 0, pollIntervalMs);
12789
- if (recheck.ok) return { restarted: false };
12790
- claimLifecycleEffect();
12791
- restartRouterStack();
12792
- const recovered = await waitForExpectedRoutes(
12793
- routes,
12794
- expectation,
12795
- recoveryTimeoutMs,
12796
- pollIntervalMs
12797
- );
12798
- if (recovered.ok) return { restarted: true };
12799
- const action2 = expectation === "loaded" ? "load" : "remove";
12800
- throw new Error(
12801
- `Traefik did not ${action2} file-provider routes after one restart: ${recovered.routes.join(", ")} (${recovered.details}). Inspect: devrouter logs --tail 100`
12802
- );
12803
- }
12804
- );
12805
- }
12806
- async function ensureTraefikRoutesRemoved(routes, options = {}) {
12807
- return ensureTraefikRouteExpectation(routes, "removed", options);
12808
- }
12809
- var import_node_child_process18, import_node_fs29, import_node_path37, 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;
12810
- var init_traefik_route_health = __esm({
12811
- "src/core/traefik-route-health.ts"() {
12812
- "use strict";
12813
- import_node_child_process18 = require("child_process");
12814
- import_node_fs29 = __toESM(require("fs"));
12815
- import_node_path37 = __toESM(require("path"));
12816
- init_file_lock();
12817
- init_host_routes();
12818
- init_reliability_context();
12819
- init_router();
12820
- ROUTER_API_BASE = "http://127.0.0.1:8080/api";
12821
- ROUTER_RELOAD_LOCK_FILE = import_node_path37.default.join(DEVROUTER_HOME, "router-reload.lock");
12822
- ROUTER_API_REQUEST_TIMEOUT_SECONDS = "2";
12823
- DEFAULT_INITIAL_TIMEOUT_MS = 3e3;
12824
- DEFAULT_RECOVERY_TIMEOUT_MS = 1e4;
12825
- DEFAULT_POLL_INTERVAL_MS = 250;
12826
- ROUTER_API_PAGE_SIZE = 1e3;
13131
+ throw new Error("Invalid output cursor.");
13132
+ if (!Number.isSafeInteger(maxJsonBytes) || maxJsonBytes <= 0)
13133
+ throw new Error("Invalid output page byte bound.");
13134
+ const cursorHasPartialChunk = cursorChunk !== void 0 && this.partialSequences.has(cursor.sequence) && cursor.offset < cursorChunk.data.byteLength;
13135
+ let page = {
13136
+ encoding: "base64",
13137
+ gap: cursor.sequence < this.droppedThrough || cursor.sequence > 0 && cursor.sequence <= this.droppedThrough && !cursorChunk || cursorHasPartialChunk,
13138
+ sequence: cursor,
13139
+ chunks: []
13140
+ };
13141
+ if (outputPageBytes(page) > maxJsonBytes)
13142
+ throw new Error("Output page byte bound is too small.");
13143
+ for (const chunk of this.chunks) {
13144
+ if (chunk.sequence < cursor.sequence) continue;
13145
+ let offset = chunk.sequence === cursor.sequence ? cursor.offset : 0;
13146
+ if (offset >= chunk.data.byteLength) {
13147
+ const nextPage = { ...page, sequence: { sequence: chunk.sequence, offset } };
13148
+ if (outputPageBytes(nextPage) > maxJsonBytes) {
13149
+ if (page.sequence.sequence === cursor.sequence && page.sequence.offset === cursor.offset)
13150
+ throw new Error("Output page byte bound is too small.");
13151
+ return page;
13152
+ }
13153
+ page = nextPage;
13154
+ continue;
13155
+ }
13156
+ while (offset < chunk.data.byteLength) {
13157
+ let low = offset + 1;
13158
+ let high = chunk.data.byteLength;
13159
+ let best;
13160
+ while (low <= high) {
13161
+ const midpoint = Math.floor((low + high) / 2);
13162
+ const candidatePage = {
13163
+ encoding: "base64",
13164
+ gap: page.gap,
13165
+ sequence: { sequence: chunk.sequence, offset: midpoint },
13166
+ chunks: [
13167
+ ...page.chunks,
13168
+ {
13169
+ stream: chunk.stream,
13170
+ data: chunk.data.subarray(offset, midpoint).toString("base64"),
13171
+ sequence: chunk.sequence
13172
+ }
13173
+ ]
13174
+ };
13175
+ if (outputPageBytes(candidatePage) <= maxJsonBytes) {
13176
+ best = candidatePage;
13177
+ low = midpoint + 1;
13178
+ } else high = midpoint - 1;
13179
+ }
13180
+ if (!best) {
13181
+ if (page.sequence.sequence === cursor.sequence && page.sequence.offset === cursor.offset)
13182
+ throw new Error("Output page byte bound is too small.");
13183
+ return page;
13184
+ }
13185
+ page = best;
13186
+ offset = best.sequence.offset;
13187
+ }
13188
+ }
13189
+ return page;
13190
+ }
13191
+ };
13192
+ LifecycleWorkerAdmissionBusyError = class extends Error {
13193
+ constructor() {
13194
+ super("Lifecycle admission lost to another operation.");
13195
+ this.name = "LifecycleWorkerAdmissionBusyError";
13196
+ }
13197
+ };
12827
13198
  }
12828
13199
  });
12829
13200
 
12830
13201
  // src/core/devpod-mutation.ts
12831
13202
  function withMutationLock2(activity, target, operation) {
12832
- import_node_fs30.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
13203
+ import_node_fs31.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
12833
13204
  return withFileLockSync(
12834
13205
  DEVPOD_MUTATION_LOCK_FILE,
12835
13206
  {
@@ -12919,13 +13290,13 @@ function deleteOwnedDevpodWorkspace(devpodId, worktreePath) {
12919
13290
  resetWorkspaceRuntimeCaches();
12920
13291
  return result;
12921
13292
  }
12922
- var import_node_child_process19, import_node_fs30, import_node_path38, DEVPOD_MUTATION_LOCK_FILE, DEVPOD_MUTATION_WAIT_MS;
13293
+ var import_node_child_process19, import_node_fs31, import_node_path39, DEVPOD_MUTATION_LOCK_FILE, DEVPOD_MUTATION_WAIT_MS;
12923
13294
  var init_devpod_mutation = __esm({
12924
13295
  "src/core/devpod-mutation.ts"() {
12925
13296
  "use strict";
12926
13297
  import_node_child_process19 = require("child_process");
12927
- import_node_fs30 = __toESM(require("fs"));
12928
- import_node_path38 = __toESM(require("path"));
13298
+ import_node_fs31 = __toESM(require("fs"));
13299
+ import_node_path39 = __toESM(require("path"));
12929
13300
  init_devpod_workspaces();
12930
13301
  init_devsy_mutation();
12931
13302
  init_file_lock();
@@ -12934,7 +13305,7 @@ var init_devpod_mutation = __esm({
12934
13305
  init_reliability_context();
12935
13306
  init_router();
12936
13307
  init_workspace_runtime();
12937
- DEVPOD_MUTATION_LOCK_FILE = import_node_path38.default.join(DEVROUTER_HOME, "devpod-mutation.lock");
13308
+ DEVPOD_MUTATION_LOCK_FILE = import_node_path39.default.join(DEVROUTER_HOME, "devpod-mutation.lock");
12938
13309
  DEVPOD_MUTATION_WAIT_MS = 18e5;
12939
13310
  }
12940
13311
  });
@@ -13140,8 +13511,8 @@ function renderManagedComposeModel(plan, workspace) {
13140
13511
  }
13141
13512
  }
13142
13513
  function attributeHolderWorktree(workingDir) {
13143
- if (import_node_path39.default.basename(workingDir) !== ".devcontainer") return void 0;
13144
- return comparableWorkspacePath(import_node_path39.default.dirname(workingDir));
13514
+ if (import_node_path40.default.basename(workingDir) !== ".devcontainer") return void 0;
13515
+ return comparableWorkspacePath(import_node_path40.default.dirname(workingDir));
13145
13516
  }
13146
13517
  function holderAttribution(holder, repoPath) {
13147
13518
  const attribution = {};
@@ -13173,7 +13544,7 @@ function detectHostPortClaimConflicts(options) {
13173
13544
  );
13174
13545
  if (desired.length === 0) return [];
13175
13546
  const holders = listHolderSnapshots();
13176
- const devcontainerDir = import_node_path39.default.join(options.repoPath, ".devcontainer");
13547
+ const devcontainerDir = import_node_path40.default.join(options.repoPath, ".devcontainer");
13177
13548
  const conflicts = [];
13178
13549
  for (const binding of desired) {
13179
13550
  for (const holder of holders) {
@@ -13195,12 +13566,12 @@ function detectHostPortClaimConflicts(options) {
13195
13566
  }
13196
13567
  return conflicts;
13197
13568
  }
13198
- var import_node_child_process20, import_node_path39, COMPOSE_RENDER_TIMEOUT_MS, COMPOSE_RENDER_MAX_BUFFER, DOCKER_TIMEOUT_MS, DOCKER_MAX_BUFFER, MAX_DESIRED_HOST_PORTS, MAX_RUNNING_HOLDERS, HOST_PORT_INSPECT_TEMPLATE;
13569
+ var import_node_child_process20, import_node_path40, COMPOSE_RENDER_TIMEOUT_MS, COMPOSE_RENDER_MAX_BUFFER, DOCKER_TIMEOUT_MS, DOCKER_MAX_BUFFER, MAX_DESIRED_HOST_PORTS, MAX_RUNNING_HOLDERS, HOST_PORT_INSPECT_TEMPLATE;
13199
13570
  var init_host_port_claims = __esm({
13200
13571
  "src/core/host-port-claims.ts"() {
13201
13572
  "use strict";
13202
13573
  import_node_child_process20 = require("child_process");
13203
- import_node_path39 = __toESM(require("path"));
13574
+ import_node_path40 = __toESM(require("path"));
13204
13575
  init_devcontainer_profile();
13205
13576
  init_network_effect_scope();
13206
13577
  init_workspace();
@@ -13374,20 +13745,20 @@ var init_http_route_probe = __esm({
13374
13745
  });
13375
13746
 
13376
13747
  // src/core/managed-host-preparation.ts
13377
- var import_node_child_process22, import_node_fs31;
13748
+ var import_node_child_process22, import_node_fs32;
13378
13749
  var init_managed_host_preparation = __esm({
13379
13750
  "src/core/managed-host-preparation.ts"() {
13380
13751
  "use strict";
13381
13752
  import_node_child_process22 = require("child_process");
13382
- import_node_fs31 = __toESM(require("fs"));
13753
+ import_node_fs32 = __toESM(require("fs"));
13383
13754
  init_repo_config();
13384
13755
  }
13385
13756
  });
13386
13757
 
13387
13758
  // src/core/managed-post-start.ts
13388
13759
  function readRegularFileBytes(filePath) {
13389
- if (!import_node_fs32.default.existsSync(filePath) || !import_node_fs32.default.lstatSync(filePath).isFile()) return void 0;
13390
- return import_node_fs32.default.readFileSync(filePath);
13760
+ if (!import_node_fs33.default.existsSync(filePath) || !import_node_fs33.default.lstatSync(filePath).isFile()) return void 0;
13761
+ return import_node_fs33.default.readFileSync(filePath);
13391
13762
  }
13392
13763
  function readRegularFile(filePath) {
13393
13764
  return readRegularFileBytes(filePath)?.toString("utf-8");
@@ -13396,11 +13767,11 @@ function adapterFingerprint(adapter) {
13396
13767
  return (0, import_node_crypto17.createHash)("sha256").update(adapter).digest("hex");
13397
13768
  }
13398
13769
  function resolveManagedPostStartPlan(repoPath) {
13399
- const adapterPath = import_node_path40.default.join(repoPath, MANAGED_ADAPTER_PATH);
13770
+ const adapterPath = import_node_path41.default.join(repoPath, MANAGED_ADAPTER_PATH);
13400
13771
  const adapterBytes = readRegularFileBytes(adapterPath);
13401
13772
  const adapterText = adapterBytes?.toString("utf-8");
13402
- const dockerfilePath = import_node_path40.default.join(repoPath, ".devcontainer", "Dockerfile");
13403
- const devcontainerPath = import_node_path40.default.join(repoPath, ".devcontainer", "devcontainer.json");
13773
+ const dockerfilePath = import_node_path41.default.join(repoPath, ".devcontainer", "Dockerfile");
13774
+ const devcontainerPath = import_node_path41.default.join(repoPath, ".devcontainer", "devcontainer.json");
13404
13775
  const dockerfile = readRegularFile(dockerfilePath) ?? "";
13405
13776
  const devcontainer = readRegularFile(devcontainerPath) ?? "";
13406
13777
  const devrouterPattern = [adapterText, dockerfile, devcontainer].filter((value) => value !== void 0).some(
@@ -13483,14 +13854,14 @@ function runManagedProcessAction(options) {
13483
13854
  }
13484
13855
  return "drifted";
13485
13856
  }
13486
- var import_node_child_process23, import_node_crypto17, import_node_fs32, import_node_path40, MANAGED_MARKER, MANAGED_ADAPTER_PATH, RUNTIME_HELPER_PATH;
13857
+ var import_node_child_process23, import_node_crypto17, import_node_fs33, import_node_path41, MANAGED_MARKER, MANAGED_ADAPTER_PATH, RUNTIME_HELPER_PATH;
13487
13858
  var init_managed_post_start = __esm({
13488
13859
  "src/core/managed-post-start.ts"() {
13489
13860
  "use strict";
13490
13861
  import_node_child_process23 = require("child_process");
13491
13862
  import_node_crypto17 = require("crypto");
13492
- import_node_fs32 = __toESM(require("fs"));
13493
- import_node_path40 = __toESM(require("path"));
13863
+ import_node_fs33 = __toESM(require("fs"));
13864
+ import_node_path41 = __toESM(require("path"));
13494
13865
  init_devcontainer_config();
13495
13866
  init_network_effect_scope();
13496
13867
  MANAGED_MARKER = "devrouter:managed devcontainer";
@@ -14429,26 +14800,26 @@ function hasLocalDockerNetworkNamespace(endpoint, procRoot = "/proc") {
14429
14800
  try {
14430
14801
  if (!endpoint.startsWith("unix:///")) return false;
14431
14802
  const socket = endpoint.slice("unix://".length);
14432
- const table = import_node_fs33.default.readFileSync(import_node_path41.default.join(procRoot, "net/unix"), "utf8");
14803
+ const table = import_node_fs34.default.readFileSync(import_node_path42.default.join(procRoot, "net/unix"), "utf8");
14433
14804
  if (Buffer.byteLength(table) > 1024 * 1024) return false;
14434
14805
  const rows2 = table.split("\n").map((line) => line.trim().split(/\s+/)).filter((row) => row[7] === socket && row[3] === "00010000");
14435
14806
  if (rows2.length !== 1 || !/^\d+$/.test(rows2[0][6])) return false;
14436
14807
  const inode = `socket:[${rows2[0][6]}]`;
14437
- const ownNamespace = import_node_fs33.default.readlinkSync(import_node_path41.default.join(procRoot, "self/ns/net"));
14438
- const pids = import_node_fs33.default.readdirSync(procRoot).filter((name) => /^\d+$/.test(name));
14808
+ const ownNamespace = import_node_fs34.default.readlinkSync(import_node_path42.default.join(procRoot, "self/ns/net"));
14809
+ const pids = import_node_fs34.default.readdirSync(procRoot).filter((name) => /^\d+$/.test(name));
14439
14810
  if (pids.length > 4096) return false;
14440
14811
  for (const pid of pids) {
14441
14812
  if (Date.now() > deadline) return false;
14442
14813
  try {
14443
- const root = import_node_path41.default.join(procRoot, pid);
14444
- if (import_node_path41.default.basename(import_node_fs33.default.readlinkSync(import_node_path41.default.join(root, "exe"))) !== "dockerd") continue;
14445
- if (import_node_fs33.default.readlinkSync(import_node_path41.default.join(root, "ns/net")) !== ownNamespace) continue;
14446
- const descriptors = import_node_fs33.default.readdirSync(import_node_path41.default.join(root, "fd"));
14814
+ const root = import_node_path42.default.join(procRoot, pid);
14815
+ if (import_node_path42.default.basename(import_node_fs34.default.readlinkSync(import_node_path42.default.join(root, "exe"))) !== "dockerd") continue;
14816
+ if (import_node_fs34.default.readlinkSync(import_node_path42.default.join(root, "ns/net")) !== ownNamespace) continue;
14817
+ const descriptors = import_node_fs34.default.readdirSync(import_node_path42.default.join(root, "fd"));
14447
14818
  if (descriptors.length > 4096) continue;
14448
14819
  if (descriptors.some((fd) => {
14449
14820
  if (Date.now() > deadline) return false;
14450
14821
  try {
14451
- return import_node_fs33.default.readlinkSync(import_node_path41.default.join(root, "fd", fd)) === inode;
14822
+ return import_node_fs34.default.readlinkSync(import_node_path42.default.join(root, "fd", fd)) === inode;
14452
14823
  } catch {
14453
14824
  return false;
14454
14825
  }
@@ -14462,12 +14833,12 @@ function hasLocalDockerNetworkNamespace(endpoint, procRoot = "/proc") {
14462
14833
  }
14463
14834
  return false;
14464
14835
  }
14465
- var import_node_fs33, import_node_path41;
14836
+ var import_node_fs34, import_node_path42;
14466
14837
  var init_network_local_daemon = __esm({
14467
14838
  "src/core/network-local-daemon.ts"() {
14468
14839
  "use strict";
14469
- import_node_fs33 = __toESM(require("fs"));
14470
- import_node_path41 = __toESM(require("path"));
14840
+ import_node_fs34 = __toESM(require("fs"));
14841
+ import_node_path42 = __toESM(require("path"));
14471
14842
  }
14472
14843
  });
14473
14844
 
@@ -14583,14 +14954,14 @@ var init_network_compose = __esm({
14583
14954
  });
14584
14955
 
14585
14956
  // src/core/network-compose-files.ts
14586
- var import_node_child_process26, import_node_crypto19, import_node_fs34, import_node_path42, import_yaml4, MAX_BYTES3;
14957
+ var import_node_child_process26, import_node_crypto19, import_node_fs35, import_node_path43, import_yaml4, MAX_BYTES3;
14587
14958
  var init_network_compose_files = __esm({
14588
14959
  "src/core/network-compose-files.ts"() {
14589
14960
  "use strict";
14590
14961
  import_node_child_process26 = require("child_process");
14591
14962
  import_node_crypto19 = require("crypto");
14592
- import_node_fs34 = __toESM(require("fs"));
14593
- import_node_path42 = __toESM(require("path"));
14963
+ import_node_fs35 = __toESM(require("fs"));
14964
+ import_node_path43 = __toESM(require("path"));
14594
14965
  import_yaml4 = __toESM(require("yaml"));
14595
14966
  init_atomic_file();
14596
14967
  init_devcontainer_profile();
@@ -14694,13 +15065,13 @@ function resolveLinkedTarget(repoPath) {
14694
15065
  gitCommonDir: resolveGitCommonDir(repoPath)
14695
15066
  };
14696
15067
  }
14697
- var import_node_child_process28, import_node_fs35, import_node_path43;
15068
+ var import_node_child_process28, import_node_fs36, import_node_path44;
14698
15069
  var init_workspace_ensure = __esm({
14699
15070
  "src/core/workspace-ensure.ts"() {
14700
15071
  "use strict";
14701
15072
  import_node_child_process28 = require("child_process");
14702
- import_node_fs35 = __toESM(require("fs"));
14703
- import_node_path43 = __toESM(require("path"));
15073
+ import_node_fs36 = __toESM(require("fs"));
15074
+ import_node_path44 = __toESM(require("path"));
14704
15075
  init_devcontainer_profile();
14705
15076
  init_devpod_environment();
14706
15077
  init_devpod_mutation();
@@ -14836,13 +15207,59 @@ function prepareManagedLifecycleOperation(input2) {
14836
15207
  };
14837
15208
  });
14838
15209
  }
14839
- function assertCurrentCapacityController(expected, directory = import_node_path44.default.join(DEVROUTER_HOME, "controller")) {
15210
+ function assertCurrentCapacityController(expected, directory = import_node_path45.default.join(DEVROUTER_HOME, "controller")) {
14840
15211
  const current = new ControllerStore(directory).read();
14841
15212
  if (!expected || current?.store !== expected.store || current.epoch !== expected.epoch)
14842
15213
  throw new Error("Capacity controller incarnation changed.");
14843
15214
  }
15215
+ function prepareRecoveryLifecycleOperation(input2) {
15216
+ const ids2 = newLifecycleIds();
15217
+ return updateReliabilityOperation(input2.identity, (record5) => {
15218
+ assertCurrentCapacityController(input2.controller);
15219
+ if (record5.version !== 2 || record5.state.executionPolicy !== "capacity-managed" || record5.enrollment?.policyRevision !== input2.policyRevision)
15220
+ throw new Error("Managed recovery requires current durable enrollment.");
15221
+ if (record5.phaseSettlement) return void 0;
15222
+ reconcileDrained(record5);
15223
+ if (record5.worker) return void 0;
15224
+ const decision = decideRecovery({
15225
+ state: record5.state,
15226
+ nowMs: Date.now(),
15227
+ actionLimit: input2.actionLimit,
15228
+ pressureDwellSatisfied: false,
15229
+ failedCapabilities: input2.failedCapabilities
15230
+ });
15231
+ if (decision.action !== "start" && decision.action !== "continue") return void 0;
15232
+ const transition2 = stepReliability(
15233
+ record5.state,
15234
+ {
15235
+ ...reliabilityFence(record5.state),
15236
+ type: "recover",
15237
+ incidentId: decision.action === "continue" ? decision.incidentId : input2.incidentId,
15238
+ actionLimit: input2.actionLimit,
15239
+ operationId: ids2.operationId
15240
+ },
15241
+ Date.now()
15242
+ );
15243
+ if (transition2.outcome !== "accepted") return void 0;
15244
+ record5.state = transition2.state;
15245
+ record5.outcome = null;
15246
+ record5.result = null;
15247
+ record5.startupWitness = null;
15248
+ return {
15249
+ operationId: ids2.operationId,
15250
+ request: {
15251
+ ...ids2,
15252
+ kind: "ensure",
15253
+ repoPath: input2.identity.repoPath,
15254
+ identity: { ...input2.identity },
15255
+ fence: reliabilityFence(record5.state),
15256
+ options: { profile: input2.profile, quiet: true }
15257
+ }
15258
+ };
15259
+ });
15260
+ }
14844
15261
  function settlePreparedLifecycleCapacity(input2) {
14845
- const directory = input2.directory ?? import_node_path44.default.join(DEVROUTER_HOME, "controller");
15262
+ const directory = input2.directory ?? import_node_path45.default.join(DEVROUTER_HOME, "controller");
14846
15263
  const store = new CapacityStore(directory);
14847
15264
  for (let attempt = 0; attempt < 3; attempt++) {
14848
15265
  const snapshot = store.read();
@@ -15056,7 +15473,7 @@ async function superviseLifecycle(kind, repoPath, options = {}, command) {
15056
15473
  removeSignals();
15057
15474
  }
15058
15475
  }
15059
- function admitLifecycleCapacity(request2, reservation, budgets, samples, nowMs, maxSampleAgeMs, directory = import_node_path44.default.join(DEVROUTER_HOME, "controller"), controller, admission) {
15476
+ function admitLifecycleCapacity(request2, reservation, budgets, samples, nowMs, maxSampleAgeMs, directory = import_node_path45.default.join(DEVROUTER_HOME, "controller"), controller, admission) {
15060
15477
  if (request2.kind === "stop") throw new Error("Stop never requires capacity admission.");
15061
15478
  const capacity = new CapacityStore(directory);
15062
15479
  const snapshot = capacity.read();
@@ -15156,7 +15573,7 @@ function admitLifecycleCapacity(request2, reservation, budgets, samples, nowMs,
15156
15573
  return decision;
15157
15574
  }
15158
15575
  function capacityPolicyTargets(repoPath) {
15159
- const policy = readCapacityPolicy(import_node_path44.default.join(DEVROUTER_HOME, "controller"));
15576
+ const policy = readCapacityPolicy(import_node_path45.default.join(DEVROUTER_HOME, "controller"));
15160
15577
  return policy?.admissions === "enabled" && policy.enrollments.some((entry) => entry.repoPath === repoPath);
15161
15578
  }
15162
15579
  function controllerWaitSeconds(deadline) {
@@ -15165,7 +15582,7 @@ function controllerWaitSeconds(deadline) {
15165
15582
  async function superviseThroughController(input2) {
15166
15583
  if (input2.kind === "exec" && !input2.command)
15167
15584
  throw new Error("Exec requires a command for controller admission.");
15168
- const directory = import_node_path44.default.join(DEVROUTER_HOME, "controller");
15585
+ const directory = import_node_path45.default.join(DEVROUTER_HOME, "controller");
15169
15586
  const cancellation = { requested: false };
15170
15587
  const removeSignals = installLifecycleWaitSignals(cancellation);
15171
15588
  const cancelled = () => {
@@ -15175,6 +15592,17 @@ async function superviseThroughController(input2) {
15175
15592
  );
15176
15593
  };
15177
15594
  const deadline = Date.now() + CAPACITY_CLIENT_WAIT_MS;
15595
+ let outputGapReported = false;
15596
+ const onOutput = (page) => {
15597
+ if (page.gap && !outputGapReported) {
15598
+ process.stderr.write("devrouter: controller output buffer dropped earlier command output.\n");
15599
+ outputGapReported = true;
15600
+ }
15601
+ for (const chunk of page.chunks) {
15602
+ const stream = chunk.stream === "stderr" ? process.stderr : process.stdout;
15603
+ stream.write(Buffer.from(chunk.data, page.encoding));
15604
+ }
15605
+ };
15178
15606
  try {
15179
15607
  const bind = () => observeControllerBinding(directory, {
15180
15608
  path: input2.repoPath,
@@ -15189,7 +15617,7 @@ async function superviseThroughController(input2) {
15189
15617
  let pending = await submitControllerOperation(
15190
15618
  directory,
15191
15619
  input2.kind === "exec" ? { ...binding, requestId: input2.requestId, kind: "exec", command: input2.command ?? [] } : { ...binding, requestId: input2.requestId, kind: "ensure" },
15192
- { waitSeconds: controllerWaitSeconds(deadline) }
15620
+ { waitSeconds: controllerWaitSeconds(deadline), onOutput }
15193
15621
  );
15194
15622
  while (pending.status !== "terminal") {
15195
15623
  cancelled();
@@ -15205,7 +15633,7 @@ async function superviseThroughController(input2) {
15205
15633
  pending = await followControllerOperation(
15206
15634
  directory,
15207
15635
  { ...binding, operationId: pending.operationId },
15208
- { waitSeconds: controllerWaitSeconds(deadline) }
15636
+ { waitSeconds: controllerWaitSeconds(deadline), onOutput }
15209
15637
  );
15210
15638
  } catch {
15211
15639
  await new Promise((resolve) => setTimeout(resolve, Math.min(1e3, deadline - Date.now())));
@@ -15226,28 +15654,33 @@ async function superviseThroughController(input2) {
15226
15654
  removeSignals();
15227
15655
  }
15228
15656
  }
15229
- function renewLifecycleCapacity(request2, policy, samples, controller, directory = import_node_path44.default.join(DEVROUTER_HOME, "controller"), nowMs = Date.now()) {
15657
+ function renewLifecycleCapacity(request2, policy, samples, controller, directory = import_node_path45.default.join(DEVROUTER_HOME, "controller"), nowMs = Date.now()) {
15230
15658
  return updateReliabilityOperation(request2.identity, (record5) => {
15231
15659
  const binding = record5.capacity;
15232
15660
  if (!matchesFence(record5, request2.fence) || !binding || binding.operationId !== request2.operationId || binding.workerId !== request2.workerId || binding.controller?.store !== controller.store || binding.controller?.epoch !== controller.epoch)
15233
15661
  throw new Error("Capacity renewal no longer owns this operation.");
15234
- binding.validUntilMs = 0;
15235
15662
  try {
15236
- if (record5.state.executionPolicy !== "capacity-managed" || record5.state.operation?.id !== request2.operationId || record5.state.operation.drained || record5.state.desired !== "running" || policy.admissions !== "enabled" || policy.revision !== binding.policyRevision || JSON.stringify(readCapacityPolicy(directory)) !== JSON.stringify(policy))
15663
+ if (record5.state.executionPolicy !== "capacity-managed" || record5.state.operation?.id !== request2.operationId || record5.state.operation.drained || record5.state.desired !== "running" || policy.admissions !== "enabled" || policy.revision !== binding.policyRevision || JSON.stringify(readCapacityPolicy(directory)) !== JSON.stringify(policy)) {
15664
+ binding.validUntilMs = 0;
15237
15665
  return false;
15666
+ }
15238
15667
  const enrolled = record5.enrollment;
15239
15668
  const runtime = enrolled && policy.domains[enrolled.runtimeDomain];
15240
15669
  const enrollment = policy.enrollments.find(
15241
15670
  (entry) => entry.repoPath === record5.identity.repoPath && entry.workspace === record5.identity.workspace && entry.provider === record5.identity.provider && entry.providerId === enrolled?.providerId && entry.gitCommonDir === enrolled.gitCommonDir && entry.estimatesDigest === enrolled.estimatesDigest && entry.hostDomain === enrolled.hostDomain && entry.runtimeDomain === enrolled.runtimeDomain && entry.profiles.includes(record5.state.profile ?? "")
15242
15671
  );
15243
- if (!enrolled || enrolled.policyRevision !== policy.revision || !enrollment || runtime?.kind !== "runtime" || runtime.endpoint !== enrolled.endpoint || runtime.daemonId !== enrolled.daemonId)
15672
+ if (!enrolled || enrolled.policyRevision !== policy.revision || !enrollment || runtime?.kind !== "runtime" || runtime.endpoint !== enrolled.endpoint || runtime.daemonId !== enrolled.daemonId) {
15673
+ binding.validUntilMs = 0;
15244
15674
  return false;
15675
+ }
15245
15676
  const snapshot = new CapacityStore(directory).read();
15246
15677
  const reservation = snapshot.reservations.find(
15247
15678
  (entry) => entry.reservationId === binding.reservationId
15248
15679
  );
15249
- if (!reservation || reservation.environmentId !== record5.state.environmentId || reservation.operationId !== request2.operationId || reservation.policyRevision !== policy.revision)
15680
+ if (!reservation || reservation.environmentId !== record5.state.environmentId || reservation.operationId !== request2.operationId || reservation.policyRevision !== policy.revision) {
15681
+ binding.validUntilMs = 0;
15250
15682
  return false;
15683
+ }
15251
15684
  const maxAge = policy.scheduling.maxSampleAgeSeconds * 1e3;
15252
15685
  const decision = evaluateCapacity(
15253
15686
  policy.domains,
@@ -15274,16 +15707,21 @@ function renewLifecycleCapacity(request2, policy, samples, controller, directory
15274
15707
  snapshot.pools
15275
15708
  ).admitted;
15276
15709
  })();
15277
- if (!tolerated) return false;
15710
+ if (!tolerated) {
15711
+ if (decision.reason !== "unknown" && decision.reason !== "stale")
15712
+ binding.validUntilMs = 0;
15713
+ return false;
15714
+ }
15278
15715
  }
15716
+ const observed = Object.keys(reservation.totals).map(
15717
+ (domain) => samples[domain]?.sampledAtMs
15718
+ );
15719
+ if (observed.some((sampledAtMs) => typeof sampledAtMs !== "number")) return false;
15279
15720
  binding.snapshotRevision = snapshot.revision;
15280
- binding.validUntilMs = Math.min(
15281
- ...Object.keys(reservation.totals).map((domain) => samples[domain]?.sampledAtMs ?? 0)
15282
- ) + maxAge;
15721
+ binding.validUntilMs = Math.min(...observed) + maxAge;
15283
15722
  assertCapacityEffect(record5, request2.workerId, nowMs, directory);
15284
15723
  return true;
15285
15724
  } catch {
15286
- binding.validUntilMs = 0;
15287
15725
  return false;
15288
15726
  }
15289
15727
  });
@@ -15304,7 +15742,7 @@ function retireQueuedLifecycle(request2, supersededOnly = false) {
15304
15742
  return true;
15305
15743
  });
15306
15744
  }
15307
- function bindLifecycleCapacity(request2, binding, directory = import_node_path44.default.join(DEVROUTER_HOME, "controller")) {
15745
+ function bindLifecycleCapacity(request2, binding, directory = import_node_path45.default.join(DEVROUTER_HOME, "controller")) {
15308
15746
  if (request2.kind === "stop") throw new Error("Stop never requires capacity admission.");
15309
15747
  updateReliabilityOperation(request2.identity, (record5) => {
15310
15748
  if (!matchesFence(record5, request2.fence) || record5.worker || record5.state.operation?.id !== request2.operationId || record5.state.operation.status !== "NOT_STARTED" || record5.state.operation.drained)
@@ -15319,12 +15757,12 @@ function bindLifecycleCapacity(request2, binding, directory = import_node_path44
15319
15757
  stepRecord(record5, { ...request2.fence, type: "admission", result: "admitted" });
15320
15758
  });
15321
15759
  }
15322
- var import_node_crypto21, import_node_path44, import_node_util10, activeWorker, lockHeld, BUSY_LIFECYCLE_WAIT_MS, BUSY_LIFECYCLE_POLL_MS, CAPACITY_CLIENT_WAIT_MS, BUSY_LIFECYCLE_PROGRESS_MS;
15760
+ var import_node_crypto21, import_node_path45, import_node_util10, activeWorker, lockHeld, BUSY_LIFECYCLE_WAIT_MS, BUSY_LIFECYCLE_POLL_MS, CAPACITY_CLIENT_WAIT_MS, BUSY_LIFECYCLE_PROGRESS_MS;
15323
15761
  var init_reliability_lifecycle = __esm({
15324
15762
  "src/core/reliability-lifecycle.ts"() {
15325
15763
  "use strict";
15326
15764
  import_node_crypto21 = require("crypto");
15327
- import_node_path44 = __toESM(require("path"));
15765
+ import_node_path45 = __toESM(require("path"));
15328
15766
  import_node_util10 = require("util");
15329
15767
  init_capacity_accounting();
15330
15768
  init_capacity_policy();
@@ -15337,6 +15775,7 @@ var init_reliability_lifecycle = __esm({
15337
15775
  init_devsy_mutation();
15338
15776
  init_file_lock();
15339
15777
  init_host_routes();
15778
+ init_managed_devsy_stop();
15340
15779
  init_managed_runtime_state();
15341
15780
  init_managed_stop_recovery();
15342
15781
  init_reliability_context();
@@ -15427,6 +15866,11 @@ var init_capacity_queue = __esm({
15427
15866
  });
15428
15867
  return request2.operationId;
15429
15868
  }
15869
+ /** True while the queue still owns the operation, awaiting admission or running. */
15870
+ hasOperation(operationId) {
15871
+ const entry = this.entries.get(operationId);
15872
+ return entry !== void 0 && entry.phase !== "terminal";
15873
+ }
15430
15874
  observe(operationId) {
15431
15875
  const entry = this.entries.get(operationId);
15432
15876
  return entry ? { operationId, phase: entry.phase, reason: entry.reason, output: entry.output.read() } : void 0;
@@ -15811,7 +16255,7 @@ function createCapacityController(options) {
15811
16255
  const record5 = readReliabilityOperation(identity2);
15812
16256
  if (!record5?.capacity || !record5.preparation && !record5.capacity.execSteady || record5.worker || !record5.state.operation?.drained || !["ensure", "exec"].includes(record5.state.operation.kind))
15813
16257
  continue;
15814
- const bytes2 = readControllerEvidence(import_node_path45.default.join(enrollment.repoPath, ".devrouter.yml"));
16258
+ const bytes2 = readControllerEvidence(import_node_path46.default.join(enrollment.repoPath, ".devrouter.yml"));
15815
16259
  const estimates = loadRepoConfig(enrollment.repoPath, () => bytes2).capacity;
15816
16260
  if (!estimates) continue;
15817
16261
  settlePreparedLifecycleCapacity({
@@ -15964,6 +16408,87 @@ function createCapacityController(options) {
15964
16408
  settlePreparations();
15965
16409
  return queue.tick();
15966
16410
  },
16411
+ /**
16412
+ * Open or advance one bounded automatic recovery for an environment whose
16413
+ * required capability has positively failed. It is inert unless the
16414
+ * operator policy enables recovery, and every decision, budget, and
16415
+ * admission rule is the one an operator-requested ensure obeys.
16416
+ */
16417
+ async recover(environment, failedCapabilities, signal) {
16418
+ signal = AbortSignal.any([signal, lifetime.signal]);
16419
+ if (signal.aborted || !policy.recovery.enabled) return;
16420
+ if (failedCapabilities.length === 0 || failedCapabilities.length > 64) return;
16421
+ const current = readCapacityPolicy(options.directory);
16422
+ if (current?.admissions !== "enabled" || !(0, import_node_util12.isDeepStrictEqual)(current, policy)) return;
16423
+ let resolved;
16424
+ try {
16425
+ resolved = await enrollCapacityLifecycle(
16426
+ current,
16427
+ { path: environment.repoPath, profile: environment.profile, require: [] },
16428
+ signal,
16429
+ options.directory
16430
+ );
16431
+ } catch {
16432
+ return;
16433
+ }
16434
+ if (signal.aborted || !(0, import_node_util12.isDeepStrictEqual)(resolved.environment, environment)) return;
16435
+ if (!(0, import_node_util12.isDeepStrictEqual)(readCapacityPolicy(options.directory), current)) return;
16436
+ const runtime = current.domains[resolved.enrollment.runtimeDomain];
16437
+ if (runtime?.kind !== "runtime" || runtime.hostDomain !== resolved.enrollment.hostDomain || current.domains[runtime.hostDomain]?.kind !== "host" || !current.enrollments.some((entry) => (0, import_node_util12.isDeepStrictEqual)(entry, resolved.enrollment)))
16438
+ return;
16439
+ const pool = {
16440
+ daemonId: runtime.daemonId,
16441
+ runtimeDomain: resolved.enrollment.runtimeDomain,
16442
+ hostDomain: runtime.hostDomain,
16443
+ hostChargeCeilingBytes: runtime.hostChargeCeilingBytes
16444
+ };
16445
+ const identity2 = {
16446
+ repoPath: environment.repoPath,
16447
+ workspace: environment.workspace || null,
16448
+ provider: environment.provider
16449
+ };
16450
+ const record5 = readReliabilityOperation(identity2);
16451
+ if (!record5) return;
16452
+ if (record5.state.operation && queue.hasOperation(record5.state.operation.id)) return;
16453
+ let prepared;
16454
+ try {
16455
+ prepared = prepareRecoveryLifecycleOperation({
16456
+ identity: identity2,
16457
+ controller,
16458
+ policyRevision: current.revision,
16459
+ actionLimit: policy.recovery.maxCorrectiveActions,
16460
+ failedCapabilities,
16461
+ profile: environment.profile,
16462
+ incidentId: `incident-${(0, import_node_crypto22.randomUUID)()}`
16463
+ });
16464
+ } catch {
16465
+ return;
16466
+ }
16467
+ if (!prepared) return;
16468
+ const charge = capacityRequest(resolved.estimates, resolved.enrollment, {
16469
+ environmentId: record5.state.environmentId,
16470
+ profile: environment.profile,
16471
+ ...record5.activeProfile ? { activeProfile: record5.activeProfile } : {},
16472
+ kind: "ensure"
16473
+ });
16474
+ try {
16475
+ queue.enqueue(
16476
+ prepared.request,
16477
+ {
16478
+ ...charge,
16479
+ operationId: prepared.operationId,
16480
+ reservationId: (0, import_node_crypto22.randomUUID)(),
16481
+ policyRevision: current.revision
16482
+ },
16483
+ { estimates: resolved.estimates, enrollment: resolved.enrollment, pool }
16484
+ );
16485
+ } catch {
16486
+ try {
16487
+ retireQueuedLifecycle(prepared.request);
16488
+ } catch {
16489
+ }
16490
+ }
16491
+ },
15967
16492
  close() {
15968
16493
  lifetime.abort();
15969
16494
  queue.close();
@@ -15971,12 +16496,12 @@ function createCapacityController(options) {
15971
16496
  }
15972
16497
  };
15973
16498
  }
15974
- var import_node_crypto22, import_node_path45, import_node_util12;
16499
+ var import_node_crypto22, import_node_path46, import_node_util12;
15975
16500
  var init_capacity_controller = __esm({
15976
16501
  "src/core/capacity-controller.ts"() {
15977
16502
  "use strict";
15978
16503
  import_node_crypto22 = require("crypto");
15979
- import_node_path45 = __toESM(require("path"));
16504
+ import_node_path46 = __toESM(require("path"));
15980
16505
  import_node_util12 = require("util");
15981
16506
  init_capacity_docker_probe();
15982
16507
  init_capacity_enrollment();
@@ -16094,12 +16619,13 @@ var init_controller_monitor = __esm({
16094
16619
  ControllerObservationBindingChanged = class extends Error {
16095
16620
  };
16096
16621
  ControllerMonitor = class {
16097
- constructor(sessions, collect, serialize, clock = () => Math.floor(performance.now()), fence = withReliabilityObservationFence) {
16622
+ constructor(sessions, collect, serialize, clock = () => Math.floor(performance.now()), fence = withReliabilityObservationFence, recover) {
16098
16623
  this.sessions = sessions;
16099
16624
  this.collect = collect;
16100
16625
  this.serialize = serialize;
16101
16626
  this.clock = clock;
16102
16627
  this.fence = fence;
16628
+ this.recover = recover;
16103
16629
  }
16104
16630
  active = /* @__PURE__ */ new Map();
16105
16631
  lastStarted = /* @__PURE__ */ new Map();
@@ -16194,6 +16720,18 @@ var init_controller_monitor = __esm({
16194
16720
  this.sessions.publish(bindings, projections, commitTime, Date.now());
16195
16721
  });
16196
16722
  });
16723
+ if (this.recover === void 0 || this.stopped) return;
16724
+ const required = new Set(requirements.map(controllerCapability));
16725
+ const failed = batch.capabilities.filter(
16726
+ (capability) => capability.infrastructure === "failed" && required.has(capability.capability)
16727
+ ).map((capability) => capability.capability);
16728
+ if (failed.length > 0)
16729
+ void this.recover(
16730
+ batch.environment,
16731
+ failed,
16732
+ this.active.get(environment.id)?.signal ?? abort.signal
16733
+ ).catch(() => {
16734
+ });
16197
16735
  }).catch(async (error) => {
16198
16736
  if (!this.stopped)
16199
16737
  await this.serialize(() => {
@@ -16217,7 +16755,7 @@ var init_controller_monitor = __esm({
16217
16755
  async function observeControllerProcess(containerId, name, signal) {
16218
16756
  if (!/^[a-f0-9]{12,64}$/.test(containerId) || !/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/.test(name))
16219
16757
  throw new Error("Process observation identity is invalid.");
16220
- const output2 = await runControllerProbe(
16758
+ const result = await runControllerProbeStatus(
16221
16759
  "docker",
16222
16760
  [
16223
16761
  "exec",
@@ -16233,12 +16771,14 @@ async function observeControllerProcess(containerId, name, signal) {
16233
16771
  ],
16234
16772
  signal
16235
16773
  );
16236
- const identity2 = output2.trim();
16774
+ if (result.code === PROCESS_ABSENT_EXIT) return { kind: "absent" };
16775
+ if (result.code !== 0) throw new Error("Process observation is unavailable.");
16776
+ const identity2 = result.output.trim();
16237
16777
  if (!/^[1-9][0-9]* [0-9]+ [a-zA-Z0-9][a-zA-Z0-9._:-]{0,4095}$/.test(identity2))
16238
16778
  throw new Error("Process observation is unavailable.");
16239
- return identity2;
16779
+ return { kind: "present", identity: identity2 };
16240
16780
  }
16241
- var PROCESS_OBSERVATION;
16781
+ var PROCESS_OBSERVATION, PROCESS_ABSENT_EXIT;
16242
16782
  var init_controller_process_observation = __esm({
16243
16783
  "src/core/controller-process-observation.ts"() {
16244
16784
  "use strict";
@@ -16248,7 +16788,9 @@ set -euo pipefail
16248
16788
  name="$1"
16249
16789
  [[ "$name" =~ ^[a-zA-Z0-9][a-zA-Z0-9._-]*$ ]] || exit 1
16250
16790
  file="${"$"}{DEVROUTER_PROCESS_STATE_DIR:-/tmp}/devrouter-process-$name.state"
16251
- [ -f "$file" ] && [ ! -L "$file" ] || exit 1
16791
+ [ ! -L "$file" ] || exit 1
16792
+ # A missing marker is positive absence: the expected process was never recorded.
16793
+ [ -f "$file" ] || exit 3
16252
16794
  [ "$(stat -c %s "$file")" -le 4096 ] || exit 1
16253
16795
  before="$(head -c 4097 "$file")"
16254
16796
  read -r pid pgid fingerprint extra <<<"$before"
@@ -16268,11 +16810,13 @@ identity() {
16268
16810
  tr '\0' '\n' <"/proc/$pid/environ" | grep -Fx "DEVROUTER_PROCESS_FINGERPRINT=$fingerprint" >/dev/null || return 1
16269
16811
  printf '%s' "${"$"}{fields[19]}"
16270
16812
  }
16271
- birth="$(identity)"
16813
+ birth="$(identity)" || exit 3
16272
16814
  [ "$birth" = "$(identity)" ] || exit 1
16273
- [ ! -L "$file" ] && [ "$before" = "$(head -c 4097 "$file")" ] || exit 1
16815
+ [ ! -L "$file" ] || exit 1
16816
+ [ "$before" = "$(head -c 4097 "$file")" ] || exit 3
16274
16817
  printf '%s %s %s\n' "$pid" "$birth" "$fingerprint"
16275
16818
  `;
16819
+ PROCESS_ABSENT_EXIT = 3;
16276
16820
  }
16277
16821
  });
16278
16822
 
@@ -16317,12 +16861,12 @@ async function containers(project, signal) {
16317
16861
  }
16318
16862
  return values.sort((a, b) => a.id.localeCompare(b.id));
16319
16863
  }
16320
- var import_node_crypto24, import_node_path46, OBSERVATION_INSPECT_TEMPLATE, collectControllerObservation;
16864
+ var import_node_crypto24, import_node_path47, OBSERVATION_INSPECT_TEMPLATE, collectControllerObservation;
16321
16865
  var init_controller_observation = __esm({
16322
16866
  "src/core/controller-observation.ts"() {
16323
16867
  "use strict";
16324
16868
  import_node_crypto24 = require("crypto");
16325
- import_node_path46 = __toESM(require("path"));
16869
+ import_node_path47 = __toESM(require("path"));
16326
16870
  init_controller_binding();
16327
16871
  init_controller_monitor();
16328
16872
  init_controller_probe();
@@ -16365,14 +16909,14 @@ var init_controller_observation = __esm({
16365
16909
  )).trim().split("\n");
16366
16910
  if (metadata.length !== 2) throw new Error("Observation ownership paths unavailable.");
16367
16911
  const [gitDir, commonDir] = metadata;
16368
- const sourcePath = import_node_path46.default.join(environment.repoPath, ".devcontainer/devcontainer.json");
16912
+ const sourcePath = import_node_path47.default.join(environment.repoPath, ".devcontainer/devcontainer.json");
16369
16913
  const files = [
16370
- import_node_path46.default.join(commonDir, "devrouter/workspaces", `${environment.workspace}.json`),
16371
- import_node_path46.default.join(environment.repoPath, ".devrouter.yml"),
16372
- import_node_path46.default.join(gitDir, "devrouter-workspace"),
16914
+ import_node_path47.default.join(commonDir, "devrouter/workspaces", `${environment.workspace}.json`),
16915
+ import_node_path47.default.join(environment.repoPath, ".devrouter.yml"),
16916
+ import_node_path47.default.join(gitDir, "devrouter-workspace"),
16373
16917
  managedRuntimeStatePath(environment.repoPath, environment.workspace),
16374
16918
  sourcePath,
16375
- import_node_path46.default.join(environment.repoPath, MANAGED_DEVCONTAINER_PATH)
16919
+ import_node_path47.default.join(environment.repoPath, MANAGED_DEVCONTAINER_PATH)
16376
16920
  ];
16377
16921
  const persisted = captureControllerEvidence(files);
16378
16922
  if (controllerBindingFingerprint(persisted.contents[0], persisted.contents[1]) !== environment.fingerprint)
@@ -16397,7 +16941,7 @@ var init_controller_observation = __esm({
16397
16941
  if (!journal) throw new Error("Observation journal is unavailable.");
16398
16942
  const routes = readRoutes().filter((route) => route.repoPath === environment.repoPath);
16399
16943
  const before = await containers(state.composeProject, signal);
16400
- const composeDirectory = import_node_path46.default.join(environment.repoPath, ".devcontainer");
16944
+ const composeDirectory = import_node_path47.default.join(environment.repoPath, ".devcontainer");
16401
16945
  const references = typeof source.dockerComposeFile === "string" ? [source.dockerComposeFile] : source.dockerComposeFile;
16402
16946
  if (!Array.isArray(references) || !references.length || references.some((value) => typeof value !== "string"))
16403
16947
  throw new Error("Observation Compose source is unavailable.");
@@ -16414,7 +16958,7 @@ var init_controller_observation = __esm({
16414
16958
  if (!recorded || container.labels["com.docker.compose.project.working_dir"] !== composeDirectory)
16415
16959
  throw new Error("Observation Compose ownership is unavailable.");
16416
16960
  const files2 = recorded.split(",").map((file) => file.trim());
16417
- if (files2.some((file) => !import_node_path46.default.isAbsolute(file)) || !hasExactComposeIdentity(container, {
16961
+ if (files2.some((file) => !import_node_path47.default.isAbsolute(file)) || !hasExactComposeIdentity(container, {
16418
16962
  repoPath: environment.repoPath,
16419
16963
  service: container.labels["com.docker.compose.service"] ?? "",
16420
16964
  composeProject: state.composeProject,
@@ -16488,10 +17032,14 @@ var init_controller_observation = __esm({
16488
17032
  ];
16489
17033
  const processes = [];
16490
17034
  let processesVerified = healthy;
17035
+ let processesAbsent = false;
16491
17036
  if (healthy && requirements.some((value) => value.startsWith("app:"))) {
16492
17037
  try {
16493
- for (const name of profile.managedRuntime.processes)
16494
- processes.push(await observeControllerProcess(primary[0].id, name, signal));
17038
+ for (const name of profile.managedRuntime.processes) {
17039
+ const observation = await observeControllerProcess(primary[0].id, name, signal);
17040
+ if (observation.kind === "absent") processesAbsent = true;
17041
+ else processes.push(observation.identity);
17042
+ }
16495
17043
  } catch (error) {
16496
17044
  if (signal.aborted) throw error;
16497
17045
  processesVerified = false;
@@ -16505,7 +17053,7 @@ var init_controller_observation = __esm({
16505
17053
  const routeMatches = routes.some(
16506
17054
  (route) => route.workspace === environment.workspace && route.name === app.name && route.host === app.host && route.mode === "proxy" && route.upstreamHost === upstream.upstreamHost && route.port === upstream.port
16507
17055
  );
16508
- const http2 = healthy && routeMatches && processesVerified ? await probeHttpReadiness(
17056
+ const http2 = healthy && routeMatches && processesVerified && !processesAbsent ? await probeHttpReadiness(
16509
17057
  app.host,
16510
17058
  app.readiness,
16511
17059
  signal,
@@ -16514,7 +17062,7 @@ var init_controller_observation = __esm({
16514
17062
  ) : void 0;
16515
17063
  capabilities.push({
16516
17064
  capability: controllerCapability(requirement),
16517
- infrastructure: healthy && routeMatches ? "healthy" : "unknown",
17065
+ infrastructure: healthy && routeMatches ? processesAbsent ? "failed" : "healthy" : "unknown",
16518
17066
  application: http2?.ok ? "verified" : http2?.classification === "application-contract" ? "unready" : "unverified",
16519
17067
  observedAtMs: 0,
16520
17068
  validForMs: 15e3
@@ -16522,13 +17070,15 @@ var init_controller_observation = __esm({
16522
17070
  }
16523
17071
  if (processesVerified && requirements.some((value) => value.startsWith("app:"))) {
16524
17072
  try {
16525
- for (let index = 0; index < profile.managedRuntime.processes.length; index++)
16526
- if (processes[index] !== await observeControllerProcess(
17073
+ for (let index = 0; index < profile.managedRuntime.processes.length; index++) {
17074
+ const recheck = await observeControllerProcess(
16527
17075
  primary[0].id,
16528
17076
  profile.managedRuntime.processes[index],
16529
17077
  signal
16530
- ))
17078
+ );
17079
+ if (recheck.kind !== "present" || processes[index] !== recheck.identity)
16531
17080
  throw new Error("Observation process changed.");
17081
+ }
16532
17082
  } catch (error) {
16533
17083
  if (signal.aborted) throw error;
16534
17084
  for (const capability of capabilities)
@@ -16773,26 +17323,26 @@ var init_controller_sessions = __esm({
16773
17323
 
16774
17324
  // src/core/controller-server.ts
16775
17325
  function privateDirectory(directory) {
16776
- const absolute2 = import_node_path47.default.resolve(directory);
16777
- let current = import_node_path47.default.parse(absolute2).root;
16778
- for (const part of absolute2.slice(current.length).split(import_node_path47.default.sep)) {
16779
- current = import_node_path47.default.join(current, part);
17326
+ const absolute2 = import_node_path48.default.resolve(directory);
17327
+ let current = import_node_path48.default.parse(absolute2).root;
17328
+ for (const part of absolute2.slice(current.length).split(import_node_path48.default.sep)) {
17329
+ current = import_node_path48.default.join(current, part);
16780
17330
  try {
16781
- const stat2 = import_node_fs36.default.lstatSync(current);
17331
+ const stat2 = import_node_fs37.default.lstatSync(current);
16782
17332
  if (!stat2.isDirectory() || stat2.isSymbolicLink())
16783
17333
  throw new Error("Unsafe controller directory.");
16784
17334
  } catch (error) {
16785
17335
  if (error.code !== "ENOENT") throw error;
16786
- import_node_fs36.default.mkdirSync(current, { mode: 448 });
17336
+ import_node_fs37.default.mkdirSync(current, { mode: 448 });
16787
17337
  }
16788
17338
  }
16789
- const stat = import_node_fs36.default.lstatSync(absolute2);
17339
+ const stat = import_node_fs37.default.lstatSync(absolute2);
16790
17340
  if (stat.uid !== process.getuid?.() || (stat.mode & 63) !== 0)
16791
17341
  throw new Error("Controller directory is not private.");
16792
17342
  }
16793
17343
  function validateOwnedFile(file, socket) {
16794
17344
  try {
16795
- const stat = import_node_fs36.default.lstatSync(file);
17345
+ const stat = import_node_fs37.default.lstatSync(file);
16796
17346
  if (stat.uid !== process.getuid?.() || (socket ? !stat.isSocket() : !stat.isFile()) || (stat.mode & 63) !== 0)
16797
17347
  throw new Error("Unsafe controller artifact.");
16798
17348
  return true;
@@ -16804,19 +17354,19 @@ function validateOwnedFile(file, socket) {
16804
17354
  async function runController(options) {
16805
17355
  if (options.operations && options.createOperations)
16806
17356
  throw new Error("Controller operations have multiple owners.");
16807
- const socketPath = import_node_path47.default.join(options.directory, "control.sock");
17357
+ const socketPath = import_node_path48.default.join(options.directory, "control.sock");
16808
17358
  if (Buffer.byteLength(socketPath) > 103) throw new Error("Controller socket path is too long.");
16809
17359
  privateDirectory(options.directory);
16810
- const lockPath = import_node_path47.default.join(options.directory, "owner.lock");
17360
+ const lockPath = import_node_path48.default.join(options.directory, "owner.lock");
16811
17361
  try {
16812
- const stat = import_node_fs36.default.lstatSync(lockPath);
17362
+ const stat = import_node_fs37.default.lstatSync(lockPath);
16813
17363
  if (!stat.isFile() || stat.uid !== process.getuid?.())
16814
17364
  throw new Error("Unsafe controller owner lock.");
16815
17365
  } catch (error) {
16816
17366
  if (error.code !== "ENOENT") throw error;
16817
17367
  }
16818
17368
  await withFileLock(lockPath, { activity: "controller ownership", waitMs: 0 }, async () => {
16819
- if (validateOwnedFile(socketPath, true)) import_node_fs36.default.unlinkSync(socketPath);
17369
+ if (validateOwnedFile(socketPath, true)) import_node_fs37.default.unlinkSync(socketPath);
16820
17370
  const sessions = new ControllerSessions(new ControllerStore(options.directory));
16821
17371
  const incarnation = sessions.read();
16822
17372
  let startupAvailable = true;
@@ -16839,12 +17389,19 @@ async function runController(options) {
16839
17389
  let serial = Promise.resolve();
16840
17390
  const monotonic = () => Math.floor(performance.now());
16841
17391
  let fatal;
16842
- const monitor = options.collect ? new ControllerMonitor(sessions, options.collect, (operation) => {
16843
- const pending = serial.then(operation);
16844
- serial = pending.catch(() => {
16845
- });
16846
- return pending;
16847
- }) : void 0;
17392
+ const monitor = options.collect ? new ControllerMonitor(
17393
+ sessions,
17394
+ options.collect,
17395
+ (operation) => {
17396
+ const pending = serial.then(operation);
17397
+ serial = pending.catch(() => {
17398
+ });
17399
+ return pending;
17400
+ },
17401
+ void 0,
17402
+ void 0,
17403
+ operations?.recover
17404
+ ) : void 0;
16848
17405
  const server = import_node_net4.default.createServer((socket) => {
16849
17406
  if (sockets.size >= 32) {
16850
17407
  socket.destroy();
@@ -17198,7 +17755,7 @@ async function runController(options) {
17198
17755
  resolve();
17199
17756
  });
17200
17757
  });
17201
- import_node_fs36.default.chmodSync(socketPath, 384);
17758
+ import_node_fs37.default.chmodSync(socketPath, 384);
17202
17759
  await new Promise((resolve) => {
17203
17760
  finish = resolve;
17204
17761
  options.signal.addEventListener("abort", shutdown, { once: true });
@@ -17217,13 +17774,13 @@ async function runController(options) {
17217
17774
  }
17218
17775
  });
17219
17776
  }
17220
- var import_node_fs36, import_node_net4, import_node_path47, FRAME_BYTES;
17777
+ var import_node_fs37, import_node_net4, import_node_path48, FRAME_BYTES;
17221
17778
  var init_controller_server = __esm({
17222
17779
  "src/core/controller-server.ts"() {
17223
17780
  "use strict";
17224
- import_node_fs36 = __toESM(require("fs"));
17781
+ import_node_fs37 = __toESM(require("fs"));
17225
17782
  import_node_net4 = __toESM(require("net"));
17226
- import_node_path47 = __toESM(require("path"));
17783
+ import_node_path48 = __toESM(require("path"));
17227
17784
  init_controller_monitor();
17228
17785
  init_controller_protocol();
17229
17786
  init_controller_sessions();
@@ -17240,7 +17797,7 @@ __export(controller_exports, {
17240
17797
  runControllerCommand: () => runControllerCommand
17241
17798
  });
17242
17799
  async function runControllerCommand(method, options, repo, dependencies = {}) {
17243
- const directory = import_node_path48.default.join(DEVROUTER_HOME, "controller");
17800
+ const directory = import_node_path49.default.join(DEVROUTER_HOME, "controller");
17244
17801
  try {
17245
17802
  if (method === "run") {
17246
17803
  const controller = new AbortController();
@@ -17275,7 +17832,7 @@ async function runControllerCommand(method, options, repo, dependencies = {}) {
17275
17832
  if (fields2.timeout !== void 0) fields2.timeout = Number(fields2.timeout);
17276
17833
  await controllerRequest(
17277
17834
  directory,
17278
- { method, ...fields2, ...repo ? { path: import_node_path48.default.resolve(repo) } : {} },
17835
+ { method, ...fields2, ...repo ? { path: import_node_path49.default.resolve(repo) } : {} },
17279
17836
  (value) => process.stdout.write(`${JSON.stringify(value)}
17280
17837
  `)
17281
17838
  );
@@ -17290,11 +17847,11 @@ async function runControllerCommand(method, options, repo, dependencies = {}) {
17290
17847
  process.exitCode = 1;
17291
17848
  }
17292
17849
  }
17293
- var import_node_path48;
17850
+ var import_node_path49;
17294
17851
  var init_controller = __esm({
17295
17852
  "src/commands/controller.ts"() {
17296
17853
  "use strict";
17297
- import_node_path48 = __toESM(require("path"));
17854
+ import_node_path49 = __toESM(require("path"));
17298
17855
  init_capacity_collector();
17299
17856
  init_capacity_controller();
17300
17857
  init_capacity_policy();
@@ -17345,28 +17902,28 @@ function replaceDevrouterSection(content) {
17345
17902
  return `${content.slice(0, start)}${buildDevrouterSection()}${content.slice(end)}`;
17346
17903
  }
17347
17904
  function writeRepoFile(repoPath, relPath, content) {
17348
- const absolutePath = (0, import_node_path49.join)(repoPath, relPath);
17349
- (0, import_node_fs37.mkdirSync)((0, import_node_path49.dirname)(absolutePath), { recursive: true });
17350
- (0, import_node_fs37.writeFileSync)(absolutePath, content, "utf-8");
17905
+ const absolutePath = (0, import_node_path50.join)(repoPath, relPath);
17906
+ (0, import_node_fs38.mkdirSync)((0, import_node_path50.dirname)(absolutePath), { recursive: true });
17907
+ (0, import_node_fs38.writeFileSync)(absolutePath, content, "utf-8");
17351
17908
  return absolutePath;
17352
17909
  }
17353
17910
  function ensureAgentsMdSection(repoPath) {
17354
- const filePath = (0, import_node_path49.join)(repoPath, AGENTS_MD);
17355
- if ((0, import_node_fs37.existsSync)(filePath)) {
17356
- const content = (0, import_node_fs37.readFileSync)(filePath, "utf-8");
17911
+ const filePath = (0, import_node_path50.join)(repoPath, AGENTS_MD);
17912
+ if ((0, import_node_fs38.existsSync)(filePath)) {
17913
+ const content = (0, import_node_fs38.readFileSync)(filePath, "utf-8");
17357
17914
  if (content.includes(DEVROUTER_SENTINEL)) {
17358
17915
  const updated = replaceDevrouterSection(content);
17359
17916
  if (updated === void 0 || updated === content) return { path: filePath, written: false };
17360
- (0, import_node_fs37.writeFileSync)(filePath, updated, "utf-8");
17917
+ (0, import_node_fs38.writeFileSync)(filePath, updated, "utf-8");
17361
17918
  return { path: filePath, written: true };
17362
17919
  }
17363
- (0, import_node_fs37.writeFileSync)(filePath, `${content.trimEnd()}
17920
+ (0, import_node_fs38.writeFileSync)(filePath, `${content.trimEnd()}
17364
17921
 
17365
17922
  ${buildDevrouterSection()}
17366
17923
  `, "utf-8");
17367
17924
  return { path: filePath, written: true };
17368
17925
  }
17369
- (0, import_node_fs37.writeFileSync)(filePath, `# AGENTS.md
17926
+ (0, import_node_fs38.writeFileSync)(filePath, `# AGENTS.md
17370
17927
 
17371
17928
  ${buildDevrouterSection()}
17372
17929
  `, "utf-8");
@@ -17376,12 +17933,12 @@ function ensureSkillFile(repoPath) {
17376
17933
  const filePath = writeRepoFile(repoPath, DEVROUTER_SKILL_REL_PATH, DEVROUTER_SKILL_CONTENT);
17377
17934
  return { path: filePath, written: true };
17378
17935
  }
17379
- var import_node_fs37, import_node_path49, DEVROUTER_SENTINEL, DEVROUTER_END_SENTINEL, LEGACY_DEVROUTER_SECTION_END, AGENTS_MD, DEVROUTER_SKILL_REL_PATH, DEVROUTER_SKILL_CONTENT;
17936
+ var import_node_fs38, import_node_path50, DEVROUTER_SENTINEL, DEVROUTER_END_SENTINEL, LEGACY_DEVROUTER_SECTION_END, AGENTS_MD, DEVROUTER_SKILL_REL_PATH, DEVROUTER_SKILL_CONTENT;
17380
17937
  var init_agents_md = __esm({
17381
17938
  "src/core/agents-md.ts"() {
17382
17939
  "use strict";
17383
- import_node_fs37 = require("fs");
17384
- import_node_path49 = require("path");
17940
+ import_node_fs38 = require("fs");
17941
+ import_node_path50 = require("path");
17385
17942
  DEVROUTER_SENTINEL = "<!-- devrouter -->";
17386
17943
  DEVROUTER_END_SENTINEL = "<!-- /devrouter -->";
17387
17944
  LEGACY_DEVROUTER_SECTION_END = "- `devrouter ls`";
@@ -17918,8 +18475,8 @@ function buildOnboardingPrompt(options = {}) {
17918
18475
  "- 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.",
17919
18476
  "- 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.",
17920
18477
  "- 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.",
17921
- "- 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.",
17922
- "- Capacity admission is opt-in: only an operator-enabled capacity policy with an enrolled checkout routes ensure and exec through controller admission. The CLI follows the decision with bounded reconnecting waits that survive controller restarts, worker results are journalled atomically, and stop bypasses admission. Without an enabled policy lifecycle behavior is unchanged.",
18478
+ "- 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 detect or prevent OOM; bounded automatic recovery runs only when the operator enables the capacity recovery policy.",
18479
+ "- Capacity admission is opt-in: only an operator-enabled capacity policy with an enrolled checkout routes ensure and exec through controller admission. The CLI follows the decision with bounded reconnecting waits that survive controller restarts, worker results are journalled atomically, and stop bypasses admission. Without an enabled policy lifecycle behavior is unchanged. An optional recovery block adds bounded automatic recovery: one journal-admitted corrective ensure per positively failed required capability, bounded by per-scope restart counts, an aggregate corrective-action cap and a rolling observation window. It is inert unless enabled.",
17923
18480
  "- A lock that cannot prove its owner fails closed with the failing inspection stage, the exact lock path, and a portable reproduction command; run the canonical command in a permitted host context. No identity fallback is attempted, and `devrouter controller` prints the underlying cause on stderr while keeping the stable `controller-unavailable` JSON on stdout.",
17924
18481
  "- 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.",
17925
18482
  "- A managed adapter plus `postCreateCommand` requires `waitFor` exactly `postCreateCommand` or `postStartCommand`; managed selective config preserves lifecycle fields and changes only `runServices`.",
@@ -18106,7 +18663,7 @@ var init_ai_prompt = __esm({
18106
18663
  },
18107
18664
  {
18108
18665
  command: "devrouter stop [path] [--delete] [--json]",
18109
- purpose: "Stop the exact checkout workspace runtime and routes; --delete explicitly deletes the ownership-proven runtime without removing the checkout."
18666
+ purpose: "Stop the exact checkout workspace runtime and routes, or prove absence after pre-registration failure; --delete explicitly deletes the ownership-proven runtime without removing the checkout."
18110
18667
  },
18111
18668
  {
18112
18669
  command: "devrouter exec [path] -- <command...>",
@@ -18660,11 +19217,11 @@ function extractCurrentVersionFromRepoConfig(repoPath) {
18660
19217
  };
18661
19218
  }
18662
19219
  function readPromptDirectory(promptDirPath) {
18663
- if (!import_node_fs38.default.existsSync(promptDirPath)) {
19220
+ if (!import_node_fs39.default.existsSync(promptDirPath)) {
18664
19221
  throw new Error(`Missing ${UPGRADE_PROMPTS_DIR} directory at ${promptDirPath}.`);
18665
19222
  }
18666
19223
  const releases = [];
18667
- const files = import_node_fs38.default.readdirSync(promptDirPath, { withFileTypes: true });
19224
+ const files = import_node_fs39.default.readdirSync(promptDirPath, { withFileTypes: true });
18668
19225
  for (const file of files) {
18669
19226
  if (!file.isFile() || !file.name.endsWith(".md")) {
18670
19227
  continue;
@@ -18674,8 +19231,8 @@ function readPromptDirectory(promptDirPath) {
18674
19231
  continue;
18675
19232
  }
18676
19233
  const version = normalizeVersion(basename);
18677
- const promptPath = import_node_path50.default.join(promptDirPath, file.name);
18678
- const prompt = import_node_fs38.default.readFileSync(promptPath, "utf-8").trim();
19234
+ const promptPath = import_node_path51.default.join(promptDirPath, file.name);
19235
+ const prompt = import_node_fs39.default.readFileSync(promptPath, "utf-8").trim();
18679
19236
  releases.push({ version, prompt, promptPath });
18680
19237
  }
18681
19238
  const deduped = /* @__PURE__ */ new Map();
@@ -18696,19 +19253,19 @@ function listAvailableUpgradeTargets(currentVersion, releases) {
18696
19253
  }
18697
19254
  function resolvePromptDirectory(explicitPath) {
18698
19255
  if (explicitPath) {
18699
- return import_node_path50.default.resolve(explicitPath);
19256
+ return import_node_path51.default.resolve(explicitPath);
18700
19257
  }
18701
- const entryFile = process.argv[1] ? import_node_path50.default.resolve(process.argv[1]) : __filename;
18702
- const entryDir = import_node_path50.default.dirname(entryFile);
18703
- const resolvedEntryDir = import_node_fs38.default.existsSync(entryFile) ? import_node_path50.default.dirname(import_node_fs38.default.realpathSync(entryFile)) : entryDir;
19258
+ const entryFile = process.argv[1] ? import_node_path51.default.resolve(process.argv[1]) : __filename;
19259
+ const entryDir = import_node_path51.default.dirname(entryFile);
19260
+ const resolvedEntryDir = import_node_fs39.default.existsSync(entryFile) ? import_node_path51.default.dirname(import_node_fs39.default.realpathSync(entryFile)) : entryDir;
18704
19261
  const candidates = [
18705
- import_node_path50.default.resolve(resolvedEntryDir, "..", UPGRADE_PROMPTS_DIR),
18706
- import_node_path50.default.resolve(entryDir, "..", UPGRADE_PROMPTS_DIR),
18707
- import_node_path50.default.resolve(__dirname, "..", UPGRADE_PROMPTS_DIR),
18708
- import_node_path50.default.resolve(__dirname, "..", "..", UPGRADE_PROMPTS_DIR),
18709
- import_node_path50.default.resolve(process.cwd(), UPGRADE_PROMPTS_DIR)
19262
+ import_node_path51.default.resolve(resolvedEntryDir, "..", UPGRADE_PROMPTS_DIR),
19263
+ import_node_path51.default.resolve(entryDir, "..", UPGRADE_PROMPTS_DIR),
19264
+ import_node_path51.default.resolve(__dirname, "..", UPGRADE_PROMPTS_DIR),
19265
+ import_node_path51.default.resolve(__dirname, "..", "..", UPGRADE_PROMPTS_DIR),
19266
+ import_node_path51.default.resolve(process.cwd(), UPGRADE_PROMPTS_DIR)
18710
19267
  ];
18711
- const existing = candidates.find((candidate) => import_node_fs38.default.existsSync(candidate));
19268
+ const existing = candidates.find((candidate) => import_node_fs39.default.existsSync(candidate));
18712
19269
  return existing ?? candidates[0];
18713
19270
  }
18714
19271
  function loadUpgradeCatalog(options = {}) {
@@ -18724,12 +19281,12 @@ function loadUpgradeCatalog(options = {}) {
18724
19281
  releases
18725
19282
  };
18726
19283
  }
18727
- var import_node_fs38, import_node_path50, SEMVER_RE, UPGRADE_PROMPTS_DIR;
19284
+ var import_node_fs39, import_node_path51, SEMVER_RE, UPGRADE_PROMPTS_DIR;
18728
19285
  var init_upgrade = __esm({
18729
19286
  "src/core/upgrade.ts"() {
18730
19287
  "use strict";
18731
- import_node_fs38 = __toESM(require("fs"));
18732
- import_node_path50 = __toESM(require("path"));
19288
+ import_node_fs39 = __toESM(require("fs"));
19289
+ import_node_path51 = __toESM(require("path"));
18733
19290
  init_repo_config();
18734
19291
  SEMVER_RE = /^v?(\d+)\.(\d+)\.(\d+)$/;
18735
19292
  UPGRADE_PROMPTS_DIR = "upgrade-prompts";
@@ -18872,7 +19429,7 @@ function isLoopbackHost(host) {
18872
19429
  return host === "127.0.0.1" || host === "localhost" || host === "host.docker.internal";
18873
19430
  }
18874
19431
  function inspectCompose(composeFile, _workspace) {
18875
- if (!import_node_fs39.default.existsSync(composeFile)) {
19432
+ if (!import_node_fs40.default.existsSync(composeFile)) {
18876
19433
  return {
18877
19434
  aliases: [],
18878
19435
  publishedPorts: [],
@@ -18881,7 +19438,7 @@ function inspectCompose(composeFile, _workspace) {
18881
19438
  };
18882
19439
  }
18883
19440
  try {
18884
- const raw = import_node_fs39.default.readFileSync(composeFile, "utf-8");
19441
+ const raw = import_node_fs40.default.readFileSync(composeFile, "utf-8");
18885
19442
  const parsed = import_yaml5.default.parse(raw);
18886
19443
  const root = asRecord(parsed);
18887
19444
  const services = asRecord(root?.services);
@@ -18977,11 +19534,11 @@ function buildHostPortClaimsCheck(repoPath, workspace) {
18977
19534
  }
18978
19535
  }
18979
19536
  function buildDevcontainerChecks(repoPath, config, workspace) {
18980
- const devcontainerDir = import_node_path51.default.join(repoPath, ".devcontainer");
18981
- if (!import_node_fs39.default.existsSync(devcontainerDir)) {
19537
+ const devcontainerDir = import_node_path52.default.join(repoPath, ".devcontainer");
19538
+ if (!import_node_fs40.default.existsSync(devcontainerDir)) {
18982
19539
  return [];
18983
19540
  }
18984
- const compose = inspectCompose(import_node_path51.default.join(devcontainerDir, "docker-compose.yml"), workspace);
19541
+ const compose = inspectCompose(import_node_path52.default.join(devcontainerDir, "docker-compose.yml"), workspace);
18985
19542
  const checks = [];
18986
19543
  if (compose.parseError) {
18987
19544
  checks.push({
@@ -19008,9 +19565,9 @@ function buildDevcontainerChecks(repoPath, config, workspace) {
19008
19565
  details: compose.publishedPorts.length > 0 ? compose.publishedPorts.join(", ") : void 0,
19009
19566
  suggestion: compose.parseError || compose.publishedPorts.length > 0 ? "Remove published ports and route services through devnet aliases with devrouter proxy apps." : void 0
19010
19567
  });
19011
- const dockerfilePath = import_node_path51.default.join(devcontainerDir, "Dockerfile");
19012
- const dockerfileExists = import_node_fs39.default.existsSync(dockerfilePath);
19013
- const dockerfile = dockerfileExists ? import_node_fs39.default.readFileSync(dockerfilePath, "utf-8") : "";
19568
+ const dockerfilePath = import_node_path52.default.join(devcontainerDir, "Dockerfile");
19569
+ const dockerfileExists = import_node_fs40.default.existsSync(dockerfilePath);
19570
+ const dockerfile = dockerfileExists ? import_node_fs40.default.readFileSync(dockerfilePath, "utf-8") : "";
19014
19571
  const devrouterArtifacts = ["@devrouter/cli", "devrouter-process"].filter(
19015
19572
  (artifact) => dockerfile.includes(artifact)
19016
19573
  );
@@ -19068,12 +19625,12 @@ function buildDevcontainerChecks(repoPath, config, workspace) {
19068
19625
  });
19069
19626
  return checks;
19070
19627
  }
19071
- var import_node_fs39, import_node_path51, import_yaml5;
19628
+ var import_node_fs40, import_node_path52, import_yaml5;
19072
19629
  var init_devcontainer_diagnostics = __esm({
19073
19630
  "src/core/devcontainer-diagnostics.ts"() {
19074
19631
  "use strict";
19075
- import_node_fs39 = __toESM(require("fs"));
19076
- import_node_path51 = __toESM(require("path"));
19632
+ import_node_fs40 = __toESM(require("fs"));
19633
+ import_node_path52 = __toESM(require("path"));
19077
19634
  import_yaml5 = __toESM(require("yaml"));
19078
19635
  init_devcontainer_profile();
19079
19636
  init_host_port_claims();
@@ -19168,11 +19725,11 @@ function reconcileRouteRunConflict(repoPath, app) {
19168
19725
  }
19169
19726
  }
19170
19727
  }
19171
- var import_node_fs40;
19728
+ var import_node_fs41;
19172
19729
  var init_route_state = __esm({
19173
19730
  "src/core/route-state.ts"() {
19174
19731
  "use strict";
19175
- import_node_fs40 = __toESM(require("fs"));
19732
+ import_node_fs41 = __toESM(require("fs"));
19176
19733
  init_host_routes();
19177
19734
  init_workspace();
19178
19735
  }
@@ -19347,7 +19904,7 @@ function toRepoStatus(repoPath) {
19347
19904
  const resolvedRepoPath = resolveRepoPath(repoPath);
19348
19905
  const configPath = getRepoConfigPath(resolvedRepoPath);
19349
19906
  const explicitRepo = typeof repoPath === "string" && repoPath.trim().length > 0;
19350
- const configExists = import_node_fs41.default.existsSync(configPath);
19907
+ const configExists = import_node_fs42.default.existsSync(configPath);
19351
19908
  if (!explicitRepo && !configExists) {
19352
19909
  return void 0;
19353
19910
  }
@@ -19495,11 +20052,11 @@ async function collectRouterStatus(repoPath) {
19495
20052
  }
19496
20053
  };
19497
20054
  }
19498
- var import_node_fs41;
20055
+ var import_node_fs42;
19499
20056
  var init_status = __esm({
19500
20057
  "src/core/status.ts"() {
19501
20058
  "use strict";
19502
- import_node_fs41 = __toESM(require("fs"));
20059
+ import_node_fs42 = __toESM(require("fs"));
19503
20060
  init_docker();
19504
20061
  init_managed_runtime_state();
19505
20062
  init_managed_runtime_status();
@@ -19573,12 +20130,12 @@ function parseMinimumNodeMajor(value) {
19573
20130
  return Number(match[1]);
19574
20131
  }
19575
20132
  function readPackageJson(repoPath) {
19576
- const packagePath = import_node_path52.default.join(repoPath, "package.json");
19577
- if (!import_node_fs42.default.existsSync(packagePath)) {
20133
+ const packagePath = import_node_path53.default.join(repoPath, "package.json");
20134
+ if (!import_node_fs43.default.existsSync(packagePath)) {
19578
20135
  return void 0;
19579
20136
  }
19580
20137
  try {
19581
- return JSON.parse(import_node_fs42.default.readFileSync(packagePath, "utf-8"));
20138
+ return JSON.parse(import_node_fs43.default.readFileSync(packagePath, "utf-8"));
19582
20139
  } catch {
19583
20140
  return void 0;
19584
20141
  }
@@ -19762,13 +20319,13 @@ function buildGlobalToolChecksWithoutTelemetry(repoPath) {
19762
20319
  checks.push(nodeToolchainCheck(repoPath));
19763
20320
  return checks;
19764
20321
  }
19765
- var import_node_child_process29, import_node_fs42, import_node_path52, DEVSY_DISABLE_TELEMETRY;
20322
+ var import_node_child_process29, import_node_fs43, import_node_path53, DEVSY_DISABLE_TELEMETRY;
19766
20323
  var init_tool_diagnostics = __esm({
19767
20324
  "src/core/tool-diagnostics.ts"() {
19768
20325
  "use strict";
19769
20326
  import_node_child_process29 = require("child_process");
19770
- import_node_fs42 = __toESM(require("fs"));
19771
- import_node_path52 = __toESM(require("path"));
20327
+ import_node_fs43 = __toESM(require("fs"));
20328
+ import_node_path53 = __toESM(require("path"));
19772
20329
  init_devsy_agent();
19773
20330
  init_workspace_runtime();
19774
20331
  DEVSY_DISABLE_TELEMETRY = "DEVSY_DISABLE_TELEMETRY";
@@ -19785,10 +20342,10 @@ function inRepositoryWorkspaceScope(repoPath, worktreePath, livePaths) {
19785
20342
  if (livePaths.some((candidate) => sameWorkspacePath(candidate, worktreePath))) return true;
19786
20343
  const comparableRepo = comparableWorkspacePath(repoPath);
19787
20344
  const comparableWorktree = comparableWorkspacePath(worktreePath);
19788
- const localRoot = import_node_path53.default.join(comparableRepo, "trees") + import_node_path53.default.sep;
20345
+ const localRoot = import_node_path54.default.join(comparableRepo, "trees") + import_node_path54.default.sep;
19789
20346
  if (comparableWorktree.startsWith(localRoot)) return true;
19790
- const legacyPrefix = `${import_node_path53.default.basename(comparableRepo)}-`;
19791
- return import_node_path53.default.dirname(comparableWorktree) === import_node_path53.default.dirname(comparableRepo) && import_node_path53.default.basename(comparableWorktree).startsWith(legacyPrefix);
20347
+ const legacyPrefix = `${import_node_path54.default.basename(comparableRepo)}-`;
20348
+ return import_node_path54.default.dirname(comparableWorktree) === import_node_path54.default.dirname(comparableRepo) && import_node_path54.default.basename(comparableWorktree).startsWith(legacyPrefix);
19792
20349
  }
19793
20350
  function previewActions(devpodStatus, routeCount, includeRecord) {
19794
20351
  const actions = [
@@ -20040,11 +20597,11 @@ function applyWorkspaceGc(plan) {
20040
20597
  candidates
20041
20598
  };
20042
20599
  }
20043
- var import_node_path53;
20600
+ var import_node_path54;
20044
20601
  var init_workspace_gc = __esm({
20045
20602
  "src/core/workspace-gc.ts"() {
20046
20603
  "use strict";
20047
- import_node_path53 = __toESM(require("path"));
20604
+ import_node_path54 = __toESM(require("path"));
20048
20605
  init_devpod_mutation();
20049
20606
  init_devpod_workspaces();
20050
20607
  init_host_routes();
@@ -20114,11 +20671,11 @@ function inspectPostgresCredentials(repoPath, config) {
20114
20671
  parseErrors.push(`${app.name}: ${composeFile} (${message})`);
20115
20672
  continue;
20116
20673
  }
20117
- if (!import_node_fs43.default.existsSync(absolutePath)) {
20674
+ if (!import_node_fs44.default.existsSync(absolutePath)) {
20118
20675
  continue;
20119
20676
  }
20120
20677
  try {
20121
- const raw = import_node_fs43.default.readFileSync(absolutePath, "utf-8");
20678
+ const raw = import_node_fs44.default.readFileSync(absolutePath, "utf-8");
20122
20679
  const parsed = import_yaml6.default.parse(raw);
20123
20680
  const root = asRecord2(parsed);
20124
20681
  const services = asRecord2(root?.services);
@@ -20398,7 +20955,7 @@ async function buildDoctorReport(options = {}) {
20398
20955
  const config = runtimeConfig.config;
20399
20956
  loadedConfig = config;
20400
20957
  loadedWorkspace = runtimeConfig.workspace;
20401
- const cliVersion = true ? "0.0.72" : "0.0.0-dev";
20958
+ const cliVersion = true ? "0.0.74" : "0.0.0-dev";
20402
20959
  const configVersion = config.devrouter?.version;
20403
20960
  if (configVersion && cliVersion !== "0.0.0-dev" && compareSemver(configVersion, cliVersion) > 0) {
20404
20961
  addCheck(checks, {
@@ -20428,9 +20985,9 @@ async function buildDoctorReport(options = {}) {
20428
20985
  (app) => app.docker.composeFiles.map((filePath) => ({
20429
20986
  app: app.name,
20430
20987
  filePath,
20431
- absolutePath: import_node_path54.default.resolve(repo.path, filePath)
20988
+ absolutePath: import_node_path55.default.resolve(repo.path, filePath)
20432
20989
  }))
20433
- ).filter((entry) => !import_node_fs43.default.existsSync(entry.absolutePath));
20990
+ ).filter((entry) => !import_node_fs44.default.existsSync(entry.absolutePath));
20434
20991
  addCheck(checks, {
20435
20992
  id: "repo.compose-files",
20436
20993
  level: missingComposeFiles.length === 0 ? "ok" : "error",
@@ -20473,8 +21030,8 @@ async function buildDoctorReport(options = {}) {
20473
21030
  const missingHostCwds = config.apps.filter((app) => app.runtime === "host").map((app) => ({
20474
21031
  app: app.name,
20475
21032
  cwd: app.hostRun.cwd,
20476
- absolutePath: import_node_path54.default.resolve(repo.path, app.hostRun.cwd)
20477
- })).filter((entry) => !import_node_fs43.default.existsSync(entry.absolutePath));
21033
+ absolutePath: import_node_path55.default.resolve(repo.path, app.hostRun.cwd)
21034
+ })).filter((entry) => !import_node_fs44.default.existsSync(entry.absolutePath));
20478
21035
  addCheck(checks, {
20479
21036
  id: "repo.host-cwd",
20480
21037
  level: missingHostCwds.length === 0 ? "ok" : "error",
@@ -20618,12 +21175,12 @@ async function buildDoctorReport(options = {}) {
20618
21175
  nextSteps
20619
21176
  };
20620
21177
  }
20621
- var import_node_fs43, import_node_path54, import_yaml6, POSTGRES_DEFAULTS;
21178
+ var import_node_fs44, import_node_path55, import_yaml6, POSTGRES_DEFAULTS;
20622
21179
  var init_doctor = __esm({
20623
21180
  "src/core/doctor.ts"() {
20624
21181
  "use strict";
20625
- import_node_fs43 = __toESM(require("fs"));
20626
- import_node_path54 = __toESM(require("path"));
21182
+ import_node_fs44 = __toESM(require("fs"));
21183
+ import_node_path55 = __toESM(require("path"));
20627
21184
  import_yaml6 = __toESM(require("yaml"));
20628
21185
  init_docker();
20629
21186
  init_host_routes();
@@ -21121,6 +21678,11 @@ async function runStopCommand(options) {
21121
21678
  return;
21122
21679
  }
21123
21680
  const label2 = result.kind === "primary" ? "Primary checkout" : `Workspace '${result.workspace}'`;
21681
+ if (result.runtimeAbsent) {
21682
+ process.stdout.write(`${label2} has no runtime or routes; stop is complete.
21683
+ `);
21684
+ return;
21685
+ }
21124
21686
  if (!result.stopped && !result.deleted && result.freedRoutes === 0) {
21125
21687
  process.stdout.write(`${label2} is already stopped; no routes needed removal.
21126
21688
  `);
@@ -21441,17 +22003,17 @@ function asRecord3(value) {
21441
22003
  return value;
21442
22004
  }
21443
22005
  function readJson(filePath) {
21444
- if (!import_node_fs44.default.existsSync(filePath)) {
22006
+ if (!import_node_fs45.default.existsSync(filePath)) {
21445
22007
  return void 0;
21446
22008
  }
21447
22009
  try {
21448
- return JSON.parse(import_node_fs44.default.readFileSync(filePath, "utf-8"));
22010
+ return JSON.parse(import_node_fs45.default.readFileSync(filePath, "utf-8"));
21449
22011
  } catch {
21450
22012
  return void 0;
21451
22013
  }
21452
22014
  }
21453
22015
  function relative(repoPath, filePath) {
21454
- return import_node_path55.default.relative(repoPath, filePath) || ".";
22016
+ return import_node_path56.default.relative(repoPath, filePath) || ".";
21455
22017
  }
21456
22018
  function redactEnvAssignments(value) {
21457
22019
  return value.replace(
@@ -21492,7 +22054,7 @@ function inspectPackageManager(repoPath, pkg) {
21492
22054
  ["bun.lock", "bun"]
21493
22055
  ];
21494
22056
  for (const [fileName, name] of lockfiles) {
21495
- if (import_node_fs44.default.existsSync(import_node_path55.default.join(repoPath, fileName))) {
22057
+ if (import_node_fs45.default.existsSync(import_node_path56.default.join(repoPath, fileName))) {
21496
22058
  return { name, source: fileName };
21497
22059
  }
21498
22060
  }
@@ -21507,9 +22069,9 @@ function inspectNode(repoPath, pkg) {
21507
22069
  if (typeof engines?.node === "string") {
21508
22070
  return { version: engines.node, source: "package.json:engines.node" };
21509
22071
  }
21510
- const nvmrc = import_node_path55.default.join(repoPath, ".nvmrc");
21511
- if (import_node_fs44.default.existsSync(nvmrc)) {
21512
- const version = import_node_fs44.default.readFileSync(nvmrc, "utf-8").trim();
22072
+ const nvmrc = import_node_path56.default.join(repoPath, ".nvmrc");
22073
+ if (import_node_fs45.default.existsSync(nvmrc)) {
22074
+ const version = import_node_fs45.default.readFileSync(nvmrc, "utf-8").trim();
21513
22075
  return { version, source: ".nvmrc" };
21514
22076
  }
21515
22077
  return void 0;
@@ -21570,7 +22132,7 @@ function configuredComposeFiles(repoPath) {
21570
22132
  const files = config.apps.filter(
21571
22133
  (app) => app.runtime === "docker"
21572
22134
  ).flatMap((app) => app.docker.composeFiles).filter(
21573
- (fileName) => !import_node_path55.default.isAbsolute(fileName) && !import_node_path55.default.normalize(fileName).startsWith("..")
22135
+ (fileName) => !import_node_path56.default.isAbsolute(fileName) && !import_node_path56.default.normalize(fileName).startsWith("..")
21574
22136
  );
21575
22137
  return Array.from(new Set(files));
21576
22138
  } catch {
@@ -21588,7 +22150,7 @@ function composeFiles(repoPath) {
21588
22150
  ...configuredComposeFiles(repoPath)
21589
22151
  ];
21590
22152
  return Array.from(new Set(candidates)).filter(
21591
- (fileName) => import_node_fs44.default.existsSync(import_node_path55.default.join(repoPath, fileName))
22153
+ (fileName) => import_node_fs45.default.existsSync(import_node_path56.default.join(repoPath, fileName))
21592
22154
  );
21593
22155
  }
21594
22156
  function stringArray2(value) {
@@ -21626,7 +22188,7 @@ function inspectServices(repoPath) {
21626
22188
  const services = [];
21627
22189
  for (const fileName of composeFiles(repoPath)) {
21628
22190
  try {
21629
- const parsed = import_yaml7.default.parse(import_node_fs44.default.readFileSync(import_node_path55.default.join(repoPath, fileName), "utf-8"));
22191
+ const parsed = import_yaml7.default.parse(import_node_fs45.default.readFileSync(import_node_path56.default.join(repoPath, fileName), "utf-8"));
21630
22192
  const serviceMap = asRecord3(asRecord3(parsed)?.services);
21631
22193
  for (const [name, value] of Object.entries(serviceMap ?? {})) {
21632
22194
  const service = asRecord3(value);
@@ -21661,8 +22223,8 @@ function inspectServices(repoPath) {
21661
22223
  return services;
21662
22224
  }
21663
22225
  function inspectEnvFiles(repoPath) {
21664
- const files = import_node_fs44.default.readdirSync(repoPath).filter((fileName) => /^\.env(\.|$)/.test(fileName)).sort().map((fileName) => {
21665
- const content = import_node_fs44.default.readFileSync(import_node_path55.default.join(repoPath, fileName), "utf-8");
22226
+ const files = import_node_fs45.default.readdirSync(repoPath).filter((fileName) => /^\.env(\.|$)/.test(fileName)).sort().map((fileName) => {
22227
+ const content = import_node_fs45.default.readFileSync(import_node_path56.default.join(repoPath, fileName), "utf-8");
21666
22228
  const names = content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#") && line.includes("=")).map((line) => line.split("=")[0]?.trim()).filter((name) => Boolean(name)).sort();
21667
22229
  return { path: fileName, names };
21668
22230
  });
@@ -21676,18 +22238,18 @@ function inspectEnvFiles(repoPath) {
21676
22238
  };
21677
22239
  }
21678
22240
  function inspectDevcontainer(repoPath) {
21679
- const dir = import_node_path55.default.join(repoPath, ".devcontainer");
21680
- if (!import_node_fs44.default.existsSync(dir)) {
22241
+ const dir = import_node_path56.default.join(repoPath, ".devcontainer");
22242
+ if (!import_node_fs45.default.existsSync(dir)) {
21681
22243
  return { exists: false, files: [] };
21682
22244
  }
21683
22245
  return {
21684
22246
  exists: true,
21685
- files: import_node_fs44.default.readdirSync(dir).filter((fileName) => import_node_fs44.default.statSync(import_node_path55.default.join(dir, fileName)).isFile()).sort().map((fileName) => `.devcontainer/${fileName}`)
22247
+ files: import_node_fs45.default.readdirSync(dir).filter((fileName) => import_node_fs45.default.statSync(import_node_path56.default.join(dir, fileName)).isFile()).sort().map((fileName) => `.devcontainer/${fileName}`)
21686
22248
  };
21687
22249
  }
21688
22250
  function inspectDevrouter(repoPath) {
21689
22251
  const configPath = getRepoConfigPath(repoPath);
21690
- if (!import_node_fs44.default.existsSync(configPath)) {
22252
+ if (!import_node_fs45.default.existsSync(configPath)) {
21691
22253
  return {
21692
22254
  exists: false,
21693
22255
  configPath,
@@ -21731,15 +22293,15 @@ function inspectAgentGuidance(repoPath) {
21731
22293
  ["AGENTS.md", "agents"],
21732
22294
  ["CLAUDE.md", "claude"]
21733
22295
  ]) {
21734
- if (import_node_fs44.default.existsSync(import_node_path55.default.join(repoPath, fileName))) {
22296
+ if (import_node_fs45.default.existsSync(import_node_path56.default.join(repoPath, fileName))) {
21735
22297
  results.push({ path: fileName, kind });
21736
22298
  }
21737
22299
  }
21738
- const skillsDir = import_node_path55.default.join(repoPath, ".agents", "skills");
21739
- if (import_node_fs44.default.existsSync(skillsDir)) {
21740
- for (const name of import_node_fs44.default.readdirSync(skillsDir).sort()) {
21741
- const skillPath = import_node_path55.default.join(skillsDir, name, "SKILL.md");
21742
- if (import_node_fs44.default.existsSync(skillPath)) {
22300
+ const skillsDir = import_node_path56.default.join(repoPath, ".agents", "skills");
22301
+ if (import_node_fs45.default.existsSync(skillsDir)) {
22302
+ for (const name of import_node_fs45.default.readdirSync(skillsDir).sort()) {
22303
+ const skillPath = import_node_path56.default.join(skillsDir, name, "SKILL.md");
22304
+ if (import_node_fs45.default.existsSync(skillPath)) {
21743
22305
  results.push({ path: relative(repoPath, skillPath), kind: "skill" });
21744
22306
  }
21745
22307
  }
@@ -21782,7 +22344,7 @@ function buildIssues(report) {
21782
22344
  }
21783
22345
  function inspectRepo(options = {}) {
21784
22346
  const repoPath = resolveRepoPath(options.repo);
21785
- const pkg = readJson(import_node_path55.default.join(repoPath, "package.json"));
22347
+ const pkg = readJson(import_node_path56.default.join(repoPath, "package.json"));
21786
22348
  const scripts = inspectScripts(pkg);
21787
22349
  const reportWithoutIssues = {
21788
22350
  repoPath,
@@ -21801,12 +22363,12 @@ function inspectRepo(options = {}) {
21801
22363
  issues: buildIssues(reportWithoutIssues)
21802
22364
  };
21803
22365
  }
21804
- var import_node_fs44, import_node_path55, import_yaml7;
22366
+ var import_node_fs45, import_node_path56, import_yaml7;
21805
22367
  var init_repo_inspect = __esm({
21806
22368
  "src/core/repo-inspect.ts"() {
21807
22369
  "use strict";
21808
- import_node_fs44 = __toESM(require("fs"));
21809
- import_node_path55 = __toESM(require("path"));
22370
+ import_node_fs45 = __toESM(require("fs"));
22371
+ import_node_path56 = __toESM(require("path"));
21810
22372
  import_yaml7 = __toESM(require("yaml"));
21811
22373
  init_repo_config();
21812
22374
  }
@@ -21911,7 +22473,7 @@ function requiredFileChecks(repoPath) {
21911
22473
  ".devcontainer/docker-compose.yml",
21912
22474
  ".devrouter.yml"
21913
22475
  ];
21914
- const missing = required.filter((fileName) => !import_node_fs45.default.existsSync(import_node_path56.default.join(repoPath, fileName)));
22476
+ const missing = required.filter((fileName) => !import_node_fs46.default.existsSync(import_node_path57.default.join(repoPath, fileName)));
21915
22477
  return {
21916
22478
  id: "repo.devcontainer.verify-files",
21917
22479
  level: missing.length === 0 ? "ok" : "error",
@@ -22136,12 +22698,12 @@ async function verifyDevcontainer(options = {}) {
22136
22698
  nextSteps: collectNextSteps3(checks)
22137
22699
  };
22138
22700
  }
22139
- var import_node_fs45, import_node_path56;
22701
+ var import_node_fs46, import_node_path57;
22140
22702
  var init_devcontainer_verify = __esm({
22141
22703
  "src/core/devcontainer-verify.ts"() {
22142
22704
  "use strict";
22143
- import_node_fs45 = __toESM(require("fs"));
22144
- import_node_path56 = __toESM(require("path"));
22705
+ import_node_fs46 = __toESM(require("fs"));
22706
+ import_node_path57 = __toESM(require("path"));
22145
22707
  init_capabilities();
22146
22708
  init_doctor();
22147
22709
  init_host_routes();
@@ -22438,7 +23000,7 @@ function packageManagerIssues(repo) {
22438
23000
  }
22439
23001
  function plannedFiles(repoPath, version) {
22440
23002
  const repo = inspectRepo({ repo: repoPath });
22441
- const projectName = sanitizeProjectName(import_node_path57.default.basename(repo.repoPath));
23003
+ const projectName = sanitizeProjectName(import_node_path58.default.basename(repo.repoPath));
22442
23004
  const nodeMajor = majorVersion(repo.node?.version, "24");
22443
23005
  const pnpmVersion = repo.packageManager?.name === "pnpm" && repo.packageManager.version ? repo.packageManager.version : DEFAULT_PNPM_VERSION;
22444
23006
  const port = inferPort2(repo);
@@ -22483,8 +23045,8 @@ function plannedFiles(repoPath, version) {
22483
23045
  };
22484
23046
  }
22485
23047
  function classifyFile(repoPath, file) {
22486
- const absolutePath = import_node_path57.default.join(repoPath, file.relativePath);
22487
- if (!import_node_fs46.default.existsSync(absolutePath)) {
23048
+ const absolutePath = import_node_path58.default.join(repoPath, file.relativePath);
23049
+ if (!import_node_fs47.default.existsSync(absolutePath)) {
22488
23050
  return {
22489
23051
  path: file.relativePath,
22490
23052
  action: "create",
@@ -22492,7 +23054,7 @@ function classifyFile(repoPath, file) {
22492
23054
  bytes: Buffer.byteLength(file.content)
22493
23055
  };
22494
23056
  }
22495
- const current = import_node_fs46.default.readFileSync(absolutePath, "utf-8");
23057
+ const current = import_node_fs47.default.readFileSync(absolutePath, "utf-8");
22496
23058
  if (!current.includes(MANAGED_MARKER2)) {
22497
23059
  return {
22498
23060
  path: file.relativePath,
@@ -22549,11 +23111,11 @@ function buildPlan(repoPath, dryRun, version) {
22549
23111
  };
22550
23112
  }
22551
23113
  function writeFile(repoPath, file) {
22552
- const absolutePath = import_node_path57.default.join(repoPath, file.relativePath);
22553
- import_node_fs46.default.mkdirSync(import_node_path57.default.dirname(absolutePath), { recursive: true });
22554
- import_node_fs46.default.writeFileSync(absolutePath, file.content, "utf-8");
23114
+ const absolutePath = import_node_path58.default.join(repoPath, file.relativePath);
23115
+ import_node_fs47.default.mkdirSync(import_node_path58.default.dirname(absolutePath), { recursive: true });
23116
+ import_node_fs47.default.writeFileSync(absolutePath, file.content, "utf-8");
22555
23117
  if (file.executable) {
22556
- import_node_fs46.default.chmodSync(absolutePath, 493);
23118
+ import_node_fs47.default.chmodSync(absolutePath, 493);
22557
23119
  }
22558
23120
  }
22559
23121
  function writeDevcontainer(options = {}) {
@@ -22591,12 +23153,12 @@ function writeDevcontainer(options = {}) {
22591
23153
  nextSteps: postWriteNextSteps(repoPath)
22592
23154
  };
22593
23155
  }
22594
- var import_node_fs46, import_node_path57, MANAGED_MARKER2, DEFAULT_DEVROUTER_VERSION, DEFAULT_PNPM_VERSION, VALID_PACKAGE_VERSION_RE;
23156
+ var import_node_fs47, import_node_path58, MANAGED_MARKER2, DEFAULT_DEVROUTER_VERSION, DEFAULT_PNPM_VERSION, VALID_PACKAGE_VERSION_RE;
22595
23157
  var init_devcontainer_write = __esm({
22596
23158
  "src/core/devcontainer-write.ts"() {
22597
23159
  "use strict";
22598
- import_node_fs46 = __toESM(require("fs"));
22599
- import_node_path57 = __toESM(require("path"));
23160
+ import_node_fs47 = __toESM(require("fs"));
23161
+ import_node_path58 = __toESM(require("path"));
22600
23162
  init_repo_config();
22601
23163
  init_repo_inspect();
22602
23164
  MANAGED_MARKER2 = "devrouter:managed devcontainer";
@@ -22930,13 +23492,13 @@ function buildProfilePlanReport(options) {
22930
23492
  return { ...report, contractPath, bindings };
22931
23493
  }
22932
23494
  function loadContract(repoPath, contractPath) {
22933
- if (import_node_path58.default.isAbsolute(contractPath)) {
23495
+ if (import_node_path59.default.isAbsolute(contractPath)) {
22934
23496
  fail3("profile plan contract path must be relative to the repository.");
22935
23497
  }
22936
23498
  const absolutePath = assertPathWithinRepo(contractPath, repoPath, "Profile plan contract");
22937
23499
  let metadata;
22938
23500
  try {
22939
- metadata = import_node_fs47.default.lstatSync(absolutePath);
23501
+ metadata = import_node_fs48.default.lstatSync(absolutePath);
22940
23502
  } catch (error) {
22941
23503
  fail3(`could not inspect profile plan contract '${contractPath}': ${error.message}`);
22942
23504
  }
@@ -22946,16 +23508,16 @@ function loadContract(repoPath, contractPath) {
22946
23508
  if (metadata.size > MAX_CONTRACT_BYTES) {
22947
23509
  fail3(`profile plan contract exceeds the ${MAX_CONTRACT_BYTES}-byte limit.`);
22948
23510
  }
22949
- const realRepo = import_node_fs47.default.realpathSync(repoPath);
22950
- const realContract = import_node_fs47.default.realpathSync(absolutePath);
23511
+ const realRepo = import_node_fs48.default.realpathSync(repoPath);
23512
+ const realContract = import_node_fs48.default.realpathSync(absolutePath);
22951
23513
  assertPathWithinRepo(realContract, realRepo, "Profile plan contract");
22952
- const source = import_node_fs47.default.readFileSync(absolutePath, "utf-8");
23514
+ const source = import_node_fs48.default.readFileSync(absolutePath, "utf-8");
22953
23515
  if (Buffer.byteLength(source, "utf-8") > MAX_CONTRACT_BYTES) {
22954
23516
  fail3(`profile plan contract exceeds the ${MAX_CONTRACT_BYTES}-byte limit.`);
22955
23517
  }
22956
23518
  return {
22957
23519
  contract: parseProfilePlanContract(source),
22958
- contractPath: import_node_path58.default.relative(repoPath, absolutePath).split(import_node_path58.default.sep).join("/")
23520
+ contractPath: import_node_path59.default.relative(repoPath, absolutePath).split(import_node_path59.default.sep).join("/")
22959
23521
  };
22960
23522
  }
22961
23523
  function resolveProfilePlan(options) {
@@ -22969,12 +23531,12 @@ function resolveProfilePlan(options) {
22969
23531
  contractPath: loaded.contractPath
22970
23532
  });
22971
23533
  }
22972
- var import_node_fs47, import_node_path58, import_yaml8, CONTRACT_VERSION, MAX_CONTRACT_BYTES, MAX_APP_MAPPINGS, MAX_BINDING_KEYS, MAX_BINDING_VALUES, MAX_LITERAL_LENGTH, BINDING_KEY;
23534
+ var import_node_fs48, import_node_path59, import_yaml8, CONTRACT_VERSION, MAX_CONTRACT_BYTES, MAX_APP_MAPPINGS, MAX_BINDING_KEYS, MAX_BINDING_VALUES, MAX_LITERAL_LENGTH, BINDING_KEY;
22973
23535
  var init_profile_plan = __esm({
22974
23536
  "src/core/profile-plan.ts"() {
22975
23537
  "use strict";
22976
- import_node_fs47 = __toESM(require("fs"));
22977
- import_node_path58 = __toESM(require("path"));
23538
+ import_node_fs48 = __toESM(require("fs"));
23539
+ import_node_path59 = __toESM(require("path"));
22978
23540
  import_yaml8 = require("yaml");
22979
23541
  init_paths();
22980
23542
  init_profile_resolution();
@@ -23006,7 +23568,7 @@ async function runProfileResolveCommand(options) {
23006
23568
  async function runProfilePlanCommand(options) {
23007
23569
  const report = resolveProfilePlan(options);
23008
23570
  if (options.output) {
23009
- writeFileAtomically(import_node_path59.default.resolve(options.output), `${JSON.stringify(report, null, 2)}
23571
+ writeFileAtomically(import_node_path60.default.resolve(options.output), `${JSON.stringify(report, null, 2)}
23010
23572
  `);
23011
23573
  }
23012
23574
  if (options.json) {
@@ -23042,14 +23604,14 @@ function printProfilePlanSummary(report, output2) {
23042
23604
  process.stdout.write(`Binding ${key}: ${renderValues(report.bindings[key] ?? [])}
23043
23605
  `);
23044
23606
  }
23045
- if (output2) process.stdout.write(`Output: ${import_node_path59.default.resolve(output2)}
23607
+ if (output2) process.stdout.write(`Output: ${import_node_path60.default.resolve(output2)}
23046
23608
  `);
23047
23609
  }
23048
- var import_node_path59;
23610
+ var import_node_path60;
23049
23611
  var init_profile = __esm({
23050
23612
  "src/commands/profile.ts"() {
23051
23613
  "use strict";
23052
- import_node_path59 = __toESM(require("path"));
23614
+ import_node_path60 = __toESM(require("path"));
23053
23615
  init_atomic_file();
23054
23616
  init_output();
23055
23617
  init_profile_plan();
@@ -23339,7 +23901,7 @@ function sanitizeRouterId(value) {
23339
23901
  return value.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
23340
23902
  }
23341
23903
  function repoHash(repoPath) {
23342
- return (0, import_node_crypto26.createHash)("sha1").update(import_node_path60.default.resolve(repoPath)).digest("hex").slice(0, 12);
23904
+ return (0, import_node_crypto26.createHash)("sha1").update(import_node_path61.default.resolve(repoPath)).digest("hex").slice(0, 12);
23343
23905
  }
23344
23906
  function asDockerApp(app) {
23345
23907
  return app.runtime === "docker";
@@ -23418,11 +23980,11 @@ function prepareDockerOverlay(repoPath, appName, apps, publishTcpPorts = false)
23418
23980
  if (dockerApps.length === 0) {
23419
23981
  throw new Error("No docker apps selected to prepare compose overlay.");
23420
23982
  }
23421
- const cachePath = import_node_path60.default.join(CACHE_DIR, repoHash(repoPath), sanitizeRouterId(appName));
23422
- import_node_fs48.default.mkdirSync(cachePath, { recursive: true });
23423
- const overlayPath = import_node_path60.default.join(cachePath, "compose.devrouter.yml");
23983
+ const cachePath = import_node_path61.default.join(CACHE_DIR, repoHash(repoPath), sanitizeRouterId(appName));
23984
+ import_node_fs49.default.mkdirSync(cachePath, { recursive: true });
23985
+ const overlayPath = import_node_path61.default.join(cachePath, "compose.devrouter.yml");
23424
23986
  const overlayDocument = buildOverlayDocument(dockerApps, publishTcpPorts);
23425
- import_node_fs48.default.writeFileSync(overlayPath, import_yaml9.default.stringify(overlayDocument, { lineWidth: 0 }), "utf-8");
23987
+ import_node_fs49.default.writeFileSync(overlayPath, import_yaml9.default.stringify(overlayDocument, { lineWidth: 0 }), "utf-8");
23426
23988
  return {
23427
23989
  overlayPath,
23428
23990
  composeFiles: ensureComposeFiles(dockerApps),
@@ -23542,14 +24104,14 @@ function queryMappedPort(repoPath, composeFiles2, overlayPath, service, internal
23542
24104
  const port = Number(match[1]);
23543
24105
  return Number.isInteger(port) && port > 0 ? port : void 0;
23544
24106
  }
23545
- var import_node_child_process33, import_node_crypto26, import_node_fs48, import_node_path60, import_yaml9;
24107
+ var import_node_child_process33, import_node_crypto26, import_node_fs49, import_node_path61, import_yaml9;
23546
24108
  var init_docker_run = __esm({
23547
24109
  "src/core/docker-run.ts"() {
23548
24110
  "use strict";
23549
24111
  import_node_child_process33 = require("child_process");
23550
24112
  import_node_crypto26 = require("crypto");
23551
- import_node_fs48 = __toESM(require("fs"));
23552
- import_node_path60 = __toESM(require("path"));
24113
+ import_node_fs49 = __toESM(require("fs"));
24114
+ import_node_path61 = __toESM(require("path"));
23553
24115
  import_yaml9 = __toESM(require("yaml"));
23554
24116
  init_docker_error_guidance();
23555
24117
  init_paths();
@@ -24371,13 +24933,13 @@ function measureWorktreeConsumption(worktreePath, options) {
24371
24933
  const startedAt = Date.now();
24372
24934
  let rootStat;
24373
24935
  try {
24374
- rootStat = import_node_fs49.default.lstatSync(worktreePath);
24936
+ rootStat = import_node_fs50.default.lstatSync(worktreePath);
24375
24937
  } catch (error) {
24376
24938
  return { status: "unknown", reason: describeError(error, worktreePath) };
24377
24939
  }
24378
24940
  let rootEntries;
24379
24941
  try {
24380
- rootEntries = import_node_fs49.default.readdirSync(worktreePath, { withFileTypes: true });
24942
+ rootEntries = import_node_fs50.default.readdirSync(worktreePath, { withFileTypes: true });
24381
24943
  } catch (error) {
24382
24944
  return { status: "unknown", reason: describeError(error, worktreePath) };
24383
24945
  }
@@ -24403,10 +24965,10 @@ function measureWorktreeConsumption(worktreePath, options) {
24403
24965
  timedOut = true;
24404
24966
  break;
24405
24967
  }
24406
- const entryPath = import_node_path61.default.join(dirPath, entry.name);
24968
+ const entryPath = import_node_path62.default.join(dirPath, entry.name);
24407
24969
  let entryStat;
24408
24970
  try {
24409
- entryStat = import_node_fs49.default.lstatSync(entryPath);
24971
+ entryStat = import_node_fs50.default.lstatSync(entryPath);
24410
24972
  } catch (error) {
24411
24973
  if (error?.code === "ENOENT") continue;
24412
24974
  unreadableReason = describeIncompleteWalk(error);
@@ -24416,7 +24978,7 @@ function measureWorktreeConsumption(worktreePath, options) {
24416
24978
  if (!entryStat.isDirectory()) continue;
24417
24979
  let childEntries;
24418
24980
  try {
24419
- childEntries = import_node_fs49.default.readdirSync(entryPath, { withFileTypes: true });
24981
+ childEntries = import_node_fs50.default.readdirSync(entryPath, { withFileTypes: true });
24420
24982
  } catch (error) {
24421
24983
  unreadableReason = describeIncompleteWalk(error);
24422
24984
  break;
@@ -24480,12 +25042,12 @@ function describeError(error, worktreePath) {
24480
25042
  }
24481
25043
  return `could not stat worktree path '${worktreePath}': ${error?.message ?? String(error)}`;
24482
25044
  }
24483
- var import_node_fs49, import_node_path61, DEFAULT_DEADLINE_MS, BLOCK_SIZE_BYTES;
25045
+ var import_node_fs50, import_node_path62, DEFAULT_DEADLINE_MS, BLOCK_SIZE_BYTES;
24484
25046
  var init_workspace_consumption = __esm({
24485
25047
  "src/core/workspace-consumption.ts"() {
24486
25048
  "use strict";
24487
- import_node_fs49 = __toESM(require("fs"));
24488
- import_node_path61 = __toESM(require("path"));
25049
+ import_node_fs50 = __toESM(require("fs"));
25050
+ import_node_path62 = __toESM(require("path"));
24489
25051
  init_devpod_environment();
24490
25052
  DEFAULT_DEADLINE_MS = 1e4;
24491
25053
  BLOCK_SIZE_BYTES = 512;
@@ -24586,7 +25148,7 @@ function evaluateWorkspaceActivity(evidence, cutoff) {
24586
25148
  }
24587
25149
  function readGitSnapshot(worktree, commandRunner) {
24588
25150
  const comparablePath = comparableWorkspacePath(worktree.path);
24589
- if (worktree.prunable || !import_node_fs50.default.existsSync(comparablePath)) {
25151
+ if (worktree.prunable || !import_node_fs51.default.existsSync(comparablePath)) {
24590
25152
  return { worktree, checkout: "missing", head: null, committerDate: null };
24591
25153
  }
24592
25154
  const head = gitOutput(comparablePath, ["rev-parse", "--verify", "HEAD"], commandRunner);
@@ -25111,7 +25673,7 @@ function buildWorkspaceCleanupReport(options = {}, dependencies = {}) {
25111
25673
  containers2 = measureContainersFn(worktreePaths);
25112
25674
  } catch (error) {
25113
25675
  const reason = `container measurement failed: ${describeCause(error)}`;
25114
- containers2 = new Map(worktreePaths.map((path62) => [path62, unknownContainers(reason)]));
25676
+ containers2 = new Map(worktreePaths.map((path63) => [path63, unknownContainers(reason)]));
25115
25677
  }
25116
25678
  }
25117
25679
  const rows2 = records.map((record5) => {
@@ -25161,12 +25723,12 @@ function buildWorkspaceCleanupReport(options = {}, dependencies = {}) {
25161
25723
  networkCapacity: (dependencies.inspectNetworkCapacity ?? inspectNetworkCapacity)()
25162
25724
  };
25163
25725
  }
25164
- var import_node_child_process35, import_node_fs50, DEFAULT_INACTIVE_FOR, READ_ONLY_GIT_ENV2, ACTIVITY_SOURCES, defaultCommandRunner;
25726
+ var import_node_child_process35, import_node_fs51, DEFAULT_INACTIVE_FOR, READ_ONLY_GIT_ENV2, ACTIVITY_SOURCES, defaultCommandRunner;
25165
25727
  var init_workspace_cleanup = __esm({
25166
25728
  "src/core/workspace-cleanup.ts"() {
25167
25729
  "use strict";
25168
25730
  import_node_child_process35 = require("child_process");
25169
- import_node_fs50 = __toESM(require("fs"));
25731
+ import_node_fs51 = __toESM(require("fs"));
25170
25732
  init_devpod_workspaces();
25171
25733
  init_host_routes();
25172
25734
  init_network_diagnostics();
@@ -25278,7 +25840,7 @@ function warnMissingWorkspaceOwnership(repoPath) {
25278
25840
  );
25279
25841
  }
25280
25842
  function defaultWorktreePath(mainRepo, ws) {
25281
- return import_node_path62.default.join(mainRepo, "trees", ws);
25843
+ return import_node_path63.default.join(mainRepo, "trees", ws);
25282
25844
  }
25283
25845
  function assertDefaultWorktreeRootIgnored(mainRepo) {
25284
25846
  const ignored = (0, import_node_child_process36.spawnSync)("git", ["-C", mainRepo, "check-ignore", "-q", "--no-index", "trees/"], {
@@ -25286,12 +25848,12 @@ function assertDefaultWorktreeRootIgnored(mainRepo) {
25286
25848
  });
25287
25849
  if (ignored.status !== 0) {
25288
25850
  throw new Error(
25289
- `Default worktree root '${import_node_path62.default.join(mainRepo, "trees")}' is not ignored. Add 'trees/' to '${import_node_path62.default.join(mainRepo, ".gitignore")}' or use --path.`
25851
+ `Default worktree root '${import_node_path63.default.join(mainRepo, "trees")}' is not ignored. Add 'trees/' to '${import_node_path63.default.join(mainRepo, ".gitignore")}' or use --path.`
25290
25852
  );
25291
25853
  }
25292
25854
  }
25293
25855
  function legacyDefaultWorktreePath(mainRepo, ws) {
25294
- return import_node_path62.default.join(import_node_path62.default.dirname(mainRepo), `${import_node_path62.default.basename(mainRepo)}-${ws}`);
25856
+ return import_node_path63.default.join(import_node_path63.default.dirname(mainRepo), `${import_node_path63.default.basename(mainRepo)}-${ws}`);
25295
25857
  }
25296
25858
  function teardownFallbackPath(mainRepo, workspace) {
25297
25859
  const candidates = [
@@ -25403,7 +25965,7 @@ function assertFullDownPreflight(mainRepo, target) {
25403
25965
  if (sameWorkspacePath(target.worktreePath, mainRepo)) {
25404
25966
  throw new Error("Refusing to remove the primary Git checkout.");
25405
25967
  }
25406
- if (!target.worktree || target.worktree.prunable || !import_node_fs51.default.existsSync(target.worktreePath)) return;
25968
+ if (!target.worktree || target.worktree.prunable || !import_node_fs52.default.existsSync(target.worktreePath)) return;
25407
25969
  if (target.worktree.locked) {
25408
25970
  throw new Error(
25409
25971
  `Worktree '${target.worktreePath}' is locked; unlock it before workspace down.`
@@ -25443,9 +26005,9 @@ async function workspaceUp(branch, opts = {}) {
25443
26005
  (candidate) => defaultWorktreePath(mainRepo, candidate)
25444
26006
  );
25445
26007
  const generatedPath = candidatePaths.find(
25446
- (candidate) => !import_node_fs51.default.existsSync(candidate) && !worktrees.some((worktree) => sameWorkspacePath(worktree.path, candidate))
26008
+ (candidate) => !import_node_fs52.default.existsSync(candidate) && !worktrees.some((worktree) => sameWorkspacePath(worktree.path, candidate))
25447
26009
  );
25448
- const selectedPath = opts.path ? import_node_path62.default.resolve(opts.path) : existingBranch?.path ?? generatedPath;
26010
+ const selectedPath = opts.path ? import_node_path63.default.resolve(opts.path) : existingBranch?.path ?? generatedPath;
25449
26011
  if (!selectedPath) {
25450
26012
  throw new Error(`Could not allocate a collision-safe worktree path for branch '${branch}'.`);
25451
26013
  }
@@ -25454,7 +26016,7 @@ async function workspaceUp(branch, opts = {}) {
25454
26016
  `Branch '${branch}' already uses worktree '${existingBranch.path}', not '${selectedPath}'.`
25455
26017
  );
25456
26018
  }
25457
- if (import_node_fs51.default.existsSync(selectedPath)) {
26019
+ if (import_node_fs52.default.existsSync(selectedPath)) {
25458
26020
  const registered = worktrees.find(
25459
26021
  (worktree) => sameWorkspacePath(worktree.path, selectedPath)
25460
26022
  );
@@ -25557,7 +26119,7 @@ async function mutateWorkspaceRuntime(action2, resolved, worktrees, quiet = fals
25557
26119
  );
25558
26120
  }
25559
26121
  const retained = action2 === "stop" ? readManagedRuntimeState(resolved.worktreePath, resolved.workspace) : void 0;
25560
- let absent = false;
26122
+ let absent = mutation.status === "proven-absent";
25561
26123
  const remove = () => {
25562
26124
  if (retained?.stopBaseline) {
25563
26125
  absent = proveManagedStop(retained).status === "proven-absent";
@@ -25566,6 +26128,11 @@ async function mutateWorkspaceRuntime(action2, resolved, worktrees, quiet = fals
25566
26128
  ))
25567
26129
  throw new Error("Absent stop contains routes outside the retained desired set.");
25568
26130
  }
26131
+ if (absent && !retained) {
26132
+ if (listRoutesForWorktreePath(resolved.worktreePath).length)
26133
+ throw new Error("Initial managed stop observed remaining workspace routes.");
26134
+ return [];
26135
+ }
25569
26136
  claimLifecycleEffect();
25570
26137
  return removeWorkspaceRoutesForWorktree(resolved.workspace, resolved.worktreePath);
25571
26138
  };
@@ -25601,7 +26168,8 @@ async function runWorkspaceLifecycle(action2, target, opts = {}) {
25601
26168
  workspace: resolved.workspace,
25602
26169
  devpodId: stopped.devpodId,
25603
26170
  freedRoutes: stopped.freedRoutes,
25604
- providerChanged: stopped.stopped
26171
+ providerChanged: stopped.stopped && !stopped.runtimeAbsent,
26172
+ ...stopped.runtimeAbsent ? { runtimeAbsent: true } : {}
25605
26173
  };
25606
26174
  }
25607
26175
  const operation = async () => {
@@ -25611,7 +26179,7 @@ async function runWorkspaceLifecycle(action2, target, opts = {}) {
25611
26179
  }
25612
26180
  const result = await mutateWorkspaceRuntime("delete", resolved, worktrees, opts.quiet);
25613
26181
  if (removeWorktree) {
25614
- if (resolved.worktree && !resolved.worktree.prunable && import_node_fs51.default.existsSync(resolved.worktreePath)) {
26182
+ if (resolved.worktree && !resolved.worktree.prunable && import_node_fs52.default.existsSync(resolved.worktreePath)) {
25615
26183
  const rm = (0, import_node_child_process36.spawnSync)("git", ["-C", mainRepo, "worktree", "remove", resolved.worktreePath], {
25616
26184
  encoding: "utf-8"
25617
26185
  });
@@ -25630,7 +26198,7 @@ async function runWorkspaceLifecycle(action2, target, opts = {}) {
25630
26198
  }
25631
26199
  return result;
25632
26200
  };
25633
- return resolved.worktree && !resolved.worktree.prunable && import_node_fs51.default.existsSync(resolved.worktreePath) ? withLifecycleOperationLock(resolved.worktreePath, operation) : operation();
26201
+ return resolved.worktree && !resolved.worktree.prunable && import_node_fs52.default.existsSync(resolved.worktreePath) ? withLifecycleOperationLock(resolved.worktreePath, operation) : operation();
25634
26202
  }
25635
26203
  async function workspaceStop(target, opts = {}) {
25636
26204
  return runWorkspaceLifecycle("stop", target, opts);
@@ -25638,13 +26206,13 @@ async function workspaceStop(target, opts = {}) {
25638
26206
  async function workspaceDown(target, opts = {}) {
25639
26207
  return runWorkspaceLifecycle("down", target, opts);
25640
26208
  }
25641
- var import_node_child_process36, import_node_fs51, import_node_path62, WORKSPACE_ALLOCATION_LOCK_WAIT_MS;
26209
+ var import_node_child_process36, import_node_fs52, import_node_path63, WORKSPACE_ALLOCATION_LOCK_WAIT_MS;
25642
26210
  var init_workspace_lifecycle = __esm({
25643
26211
  "src/core/workspace-lifecycle.ts"() {
25644
26212
  "use strict";
25645
26213
  import_node_child_process36 = require("child_process");
25646
- import_node_fs51 = __toESM(require("fs"));
25647
- import_node_path62 = __toESM(require("path"));
26214
+ import_node_fs52 = __toESM(require("fs"));
26215
+ import_node_path63 = __toESM(require("path"));
25648
26216
  init_devpod_mutation();
25649
26217
  init_devpod_workspaces();
25650
26218
  init_devsy_mutation();
@@ -25828,7 +26396,7 @@ var init_version = __esm({
25828
26396
 
25829
26397
  // src/cli.ts
25830
26398
  var import_commander = require("commander");
25831
- var CLI_VERSION = true ? "0.0.72" : "0.0.0-dev";
26399
+ var CLI_VERSION = true ? "0.0.74" : "0.0.0-dev";
25832
26400
  var VERSION_FLAGS = /* @__PURE__ */ new Set(["-V", "--version"]);
25833
26401
  function withErrorHandling(action2) {
25834
26402
  return async (...args) => {
@@ -26201,10 +26769,10 @@ workspaceCommand2.command("down").description("Delete a workspace's runtime and
26201
26769
  workspaceCommand2.command("journal").description("Inspect and recover workspace lifecycle journals").command("settle").description(
26202
26770
  "Settle an interrupted lifecycle operation as unobservable so ensure and stop can proceed"
26203
26771
  ).argument("[path]", "Checkout path (defaults to current directory)").option("--json", "Output JSON").action(
26204
- withErrorHandling(async (path62, _options, command) => {
26772
+ withErrorHandling(async (path63, _options, command) => {
26205
26773
  const options = command.opts();
26206
26774
  const { runWorkspaceJournalSettleCommand: runWorkspaceJournalSettleCommand2 } = await Promise.resolve().then(() => (init_workspace2(), workspace_exports));
26207
- await runWorkspaceJournalSettleCommand2({ ...path62 ? { path: path62 } : {}, ...options });
26775
+ await runWorkspaceJournalSettleCommand2({ ...path63 ? { path: path63 } : {}, ...options });
26208
26776
  })
26209
26777
  );
26210
26778
  function parseVersionRequest(argv) {