@devrouter/cli 0.0.79 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/devrouter.js CHANGED
@@ -4275,7 +4275,7 @@ function loadRepoConfig(repoPath, read2 = (file) => import_node_fs7.default.read
4275
4275
  const config = parseConfig(parsed ?? {}, configPath);
4276
4276
  const requiredVersion = config.devrouter?.version;
4277
4277
  if (requiredVersion && !hasWarnedVersionMismatch) {
4278
- const cliVersion = true ? "0.0.79" : "0.0.0-dev";
4278
+ const cliVersion = true ? "0.1.0" : "0.0.0-dev";
4279
4279
  if (cliVersion !== "0.0.0-dev" && compareSemver(requiredVersion, cliVersion) > 0) {
4280
4280
  hasWarnedVersionMismatch = true;
4281
4281
  process.stderr.write(
@@ -5299,6 +5299,8 @@ var init_capacity_store = __esm({
5299
5299
  file;
5300
5300
  establishedFile;
5301
5301
  observedLedger = false;
5302
+ /** Identity of the snapshot file read last, so a replacement cannot reuse its revision. */
5303
+ ledgerGeneration;
5302
5304
  readEstablished() {
5303
5305
  let descriptor;
5304
5306
  try {
@@ -5325,7 +5327,7 @@ var init_capacity_store = __esm({
5325
5327
  import_node_fs9.default.closeSync(descriptor);
5326
5328
  }
5327
5329
  }
5328
- read() {
5330
+ inspect() {
5329
5331
  const minimumRevision = this.minimumRevision();
5330
5332
  const established = this.readEstablished();
5331
5333
  let descriptor;
@@ -5336,9 +5338,9 @@ var init_capacity_store = __esm({
5336
5338
  );
5337
5339
  } catch (error) {
5338
5340
  if (error.code === "ENOENT") {
5339
- if (established || this.observedLedger || minimumRevision > 0)
5340
- throw new CapacityHistoryError("capacity-ledger-lost");
5341
- return { version: 1, revision: 0, reservations: [] };
5341
+ this.ledgerGeneration = void 0;
5342
+ if (established || this.observedLedger || minimumRevision > 0) return { kind: "absent" };
5343
+ return { kind: "pristine" };
5342
5344
  }
5343
5345
  throw error;
5344
5346
  }
@@ -5351,16 +5353,36 @@ var init_capacity_store = __esm({
5351
5353
  if (count > MAX_BYTES2) throw new Error("Capacity reservation snapshot exceeds byte limit.");
5352
5354
  const value = JSON.parse(buffer.subarray(0, count).toString("utf8"));
5353
5355
  validate(value);
5354
- if (value.revision < minimumRevision) throw new CapacityHistoryError("capacity-ledger-lost");
5356
+ this.ledgerGeneration = `${stat.dev}:${stat.ino}`;
5357
+ if (value.revision < minimumRevision) return { kind: "stale", revision: value.revision };
5355
5358
  this.observedLedger = true;
5356
- return value;
5359
+ return { kind: "intact", revision: value.revision, snapshot: value };
5357
5360
  } finally {
5358
5361
  import_node_fs9.default.closeSync(descriptor);
5359
5362
  }
5360
5363
  }
5364
+ read() {
5365
+ const state = this.inspect();
5366
+ if (state.kind === "intact") return state.snapshot;
5367
+ if (state.kind === "pristine") return { version: 1, revision: 0, reservations: [] };
5368
+ throw new CapacityHistoryError("capacity-ledger-lost");
5369
+ }
5361
5370
  establish() {
5362
5371
  if (!this.readEstablished()) this.write(this.establishedFile, '{"version":1}\n');
5363
- for (const file of [this.file, this.establishedFile, this.directory]) {
5372
+ this.syncDurability([this.file, this.establishedFile, this.directory]);
5373
+ }
5374
+ /**
5375
+ * Witness for a ledger that is positively absent. The marker records that
5376
+ * history existed here, so no later process can read the store as a fresh
5377
+ * install and start admitting; the snapshot stays absent until reconciliation.
5378
+ * The caller holds the capacity lock and has proven environment cessation.
5379
+ */
5380
+ establishMarkerOnly() {
5381
+ if (!this.readEstablished()) this.write(this.establishedFile, '{"version":1}\n');
5382
+ this.syncDurability([this.establishedFile, this.directory]);
5383
+ }
5384
+ syncDurability(files) {
5385
+ for (const file of files) {
5364
5386
  const descriptor = import_node_fs9.default.openSync(
5365
5387
  file,
5366
5388
  import_node_fs9.default.constants.O_RDONLY | import_node_fs9.default.constants.O_NOFOLLOW | import_node_fs9.default.constants.O_NONBLOCK
@@ -5376,15 +5398,90 @@ var init_capacity_store = __esm({
5376
5398
  }
5377
5399
  }
5378
5400
  readForMutation() {
5401
+ const expectedGeneration = this.ledgerGeneration;
5379
5402
  const snapshot = this.read();
5403
+ if (expectedGeneration !== void 0 && expectedGeneration !== this.ledgerGeneration)
5404
+ throw new CapacitySnapshotChangedError();
5380
5405
  if (this.observedLedger) this.establish();
5381
5406
  return snapshot;
5382
5407
  }
5383
5408
  commit(snapshot) {
5384
5409
  this.write(this.file, serializeSnapshot(snapshot));
5385
5410
  this.observedLedger = true;
5411
+ this.rememberGeneration();
5386
5412
  this.establish();
5387
5413
  }
5414
+ /** Remember the published snapshot identity for the next generation fence. */
5415
+ rememberGeneration() {
5416
+ try {
5417
+ const stat = import_node_fs9.default.statSync(this.file);
5418
+ this.ledgerGeneration = stat.isFile() ? `${stat.dev}:${stat.ino}` : void 0;
5419
+ } catch {
5420
+ this.ledgerGeneration = void 0;
5421
+ }
5422
+ }
5423
+ /**
5424
+ * Durable evidence that recorded history is gone, for a stop whose physical
5425
+ * cessation is already proven. The caller holds that proof; this writes no
5426
+ * row and asserts nothing about any other environment's charge.
5427
+ */
5428
+ recordLedgerLoss() {
5429
+ assertPrivateDirectory(this.directory);
5430
+ withFileLockSync(
5431
+ `${this.file}.lock`,
5432
+ { activity: "capacity loss witness", waitMs: 100 },
5433
+ () => {
5434
+ if (this.inspect().kind !== "absent")
5435
+ throw new CapacityHistoryError("capacity-ledger-lost");
5436
+ this.establishMarkerOnly();
5437
+ }
5438
+ );
5439
+ }
5440
+ /**
5441
+ * Replace a provably absent ledger with a fresh baseline under the capacity
5442
+ * lock. `plan` runs inside the lock with the authoritative classification and
5443
+ * re-verifies the caller's evidence, returning the baseline to write or
5444
+ * undefined to refuse without mutation. `confirm` runs in the same lock after
5445
+ * the write; returning false withdraws the fresh snapshot, so the store reads
5446
+ * as lost again instead of publishing a baseline the caller cannot stand behind.
5447
+ */
5448
+ reconcileLostHistory(plan, confirm) {
5449
+ assertPrivateDirectory(this.directory);
5450
+ return withFileLockSync(
5451
+ `${this.file}.lock`,
5452
+ { activity: "capacity reconciliation", waitMs: 100 },
5453
+ () => {
5454
+ const baseline = plan(this.inspect());
5455
+ if (!baseline) return { reconciled: false };
5456
+ if (!Number.isSafeInteger(baseline.revision) || baseline.revision < 1)
5457
+ throw new Error("Invalid capacity reconciliation revision.");
5458
+ const pools = structuredClone(baseline.pools ?? []);
5459
+ validatePools(pools);
5460
+ const snapshot = pools.length === 0 ? { version: 1, revision: baseline.revision, reservations: [] } : { version: 1, revision: baseline.revision, reservations: [], pools };
5461
+ validate(snapshot);
5462
+ this.commit(snapshot);
5463
+ if (confirm()) return { reconciled: true, revision: snapshot.revision };
5464
+ this.withdrawSnapshot();
5465
+ return { reconciled: false };
5466
+ }
5467
+ );
5468
+ }
5469
+ /** Remove a snapshot this store just wrote, keeping the loss witness durable. */
5470
+ withdrawSnapshot() {
5471
+ try {
5472
+ import_node_fs9.default.unlinkSync(this.file);
5473
+ } catch (error) {
5474
+ if (error.code !== "ENOENT") throw error;
5475
+ }
5476
+ this.observedLedger = false;
5477
+ this.ledgerGeneration = void 0;
5478
+ const directory = import_node_fs9.default.openSync(this.directory, "r");
5479
+ try {
5480
+ import_node_fs9.default.fsyncSync(directory);
5481
+ } finally {
5482
+ import_node_fs9.default.closeSync(directory);
5483
+ }
5484
+ }
5388
5485
  /**
5389
5486
  * Caller must revoke journal authority and prove exact physical cessation first.
5390
5487
  * An absent reservation still advances the revision to fence that proof.
@@ -6162,7 +6259,7 @@ var init_reliability_contract = __esm({
6162
6259
 
6163
6260
  // src/core/reliability-operation-store.ts
6164
6261
  function installedCliVersion() {
6165
- return true ? "0.0.79" : "0.0.0-dev";
6262
+ return true ? "0.1.0" : "0.0.0-dev";
6166
6263
  }
6167
6264
  function compareCliVersions(left, right) {
6168
6265
  const parts = (value) => value.split(".").map((part) => Number.parseInt(part, 10) || 0);
@@ -6480,16 +6577,26 @@ function validate2(record5, identity2) {
6480
6577
  function createLifecycleCapacityStore(directory = import_node_path13.default.join(DEVROUTER_HOME, "controller")) {
6481
6578
  return new CapacityStore(directory, void 0, () => {
6482
6579
  try {
6483
- let revision = 0;
6484
- for (const record5 of listReliabilityOperations()) {
6485
- if (record5.capacity) revision = Math.max(revision, record5.capacity.snapshotRevision ?? 1);
6486
- }
6487
- return revision;
6580
+ return listUnsettledCapacityBindings().floor;
6488
6581
  } catch {
6489
6582
  throw new CapacityHistoryError("capacity-history-unprovable");
6490
6583
  }
6491
6584
  });
6492
6585
  }
6586
+ function listUnsettledCapacityBindings() {
6587
+ let floor = 0;
6588
+ const bindings = [];
6589
+ for (const record5 of listReliabilityOperations()) {
6590
+ if (!record5.capacity) continue;
6591
+ floor = Math.max(floor, record5.capacity.snapshotRevision ?? 1);
6592
+ bindings.push({
6593
+ environmentId: record5.state.environmentId,
6594
+ repoPath: record5.identity.repoPath,
6595
+ workspace: record5.identity.workspace
6596
+ });
6597
+ }
6598
+ return { floor, bindings };
6599
+ }
6493
6600
  function assertCapacityEffect(record5, workerId, nowMs, directory = import_node_path13.default.join(DEVROUTER_HOME, "controller")) {
6494
6601
  if (record5.version === 1) return;
6495
6602
  const binding = record5.capacity;
@@ -9167,19 +9274,165 @@ function parseVersion2(output2) {
9167
9274
  /\bv?(\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?)\b/
9168
9275
  )?.[1];
9169
9276
  }
9170
- function compareVersionCore(left, right) {
9171
- const core = (value) => value.split(/[-+]/, 1)[0].split(".").map((part) => Number.parseInt(part, 10));
9172
- const leftParts = core(left);
9173
- const rightParts = core(right);
9174
- for (let index = 0; index < 3; index += 1) {
9175
- const difference = (leftParts[index] ?? 0) - (rightParts[index] ?? 0);
9176
- if (difference !== 0) return difference > 0 ? 1 : -1;
9277
+ function parseSemver(value) {
9278
+ const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(
9279
+ value.trim()
9280
+ );
9281
+ if (!match) return void 0;
9282
+ return {
9283
+ major: Number(match[1]),
9284
+ minor: Number(match[2]),
9285
+ patch: Number(match[3]),
9286
+ prerelease: match[4] ? match[4].split(".") : []
9287
+ };
9288
+ }
9289
+ function comparePrerelease(left, right) {
9290
+ if (left.length === 0 && right.length === 0) return 0;
9291
+ if (left.length === 0) return 1;
9292
+ if (right.length === 0) return -1;
9293
+ for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
9294
+ const leftPart = left[index];
9295
+ const rightPart = right[index];
9296
+ if (leftPart === void 0) return -1;
9297
+ if (rightPart === void 0) return 1;
9298
+ const leftNumeric = /^\d+$/.test(leftPart);
9299
+ const rightNumeric = /^\d+$/.test(rightPart);
9300
+ if (leftNumeric && rightNumeric) {
9301
+ const difference = Number(leftPart) - Number(rightPart);
9302
+ if (difference !== 0) return difference < 0 ? -1 : 1;
9303
+ continue;
9304
+ }
9305
+ if (leftNumeric) return -1;
9306
+ if (rightNumeric) return 1;
9307
+ if (leftPart !== rightPart) return leftPart < rightPart ? -1 : 1;
9177
9308
  }
9178
9309
  return 0;
9179
9310
  }
9311
+ function compareSemver2(left, right) {
9312
+ for (const part of ["major", "minor", "patch"]) {
9313
+ if (left[part] !== right[part]) return left[part] < right[part] ? -1 : 1;
9314
+ }
9315
+ return comparePrerelease(left.prerelease, right.prerelease);
9316
+ }
9317
+ function isSupportedDevsyVersion(version) {
9318
+ const parsed = version === void 0 ? void 0 : parseSemver(version);
9319
+ const minimum = parseSemver(SUPPORTED_DEVSY_MIN_VERSION);
9320
+ const maximum = parseSemver(SUPPORTED_DEVSY_MAX_VERSION);
9321
+ if (!parsed || !minimum || !maximum) return false;
9322
+ if (compareSemver2(parsed, minimum) < 0) return false;
9323
+ if (parsed.prerelease.length > 0 && parsed.major === maximum.major && parsed.minor === maximum.minor && parsed.patch === maximum.patch) {
9324
+ return false;
9325
+ }
9326
+ return compareSemver2(parsed, maximum) < 0;
9327
+ }
9328
+ function isDevsyAgentAsset(value) {
9329
+ if (!value || typeof value !== "object") return false;
9330
+ const asset = value;
9331
+ return typeof asset.githubAssetId === "number" && Number.isSafeInteger(asset.githubAssetId) && typeof asset.name === "string" && typeof asset.size === "number" && Number.isSafeInteger(asset.size) && asset.size > 0 && typeof asset.sha256 === "string" && /^[0-9a-f]{64}$/.test(asset.sha256) && typeof asset.url === "string" && asset.url.startsWith("https://");
9332
+ }
9333
+ function devsyAgentManifestPath(cacheRoot, devsyVersion) {
9334
+ return import_node_path22.default.join(cacheRoot, `v${devsyVersion}`, DEVSY_AGENT_MANIFEST_FILE);
9335
+ }
9336
+ function readManifestRecord(cacheRoot, devsyVersion) {
9337
+ try {
9338
+ const parsed = JSON.parse(
9339
+ import_node_fs16.default.readFileSync(devsyAgentManifestPath(cacheRoot, devsyVersion), "utf-8")
9340
+ );
9341
+ if (parsed.version !== 1 || parsed.devsyVersion !== devsyVersion) return void 0;
9342
+ if (parsed.origin !== "release") return void 0;
9343
+ if (!Array.isArray(parsed.assets) || parsed.assets.length === 0) return void 0;
9344
+ if (!parsed.assets.every(isDevsyAgentAsset)) return void 0;
9345
+ return { version: devsyVersion, origin: "release", assets: parsed.assets };
9346
+ } catch {
9347
+ return void 0;
9348
+ }
9349
+ }
9350
+ function writeManifestRecord(cacheRoot, manifest) {
9351
+ const target = devsyAgentManifestPath(cacheRoot, manifest.version);
9352
+ const directory = import_node_path22.default.dirname(target);
9353
+ import_node_fs16.default.mkdirSync(directory, { recursive: true, mode: 448 });
9354
+ const temporary = import_node_path22.default.join(
9355
+ directory,
9356
+ `.${DEVSY_AGENT_MANIFEST_FILE}.${process.pid}.${(0, import_node_crypto11.randomUUID)()}.tmp`
9357
+ );
9358
+ const record5 = {
9359
+ version: 1,
9360
+ devsyVersion: manifest.version,
9361
+ origin: "release",
9362
+ resolvedAt: (/* @__PURE__ */ new Date()).toISOString(),
9363
+ assets: [...manifest.assets]
9364
+ };
9365
+ try {
9366
+ import_node_fs16.default.writeFileSync(temporary, `${JSON.stringify(record5, null, 2)}
9367
+ `, { mode: 384 });
9368
+ import_node_fs16.default.renameSync(temporary, target);
9369
+ } finally {
9370
+ import_node_fs16.default.rmSync(temporary, { force: true });
9371
+ }
9372
+ }
9373
+ function pinnedManifest(version) {
9374
+ return version === PINNED_DEVSY_AGENT_VERSION ? { version, origin: "pinned", assets: DEVSY_AGENT_ASSETS } : void 0;
9375
+ }
9376
+ function manifestForVersion(resolved, version) {
9377
+ if (resolved.assets) return { version, origin: "pinned", assets: resolved.assets };
9378
+ return pinnedManifest(version) ?? readManifestRecord(resolved.cacheRoot, version);
9379
+ }
9380
+ async function resolveReleaseDevsyAgentManifest(version, fetcher) {
9381
+ const url = `${DEVSY_RELEASE_METADATA_BASE_URL}/${DEVSY_RELEASE_TAG_PREFIX}${version}`;
9382
+ let response;
9383
+ try {
9384
+ response = await fetcher(url, { headers: { accept: "application/vnd.github+json" } });
9385
+ } catch (error) {
9386
+ throw new Error(`Devsy ${version} release metadata could not be read: ${errorMessage(error)}`);
9387
+ }
9388
+ if (!response.ok) {
9389
+ throw new Error(`Devsy ${version} release metadata is unavailable (HTTP ${response.status}).`);
9390
+ }
9391
+ let payload;
9392
+ try {
9393
+ payload = await response.json();
9394
+ } catch (error) {
9395
+ throw new Error(`Devsy ${version} release metadata is not valid JSON: ${errorMessage(error)}`);
9396
+ }
9397
+ const release = payload;
9398
+ if (release?.tag_name !== `${DEVSY_RELEASE_TAG_PREFIX}${version}`) {
9399
+ throw new Error(`Devsy ${version} release metadata reported an unexpected tag.`);
9400
+ }
9401
+ if (!Array.isArray(release.assets)) {
9402
+ throw new Error(`Devsy ${version} release metadata reported no assets.`);
9403
+ }
9404
+ const assets = [];
9405
+ for (const entry of release.assets) {
9406
+ const candidate = entry ?? {};
9407
+ const name = candidate.name;
9408
+ if (typeof name !== "string" || !DEVSY_AGENT_ASSET_NAMES.some((assetName) => assetName === name))
9409
+ continue;
9410
+ const id5 = candidate.id;
9411
+ const size = candidate.size;
9412
+ const digest3 = candidate.digest;
9413
+ const downloadUrl = candidate.browser_download_url;
9414
+ if (typeof id5 !== "number" || !Number.isSafeInteger(id5) || typeof size !== "number" || !Number.isSafeInteger(size) || size <= 0) {
9415
+ throw new Error(`Devsy ${version} reported an unusable ${String(name)} asset.`);
9416
+ }
9417
+ const sha2562 = typeof digest3 === "string" && digest3.startsWith("sha256:") ? digest3.slice("sha256:".length).toLowerCase() : "";
9418
+ if (!/^[0-9a-f]{64}$/.test(sha2562)) {
9419
+ throw new Error(
9420
+ `Devsy ${version} does not publish a SHA-256 digest for ${String(name)}; Devrouter cannot verify this release.`
9421
+ );
9422
+ }
9423
+ if (typeof downloadUrl !== "string" || !downloadUrl.startsWith("https://")) {
9424
+ throw new Error(`Devsy ${version} reported an unusable ${String(name)} download URL.`);
9425
+ }
9426
+ assets.push({ githubAssetId: id5, name, size, sha256: sha2562, url: downloadUrl });
9427
+ }
9428
+ if (assets.length === 0) {
9429
+ throw new Error(`Devsy ${version} publishes no official Linux agent asset.`);
9430
+ }
9431
+ return { version, origin: "release", assets };
9432
+ }
9180
9433
  function devsyAgentRepairSuggestion(inspection) {
9181
9434
  if (inspection.state === "stale") {
9182
- return `Install Devsy ${SUPPORTED_DEVSY_VERSION} for a supported host, then run: ${DEVSY_AGENT_SETUP_COMMAND}`;
9435
+ return `Install a supported Devsy release (${SUPPORTED_DEVSY_RANGE}) for a supported host, then run: ${DEVSY_AGENT_SETUP_COMMAND}`;
9183
9436
  }
9184
9437
  return inspection.source === "explicit" ? `Fix or unset DEVSY_AGENT_BINARY, then run: ${DEVSY_AGENT_SETUP_COMMAND}` : `Run: ${DEVSY_AGENT_SETUP_COMMAND}`;
9185
9438
  }
@@ -9194,8 +9447,8 @@ function nativeAssetName(platform, arch) {
9194
9447
  if (arch === "x64") return "devsy-linux-amd64";
9195
9448
  return void 0;
9196
9449
  }
9197
- function managedBinaryPath(cacheRoot, asset) {
9198
- return import_node_path22.default.join(cacheRoot, `v${SUPPORTED_DEVSY_VERSION}`, asset.name);
9450
+ function managedBinaryPath(cacheRoot, devsyVersion, asset) {
9451
+ return import_node_path22.default.join(cacheRoot, `v${devsyVersion}`, asset.name);
9199
9452
  }
9200
9453
  function hashOpenFile(file) {
9201
9454
  const digest3 = (0, import_node_crypto11.createHash)("sha256");
@@ -9239,7 +9492,7 @@ function resolvedOptions(options) {
9239
9492
  versionOutput: options.versionOutput ?? installedVersionOutput(),
9240
9493
  cacheRoot: options.cacheRoot ?? import_node_path22.default.join(CACHE_DIR, "devsy", "agents"),
9241
9494
  lockPath: options.lockPath ?? DEVSY_AGENT_CACHE_LOCK,
9242
- assets: options.assets ?? DEVSY_AGENT_ASSETS,
9495
+ assets: options.assets,
9243
9496
  nativeAssetName: options.nativeAssetName
9244
9497
  };
9245
9498
  }
@@ -9248,48 +9501,67 @@ function inspectDevsyAgent(options = {}) {
9248
9501
  const installedVersion = parseVersion2(resolved.versionOutput);
9249
9502
  const explicitPath = resolved.env.DEVSY_AGENT_BINARY?.trim();
9250
9503
  const source = explicitPath ? "explicit" : "managed";
9251
- if (installedVersion === void 0 || installedVersion !== SUPPORTED_DEVSY_VERSION && compareVersionCore(installedVersion, SUPPORTED_DEVSY_VERSION) <= 0) {
9504
+ if (!installedVersion) {
9252
9505
  return {
9253
9506
  state: "stale",
9254
9507
  source,
9255
- reason: installedVersion ? `installed Devsy ${installedVersion} is not supported by this Devrouter release` : "the supported Devsy version is not available",
9256
- installedVersion
9508
+ reason: `the installed Devsy version could not be determined; Devrouter supports ${SUPPORTED_DEVSY_RANGE}`
9257
9509
  };
9258
9510
  }
9259
- if (explicitPath) {
9511
+ if (!isSupportedDevsyVersion(installedVersion)) {
9260
9512
  return {
9261
- ...inspectBinary(explicitPath, resolved.assets, "invalid"),
9513
+ state: "stale",
9262
9514
  source,
9263
- binaryPath: explicitPath,
9515
+ reason: `installed Devsy ${installedVersion} is outside the supported range ${SUPPORTED_DEVSY_RANGE}`,
9264
9516
  installedVersion
9265
9517
  };
9266
9518
  }
9267
- if (installedVersion !== SUPPORTED_DEVSY_VERSION) {
9519
+ const manifest = manifestForVersion(resolved, installedVersion);
9520
+ if (!manifest) {
9521
+ if (explicitPath) {
9522
+ return {
9523
+ ...inspectBinary(explicitPath, resolved.assets ?? DEVSY_AGENT_ASSETS, "invalid"),
9524
+ source,
9525
+ binaryPath: explicitPath,
9526
+ installedVersion
9527
+ };
9528
+ }
9268
9529
  return {
9269
9530
  state: "ready",
9270
9531
  source: "host",
9271
- reason: `installed Devsy ${installedVersion} is newer than the verified ${SUPPORTED_DEVSY_VERSION} agent; the host CLI governs its own agent`,
9532
+ reason: `no verified Devsy ${installedVersion} agent manifest is recorded yet; the host CLI governs its own agent`,
9272
9533
  installedVersion,
9273
- drift: { installed: installedVersion, supported: SUPPORTED_DEVSY_VERSION }
9534
+ drift: { installed: installedVersion, supported: PINNED_DEVSY_AGENT_VERSION }
9274
9535
  };
9275
9536
  }
9276
9537
  const expectedName = resolved.nativeAssetName ?? nativeAssetName(resolved.platform, resolved.arch);
9277
- const asset = resolved.assets.find((candidate) => candidate.name === expectedName);
9538
+ const asset = manifest.assets.find((candidate) => candidate.name === expectedName);
9278
9539
  if (!asset) {
9279
9540
  return {
9280
9541
  state: "stale",
9281
9542
  source,
9282
- reason: `this Devrouter release has no pinned Devsy agent for ${resolved.platform}/${resolved.arch}`,
9283
- installedVersion
9543
+ reason: `Devsy ${installedVersion} has no official agent for ${resolved.platform}/${resolved.arch}`,
9544
+ installedVersion,
9545
+ manifestOrigin: manifest.origin
9546
+ };
9547
+ }
9548
+ if (explicitPath) {
9549
+ return {
9550
+ ...inspectBinary(explicitPath, [asset], "invalid"),
9551
+ source,
9552
+ binaryPath: explicitPath,
9553
+ installedVersion,
9554
+ manifestOrigin: manifest.origin
9284
9555
  };
9285
9556
  }
9286
- const binaryPath = managedBinaryPath(resolved.cacheRoot, asset);
9557
+ const binaryPath = managedBinaryPath(resolved.cacheRoot, installedVersion, asset);
9287
9558
  return {
9288
9559
  ...inspectBinary(binaryPath, [asset], "missing"),
9289
9560
  source,
9290
9561
  binaryPath,
9291
9562
  asset,
9292
- installedVersion
9563
+ installedVersion,
9564
+ manifestOrigin: manifest.origin
9293
9565
  };
9294
9566
  }
9295
9567
  async function writeAll(handle, chunk, position) {
@@ -9392,7 +9664,7 @@ async function downloadAndPublish(asset, binaryPath, fetcher, githubCliDownloade
9392
9664
  const writeChunk = async (chunk) => {
9393
9665
  size += chunk.byteLength;
9394
9666
  if (size > asset.size) {
9395
- throw new Error("Devsy agent download exceeded the pinned size");
9667
+ throw new Error("Devsy agent download exceeded the expected size");
9396
9668
  }
9397
9669
  digest3.update(chunk);
9398
9670
  await writeAll(handle, chunk, size - chunk.byteLength);
@@ -9442,21 +9714,36 @@ async function downloadAndPublish(asset, binaryPath, fetcher, githubCliDownloade
9442
9714
  await import_node_fs16.default.promises.rm(temporaryPath, { force: true });
9443
9715
  }
9444
9716
  }
9717
+ function isReadyInspection(inspection) {
9718
+ return inspection.state === "ready" && inspection.installedVersion !== void 0;
9719
+ }
9720
+ function preparedFromInspection(inspection) {
9721
+ return {
9722
+ version: inspection.installedVersion,
9723
+ binaryPath: inspection.binaryPath,
9724
+ source: inspection.source,
9725
+ asset: inspection.asset,
9726
+ changed: false,
9727
+ transport: "existing"
9728
+ };
9729
+ }
9730
+ function expectedAgentName(resolved) {
9731
+ return resolved.nativeAssetName ?? nativeAssetName(resolved.platform, resolved.arch);
9732
+ }
9733
+ async function loadOrResolveManifest(resolved, version, options) {
9734
+ const manifest = manifestForVersion(resolved, version);
9735
+ if (manifest) return manifest;
9736
+ const release = await resolveReleaseDevsyAgentManifest(version, options.fetcher ?? fetch);
9737
+ writeManifestRecord(resolved.cacheRoot, release);
9738
+ return release;
9739
+ }
9445
9740
  async function prepareDevsyAgent(options = {}) {
9446
9741
  const resolved = resolvedOptions(options);
9447
9742
  const inspectOptions = resolved;
9448
9743
  const before = inspectDevsyAgent(inspectOptions);
9449
- if (before.state === "ready") {
9450
- return {
9451
- binaryPath: before.binaryPath,
9452
- source: before.source,
9453
- asset: before.asset,
9454
- changed: false,
9455
- transport: "existing"
9456
- };
9457
- }
9458
- if (before.source === "explicit" || before.state === "stale" || !before.binaryPath || !before.asset) {
9459
- throw new DevsyAgentReadinessError(before);
9744
+ if (before.state === "stale") throw new DevsyAgentReadinessError(before);
9745
+ if (isReadyInspection(before) && before.source !== "host") {
9746
+ return preparedFromInspection(before);
9460
9747
  }
9461
9748
  const lock = options.withLock ?? ((operation) => {
9462
9749
  import_node_fs16.default.mkdirSync(import_node_path22.default.dirname(resolved.lockPath), { recursive: true });
@@ -9473,38 +9760,42 @@ async function prepareDevsyAgent(options = {}) {
9473
9760
  });
9474
9761
  return lock(async () => {
9475
9762
  const current = inspectDevsyAgent(inspectOptions);
9476
- if (current.state === "ready") {
9477
- return {
9478
- binaryPath: current.binaryPath,
9763
+ if (current.state === "stale") throw new DevsyAgentReadinessError(current);
9764
+ if (isReadyInspection(current) && current.source !== "host") {
9765
+ return preparedFromInspection(current);
9766
+ }
9767
+ const version = current.installedVersion;
9768
+ if (!version) throw new DevsyAgentReadinessError(current);
9769
+ const manifest = await loadOrResolveManifest(resolved, version, options);
9770
+ if (current.source === "explicit") {
9771
+ const verified = inspectDevsyAgent(inspectOptions);
9772
+ if (isReadyInspection(verified)) return preparedFromInspection(verified);
9773
+ throw new DevsyAgentReadinessError(verified);
9774
+ }
9775
+ const asset = manifest.assets.find(
9776
+ (candidate) => candidate.name === expectedAgentName(resolved)
9777
+ );
9778
+ if (!asset) {
9779
+ throw new DevsyAgentReadinessError({
9780
+ state: "stale",
9479
9781
  source: current.source,
9480
- asset: current.asset,
9481
- changed: false,
9482
- transport: "existing"
9483
- };
9484
- }
9485
- if (current.source === "explicit" || current.state === "stale" || !current.binaryPath || !current.asset) {
9486
- throw new DevsyAgentReadinessError(current);
9782
+ reason: `Devsy ${version} has no official agent for ${resolved.platform}/${resolved.arch}`,
9783
+ installedVersion: version,
9784
+ manifestOrigin: manifest.origin
9785
+ });
9487
9786
  }
9488
9787
  const transport = await downloadAndPublish(
9489
- current.asset,
9490
- current.binaryPath,
9788
+ asset,
9789
+ managedBinaryPath(resolved.cacheRoot, version, asset),
9491
9790
  options.fetcher ?? fetch,
9492
9791
  options.githubCliDownloader ?? downloadWithGitHubCli
9493
9792
  );
9494
9793
  const published = inspectDevsyAgent(inspectOptions);
9495
- if (published.state !== "ready" || !published.binaryPath || !published.asset) {
9496
- throw new DevsyAgentReadinessError(published);
9497
- }
9498
- return {
9499
- binaryPath: published.binaryPath,
9500
- source: published.source,
9501
- asset: published.asset,
9502
- changed: true,
9503
- transport
9504
- };
9794
+ if (!isReadyInspection(published)) throw new DevsyAgentReadinessError(published);
9795
+ return { ...preparedFromInspection(published), changed: true, transport };
9505
9796
  });
9506
9797
  }
9507
- var import_node_child_process7, import_node_crypto11, import_node_fs16, import_node_path22, SUPPORTED_DEVSY_VERSION, DEVSY_AGENT_SETUP_COMMAND, DEVSY_AGENT_ASSETS, DevsyAgentReadinessError, DEVSY_AGENT_CACHE_LOCK, DEVSY_AGENT_LOCK_WAIT_MS, GITHUB_CLI_STDERR_TAIL_BYTES;
9798
+ var import_node_child_process7, import_node_crypto11, import_node_fs16, import_node_path22, PINNED_DEVSY_AGENT_VERSION, SUPPORTED_DEVSY_MIN_VERSION, SUPPORTED_DEVSY_MAX_VERSION, SUPPORTED_DEVSY_RANGE, DEVSY_AGENT_SETUP_COMMAND, DEVSY_RELEASE_TAG_PREFIX, DEVSY_RELEASE_METADATA_BASE_URL, DEVSY_AGENT_ASSETS, DevsyAgentReadinessError, DEVSY_AGENT_CACHE_LOCK, DEVSY_AGENT_LOCK_WAIT_MS, DEVSY_AGENT_MANIFEST_FILE, DEVSY_AGENT_ASSET_NAMES, GITHUB_CLI_STDERR_TAIL_BYTES;
9508
9799
  var init_devsy_agent = __esm({
9509
9800
  "src/core/devsy-agent.ts"() {
9510
9801
  "use strict";
@@ -9514,8 +9805,13 @@ var init_devsy_agent = __esm({
9514
9805
  import_node_path22 = __toESM(require("path"));
9515
9806
  init_file_lock();
9516
9807
  init_router();
9517
- SUPPORTED_DEVSY_VERSION = "1.16.2";
9808
+ PINNED_DEVSY_AGENT_VERSION = "1.16.2";
9809
+ SUPPORTED_DEVSY_MIN_VERSION = "1.16.2";
9810
+ SUPPORTED_DEVSY_MAX_VERSION = "2.0.0";
9811
+ SUPPORTED_DEVSY_RANGE = `>=${SUPPORTED_DEVSY_MIN_VERSION} <${SUPPORTED_DEVSY_MAX_VERSION}`;
9518
9812
  DEVSY_AGENT_SETUP_COMMAND = "devrouter setup --yes --workspace-runtime devsy";
9813
+ DEVSY_RELEASE_TAG_PREFIX = "v";
9814
+ DEVSY_RELEASE_METADATA_BASE_URL = "https://api.github.com/repos/devsy-org/devsy/releases/tags";
9519
9815
  DEVSY_AGENT_ASSETS = [
9520
9816
  {
9521
9817
  githubAssetId: 529830010,
@@ -9541,6 +9837,8 @@ var init_devsy_agent = __esm({
9541
9837
  };
9542
9838
  DEVSY_AGENT_CACHE_LOCK = import_node_path22.default.join(DEVROUTER_HOME, "devsy-agent-cache.lock");
9543
9839
  DEVSY_AGENT_LOCK_WAIT_MS = 18e5;
9840
+ DEVSY_AGENT_MANIFEST_FILE = "manifest.json";
9841
+ DEVSY_AGENT_ASSET_NAMES = ["devsy-linux-arm64", "devsy-linux-amd64"];
9544
9842
  GITHUB_CLI_STDERR_TAIL_BYTES = 4096;
9545
9843
  }
9546
9844
  });
@@ -13413,8 +13711,9 @@ function inspectNetworkProviderBinding(input2) {
13413
13711
  if (!/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}$/.test(providerContext)) throw new Error();
13414
13712
  const contextArgs = ["--context", providerContext];
13415
13713
  const version = read(provider, provider === "devsy" ? ["--version"] : ["version"]);
13416
- const expectedVersion = provider === "devsy" ? "1.16.2" : "0.6.15";
13417
- if (version.match(/\b(?:v)?(\d+\.\d+\.\d+)\b/)?.[1] !== expectedVersion) throw new Error();
13714
+ const parsedVersion = version.match(/\b(?:v)?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)\b/)?.[1];
13715
+ if (provider === "devsy" ? !isSupportedDevsyVersion(parsedVersion) : parsedVersion !== "0.6.15")
13716
+ throw new Error();
13418
13717
  const definitions = object3(
13419
13718
  JSON.parse(
13420
13719
  read(
@@ -13480,6 +13779,7 @@ var init_network_provider_inspect = __esm({
13480
13779
  import_node_child_process16 = require("child_process");
13481
13780
  import_node_crypto16 = require("crypto");
13482
13781
  init_devpod_environment();
13782
+ init_devsy_agent();
13483
13783
  }
13484
13784
  });
13485
13785
 
@@ -15255,6 +15555,9 @@ function holderAttribution(holder, repoPath) {
15255
15555
  return attribution;
15256
15556
  }
15257
15557
  function remediationFor(holder, attribution) {
15558
+ if (holder.name === ROUTER_CONTAINER_NAME) {
15559
+ return "The shared devrouter router owns this port as a platform entrypoint and must keep running; change the consumer's fixed host binding, or route the dependency through devrouter's TCP protocol route, then run ensure again.";
15560
+ }
15258
15561
  if (attribution.holderWorktreePath) {
15259
15562
  return `Stop the holding workspace with 'devrouter stop ${attribution.holderWorktreePath}', or override the consumer's published host binding through its own configuration, then run ensure again.`;
15260
15563
  }
@@ -15296,6 +15599,7 @@ var init_host_port_claims = __esm({
15296
15599
  import_node_path40 = __toESM(require("path"));
15297
15600
  init_devcontainer_profile();
15298
15601
  init_network_effect_scope();
15602
+ init_router();
15299
15603
  init_workspace();
15300
15604
  init_workspace_ownership();
15301
15605
  COMPOSE_RENDER_TIMEOUT_MS = 3e4;
@@ -20253,6 +20557,155 @@ var init_controller = __esm({
20253
20557
  }
20254
20558
  });
20255
20559
 
20560
+ // src/commands/capacity.ts
20561
+ var capacity_exports = {};
20562
+ __export(capacity_exports, {
20563
+ CapacityReconcileRefusal: () => CapacityReconcileRefusal,
20564
+ runCapacityCommand: () => runCapacityCommand
20565
+ });
20566
+ function shellQuote(value) {
20567
+ return "'" + value.split("'").join("'\\''") + "'";
20568
+ }
20569
+ function enumerateBindings() {
20570
+ try {
20571
+ return listUnsettledCapacityBindings();
20572
+ } catch {
20573
+ throw new CapacityReconcileRefusal("capacity-history-unprovable");
20574
+ }
20575
+ }
20576
+ async function observeDeclaredPools(directory) {
20577
+ const policy = readCapacityPolicy(directory);
20578
+ if (!policy) return [];
20579
+ const observed = [];
20580
+ const unresolved = [];
20581
+ for (const [runtimeDomain, domain] of Object.entries(policy.domains)) {
20582
+ if (domain.kind !== "runtime") continue;
20583
+ const controller = new AbortController();
20584
+ const timer = setTimeout(() => controller.abort(), POOL_PROBE_TIMEOUT_MS);
20585
+ try {
20586
+ const info = await readDockerCapacityInfo(domain.endpoint, controller.signal);
20587
+ if (info.ID !== domain.daemonId) throw new Error("Pool observation identity changed.");
20588
+ observed.push({
20589
+ daemonId: domain.daemonId,
20590
+ runtimeDomain,
20591
+ hostDomain: domain.hostDomain,
20592
+ hostChargeCeilingBytes: domain.hostChargeCeilingBytes
20593
+ });
20594
+ } catch {
20595
+ unresolved.push(runtimeDomain);
20596
+ } finally {
20597
+ clearTimeout(timer);
20598
+ }
20599
+ }
20600
+ if (unresolved.length > 0)
20601
+ throw new CapacityReconcileRefusal("capacity-pools-unresolved", { domains: unresolved });
20602
+ return observed;
20603
+ }
20604
+ function refusalLines(refusal) {
20605
+ const bindings = refusal.detail.bindings ?? [];
20606
+ const domains = refusal.detail.domains ?? [];
20607
+ switch (refusal.reason) {
20608
+ case "capacity-reconcile-confirmation-required":
20609
+ return "Refusing without --yes: reconciliation publishes a fresh empty capacity baseline.";
20610
+ case "capacity-history-intact":
20611
+ return "Capacity ledger history is intact or absent; there is no loss to reconcile.";
20612
+ case "capacity-charges-pending":
20613
+ return [
20614
+ "Capacity reconciliation is blocked by " + bindings.length + " unsettled capacity binding" + (bindings.length === 1 ? "" : "s") + ":",
20615
+ ...bindings.map(
20616
+ (binding) => " - " + binding.environmentId + (binding.workspace ? " (workspace " + binding.workspace + ")" : "") + ": run: devrouter stop " + shellQuote(binding.repoPath)
20617
+ )
20618
+ ].join("\n");
20619
+ case "capacity-pools-unresolved":
20620
+ return [
20621
+ "Runtime domain" + (domains.length === 1 ? "" : "s") + " could not be observed: " + domains.join(", ") + ".",
20622
+ "Start them so their pool ceiling can be recorded, or remove them from the capacity policy, then retry."
20623
+ ].join("\n");
20624
+ case "capacity-ledger-unusable":
20625
+ return "The capacity ledger exists but cannot satisfy the surviving lifecycle evidence; preserve it and investigate before retrying.";
20626
+ case "capacity-history-unprovable":
20627
+ return "Capacity history cannot be proven; inspect the private lifecycle journals before retrying.";
20628
+ }
20629
+ }
20630
+ async function runCapacityCommand(method, options, dependencies = {}) {
20631
+ if (method !== "reconcile") throw new Error(`Unsupported capacity command: ${method}`);
20632
+ const directory = dependencies.directory ?? import_node_path50.default.join(DEVROUTER_HOME, "controller");
20633
+ const json = Boolean(options.json);
20634
+ const emit = (payload, human) => {
20635
+ process.stdout.write(json ? `${JSON.stringify(payload, null, 2)}
20636
+ ` : `${human}
20637
+ `);
20638
+ };
20639
+ try {
20640
+ if (!options.yes)
20641
+ throw new CapacityReconcileRefusal("capacity-reconcile-confirmation-required");
20642
+ const store = dependencies.store ?? createLifecycleCapacityStore(directory);
20643
+ const preflight = store.inspect();
20644
+ if (preflight.kind === "intact" || preflight.kind === "pristine")
20645
+ throw new CapacityReconcileRefusal("capacity-history-intact");
20646
+ if (preflight.kind === "stale") throw new CapacityReconcileRefusal("capacity-ledger-unusable");
20647
+ const pending = enumerateBindings().bindings;
20648
+ if (pending.length > 0)
20649
+ throw new CapacityReconcileRefusal("capacity-charges-pending", { bindings: pending });
20650
+ const pools = await (dependencies.observePools ?? observeDeclaredPools)(directory);
20651
+ let appeared = [];
20652
+ const outcome = store.reconcileLostHistory(
20653
+ (state) => {
20654
+ if (state.kind === "intact" || state.kind === "pristine")
20655
+ throw new CapacityReconcileRefusal("capacity-history-intact");
20656
+ if (state.kind === "stale") throw new CapacityReconcileRefusal("capacity-ledger-unusable");
20657
+ const { floor, bindings } = enumerateBindings();
20658
+ if (bindings.length > 0)
20659
+ throw new CapacityReconcileRefusal("capacity-charges-pending", { bindings });
20660
+ return { revision: floor + 1, pools };
20661
+ },
20662
+ () => {
20663
+ appeared = enumerateBindings().bindings;
20664
+ return appeared.length === 0;
20665
+ }
20666
+ );
20667
+ if (!outcome.reconciled)
20668
+ throw new CapacityReconcileRefusal("capacity-charges-pending", { bindings: appeared });
20669
+ emit(
20670
+ { version: 1, ok: true, reconciled: true, revision: outcome.revision, unresolved: 0 },
20671
+ "Reconciled the lost capacity ledger: baseline revision " + outcome.revision + ", no charges carried. No journal-visible charge remained. Charges that were never journal-bound are not recoverable from the journals."
20672
+ );
20673
+ } catch (error) {
20674
+ if (!(error instanceof CapacityReconcileRefusal)) throw error;
20675
+ process.exitCode = 1;
20676
+ emit(
20677
+ {
20678
+ version: 1,
20679
+ ok: false,
20680
+ reason: error.reason,
20681
+ ...error.detail.bindings ? { bindings: error.detail.bindings } : {},
20682
+ ...error.detail.domains ? { domains: error.detail.domains } : {}
20683
+ },
20684
+ refusalLines(error)
20685
+ );
20686
+ }
20687
+ }
20688
+ var import_node_path50, POOL_PROBE_TIMEOUT_MS, CapacityReconcileRefusal;
20689
+ var init_capacity = __esm({
20690
+ "src/commands/capacity.ts"() {
20691
+ "use strict";
20692
+ import_node_path50 = __toESM(require("path"));
20693
+ init_capacity_docker_probe();
20694
+ init_capacity_policy();
20695
+ init_reliability_operation_store();
20696
+ init_router();
20697
+ POOL_PROBE_TIMEOUT_MS = 3e3;
20698
+ CapacityReconcileRefusal = class extends Error {
20699
+ constructor(reason, detail = {}) {
20700
+ super(reason);
20701
+ this.reason = reason;
20702
+ this.detail = detail;
20703
+ this.name = "CapacityReconcileRefusal";
20704
+ }
20705
+ };
20706
+ }
20707
+ });
20708
+
20256
20709
  // src/core/agents-md.ts
20257
20710
  function buildDevrouterSection() {
20258
20711
  return [
@@ -20292,13 +20745,13 @@ function replaceDevrouterSection(content) {
20292
20745
  return `${content.slice(0, start)}${buildDevrouterSection()}${content.slice(end)}`;
20293
20746
  }
20294
20747
  function writeRepoFile(repoPath, relPath, content) {
20295
- const absolutePath = (0, import_node_path50.join)(repoPath, relPath);
20296
- (0, import_node_fs38.mkdirSync)((0, import_node_path50.dirname)(absolutePath), { recursive: true });
20748
+ const absolutePath = (0, import_node_path51.join)(repoPath, relPath);
20749
+ (0, import_node_fs38.mkdirSync)((0, import_node_path51.dirname)(absolutePath), { recursive: true });
20297
20750
  (0, import_node_fs38.writeFileSync)(absolutePath, content, "utf-8");
20298
20751
  return absolutePath;
20299
20752
  }
20300
20753
  function ensureAgentsMdSection(repoPath) {
20301
- const filePath = (0, import_node_path50.join)(repoPath, AGENTS_MD);
20754
+ const filePath = (0, import_node_path51.join)(repoPath, AGENTS_MD);
20302
20755
  if ((0, import_node_fs38.existsSync)(filePath)) {
20303
20756
  const content = (0, import_node_fs38.readFileSync)(filePath, "utf-8");
20304
20757
  if (content.includes(DEVROUTER_SENTINEL)) {
@@ -20323,12 +20776,12 @@ function ensureSkillFile(repoPath) {
20323
20776
  const filePath = writeRepoFile(repoPath, DEVROUTER_SKILL_REL_PATH, DEVROUTER_SKILL_CONTENT);
20324
20777
  return { path: filePath, written: true };
20325
20778
  }
20326
- 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;
20779
+ var import_node_fs38, import_node_path51, DEVROUTER_SENTINEL, DEVROUTER_END_SENTINEL, LEGACY_DEVROUTER_SECTION_END, AGENTS_MD, DEVROUTER_SKILL_REL_PATH, DEVROUTER_SKILL_CONTENT;
20327
20780
  var init_agents_md = __esm({
20328
20781
  "src/core/agents-md.ts"() {
20329
20782
  "use strict";
20330
20783
  import_node_fs38 = require("fs");
20331
- import_node_path50 = require("path");
20784
+ import_node_path51 = require("path");
20332
20785
  DEVROUTER_SENTINEL = "<!-- devrouter -->";
20333
20786
  DEVROUTER_END_SENTINEL = "<!-- /devrouter -->";
20334
20787
  LEGACY_DEVROUTER_SECTION_END = "- `devrouter ls`";
@@ -20570,7 +21023,7 @@ Run several worktrees of one repo in parallel without host/route collisions. A *
20570
21023
  - **When active**: hosts auto-namespace (\`web.localhost\` \u2192 \`web.<ws>.localhost\`), \`\${WORKSPACE}\` in \`upstream\` is substituted with the token, and the docker \`router\` key is suffixed per workspace. Managed \`ensure\` rejects every HTTP/TCP proxy upstream outside that exact alias namespace before it mutates DevPod or routes. The runtime config is computed in memory only \u2014 the committed \`.devrouter.yml\` is never rewritten.
20571
21024
  - **TLS**: namespaced hosts (\`web.<ws>.localhost\`) are not covered by the \`*.localhost\` wildcard; devrouter auto-extends the mkcert cert SANs for active hosts when TLS is enabled.
20572
21025
  - **devcontainer integration**: managed scaffolds list the base compose file, then \`\${localEnv:DEVCONTAINER_COMPOSE_OVERLAY:docker-compose.default.yml}\`; custom repositories may keep another default overlay. Selecting \`.devcontainer/docker-compose.devrouter.yml\` for linked worktrees must pass \`WORKSPACE\` and \`DEVROUTER_WORKSPACE\` across the combined base/overlay config and bind-mount \`\${DEVROUTER_GIT_COMMON_DIR}\` to the same absolute app-container path. The app exposes \`\${WORKSPACE}-app\`; the proxy uses \`upstream: \${WORKSPACE}-app:<port>\`.
20573
- - **Lifecycle**: after one-time \`setup\`, use \`ensure .\` for both primary and linked checkouts; never branch manually on checkout kind or use live verify as startup. \`stop .\` is non-destructive; \`stop . --delete\` is explicit exact-owner cleanup without worktree removal; and \`exec . -- <command...>\` runs one-shot commands only in the exact running workspace runtime (DevPod or Devsy). Never substitute raw \`devpod up\`, \`stop\`, \`delete\`, or the Devsy equivalents: they bypass devrouter's 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. Runtime selection is path-aware: \`DEVROUTER_WORKSPACE_RUNTIME=devpod|devsy\` forces one runtime, an exact-path registry owner wins next, then the machine preference from \`devrouter setup --workspace-runtime\`, then installed-CLI auto-detection. \`workspace up\` creates linked worktrees; destructive worktree removal and GC remain ledger-scoped. Dirty or locked full down fails before side effects. For Devsy, 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.
21026
+ - **Lifecycle**: after one-time \`setup\`, use \`ensure .\` for both primary and linked checkouts; never branch manually on checkout kind or use live verify as startup. \`stop .\` is non-destructive; \`stop . --delete\` is explicit exact-owner cleanup without worktree removal; and \`exec . -- <command...>\` runs one-shot commands only in the exact running workspace runtime (DevPod or Devsy). Never substitute raw \`devpod up\`, \`stop\`, \`delete\`, or the Devsy equivalents: they bypass devrouter's 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. Runtime selection is path-aware: \`DEVROUTER_WORKSPACE_RUNTIME=devpod|devsy\` forces one runtime, an exact-path registry owner wins next, then the machine preference from \`devrouter setup --workspace-runtime\`, then installed-CLI auto-detection. \`workspace up\` creates linked worktrees; destructive worktree removal and GC remain ledger-scoped. Dirty or locked full down fails before side effects. For Devsy, run \`devrouter setup --yes --workspace-runtime devsy\` once per installed Devsy release, rerunning it after a Devsy update, so Devrouter acquires and verifies the official Linux agent in its own machine cache. Devrouter supports Devsy releases in the range >=1.16.2 <2.0.0 and refuses a release whose published asset has no SHA-256 digest. \`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 the official asset for the installed release.
20574
21027
  - **Interrupted lifecycle**: ensure reconciles an interrupted or never-dispatched ensure after positive worker drainage and automatically repairs retained degraded runtimes. Unknown arbitrary exec is never replayed; explicit stop reconciles that uncertainty. Preserve journals and retained configuration when evidence is unavailable. This does not provide OOM prevention or capacity-managed automatic recovery.
20575
21028
  - **Managed process identity**: \`ensure\` executes an exact captured adapter snapshot. Default reuse includes command argv, workspace, and adapter SHA-256. Set \`DEVROUTER_PROCESS_FINGERPRINT_ENV\` only to comma-separated non-secret environment names whose values affect runtime identity; secret-like names are rejected and raw values are never persisted.
20576
21029
  - **Route state**: the versioned Traefik dynamic file is authoritative for both metadata and rendering. JSON is a compatibility mirror; valid headerless generations migrate automatically, while corrupt canonical metadata fails closed.
@@ -20631,6 +21084,8 @@ Run several worktrees of one repo in parallel without host/route collisions. A *
20631
21084
  - \`devrouter profile plan --repo <path> [--profile <selection>] --contract <repo-relative-yaml> [--output <path>] [--json]\`: validate repository-owned resource policy and emit literal bindings without runtime access
20632
21085
  - \`devrouter stop [path] [--delete] [--json]\`: stop the exact workspace runtime and remove exact routes; \`--delete\` explicitly deletes its ownership-proven data without removing the checkout
20633
21086
  - \`devrouter exec [path] -- <command...>\`: literal one-shot command inside the exact running workspace runtime
21087
+ - \`devrouter harness gate [--repo <path>] [--wait-budget-ms <ms>] [--json]\`: defer one harness tool call until this checkout's lifecycle phase settles; prints the harness permission decision
21088
+ - \`devrouter capacity reconcile --yes [--json]\`: replace a provably absent capacity ledger with a fresh empty baseline once no journal-visible charge remains
20634
21089
  - \`devrouter up\` / \`devrouter down\`: start/stop shared Traefik router
20635
21090
  - \`devrouter status\`: router/container/network/TLS health
20636
21091
  - \`devrouter doctor [--repo .]\`: deep diagnostics (global + repo)
@@ -20658,6 +21113,61 @@ Run several worktrees of one repo in parallel without host/route collisions. A *
20658
21113
  - \`devrouter workspace down <workspace|branch> [--keep-worktree]\`: delete runtime/routes and optionally remove the clean worktree and record
20659
21114
  - \`devrouter workspace gc [--json] [--yes]\`: report missing owners by default; apply exact eligible cleanup with \`--yes\`
20660
21115
 
21116
+ ## Agent harness gating
21117
+
21118
+ A managed consumer can gate agent tool calls so a command never starts while the
21119
+ checkout's environment is mid-transition. Wire \`devrouter harness gate\` as a
21120
+ \`PreToolUse\` hook. Claude Code reads it from a settings file:
21121
+
21122
+ {
21123
+ "hooks": {
21124
+ "PreToolUse": [
21125
+ { "matcher": "Bash", "hooks": [
21126
+ { "type": "command", "command": "devrouter harness gate", "timeout": 90 }
21127
+ ] }
21128
+ ]
21129
+ }
21130
+ }
21131
+
21132
+ The Codex CLI reads the same hook from \`$CODEX_HOME/hooks.json\`, where the shell
21133
+ tool is named \`exec_command\`:
21134
+
21135
+ {
21136
+ "hooks": {
21137
+ "PreToolUse": [
21138
+ { "matcher": "exec_command|Bash|shell", "hooks": [
21139
+ { "type": "command", "command": "devrouter harness gate", "timeout": 180 }
21140
+ ] }
21141
+ ]
21142
+ }
21143
+ }
21144
+
21145
+ The gate identifies the requesting harness from its payload and answers in that
21146
+ harness's accepted shape: a refusal is a \`deny\` in both, while an allowed call is
21147
+ \`permissionDecision: allow\` for Claude Code and Codex receives a bare completion
21148
+ carrying the same guidance as \`additionalContext\`, because Codex rejects an
21149
+ \`allow\` decision as unsupported hook output.
21150
+
21151
+
21152
+ the checkout's durable phase is \`queued\`, \`starting\`, \`verifying\`, \`recovering\`
21153
+ or \`stopping\`, and prints the harness permission decision. The deferral consumes
21154
+ no model turns because the harness is blocked on the hook. \`--wait-budget-ms\`
21155
+ bounds the wait (default 30000); a phase that outlasts it returns one \`deny\` that
21156
+ names the phase and asks the agent not to retry automatically. Lifecycle
21157
+ commands (\`devrouter ...\`) always pass through, and unreadable journal evidence
21158
+ is allowed rather than blocking the agent. Set the harness hook \`timeout\` above
21159
+ the wait budget: an overrunning hook is not honored, and the tool proceeds under
21160
+ the harness's normal permission rules. Use \`--json\` for the devrouter decision
21161
+ envelope with wait metrics.
21162
+
21163
+ Gated calls are keyed by the harness \`tool_use_id\` and recorded durably. If the
21164
+ harness re-delivers a call it already granted or cancelled, the gate returns one
21165
+ refusal naming the earlier decision instead of waiting again, because that call
21166
+ may already have run; the agent should verify it and issue a new call. Entries
21167
+ expire after 24 hours, the ledger keeps the newest 64 per checkout, and an
21168
+ unreadable or unwritable ledger never blocks the agent. Payloads without a
21169
+ \`tool_use_id\` keep the wait-only behavior.
21170
+
20661
21171
  ## Validation workflow
20662
21172
 
20663
21173
  For devcontainer onboarding:
@@ -20863,12 +21373,13 @@ function buildOnboardingPrompt(options = {}) {
20863
21373
  `- When a workspace is active: hosts auto-namespace (\`web.localhost\` \u2192 \`web.<ws>.localhost\`), \`${WORKSPACE_PLACEHOLDER}\` in \`upstream\` is substituted with the token, and the docker \`router\` key is suffixed per workspace. Managed ensure rejects every HTTP/TCP upstream outside that exact alias namespace before mutation. The runtime config is computed in memory only \u2014 the committed \`.devrouter.yml\` is never rewritten.`,
20864
21374
  "- TLS: namespaced hosts (`web.<ws>.localhost`) are not covered by the `*.localhost` wildcard; devrouter auto-extends the mkcert cert SANs for active hosts when TLS is enabled.",
20865
21375
  "- 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.",
21376
+ "- Managed agent harnesses can defer tool calls through `devrouter harness gate` wired as a `PreToolUse` hook (Claude Code settings, or `$CODEX_HOME/hooks.json` for the Codex CLI with the `exec_command` shell tool): it waits inside the hook process while the checkout's durable phase is queued, starting, verifying, recovering or stopping (no model turns), allows the tool once the phase settles, and refuses once with recovery guidance when `--wait-budget-ms` (default 30000) runs out. It identifies the requesting harness from its payload and answers in that harness's accepted shape, because Codex rejects an `allow` decision. It also records the gated call by harness tool-use id, so a call re-delivered after a granted or cancelled wait returns one refusal instead of repeating a command that may already have run. Lifecycle commands pass through, unreadable journal evidence is allowed rather than blocking the agent, and the hook timeout must exceed the wait budget.",
20866
21377
  "- 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.",
20867
21378
  "- 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.",
20868
21379
  "- 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.",
20869
21380
  "- 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.",
20870
21381
  "- 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.",
20871
- "- 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; a host CLI newer than the verified pin is `ready` with a drift warning and no injected agent, while an older or unparseable version stays unsupported. `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. `ensure` and `stop` stream bounded phase progress on stderr, so parse command JSON from stdout only.",
21382
+ "- Devsy agent readiness: run `devrouter setup --yes --workspace-runtime devsy` once per installed Devsy release, rerunning it after a Devsy update, so Devrouter acquires and verifies the official Linux agent in its own machine cache. Devrouter supports releases in the range >=1.16.2 <2.0.0, verifies a release's published asset against the SHA-256 digest GitHub reports, and never injects a release without one. `doctor` reports `ready`, `missing`, `stale`, or `invalid` without network access; a supported release with no recorded manifest is `ready` with a drift warning and no injected agent until `setup` records one, while a version outside the supported range or unparseable stays `stale`. `ensure` fails before the provider queue when readiness is not `ready`. An explicit `DEVSY_AGENT_BINARY` remains authoritative and must match the official asset for the installed supported release. `ensure` and `stop` stream bounded phase progress on stderr, so parse command JSON from stdout only.",
20872
21383
  "- A managed adapter plus `postCreateCommand` requires `waitFor` exactly `postCreateCommand` or `postStartCommand`; managed selective config preserves lifecycle fields and changes only `runServices`.",
20873
21384
  "- Managed process reuse fingerprints command argv, workspace identity, and the exact adapter snapshot. If a non-secret runtime value also affects reuse, set `DEVROUTER_PROCESS_FINGERPRINT_ENV` to its comma-separated environment names; secret-like names are rejected and raw values are never persisted.",
20874
21385
  "- Managed adapters may use devrouter-process ensure --prepare-command <command> for synchronous dependency preparation under the process lock before app launch. Unchanged owned processes skip preparation; it participates in default fingerprints and must not daemonize or detach.",
@@ -20897,6 +21408,7 @@ function buildOnboardingPrompt(options = {}) {
20897
21408
  "- `devrouter doctor --repo <REPO_PATH>` warns on risky pre-wrapper DB assignments before `run --` for host apps that depend on postgres.",
20898
21409
  "- With TLS enabled, `devrouter doctor --repo <REPO_PATH>` also warns on cert SAN mismatches for configured hosts (`repo.tls-host-coverage`).",
20899
21410
  "- `devrouter doctor --repo <REPO_PATH>` errors on configured fixed host-port bindings already held by running containers (`repo.host-port-claims`) and warns when that comparison is unavailable.",
21411
+ "- `devrouter doctor` compares every `devrouter` executable on PATH with the running CLI (`global.cli-path`) and warns when a shell would silently run a different version.",
20900
21412
  "- Robust one-shot migrate example (argv-safe, no nested shell quoting): `devrouter app exec <name> --repo <REPO_PATH> --yes -- infisical run --projectId <id> --env=<env> -- pnpm payload migrate`.",
20901
21413
  "- Robust one-shot seed example: `devrouter app exec <name> --repo <REPO_PATH> --yes -- infisical run --projectId <id> --env=<env> -- pnpm payload seed`.",
20902
21414
  "- Environment probe/debug example (run before migrations): `devrouter app exec <name> --repo <REPO_PATH> --yes -- printenv DB_URL DB_HOST DB_PORT DB_SHADOW_URL`.",
@@ -21571,7 +22083,7 @@ var init_init = __esm({
21571
22083
  });
21572
22084
 
21573
22085
  // src/core/upgrade.ts
21574
- function parseSemver(version) {
22086
+ function parseSemver2(version) {
21575
22087
  const normalized = version.trim();
21576
22088
  const match = normalized.match(SEMVER_RE);
21577
22089
  if (!match) {
@@ -21584,12 +22096,12 @@ function parseSemver(version) {
21584
22096
  };
21585
22097
  }
21586
22098
  function normalizeVersion(version) {
21587
- const parsed = parseSemver(version);
22099
+ const parsed = parseSemver2(version);
21588
22100
  return `${parsed.major}.${parsed.minor}.${parsed.patch}`;
21589
22101
  }
21590
22102
  function compareVersions(a, b) {
21591
- const left = parseSemver(a);
21592
- const right = parseSemver(b);
22103
+ const left = parseSemver2(a);
22104
+ const right = parseSemver2(b);
21593
22105
  if (left.major !== right.major) {
21594
22106
  return left.major - right.major;
21595
22107
  }
@@ -21636,7 +22148,7 @@ function readPromptDirectory(promptDirPath) {
21636
22148
  continue;
21637
22149
  }
21638
22150
  const version = normalizeVersion(basename);
21639
- const promptPath = import_node_path51.default.join(promptDirPath, file.name);
22151
+ const promptPath = import_node_path52.default.join(promptDirPath, file.name);
21640
22152
  const prompt = import_node_fs39.default.readFileSync(promptPath, "utf-8").trim();
21641
22153
  releases.push({ version, prompt, promptPath });
21642
22154
  }
@@ -21658,17 +22170,17 @@ function listAvailableUpgradeTargets(currentVersion, releases) {
21658
22170
  }
21659
22171
  function resolvePromptDirectory(explicitPath) {
21660
22172
  if (explicitPath) {
21661
- return import_node_path51.default.resolve(explicitPath);
22173
+ return import_node_path52.default.resolve(explicitPath);
21662
22174
  }
21663
- const entryFile = process.argv[1] ? import_node_path51.default.resolve(process.argv[1]) : __filename;
21664
- const entryDir = import_node_path51.default.dirname(entryFile);
21665
- const resolvedEntryDir = import_node_fs39.default.existsSync(entryFile) ? import_node_path51.default.dirname(import_node_fs39.default.realpathSync(entryFile)) : entryDir;
22175
+ const entryFile = process.argv[1] ? import_node_path52.default.resolve(process.argv[1]) : __filename;
22176
+ const entryDir = import_node_path52.default.dirname(entryFile);
22177
+ const resolvedEntryDir = import_node_fs39.default.existsSync(entryFile) ? import_node_path52.default.dirname(import_node_fs39.default.realpathSync(entryFile)) : entryDir;
21666
22178
  const candidates = [
21667
- import_node_path51.default.resolve(resolvedEntryDir, "..", UPGRADE_PROMPTS_DIR),
21668
- import_node_path51.default.resolve(entryDir, "..", UPGRADE_PROMPTS_DIR),
21669
- import_node_path51.default.resolve(__dirname, "..", UPGRADE_PROMPTS_DIR),
21670
- import_node_path51.default.resolve(__dirname, "..", "..", UPGRADE_PROMPTS_DIR),
21671
- import_node_path51.default.resolve(process.cwd(), UPGRADE_PROMPTS_DIR)
22179
+ import_node_path52.default.resolve(resolvedEntryDir, "..", UPGRADE_PROMPTS_DIR),
22180
+ import_node_path52.default.resolve(entryDir, "..", UPGRADE_PROMPTS_DIR),
22181
+ import_node_path52.default.resolve(__dirname, "..", UPGRADE_PROMPTS_DIR),
22182
+ import_node_path52.default.resolve(__dirname, "..", "..", UPGRADE_PROMPTS_DIR),
22183
+ import_node_path52.default.resolve(process.cwd(), UPGRADE_PROMPTS_DIR)
21672
22184
  ];
21673
22185
  const existing = candidates.find((candidate) => import_node_fs39.default.existsSync(candidate));
21674
22186
  return existing ?? candidates[0];
@@ -21686,12 +22198,12 @@ function loadUpgradeCatalog(options = {}) {
21686
22198
  releases
21687
22199
  };
21688
22200
  }
21689
- var import_node_fs39, import_node_path51, SEMVER_RE, UPGRADE_PROMPTS_DIR;
22201
+ var import_node_fs39, import_node_path52, SEMVER_RE, UPGRADE_PROMPTS_DIR;
21690
22202
  var init_upgrade = __esm({
21691
22203
  "src/core/upgrade.ts"() {
21692
22204
  "use strict";
21693
22205
  import_node_fs39 = __toESM(require("fs"));
21694
- import_node_path51 = __toESM(require("path"));
22206
+ import_node_path52 = __toESM(require("path"));
21695
22207
  init_repo_config();
21696
22208
  SEMVER_RE = /^v?(\d+)\.(\d+)\.(\d+)$/;
21697
22209
  UPGRADE_PROMPTS_DIR = "upgrade-prompts";
@@ -21926,7 +22438,7 @@ function buildHostPortClaimsCheck(repoPath, workspace) {
21926
22438
  details: conflicts.map(
21927
22439
  (conflict) => `${conflict.service}: ${conflict.hostIp ?? "*"}:${conflict.hostPort}/${conflict.protocol} held by '${conflict.holderContainer}'` + (conflict.holderWorkspace ? ` (workspace '${conflict.holderWorkspace}')` : "")
21928
22440
  ).join("; "),
21929
- suggestion: "Stop the holding workspace or change the consumer's own published binding, then run ensure again. Devrouter never rewrites consumer-declared host bindings."
22441
+ suggestion: conflicts.some((conflict) => conflict.holderContainer === ROUTER_CONTAINER_NAME) ? "The shared devrouter router owns its entrypoint ports and must keep running; change the consumer's own published binding or route that dependency through devrouter's TCP protocol route, then run ensure again. Devrouter never rewrites consumer-declared host bindings." : "Stop the holding workspace or change the consumer's own published binding, then run ensure again. Devrouter never rewrites consumer-declared host bindings."
21930
22442
  };
21931
22443
  } catch (error) {
21932
22444
  return {
@@ -21939,11 +22451,11 @@ function buildHostPortClaimsCheck(repoPath, workspace) {
21939
22451
  }
21940
22452
  }
21941
22453
  function buildDevcontainerChecks(repoPath, config, workspace) {
21942
- const devcontainerDir = import_node_path52.default.join(repoPath, ".devcontainer");
22454
+ const devcontainerDir = import_node_path53.default.join(repoPath, ".devcontainer");
21943
22455
  if (!import_node_fs40.default.existsSync(devcontainerDir)) {
21944
22456
  return [];
21945
22457
  }
21946
- const compose = inspectCompose(import_node_path52.default.join(devcontainerDir, "docker-compose.yml"), workspace);
22458
+ const compose = inspectCompose(import_node_path53.default.join(devcontainerDir, "docker-compose.yml"), workspace);
21947
22459
  const checks = [];
21948
22460
  if (compose.parseError) {
21949
22461
  checks.push({
@@ -21970,7 +22482,7 @@ function buildDevcontainerChecks(repoPath, config, workspace) {
21970
22482
  details: compose.publishedPorts.length > 0 ? compose.publishedPorts.join(", ") : void 0,
21971
22483
  suggestion: compose.parseError || compose.publishedPorts.length > 0 ? "Remove published ports and route services through devnet aliases with devrouter proxy apps." : void 0
21972
22484
  });
21973
- const dockerfilePath = import_node_path52.default.join(devcontainerDir, "Dockerfile");
22485
+ const dockerfilePath = import_node_path53.default.join(devcontainerDir, "Dockerfile");
21974
22486
  const dockerfileExists = import_node_fs40.default.existsSync(dockerfilePath);
21975
22487
  const dockerfile = dockerfileExists ? import_node_fs40.default.readFileSync(dockerfilePath, "utf-8") : "";
21976
22488
  const devrouterArtifacts = ["@devrouter/cli", "devrouter-process"].filter(
@@ -22030,17 +22542,18 @@ function buildDevcontainerChecks(repoPath, config, workspace) {
22030
22542
  });
22031
22543
  return checks;
22032
22544
  }
22033
- var import_node_fs40, import_node_path52, import_yaml5;
22545
+ var import_node_fs40, import_node_path53, import_yaml5;
22034
22546
  var init_devcontainer_diagnostics = __esm({
22035
22547
  "src/core/devcontainer-diagnostics.ts"() {
22036
22548
  "use strict";
22037
22549
  import_node_fs40 = __toESM(require("fs"));
22038
- import_node_path52 = __toESM(require("path"));
22550
+ import_node_path53 = __toESM(require("path"));
22039
22551
  import_yaml5 = __toESM(require("yaml"));
22040
22552
  init_devcontainer_profile();
22041
22553
  init_host_port_claims();
22042
22554
  init_managed_post_start();
22043
22555
  init_repo_config();
22556
+ init_router();
22044
22557
  init_workspace();
22045
22558
  init_workspace_ownership();
22046
22559
  }
@@ -22535,7 +23048,7 @@ function parseMinimumNodeMajor(value) {
22535
23048
  return Number(match[1]);
22536
23049
  }
22537
23050
  function readPackageJson(repoPath) {
22538
- const packagePath = import_node_path53.default.join(repoPath, "package.json");
23051
+ const packagePath = import_node_path54.default.join(repoPath, "package.json");
22539
23052
  if (!import_node_fs43.default.existsSync(packagePath)) {
22540
23053
  return void 0;
22541
23054
  }
@@ -22610,16 +23123,18 @@ function devsyAgentCheck(inspection) {
22610
23123
  `state=${inspection.state}`,
22611
23124
  `source=${inspection.source}`,
22612
23125
  ...inspection.installedVersion ? [`version=${inspection.installedVersion}`] : [],
22613
- ...inspection.asset ? [`asset=${inspection.asset.name}`] : []
23126
+ ...inspection.asset ? [`asset=${inspection.asset.name}`] : [],
23127
+ ...inspection.manifestOrigin ? [`manifest=${inspection.manifestOrigin}`] : [],
23128
+ `supported=${SUPPORTED_DEVSY_RANGE}`
22614
23129
  ].join(", ");
22615
23130
  if (inspection.state === "ready") {
22616
23131
  if (inspection.drift) {
22617
23132
  return {
22618
23133
  id: "global.devsy-agent",
22619
23134
  level: "warn",
22620
- summary: `Devsy ${inspection.drift.installed} is newer than the Devrouter-verified ${inspection.drift.supported} agent, so the host CLI manages its own agent.`,
23135
+ summary: `Devsy ${inspection.drift.installed} governs its own agent; Devrouter injects no agent for it.`,
22621
23136
  details,
22622
- suggestion: `Devrouter injects its verified agent only for Devsy ${inspection.drift.supported}. To use that pairing, install Devsy ${inspection.drift.supported} and run: ${DEVSY_AGENT_SETUP_COMMAND}`
23137
+ suggestion: `Run: devrouter setup --yes --workspace-runtime devsy to verify and inject the official Devsy ${inspection.drift.installed} agent for managed starts`
22623
23138
  };
22624
23139
  }
22625
23140
  return {
@@ -22629,7 +23144,7 @@ function devsyAgentCheck(inspection) {
22629
23144
  details
22630
23145
  };
22631
23146
  }
22632
- const summary = inspection.state === "missing" ? "Managed Devsy agent source is missing." : inspection.state === "stale" ? "Devsy agent source is stale for this Devrouter release." : "Devsy agent source is invalid.";
23147
+ const summary = inspection.state === "missing" ? "No verified Devsy agent is cached for the installed Devsy version." : inspection.state === "stale" ? "The installed Devsy version is outside the supported range." : "Devsy agent source is invalid.";
22633
23148
  return {
22634
23149
  id: "global.devsy-agent",
22635
23150
  level: "error",
@@ -22638,6 +23153,125 @@ function devsyAgentCheck(inspection) {
22638
23153
  suggestion: devsyAgentRepairSuggestion(inspection)
22639
23154
  };
22640
23155
  }
23156
+ function realpathOrUndefined(value) {
23157
+ if (!value) {
23158
+ return void 0;
23159
+ }
23160
+ try {
23161
+ return import_node_fs43.default.realpathSync(value);
23162
+ } catch {
23163
+ return void 0;
23164
+ }
23165
+ }
23166
+ function isExecutableFile(filePath) {
23167
+ try {
23168
+ if (!import_node_fs43.default.statSync(filePath).isFile()) {
23169
+ return false;
23170
+ }
23171
+ import_node_fs43.default.accessSync(filePath, import_node_fs43.default.constants.X_OK);
23172
+ return true;
23173
+ } catch {
23174
+ return false;
23175
+ }
23176
+ }
23177
+ function displayPath(filePath) {
23178
+ const home = process.env.HOME;
23179
+ if (!home || home.length < 2 || !filePath.startsWith(`${home}${import_node_path54.default.sep}`)) {
23180
+ return filePath;
23181
+ }
23182
+ return `~${filePath.slice(home.length)}`;
23183
+ }
23184
+ function probeInstalledCliVersion(executable) {
23185
+ const result = (0, import_node_child_process29.spawnSync)(executable, ["-V"], {
23186
+ encoding: "utf-8",
23187
+ timeout: CLI_PATH_PROBE_TIMEOUT_MS
23188
+ });
23189
+ if (result.error) {
23190
+ return void 0;
23191
+ }
23192
+ return INSTALLED_CLI_VERSION_RE.exec(outputFromResult(result) ?? "")?.[1];
23193
+ }
23194
+ function buildCliPathCheck(options = {}) {
23195
+ const id5 = "global.cli-path";
23196
+ const runningVersion = options.runningVersion ?? (true ? "0.1.0" : "0.0.0-dev");
23197
+ if (runningVersion === "0.0.0-dev") {
23198
+ return {
23199
+ id: id5,
23200
+ level: "ok",
23201
+ summary: "Devrouter runs from an unstamped build, so PATH installs are not compared.",
23202
+ details: "installedVersion=unknown; comparison=skipped"
23203
+ };
23204
+ }
23205
+ const runningEntry = realpathOrUndefined(options.runningEntry ?? process.argv[1]);
23206
+ const probeVersion = options.probeVersion ?? probeInstalledCliVersion;
23207
+ const entries = [];
23208
+ const seen = /* @__PURE__ */ new Set();
23209
+ for (const dir of (options.pathValue ?? process.env.PATH ?? "").split(import_node_path54.default.delimiter)) {
23210
+ if (dir.length === 0) {
23211
+ continue;
23212
+ }
23213
+ const executable = import_node_path54.default.join(dir, "devrouter");
23214
+ if (!isExecutableFile(executable)) {
23215
+ continue;
23216
+ }
23217
+ const resolved = realpathOrUndefined(executable) ?? executable;
23218
+ if (seen.has(resolved)) {
23219
+ continue;
23220
+ }
23221
+ seen.add(resolved);
23222
+ const running = runningEntry !== void 0 && resolved === runningEntry;
23223
+ entries.push({
23224
+ executable,
23225
+ running,
23226
+ version: running ? runningVersion : probeVersion(executable)
23227
+ });
23228
+ }
23229
+ const shellInstall = entries[0];
23230
+ if (!shellInstall) {
23231
+ return {
23232
+ id: id5,
23233
+ level: "ok",
23234
+ summary: "No devrouter executable is on PATH.",
23235
+ details: `runningVersion=${runningVersion}`
23236
+ };
23237
+ }
23238
+ const details = entries.slice(0, 4).map(
23239
+ (entry) => `${displayPath(entry.executable)}=${entry.version ?? "unknown"}${entry.running ? " (running)" : ""}`
23240
+ ).join(", ");
23241
+ let newestVersion;
23242
+ for (const entry of entries) {
23243
+ if (entry.version === void 0) {
23244
+ continue;
23245
+ }
23246
+ if (newestVersion === void 0 || compareSemver(entry.version, newestVersion) > 0) {
23247
+ newestVersion = entry.version;
23248
+ }
23249
+ }
23250
+ if (newestVersion !== void 0 && compareSemver(newestVersion, runningVersion) > 0) {
23251
+ return {
23252
+ id: id5,
23253
+ level: "warn",
23254
+ summary: `Another Devrouter install on PATH (${newestVersion}) is newer than the running CLI (${runningVersion}).`,
23255
+ details,
23256
+ suggestion: `Update the installs so one version serves every shell: npm install -g @devrouter/cli@${newestVersion}`
23257
+ };
23258
+ }
23259
+ if (!shellInstall.running && shellInstall.version !== runningVersion) {
23260
+ return {
23261
+ id: id5,
23262
+ level: "warn",
23263
+ summary: `devrouter on PATH resolves to a different install (${shellInstall.version ?? "unknown version"}) than the running CLI (${runningVersion}).`,
23264
+ details,
23265
+ suggestion: `Align the installs so shell commands run the same CLI: npm install -g @devrouter/cli@${runningVersion}`
23266
+ };
23267
+ }
23268
+ return {
23269
+ id: id5,
23270
+ level: "ok",
23271
+ summary: "Devrouter installs on PATH match the running CLI.",
23272
+ details: `${details}; runningVersion=${runningVersion}`
23273
+ };
23274
+ }
22641
23275
  function buildGlobalToolChecks(repoPath) {
22642
23276
  const previous = process.env[DEVSY_DISABLE_TELEMETRY];
22643
23277
  process.env[DEVSY_DISABLE_TELEMETRY] = "true";
@@ -22730,19 +23364,23 @@ function buildGlobalToolChecksWithoutTelemetry(repoPath) {
22730
23364
  details: configProblems.length > 0 ? [configDetails, ...configProblems].join(" ") : configDetails,
22731
23365
  suggestion: configProblems.length > 0 ? "Run: devrouter setup --yes --workspace-runtime <devpod|devsy>" : void 0
22732
23366
  });
23367
+ checks.push(buildCliPathCheck());
22733
23368
  checks.push(nodeToolchainCheck(repoPath));
22734
23369
  return checks;
22735
23370
  }
22736
- var import_node_child_process29, import_node_fs43, import_node_path53, DEVSY_DISABLE_TELEMETRY;
23371
+ var import_node_child_process29, import_node_fs43, import_node_path54, DEVSY_DISABLE_TELEMETRY, CLI_PATH_PROBE_TIMEOUT_MS, INSTALLED_CLI_VERSION_RE;
22737
23372
  var init_tool_diagnostics = __esm({
22738
23373
  "src/core/tool-diagnostics.ts"() {
22739
23374
  "use strict";
22740
23375
  import_node_child_process29 = require("child_process");
22741
23376
  import_node_fs43 = __toESM(require("fs"));
22742
- import_node_path53 = __toESM(require("path"));
23377
+ import_node_path54 = __toESM(require("path"));
22743
23378
  init_devsy_agent();
23379
+ init_repo_config();
22744
23380
  init_workspace_runtime();
22745
23381
  DEVSY_DISABLE_TELEMETRY = "DEVSY_DISABLE_TELEMETRY";
23382
+ CLI_PATH_PROBE_TIMEOUT_MS = 5e3;
23383
+ INSTALLED_CLI_VERSION_RE = /Installed CLI version:\s*(\S+)/;
22746
23384
  }
22747
23385
  });
22748
23386
 
@@ -22756,10 +23394,10 @@ function inRepositoryWorkspaceScope(repoPath, worktreePath, livePaths) {
22756
23394
  if (livePaths.some((candidate) => sameWorkspacePath(candidate, worktreePath))) return true;
22757
23395
  const comparableRepo = comparableWorkspacePath(repoPath);
22758
23396
  const comparableWorktree = comparableWorkspacePath(worktreePath);
22759
- const localRoot = import_node_path54.default.join(comparableRepo, "trees") + import_node_path54.default.sep;
23397
+ const localRoot = import_node_path55.default.join(comparableRepo, "trees") + import_node_path55.default.sep;
22760
23398
  if (comparableWorktree.startsWith(localRoot)) return true;
22761
- const legacyPrefix = `${import_node_path54.default.basename(comparableRepo)}-`;
22762
- return import_node_path54.default.dirname(comparableWorktree) === import_node_path54.default.dirname(comparableRepo) && import_node_path54.default.basename(comparableWorktree).startsWith(legacyPrefix);
23399
+ const legacyPrefix = `${import_node_path55.default.basename(comparableRepo)}-`;
23400
+ return import_node_path55.default.dirname(comparableWorktree) === import_node_path55.default.dirname(comparableRepo) && import_node_path55.default.basename(comparableWorktree).startsWith(legacyPrefix);
22763
23401
  }
22764
23402
  function previewActions(devpodStatus, routeCount, includeRecord) {
22765
23403
  const actions = [
@@ -23011,11 +23649,11 @@ function applyWorkspaceGc(plan) {
23011
23649
  candidates
23012
23650
  };
23013
23651
  }
23014
- var import_node_path54;
23652
+ var import_node_path55;
23015
23653
  var init_workspace_gc = __esm({
23016
23654
  "src/core/workspace-gc.ts"() {
23017
23655
  "use strict";
23018
- import_node_path54 = __toESM(require("path"));
23656
+ import_node_path55 = __toESM(require("path"));
23019
23657
  init_devpod_mutation();
23020
23658
  init_devpod_workspaces();
23021
23659
  init_host_routes();
@@ -23255,12 +23893,19 @@ async function buildDoctorReport(options = {}) {
23255
23893
  summary: "Capacity ledger is consistent with available lifecycle history."
23256
23894
  });
23257
23895
  } catch (error) {
23896
+ const absent = (() => {
23897
+ try {
23898
+ return createLifecycleCapacityStore().inspect().kind === "absent";
23899
+ } catch {
23900
+ return false;
23901
+ }
23902
+ })();
23258
23903
  addCheck(checks, {
23259
23904
  id: "global.capacity-ledger",
23260
23905
  level: "error",
23261
23906
  summary: "Capacity ledger history is unavailable for safe admission.",
23262
23907
  details: error instanceof CapacityHistoryError ? error.code : "capacity-history-unprovable",
23263
- suggestion: "Preserve ledger and lifecycle records; restore verified history before retrying admission."
23908
+ suggestion: absent ? "Preserve ledger and lifecycle records; with every enrolled environment stopped, run: devrouter capacity reconcile --yes" : "Preserve ledger and lifecycle records; restore verified history before retrying admission."
23264
23909
  });
23265
23910
  }
23266
23911
  let statusNextSteps = [];
@@ -23396,7 +24041,7 @@ async function buildDoctorReport(options = {}) {
23396
24041
  suggestion: `Remedy ${expansionNotice.remedy.code}: set a named default profile other than '${expansionNotice.profile}'${remedyCandidates}.`
23397
24042
  });
23398
24043
  }
23399
- const cliVersion = true ? "0.0.79" : "0.0.0-dev";
24044
+ const cliVersion = true ? "0.1.0" : "0.0.0-dev";
23400
24045
  const configVersion = config.devrouter?.version;
23401
24046
  const installedKnown = cliVersion !== "0.0.0-dev";
23402
24047
  const versionComparison = installedKnown && configVersion ? compareSemver(configVersion, cliVersion) : void 0;
@@ -23432,7 +24077,7 @@ async function buildDoctorReport(options = {}) {
23432
24077
  (app) => app.docker.composeFiles.map((filePath) => ({
23433
24078
  app: app.name,
23434
24079
  filePath,
23435
- absolutePath: import_node_path55.default.resolve(repo.path, filePath)
24080
+ absolutePath: import_node_path56.default.resolve(repo.path, filePath)
23436
24081
  }))
23437
24082
  ).filter((entry) => !import_node_fs44.default.existsSync(entry.absolutePath));
23438
24083
  addCheck(checks, {
@@ -23477,7 +24122,7 @@ async function buildDoctorReport(options = {}) {
23477
24122
  const missingHostCwds = config.apps.filter((app) => app.runtime === "host").map((app) => ({
23478
24123
  app: app.name,
23479
24124
  cwd: app.hostRun.cwd,
23480
- absolutePath: import_node_path55.default.resolve(repo.path, app.hostRun.cwd)
24125
+ absolutePath: import_node_path56.default.resolve(repo.path, app.hostRun.cwd)
23481
24126
  })).filter((entry) => !import_node_fs44.default.existsSync(entry.absolutePath));
23482
24127
  addCheck(checks, {
23483
24128
  id: "repo.host-cwd",
@@ -23622,12 +24267,12 @@ async function buildDoctorReport(options = {}) {
23622
24267
  nextSteps
23623
24268
  };
23624
24269
  }
23625
- var import_node_fs44, import_node_path55, import_yaml6, POSTGRES_DEFAULTS;
24270
+ var import_node_fs44, import_node_path56, import_yaml6, POSTGRES_DEFAULTS;
23626
24271
  var init_doctor = __esm({
23627
24272
  "src/core/doctor.ts"() {
23628
24273
  "use strict";
23629
24274
  import_node_fs44 = __toESM(require("fs"));
23630
- import_node_path55 = __toESM(require("path"));
24275
+ import_node_path56 = __toESM(require("path"));
23631
24276
  import_yaml6 = __toESM(require("yaml"));
23632
24277
  init_capacity_store();
23633
24278
  init_docker();
@@ -23775,7 +24420,7 @@ async function runSetup(options = {}) {
23775
24420
  id: "global.devsy-agent",
23776
24421
  summary: prepared.changed && asset ? `Downloaded and verified ${asset.name} for Devsy.` : prepared.source === "host" ? "The host Devsy CLI manages its own agent; no Devrouter-managed agent is required." : `Verified the ${prepared.source} Devsy agent source.`,
23777
24422
  details: [
23778
- `version=${SUPPORTED_DEVSY_VERSION}`,
24423
+ `version=${prepared.version}`,
23779
24424
  ...asset ? [`asset=${asset.name}`] : [],
23780
24425
  `source=${prepared.source}`,
23781
24426
  `transport=${prepared.transport}`
@@ -24257,6 +24902,519 @@ var init_status2 = __esm({
24257
24902
  }
24258
24903
  });
24259
24904
 
24905
+ // src/core/harness-continuation.ts
24906
+ function harnessContinuationPath(repoRoot) {
24907
+ const key = (0, import_node_crypto26.createHash)("sha256").update(comparableWorkspacePath(repoRoot)).digest("hex");
24908
+ return import_node_path57.default.join(DEVROUTER_HOME, "harness", `${key}.json`);
24909
+ }
24910
+ function counter4(value) {
24911
+ return Number.isSafeInteger(value) && value >= 0;
24912
+ }
24913
+ function validEntry(value) {
24914
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
24915
+ const entry = value;
24916
+ return typeof entry.toolUseId === "string" && entry.toolUseId.length > 0 && entry.toolUseId.length <= MAX_TOOL_USE_ID_LENGTH && typeof entry.payloadSha256 === "string" && /^[0-9a-f]{64}$/.test(entry.payloadSha256) && (entry.state === "waiting" || entry.state === "granted" || entry.state === "refused" || entry.state === "interrupted") && typeof entry.phase === "string" && entry.phase.length <= MAX_PHASE_LENGTH && counter4(entry.budgetMs) && counter4(entry.claimedAtMs) && (entry.settledAtMs === void 0 || counter4(entry.settledAtMs)) && (entry.waitedMs === void 0 || counter4(entry.waitedMs));
24917
+ }
24918
+ function readLedgerSync(file, nowMs) {
24919
+ let stat;
24920
+ try {
24921
+ stat = import_node_fs45.default.lstatSync(file);
24922
+ } catch {
24923
+ return [];
24924
+ }
24925
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_LEDGER_BYTES) return [];
24926
+ if (stat.uid !== process.getuid?.() || (stat.mode & 63) !== 0) return [];
24927
+ let parsed;
24928
+ try {
24929
+ parsed = JSON.parse(import_node_fs45.default.readFileSync(file, "utf-8"));
24930
+ } catch {
24931
+ return [];
24932
+ }
24933
+ if (!Array.isArray(parsed)) return [];
24934
+ return parsed.filter(validEntry).filter(
24935
+ (entry) => nowMs - (entry.settledAtMs ?? entry.claimedAtMs) < HARNESS_CONTINUATION_TTL_MS
24936
+ );
24937
+ }
24938
+ function readHarnessContinuations(repoRoot, nowMs = Date.now()) {
24939
+ try {
24940
+ return readLedgerSync(harnessContinuationPath(repoRoot), nowMs);
24941
+ } catch {
24942
+ return [];
24943
+ }
24944
+ }
24945
+ function claimHarnessContinuation(repoRoot, claim2) {
24946
+ const nowMs = claim2.nowMs ?? Date.now();
24947
+ try {
24948
+ const file = harnessContinuationPath(repoRoot);
24949
+ import_node_fs45.default.mkdirSync(import_node_path57.default.dirname(file), { recursive: true, mode: 448 });
24950
+ return withFileLockSync(
24951
+ `${file}.lock`,
24952
+ { activity: "harness continuation claim", waitMs: HARNESS_CONTINUATION_LOCK_WAIT_MS },
24953
+ () => {
24954
+ const entries = readLedgerSync(file, nowMs);
24955
+ const existing = entries.find((entry) => entry.toolUseId === claim2.toolUseId);
24956
+ if (existing) return { kind: "replay", entry: existing };
24957
+ entries.push({
24958
+ toolUseId: claim2.toolUseId,
24959
+ payloadSha256: claim2.payloadSha256,
24960
+ state: "waiting",
24961
+ phase: claim2.phase,
24962
+ budgetMs: claim2.budgetMs,
24963
+ claimedAtMs: nowMs
24964
+ });
24965
+ writeFileAtomically(
24966
+ file,
24967
+ `${JSON.stringify(entries.slice(-HARNESS_CONTINUATION_MAX_ENTRIES))}
24968
+ `
24969
+ );
24970
+ return { kind: "claimed", recorded: true };
24971
+ }
24972
+ );
24973
+ } catch {
24974
+ return { kind: "claimed", recorded: false };
24975
+ }
24976
+ }
24977
+ function settleHarnessContinuation(repoRoot, toolUseId, settlement) {
24978
+ const nowMs = settlement.nowMs ?? Date.now();
24979
+ try {
24980
+ const file = harnessContinuationPath(repoRoot);
24981
+ import_node_fs45.default.mkdirSync(import_node_path57.default.dirname(file), { recursive: true, mode: 448 });
24982
+ withFileLockSync(
24983
+ `${file}.lock`,
24984
+ { activity: "harness continuation settlement", waitMs: HARNESS_CONTINUATION_LOCK_WAIT_MS },
24985
+ () => {
24986
+ const entries = readLedgerSync(file, nowMs);
24987
+ const index = entries.findIndex((entry) => entry.toolUseId === toolUseId);
24988
+ if (index < 0 || entries[index]?.state !== "waiting") return;
24989
+ entries[index] = {
24990
+ ...entries[index],
24991
+ state: settlement.state,
24992
+ settledAtMs: nowMs,
24993
+ ...settlement.waitedMs === void 0 ? {} : { waitedMs: settlement.waitedMs }
24994
+ };
24995
+ writeFileAtomically(
24996
+ file,
24997
+ `${JSON.stringify(entries.slice(-HARNESS_CONTINUATION_MAX_ENTRIES))}
24998
+ `
24999
+ );
25000
+ }
25001
+ );
25002
+ } catch {
25003
+ }
25004
+ }
25005
+ var import_node_crypto26, import_node_fs45, import_node_path57, HARNESS_CONTINUATION_MAX_ENTRIES, HARNESS_CONTINUATION_TTL_MS, HARNESS_CONTINUATION_LOCK_WAIT_MS, MAX_LEDGER_BYTES, MAX_TOOL_USE_ID_LENGTH, MAX_PHASE_LENGTH;
25006
+ var init_harness_continuation = __esm({
25007
+ "src/core/harness-continuation.ts"() {
25008
+ "use strict";
25009
+ import_node_crypto26 = require("crypto");
25010
+ import_node_fs45 = __toESM(require("fs"));
25011
+ import_node_path57 = __toESM(require("path"));
25012
+ init_atomic_file();
25013
+ init_file_lock();
25014
+ init_router();
25015
+ init_workspace();
25016
+ HARNESS_CONTINUATION_MAX_ENTRIES = 64;
25017
+ HARNESS_CONTINUATION_TTL_MS = 24 * 60 * 60 * 1e3;
25018
+ HARNESS_CONTINUATION_LOCK_WAIT_MS = 200;
25019
+ MAX_LEDGER_BYTES = 256 * 1024;
25020
+ MAX_TOOL_USE_ID_LENGTH = 256;
25021
+ MAX_PHASE_LENGTH = 32;
25022
+ }
25023
+ });
25024
+
25025
+ // src/core/harness-gate.ts
25026
+ function isTransitionalHarnessPhase(phase) {
25027
+ return TRANSITIONAL_PHASES.includes(phase);
25028
+ }
25029
+ function findHarnessGateRepoRoot(cwd) {
25030
+ let current = import_node_path58.default.resolve(cwd);
25031
+ for (let depth = 0; depth < REPO_ROOT_MAX_DEPTH; depth += 1) {
25032
+ if (import_node_fs46.default.existsSync(import_node_path58.default.join(current, ".devrouter.yml"))) {
25033
+ return current;
25034
+ }
25035
+ const parent = import_node_path58.default.dirname(current);
25036
+ if (parent === current) {
25037
+ return void 0;
25038
+ }
25039
+ current = parent;
25040
+ }
25041
+ return void 0;
25042
+ }
25043
+ function readHarnessGateObservation(repoRoot) {
25044
+ try {
25045
+ const identity2 = {
25046
+ repoPath: comparableWorkspacePath(repoRoot),
25047
+ workspace: resolveWorktreeWorkspace(repoRoot) ?? null,
25048
+ provider: resolveWorkspaceRuntimeOrDefault(repoRoot)
25049
+ };
25050
+ const record5 = readReliabilityOperation(identity2);
25051
+ if (!record5) {
25052
+ return { phase: "unknown", detail: "no-lifecycle-record" };
25053
+ }
25054
+ return { phase: record5.state.phase, detail: "lifecycle-record" };
25055
+ } catch {
25056
+ return { phase: "unknown", detail: "evidence-unavailable" };
25057
+ }
25058
+ }
25059
+ async function waitForHarnessGate(options) {
25060
+ const now = options.now ?? (() => Date.now());
25061
+ const sleep3 = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
25062
+ const interval = Math.max(1, options.pollIntervalMs ?? HARNESS_GATE_POLL_INTERVAL_MS);
25063
+ const budget = Math.max(0, Math.min(options.budgetMs, HARNESS_GATE_MAX_BUDGET_MS));
25064
+ let started;
25065
+ let observations = 0;
25066
+ let deferred = false;
25067
+ for (; ; ) {
25068
+ const observation = options.observe();
25069
+ if (started === void 0) {
25070
+ started = now();
25071
+ }
25072
+ observations += 1;
25073
+ options.onObservation?.(observation, now() - started);
25074
+ if (!isTransitionalHarnessPhase(observation.phase)) {
25075
+ const waitedMs2 = now() - started;
25076
+ return {
25077
+ version: 1,
25078
+ decision: deferred ? "deferred-allow" : "allow",
25079
+ reason: deferred ? "settled-after-wait" : "settled",
25080
+ waitedMs: waitedMs2,
25081
+ observations,
25082
+ observedPhase: observation.phase
25083
+ };
25084
+ }
25085
+ const waitedMs = now() - started;
25086
+ if (waitedMs + interval > budget) {
25087
+ return {
25088
+ version: 1,
25089
+ decision: "refuse",
25090
+ reason: "budget-exhausted",
25091
+ waitedMs,
25092
+ observations,
25093
+ observedPhase: observation.phase
25094
+ };
25095
+ }
25096
+ if (!deferred && options.claim) {
25097
+ const claim2 = options.claim(observation);
25098
+ if (claim2.kind === "replay") {
25099
+ return {
25100
+ version: 1,
25101
+ decision: "refuse",
25102
+ reason: "continuation-replay",
25103
+ waitedMs,
25104
+ observations,
25105
+ observedPhase: observation.phase,
25106
+ continuation: { state: claim2.state, settledAtMs: claim2.settledAtMs }
25107
+ };
25108
+ }
25109
+ }
25110
+ deferred = true;
25111
+ await sleep3(Math.min(interval, budget - waitedMs));
25112
+ }
25113
+ }
25114
+ function parseHarnessGateBudget(value) {
25115
+ if (value === void 0) {
25116
+ return HARNESS_GATE_DEFAULT_BUDGET_MS;
25117
+ }
25118
+ const parsed = typeof value === "number" ? value : Number(String(value));
25119
+ if (!Number.isFinite(parsed) || parsed < 0) {
25120
+ return HARNESS_GATE_DEFAULT_BUDGET_MS;
25121
+ }
25122
+ return Math.min(Math.trunc(parsed), HARNESS_GATE_MAX_BUDGET_MS);
25123
+ }
25124
+ var import_node_fs46, import_node_path58, HARNESS_GATE_DEFAULT_BUDGET_MS, HARNESS_GATE_MAX_BUDGET_MS, HARNESS_GATE_POLL_INTERVAL_MS, TRANSITIONAL_PHASES, REPO_ROOT_MAX_DEPTH;
25125
+ var init_harness_gate = __esm({
25126
+ "src/core/harness-gate.ts"() {
25127
+ "use strict";
25128
+ import_node_fs46 = __toESM(require("fs"));
25129
+ import_node_path58 = __toESM(require("path"));
25130
+ init_reliability_operation_store();
25131
+ init_workspace();
25132
+ init_workspace_runtime();
25133
+ HARNESS_GATE_DEFAULT_BUDGET_MS = 3e4;
25134
+ HARNESS_GATE_MAX_BUDGET_MS = 6e5;
25135
+ HARNESS_GATE_POLL_INTERVAL_MS = 2e3;
25136
+ TRANSITIONAL_PHASES = [
25137
+ "queued",
25138
+ "starting",
25139
+ "verifying",
25140
+ "recovering",
25141
+ "stopping"
25142
+ ];
25143
+ REPO_ROOT_MAX_DEPTH = 64;
25144
+ }
25145
+ });
25146
+
25147
+ // src/commands/harness.ts
25148
+ var harness_exports = {};
25149
+ __export(harness_exports, {
25150
+ detectHarnessHook: () => detectHarnessHook,
25151
+ runHarnessCommand: () => runHarnessCommand
25152
+ });
25153
+ function detectHarnessHook(payload) {
25154
+ const turnId = payload.turn_id;
25155
+ return typeof turnId === "string" && turnId.length > 0 ? "codex" : "claude";
25156
+ }
25157
+ function readStdin() {
25158
+ if (process.stdin.isTTY) {
25159
+ return Promise.resolve("");
25160
+ }
25161
+ return new Promise((resolve) => {
25162
+ let input2 = "";
25163
+ let settled = false;
25164
+ const finish = () => {
25165
+ if (settled) return;
25166
+ settled = true;
25167
+ clearTimeout(timer);
25168
+ resolve(input2);
25169
+ };
25170
+ const timer = setTimeout(finish, STDIN_TIMEOUT_MS);
25171
+ process.stdin.setEncoding("utf-8");
25172
+ process.stdin.on("data", (chunk) => {
25173
+ input2 += chunk;
25174
+ if (input2.length > MAX_HOOK_PAYLOAD_BYTES) {
25175
+ finish();
25176
+ process.stdin.destroy();
25177
+ }
25178
+ });
25179
+ process.stdin.on("end", finish);
25180
+ process.stdin.on("error", finish);
25181
+ });
25182
+ }
25183
+ function isRecord7(value) {
25184
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
25185
+ }
25186
+ function permitReason(decision) {
25187
+ const seconds = (decision.waitedMs / 1e3).toFixed(1);
25188
+ if (decision.reason === "continuation-replay") {
25189
+ const settled = decision.continuation?.settledAtMs;
25190
+ const age = settled === void 0 ? "" : ` ${Math.max(0, Math.round((Date.now() - settled) / 1e3))}s ago`;
25191
+ if (decision.continuation?.state === "granted") {
25192
+ return `devrouter: this tool call was already permitted after a wait${age}; it is not re-evaluated. Verify whether it ran before issuing a new call.`;
25193
+ }
25194
+ if (decision.continuation?.state === "refused") {
25195
+ return `devrouter: this tool call was already refused${age}; do not repeat it. Run 'devrouter status .' and issue a new call once it settles.`;
25196
+ }
25197
+ return `devrouter: this tool call's wait was cancelled${age}; the harness may have run it. Verify whether it ran before issuing a new call.`;
25198
+ }
25199
+ switch (decision.decision) {
25200
+ case "refuse":
25201
+ return `devrouter: the managed environment is still ${decision.observedPhase} after waiting ${seconds}s. Do not retry automatically; run 'devrouter status .' and continue once it settles.`;
25202
+ case "deferred-allow":
25203
+ return `devrouter: environment settled after ${seconds}s; the tool may run now.`;
25204
+ default:
25205
+ return "devrouter: environment settled.";
25206
+ }
25207
+ }
25208
+ function hookOutput(decision, kind) {
25209
+ if (decision.decision === "refuse") {
25210
+ return JSON.stringify({
25211
+ hookSpecificOutput: {
25212
+ hookEventName: "PreToolUse",
25213
+ permissionDecision: "deny",
25214
+ permissionDecisionReason: permitReason(decision)
25215
+ }
25216
+ });
25217
+ }
25218
+ if (kind === "codex") {
25219
+ return JSON.stringify({
25220
+ hookSpecificOutput: {
25221
+ hookEventName: "PreToolUse",
25222
+ additionalContext: permitReason(decision)
25223
+ }
25224
+ });
25225
+ }
25226
+ return JSON.stringify({
25227
+ hookSpecificOutput: {
25228
+ hookEventName: "PreToolUse",
25229
+ permissionDecision: "allow",
25230
+ permissionDecisionReason: permitReason(decision)
25231
+ }
25232
+ });
25233
+ }
25234
+ function gateDecision(decision, reason, observedPhase = "unknown") {
25235
+ return {
25236
+ version: 1,
25237
+ decision,
25238
+ reason,
25239
+ waitedMs: 0,
25240
+ observations: 0,
25241
+ observedPhase
25242
+ };
25243
+ }
25244
+ async function runHarnessCommand(method, options, repoPath, dependencies = {}) {
25245
+ if (method !== "gate") {
25246
+ throw new Error(`Unsupported harness method: ${method}`);
25247
+ }
25248
+ const json = options.json === true;
25249
+ const stderr = dependencies.stderr ?? ((line) => process.stderr.write(`${line}
25250
+ `));
25251
+ const observe = dependencies.observe ?? readHarnessGateObservation;
25252
+ const raw = await (dependencies.stdin ?? readStdin)();
25253
+ let kind = "claude";
25254
+ let payload;
25255
+ try {
25256
+ const parsed = JSON.parse(raw);
25257
+ if (isRecord7(parsed)) payload = parsed;
25258
+ } catch {
25259
+ payload = void 0;
25260
+ }
25261
+ if (!payload) {
25262
+ const decision2 = gateDecision("allow", "hook-payload-invalid");
25263
+ process.stdout.write(
25264
+ json ? `${JSON.stringify(decision2)}
25265
+ ` : `${hookOutput(decision2, kind)}
25266
+ `
25267
+ );
25268
+ return;
25269
+ }
25270
+ kind = detectHarnessHook(payload);
25271
+ const toolName = typeof payload.tool_name === "string" ? payload.tool_name : "";
25272
+ const toolInput = isRecord7(payload.tool_input) ? payload.tool_input : {};
25273
+ const command = typeof toolInput.command === "string" ? toolInput.command : "";
25274
+ if (toolName === "Bash" && DEVROUTER_COMMAND_RE.test(command)) {
25275
+ const decision2 = gateDecision("allow", "devrouter-command");
25276
+ process.stdout.write(
25277
+ json ? `${JSON.stringify(decision2)}
25278
+ ` : `${hookOutput(decision2, kind)}
25279
+ `
25280
+ );
25281
+ return;
25282
+ }
25283
+ const sessionCwd = repoPath ?? (typeof payload.cwd === "string" && payload.cwd.length > 0 ? payload.cwd : void 0);
25284
+ const repoRoot = sessionCwd ? findHarnessGateRepoRoot(sessionCwd) : void 0;
25285
+ if (!repoRoot) {
25286
+ const decision2 = gateDecision("allow", "unmanaged-checkout");
25287
+ process.stdout.write(
25288
+ json ? `${JSON.stringify(decision2)}
25289
+ ` : `${hookOutput(decision2, kind)}
25290
+ `
25291
+ );
25292
+ return;
25293
+ }
25294
+ const budgetMs = parseHarnessGateBudget(options.waitBudgetMs);
25295
+ const toolUseId = typeof payload.tool_use_id === "string" && payload.tool_use_id.length > 0 && payload.tool_use_id.length <= MAX_TOOL_USE_ID_LENGTH2 ? payload.tool_use_id : void 0;
25296
+ const payloadSha256 = (0, import_node_crypto27.createHash)("sha256").update(JSON.stringify(toolInput)).digest("hex");
25297
+ const claim2 = dependencies.claim ?? ((root, input2) => {
25298
+ const outcome = claimHarnessContinuation(root, input2);
25299
+ if (outcome.kind === "claimed") {
25300
+ if (!outcome.recorded) {
25301
+ stderr(
25302
+ "devrouter harness gate: continuation ledger is unavailable; repeat protection is off for this checkout"
25303
+ );
25304
+ }
25305
+ return { kind: "claimed" };
25306
+ }
25307
+ const entry = outcome.entry;
25308
+ return {
25309
+ kind: "replay",
25310
+ state: entry.state === "granted" ? "granted" : entry.state === "refused" ? "refused" : "interrupted",
25311
+ settledAtMs: entry.settledAtMs
25312
+ };
25313
+ });
25314
+ const settle = dependencies.settle ?? ((root, id5, settlement) => {
25315
+ settleHarnessContinuation(root, id5, settlement);
25316
+ });
25317
+ const lookup = dependencies.lookup ?? ((root, id5) => {
25318
+ const entry = readHarnessContinuations(root).find((candidate) => candidate.toolUseId === id5);
25319
+ if (!entry) return void 0;
25320
+ return {
25321
+ state: entry.state === "granted" ? "granted" : entry.state === "refused" ? "refused" : (
25322
+ // A claim still waiting belongs to a hook that never returned.
25323
+ "interrupted"
25324
+ ),
25325
+ settledAtMs: entry.settledAtMs
25326
+ };
25327
+ });
25328
+ if (toolUseId) {
25329
+ const replay = lookup(repoRoot, toolUseId);
25330
+ if (replay) {
25331
+ const decision2 = {
25332
+ version: 1,
25333
+ decision: "refuse",
25334
+ reason: "continuation-replay",
25335
+ waitedMs: 0,
25336
+ observations: 0,
25337
+ observedPhase: "unknown",
25338
+ continuation: { state: replay.state, settledAtMs: replay.settledAtMs }
25339
+ };
25340
+ process.stdout.write(
25341
+ json ? `${JSON.stringify({ ...decision2, checkout: repoRoot })}
25342
+ ` : `${hookOutput(decision2, kind)}
25343
+ `
25344
+ );
25345
+ return;
25346
+ }
25347
+ }
25348
+ const interruption = (signal) => {
25349
+ if (toolUseId) {
25350
+ settle(repoRoot, toolUseId, { state: "interrupted" });
25351
+ }
25352
+ process.exit(signal === "SIGINT" ? 130 : 143);
25353
+ };
25354
+ if (toolUseId) {
25355
+ process.once("SIGINT", interruption);
25356
+ process.once("SIGTERM", interruption);
25357
+ }
25358
+ let announced = false;
25359
+ let decision;
25360
+ try {
25361
+ decision = await waitForHarnessGate({
25362
+ observe: () => observe(repoRoot),
25363
+ budgetMs,
25364
+ now: dependencies.now,
25365
+ sleep: dependencies.sleep,
25366
+ claim: toolUseId ? (observation) => claim2(repoRoot, {
25367
+ toolUseId,
25368
+ payloadSha256,
25369
+ phase: observation.phase,
25370
+ budgetMs
25371
+ }) : void 0,
25372
+ onObservation: (observation) => {
25373
+ if (!announced && isTransitionalHarnessPhase(observation.phase)) {
25374
+ announced = true;
25375
+ stderr(
25376
+ `devrouter harness gate: ${observation.phase} in progress; deferring this tool call for up to ${Math.round(budgetMs / 1e3)}s without model turns`
25377
+ );
25378
+ }
25379
+ }
25380
+ });
25381
+ } finally {
25382
+ if (toolUseId) {
25383
+ process.removeListener("SIGINT", interruption);
25384
+ process.removeListener("SIGTERM", interruption);
25385
+ }
25386
+ }
25387
+ if (toolUseId && decision.reason !== "continuation-replay" && decision.decision !== "allow") {
25388
+ settle(repoRoot, toolUseId, {
25389
+ state: decision.decision === "refuse" ? "refused" : "granted",
25390
+ waitedMs: decision.waitedMs
25391
+ });
25392
+ }
25393
+ if (decision.decision === "deferred-allow") {
25394
+ stderr(
25395
+ `devrouter harness gate: settled after ${(decision.waitedMs / 1e3).toFixed(1)}s (${decision.observations} checks)`
25396
+ );
25397
+ }
25398
+ process.stdout.write(
25399
+ json ? `${JSON.stringify({ ...decision, checkout: repoRoot })}
25400
+ ` : `${hookOutput(decision, kind)}
25401
+ `
25402
+ );
25403
+ }
25404
+ var import_node_crypto27, MAX_HOOK_PAYLOAD_BYTES, STDIN_TIMEOUT_MS, MAX_TOOL_USE_ID_LENGTH2, DEVROUTER_COMMAND_RE;
25405
+ var init_harness = __esm({
25406
+ "src/commands/harness.ts"() {
25407
+ "use strict";
25408
+ import_node_crypto27 = require("crypto");
25409
+ init_harness_continuation();
25410
+ init_harness_gate();
25411
+ MAX_HOOK_PAYLOAD_BYTES = 256 * 1024;
25412
+ STDIN_TIMEOUT_MS = 5e3;
25413
+ MAX_TOOL_USE_ID_LENGTH2 = 256;
25414
+ DEVROUTER_COMMAND_RE = /(?:^|[\s;&|()])(?:npx\s+|pnpm\s+(?:exec\s+)?|npm\s+(?:exec\s+)?|yarn\s+)?devrouter(?:-process)?(?:\s|$)/;
25415
+ }
25416
+ });
25417
+
24260
25418
  // src/commands/doctor.ts
24261
25419
  var doctor_exports = {};
24262
25420
  __export(doctor_exports, {
@@ -24459,17 +25617,17 @@ function asRecord3(value) {
24459
25617
  return value;
24460
25618
  }
24461
25619
  function readJson(filePath) {
24462
- if (!import_node_fs45.default.existsSync(filePath)) {
25620
+ if (!import_node_fs47.default.existsSync(filePath)) {
24463
25621
  return void 0;
24464
25622
  }
24465
25623
  try {
24466
- return JSON.parse(import_node_fs45.default.readFileSync(filePath, "utf-8"));
25624
+ return JSON.parse(import_node_fs47.default.readFileSync(filePath, "utf-8"));
24467
25625
  } catch {
24468
25626
  return void 0;
24469
25627
  }
24470
25628
  }
24471
25629
  function relative(repoPath, filePath) {
24472
- return import_node_path56.default.relative(repoPath, filePath) || ".";
25630
+ return import_node_path59.default.relative(repoPath, filePath) || ".";
24473
25631
  }
24474
25632
  function redactEnvAssignments(value) {
24475
25633
  return value.replace(
@@ -24510,7 +25668,7 @@ function inspectPackageManager(repoPath, pkg) {
24510
25668
  ["bun.lock", "bun"]
24511
25669
  ];
24512
25670
  for (const [fileName, name] of lockfiles) {
24513
- if (import_node_fs45.default.existsSync(import_node_path56.default.join(repoPath, fileName))) {
25671
+ if (import_node_fs47.default.existsSync(import_node_path59.default.join(repoPath, fileName))) {
24514
25672
  return { name, source: fileName };
24515
25673
  }
24516
25674
  }
@@ -24525,9 +25683,9 @@ function inspectNode(repoPath, pkg) {
24525
25683
  if (typeof engines?.node === "string") {
24526
25684
  return { version: engines.node, source: "package.json:engines.node" };
24527
25685
  }
24528
- const nvmrc = import_node_path56.default.join(repoPath, ".nvmrc");
24529
- if (import_node_fs45.default.existsSync(nvmrc)) {
24530
- const version = import_node_fs45.default.readFileSync(nvmrc, "utf-8").trim();
25686
+ const nvmrc = import_node_path59.default.join(repoPath, ".nvmrc");
25687
+ if (import_node_fs47.default.existsSync(nvmrc)) {
25688
+ const version = import_node_fs47.default.readFileSync(nvmrc, "utf-8").trim();
24531
25689
  return { version, source: ".nvmrc" };
24532
25690
  }
24533
25691
  return void 0;
@@ -24588,7 +25746,7 @@ function configuredComposeFiles(repoPath) {
24588
25746
  const files = config.apps.filter(
24589
25747
  (app) => app.runtime === "docker"
24590
25748
  ).flatMap((app) => app.docker.composeFiles).filter(
24591
- (fileName) => !import_node_path56.default.isAbsolute(fileName) && !import_node_path56.default.normalize(fileName).startsWith("..")
25749
+ (fileName) => !import_node_path59.default.isAbsolute(fileName) && !import_node_path59.default.normalize(fileName).startsWith("..")
24592
25750
  );
24593
25751
  return Array.from(new Set(files));
24594
25752
  } catch {
@@ -24606,7 +25764,7 @@ function composeFiles(repoPath) {
24606
25764
  ...configuredComposeFiles(repoPath)
24607
25765
  ];
24608
25766
  return Array.from(new Set(candidates)).filter(
24609
- (fileName) => import_node_fs45.default.existsSync(import_node_path56.default.join(repoPath, fileName))
25767
+ (fileName) => import_node_fs47.default.existsSync(import_node_path59.default.join(repoPath, fileName))
24610
25768
  );
24611
25769
  }
24612
25770
  function stringArray2(value) {
@@ -24644,7 +25802,7 @@ function inspectServices(repoPath) {
24644
25802
  const services = [];
24645
25803
  for (const fileName of composeFiles(repoPath)) {
24646
25804
  try {
24647
- const parsed = import_yaml7.default.parse(import_node_fs45.default.readFileSync(import_node_path56.default.join(repoPath, fileName), "utf-8"));
25805
+ const parsed = import_yaml7.default.parse(import_node_fs47.default.readFileSync(import_node_path59.default.join(repoPath, fileName), "utf-8"));
24648
25806
  const serviceMap = asRecord3(asRecord3(parsed)?.services);
24649
25807
  for (const [name, value] of Object.entries(serviceMap ?? {})) {
24650
25808
  const service = asRecord3(value);
@@ -24679,8 +25837,8 @@ function inspectServices(repoPath) {
24679
25837
  return services;
24680
25838
  }
24681
25839
  function inspectEnvFiles(repoPath) {
24682
- const files = import_node_fs45.default.readdirSync(repoPath).filter((fileName) => /^\.env(\.|$)/.test(fileName)).sort().map((fileName) => {
24683
- const content = import_node_fs45.default.readFileSync(import_node_path56.default.join(repoPath, fileName), "utf-8");
25840
+ const files = import_node_fs47.default.readdirSync(repoPath).filter((fileName) => /^\.env(\.|$)/.test(fileName)).sort().map((fileName) => {
25841
+ const content = import_node_fs47.default.readFileSync(import_node_path59.default.join(repoPath, fileName), "utf-8");
24684
25842
  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();
24685
25843
  return { path: fileName, names };
24686
25844
  });
@@ -24694,18 +25852,18 @@ function inspectEnvFiles(repoPath) {
24694
25852
  };
24695
25853
  }
24696
25854
  function inspectDevcontainer(repoPath) {
24697
- const dir = import_node_path56.default.join(repoPath, ".devcontainer");
24698
- if (!import_node_fs45.default.existsSync(dir)) {
25855
+ const dir = import_node_path59.default.join(repoPath, ".devcontainer");
25856
+ if (!import_node_fs47.default.existsSync(dir)) {
24699
25857
  return { exists: false, files: [] };
24700
25858
  }
24701
25859
  return {
24702
25860
  exists: true,
24703
- 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}`)
25861
+ files: import_node_fs47.default.readdirSync(dir).filter((fileName) => import_node_fs47.default.statSync(import_node_path59.default.join(dir, fileName)).isFile()).sort().map((fileName) => `.devcontainer/${fileName}`)
24704
25862
  };
24705
25863
  }
24706
25864
  function inspectDevrouter(repoPath) {
24707
25865
  const configPath = getRepoConfigPath(repoPath);
24708
- if (!import_node_fs45.default.existsSync(configPath)) {
25866
+ if (!import_node_fs47.default.existsSync(configPath)) {
24709
25867
  return {
24710
25868
  exists: false,
24711
25869
  configPath,
@@ -24749,15 +25907,15 @@ function inspectAgentGuidance(repoPath) {
24749
25907
  ["AGENTS.md", "agents"],
24750
25908
  ["CLAUDE.md", "claude"]
24751
25909
  ]) {
24752
- if (import_node_fs45.default.existsSync(import_node_path56.default.join(repoPath, fileName))) {
25910
+ if (import_node_fs47.default.existsSync(import_node_path59.default.join(repoPath, fileName))) {
24753
25911
  results.push({ path: fileName, kind });
24754
25912
  }
24755
25913
  }
24756
- const skillsDir = import_node_path56.default.join(repoPath, ".agents", "skills");
24757
- if (import_node_fs45.default.existsSync(skillsDir)) {
24758
- for (const name of import_node_fs45.default.readdirSync(skillsDir).sort()) {
24759
- const skillPath = import_node_path56.default.join(skillsDir, name, "SKILL.md");
24760
- if (import_node_fs45.default.existsSync(skillPath)) {
25914
+ const skillsDir = import_node_path59.default.join(repoPath, ".agents", "skills");
25915
+ if (import_node_fs47.default.existsSync(skillsDir)) {
25916
+ for (const name of import_node_fs47.default.readdirSync(skillsDir).sort()) {
25917
+ const skillPath = import_node_path59.default.join(skillsDir, name, "SKILL.md");
25918
+ if (import_node_fs47.default.existsSync(skillPath)) {
24761
25919
  results.push({ path: relative(repoPath, skillPath), kind: "skill" });
24762
25920
  }
24763
25921
  }
@@ -24800,7 +25958,7 @@ function buildIssues(report) {
24800
25958
  }
24801
25959
  function inspectRepo(options = {}) {
24802
25960
  const repoPath = resolveRepoPath(options.repo);
24803
- const pkg = readJson(import_node_path56.default.join(repoPath, "package.json"));
25961
+ const pkg = readJson(import_node_path59.default.join(repoPath, "package.json"));
24804
25962
  const scripts = inspectScripts(pkg);
24805
25963
  const reportWithoutIssues = {
24806
25964
  repoPath,
@@ -24819,12 +25977,12 @@ function inspectRepo(options = {}) {
24819
25977
  issues: buildIssues(reportWithoutIssues)
24820
25978
  };
24821
25979
  }
24822
- var import_node_fs45, import_node_path56, import_yaml7;
25980
+ var import_node_fs47, import_node_path59, import_yaml7;
24823
25981
  var init_repo_inspect = __esm({
24824
25982
  "src/core/repo-inspect.ts"() {
24825
25983
  "use strict";
24826
- import_node_fs45 = __toESM(require("fs"));
24827
- import_node_path56 = __toESM(require("path"));
25984
+ import_node_fs47 = __toESM(require("fs"));
25985
+ import_node_path59 = __toESM(require("path"));
24828
25986
  import_yaml7 = __toESM(require("yaml"));
24829
25987
  init_repo_config();
24830
25988
  }
@@ -24929,7 +26087,7 @@ function requiredFileChecks(repoPath) {
24929
26087
  ".devcontainer/docker-compose.yml",
24930
26088
  ".devrouter.yml"
24931
26089
  ];
24932
- const missing = required.filter((fileName) => !import_node_fs46.default.existsSync(import_node_path57.default.join(repoPath, fileName)));
26090
+ const missing = required.filter((fileName) => !import_node_fs48.default.existsSync(import_node_path60.default.join(repoPath, fileName)));
24933
26091
  return {
24934
26092
  id: "repo.devcontainer.verify-files",
24935
26093
  level: missing.length === 0 ? "ok" : "error",
@@ -25154,12 +26312,12 @@ async function verifyDevcontainer(options = {}) {
25154
26312
  nextSteps: collectNextSteps3(checks)
25155
26313
  };
25156
26314
  }
25157
- var import_node_fs46, import_node_path57;
26315
+ var import_node_fs48, import_node_path60;
25158
26316
  var init_devcontainer_verify = __esm({
25159
26317
  "src/core/devcontainer-verify.ts"() {
25160
26318
  "use strict";
25161
- import_node_fs46 = __toESM(require("fs"));
25162
- import_node_path57 = __toESM(require("path"));
26319
+ import_node_fs48 = __toESM(require("fs"));
26320
+ import_node_path60 = __toESM(require("path"));
25163
26321
  init_capabilities();
25164
26322
  init_doctor();
25165
26323
  init_host_routes();
@@ -25456,7 +26614,7 @@ function packageManagerIssues(repo) {
25456
26614
  }
25457
26615
  function plannedFiles(repoPath, version) {
25458
26616
  const repo = inspectRepo({ repo: repoPath });
25459
- const projectName = sanitizeProjectName(import_node_path58.default.basename(repo.repoPath));
26617
+ const projectName = sanitizeProjectName(import_node_path61.default.basename(repo.repoPath));
25460
26618
  const nodeMajor = majorVersion(repo.node?.version, "24");
25461
26619
  const pnpmVersion = repo.packageManager?.name === "pnpm" && repo.packageManager.version ? repo.packageManager.version : DEFAULT_PNPM_VERSION;
25462
26620
  const port = inferPort2(repo);
@@ -25501,8 +26659,8 @@ function plannedFiles(repoPath, version) {
25501
26659
  };
25502
26660
  }
25503
26661
  function classifyFile(repoPath, file) {
25504
- const absolutePath = import_node_path58.default.join(repoPath, file.relativePath);
25505
- if (!import_node_fs47.default.existsSync(absolutePath)) {
26662
+ const absolutePath = import_node_path61.default.join(repoPath, file.relativePath);
26663
+ if (!import_node_fs49.default.existsSync(absolutePath)) {
25506
26664
  return {
25507
26665
  path: file.relativePath,
25508
26666
  action: "create",
@@ -25510,7 +26668,7 @@ function classifyFile(repoPath, file) {
25510
26668
  bytes: Buffer.byteLength(file.content)
25511
26669
  };
25512
26670
  }
25513
- const current = import_node_fs47.default.readFileSync(absolutePath, "utf-8");
26671
+ const current = import_node_fs49.default.readFileSync(absolutePath, "utf-8");
25514
26672
  if (!current.includes(MANAGED_MARKER2)) {
25515
26673
  return {
25516
26674
  path: file.relativePath,
@@ -25567,11 +26725,11 @@ function buildPlan(repoPath, dryRun, version) {
25567
26725
  };
25568
26726
  }
25569
26727
  function writeFile(repoPath, file) {
25570
- const absolutePath = import_node_path58.default.join(repoPath, file.relativePath);
25571
- import_node_fs47.default.mkdirSync(import_node_path58.default.dirname(absolutePath), { recursive: true });
25572
- import_node_fs47.default.writeFileSync(absolutePath, file.content, "utf-8");
26728
+ const absolutePath = import_node_path61.default.join(repoPath, file.relativePath);
26729
+ import_node_fs49.default.mkdirSync(import_node_path61.default.dirname(absolutePath), { recursive: true });
26730
+ import_node_fs49.default.writeFileSync(absolutePath, file.content, "utf-8");
25573
26731
  if (file.executable) {
25574
- import_node_fs47.default.chmodSync(absolutePath, 493);
26732
+ import_node_fs49.default.chmodSync(absolutePath, 493);
25575
26733
  }
25576
26734
  }
25577
26735
  function writeDevcontainer(options = {}) {
@@ -25609,12 +26767,12 @@ function writeDevcontainer(options = {}) {
25609
26767
  nextSteps: postWriteNextSteps(repoPath)
25610
26768
  };
25611
26769
  }
25612
- var import_node_fs47, import_node_path58, MANAGED_MARKER2, DEFAULT_DEVROUTER_VERSION, DEFAULT_PNPM_VERSION, VALID_PACKAGE_VERSION_RE;
26770
+ var import_node_fs49, import_node_path61, MANAGED_MARKER2, DEFAULT_DEVROUTER_VERSION, DEFAULT_PNPM_VERSION, VALID_PACKAGE_VERSION_RE;
25613
26771
  var init_devcontainer_write = __esm({
25614
26772
  "src/core/devcontainer-write.ts"() {
25615
26773
  "use strict";
25616
- import_node_fs47 = __toESM(require("fs"));
25617
- import_node_path58 = __toESM(require("path"));
26774
+ import_node_fs49 = __toESM(require("fs"));
26775
+ import_node_path61 = __toESM(require("path"));
25618
26776
  init_repo_config();
25619
26777
  init_repo_inspect();
25620
26778
  MANAGED_MARKER2 = "devrouter:managed devcontainer";
@@ -25727,11 +26885,11 @@ var init_repo_devcontainer = __esm({
25727
26885
  function fail3(message) {
25728
26886
  throw new Error(message);
25729
26887
  }
25730
- function isRecord7(value) {
26888
+ function isRecord8(value) {
25731
26889
  return typeof value === "object" && value !== null && !Array.isArray(value);
25732
26890
  }
25733
26891
  function requireRecord(value, label2) {
25734
- if (!isRecord7(value)) fail3(`${label2} must be a mapping.`);
26892
+ if (!isRecord8(value)) fail3(`${label2} must be a mapping.`);
25735
26893
  return value;
25736
26894
  }
25737
26895
  function requireExactKeys(value, allowed, label2) {
@@ -25948,13 +27106,13 @@ function buildProfilePlanReport(options) {
25948
27106
  return { ...report, contractPath, bindings };
25949
27107
  }
25950
27108
  function loadContract(repoPath, contractPath) {
25951
- if (import_node_path59.default.isAbsolute(contractPath)) {
27109
+ if (import_node_path62.default.isAbsolute(contractPath)) {
25952
27110
  fail3("profile plan contract path must be relative to the repository.");
25953
27111
  }
25954
27112
  const absolutePath = assertPathWithinRepo(contractPath, repoPath, "Profile plan contract");
25955
27113
  let metadata;
25956
27114
  try {
25957
- metadata = import_node_fs48.default.lstatSync(absolutePath);
27115
+ metadata = import_node_fs50.default.lstatSync(absolutePath);
25958
27116
  } catch (error) {
25959
27117
  fail3(`could not inspect profile plan contract '${contractPath}': ${error.message}`);
25960
27118
  }
@@ -25964,16 +27122,16 @@ function loadContract(repoPath, contractPath) {
25964
27122
  if (metadata.size > MAX_CONTRACT_BYTES) {
25965
27123
  fail3(`profile plan contract exceeds the ${MAX_CONTRACT_BYTES}-byte limit.`);
25966
27124
  }
25967
- const realRepo = import_node_fs48.default.realpathSync(repoPath);
25968
- const realContract = import_node_fs48.default.realpathSync(absolutePath);
27125
+ const realRepo = import_node_fs50.default.realpathSync(repoPath);
27126
+ const realContract = import_node_fs50.default.realpathSync(absolutePath);
25969
27127
  assertPathWithinRepo(realContract, realRepo, "Profile plan contract");
25970
- const source = import_node_fs48.default.readFileSync(absolutePath, "utf-8");
27128
+ const source = import_node_fs50.default.readFileSync(absolutePath, "utf-8");
25971
27129
  if (Buffer.byteLength(source, "utf-8") > MAX_CONTRACT_BYTES) {
25972
27130
  fail3(`profile plan contract exceeds the ${MAX_CONTRACT_BYTES}-byte limit.`);
25973
27131
  }
25974
27132
  return {
25975
27133
  contract: parseProfilePlanContract(source),
25976
- contractPath: import_node_path59.default.relative(repoPath, absolutePath).split(import_node_path59.default.sep).join("/")
27134
+ contractPath: import_node_path62.default.relative(repoPath, absolutePath).split(import_node_path62.default.sep).join("/")
25977
27135
  };
25978
27136
  }
25979
27137
  function resolveProfilePlan(options) {
@@ -25987,12 +27145,12 @@ function resolveProfilePlan(options) {
25987
27145
  contractPath: loaded.contractPath
25988
27146
  });
25989
27147
  }
25990
- 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;
27148
+ var import_node_fs50, import_node_path62, import_yaml8, CONTRACT_VERSION, MAX_CONTRACT_BYTES, MAX_APP_MAPPINGS, MAX_BINDING_KEYS, MAX_BINDING_VALUES, MAX_LITERAL_LENGTH, BINDING_KEY;
25991
27149
  var init_profile_plan = __esm({
25992
27150
  "src/core/profile-plan.ts"() {
25993
27151
  "use strict";
25994
- import_node_fs48 = __toESM(require("fs"));
25995
- import_node_path59 = __toESM(require("path"));
27152
+ import_node_fs50 = __toESM(require("fs"));
27153
+ import_node_path62 = __toESM(require("path"));
25996
27154
  import_yaml8 = require("yaml");
25997
27155
  init_paths();
25998
27156
  init_profile_resolution();
@@ -26024,7 +27182,7 @@ async function runProfileResolveCommand(options) {
26024
27182
  async function runProfilePlanCommand(options) {
26025
27183
  const report = resolveProfilePlan(options);
26026
27184
  if (options.output) {
26027
- writeFileAtomically(import_node_path60.default.resolve(options.output), `${JSON.stringify(report, null, 2)}
27185
+ writeFileAtomically(import_node_path63.default.resolve(options.output), `${JSON.stringify(report, null, 2)}
26028
27186
  `);
26029
27187
  }
26030
27188
  if (options.json) {
@@ -26071,14 +27229,14 @@ function printProfilePlanSummary(report, output2) {
26071
27229
  process.stdout.write(`Binding ${key}: ${renderValues(report.bindings[key] ?? [])}
26072
27230
  `);
26073
27231
  }
26074
- if (output2) process.stdout.write(`Output: ${import_node_path60.default.resolve(output2)}
27232
+ if (output2) process.stdout.write(`Output: ${import_node_path63.default.resolve(output2)}
26075
27233
  `);
26076
27234
  }
26077
- var import_node_path60;
27235
+ var import_node_path63;
26078
27236
  var init_profile = __esm({
26079
27237
  "src/commands/profile.ts"() {
26080
27238
  "use strict";
26081
- import_node_path60 = __toESM(require("path"));
27239
+ import_node_path63 = __toESM(require("path"));
26082
27240
  init_atomic_file();
26083
27241
  init_output();
26084
27242
  init_profile_plan();
@@ -26368,7 +27526,7 @@ function sanitizeRouterId(value) {
26368
27526
  return value.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
26369
27527
  }
26370
27528
  function repoHash(repoPath) {
26371
- return (0, import_node_crypto26.createHash)("sha1").update(import_node_path61.default.resolve(repoPath)).digest("hex").slice(0, 12);
27529
+ return (0, import_node_crypto28.createHash)("sha1").update(import_node_path64.default.resolve(repoPath)).digest("hex").slice(0, 12);
26372
27530
  }
26373
27531
  function asDockerApp(app) {
26374
27532
  return app.runtime === "docker";
@@ -26447,11 +27605,11 @@ function prepareDockerOverlay(repoPath, appName, apps, publishTcpPorts = false)
26447
27605
  if (dockerApps.length === 0) {
26448
27606
  throw new Error("No docker apps selected to prepare compose overlay.");
26449
27607
  }
26450
- const cachePath = import_node_path61.default.join(CACHE_DIR, repoHash(repoPath), sanitizeRouterId(appName));
26451
- import_node_fs49.default.mkdirSync(cachePath, { recursive: true });
26452
- const overlayPath = import_node_path61.default.join(cachePath, "compose.devrouter.yml");
27608
+ const cachePath = import_node_path64.default.join(CACHE_DIR, repoHash(repoPath), sanitizeRouterId(appName));
27609
+ import_node_fs51.default.mkdirSync(cachePath, { recursive: true });
27610
+ const overlayPath = import_node_path64.default.join(cachePath, "compose.devrouter.yml");
26453
27611
  const overlayDocument = buildOverlayDocument(dockerApps, publishTcpPorts);
26454
- import_node_fs49.default.writeFileSync(overlayPath, import_yaml9.default.stringify(overlayDocument, { lineWidth: 0 }), "utf-8");
27612
+ import_node_fs51.default.writeFileSync(overlayPath, import_yaml9.default.stringify(overlayDocument, { lineWidth: 0 }), "utf-8");
26455
27613
  return {
26456
27614
  overlayPath,
26457
27615
  composeFiles: ensureComposeFiles(dockerApps),
@@ -26571,14 +27729,14 @@ function queryMappedPort(repoPath, composeFiles2, overlayPath, service, internal
26571
27729
  const port = Number(match[1]);
26572
27730
  return Number.isInteger(port) && port > 0 ? port : void 0;
26573
27731
  }
26574
- var import_node_child_process33, import_node_crypto26, import_node_fs49, import_node_path61, import_yaml9;
27732
+ var import_node_child_process33, import_node_crypto28, import_node_fs51, import_node_path64, import_yaml9;
26575
27733
  var init_docker_run = __esm({
26576
27734
  "src/core/docker-run.ts"() {
26577
27735
  "use strict";
26578
27736
  import_node_child_process33 = require("child_process");
26579
- import_node_crypto26 = require("crypto");
26580
- import_node_fs49 = __toESM(require("fs"));
26581
- import_node_path61 = __toESM(require("path"));
27737
+ import_node_crypto28 = require("crypto");
27738
+ import_node_fs51 = __toESM(require("fs"));
27739
+ import_node_path64 = __toESM(require("path"));
26582
27740
  import_yaml9 = __toESM(require("yaml"));
26583
27741
  init_docker_error_guidance();
26584
27742
  init_paths();
@@ -27400,13 +28558,13 @@ function measureWorktreeConsumption(worktreePath, options) {
27400
28558
  const startedAt = Date.now();
27401
28559
  let rootStat;
27402
28560
  try {
27403
- rootStat = import_node_fs50.default.lstatSync(worktreePath);
28561
+ rootStat = import_node_fs52.default.lstatSync(worktreePath);
27404
28562
  } catch (error) {
27405
28563
  return { status: "unknown", reason: describeError(error, worktreePath) };
27406
28564
  }
27407
28565
  let rootEntries;
27408
28566
  try {
27409
- rootEntries = import_node_fs50.default.readdirSync(worktreePath, { withFileTypes: true });
28567
+ rootEntries = import_node_fs52.default.readdirSync(worktreePath, { withFileTypes: true });
27410
28568
  } catch (error) {
27411
28569
  return { status: "unknown", reason: describeError(error, worktreePath) };
27412
28570
  }
@@ -27432,10 +28590,10 @@ function measureWorktreeConsumption(worktreePath, options) {
27432
28590
  timedOut = true;
27433
28591
  break;
27434
28592
  }
27435
- const entryPath = import_node_path62.default.join(dirPath, entry.name);
28593
+ const entryPath = import_node_path65.default.join(dirPath, entry.name);
27436
28594
  let entryStat;
27437
28595
  try {
27438
- entryStat = import_node_fs50.default.lstatSync(entryPath);
28596
+ entryStat = import_node_fs52.default.lstatSync(entryPath);
27439
28597
  } catch (error) {
27440
28598
  if (error?.code === "ENOENT") continue;
27441
28599
  unreadableReason = describeIncompleteWalk(error);
@@ -27445,7 +28603,7 @@ function measureWorktreeConsumption(worktreePath, options) {
27445
28603
  if (!entryStat.isDirectory()) continue;
27446
28604
  let childEntries;
27447
28605
  try {
27448
- childEntries = import_node_fs50.default.readdirSync(entryPath, { withFileTypes: true });
28606
+ childEntries = import_node_fs52.default.readdirSync(entryPath, { withFileTypes: true });
27449
28607
  } catch (error) {
27450
28608
  unreadableReason = describeIncompleteWalk(error);
27451
28609
  break;
@@ -27509,12 +28667,12 @@ function describeError(error, worktreePath) {
27509
28667
  }
27510
28668
  return `could not stat worktree path '${worktreePath}': ${error?.message ?? String(error)}`;
27511
28669
  }
27512
- var import_node_fs50, import_node_path62, DEFAULT_DEADLINE_MS, BLOCK_SIZE_BYTES;
28670
+ var import_node_fs52, import_node_path65, DEFAULT_DEADLINE_MS, BLOCK_SIZE_BYTES;
27513
28671
  var init_workspace_consumption = __esm({
27514
28672
  "src/core/workspace-consumption.ts"() {
27515
28673
  "use strict";
27516
- import_node_fs50 = __toESM(require("fs"));
27517
- import_node_path62 = __toESM(require("path"));
28674
+ import_node_fs52 = __toESM(require("fs"));
28675
+ import_node_path65 = __toESM(require("path"));
27518
28676
  init_devpod_environment();
27519
28677
  DEFAULT_DEADLINE_MS = 1e4;
27520
28678
  BLOCK_SIZE_BYTES = 512;
@@ -27522,7 +28680,7 @@ var init_workspace_consumption = __esm({
27522
28680
  });
27523
28681
 
27524
28682
  // src/core/workspace-cleanup.ts
27525
- function isRecord8(value) {
28683
+ function isRecord9(value) {
27526
28684
  return value !== null && typeof value === "object" && !Array.isArray(value);
27527
28685
  }
27528
28686
  function isSha(value) {
@@ -27615,7 +28773,7 @@ function evaluateWorkspaceActivity(evidence, cutoff) {
27615
28773
  }
27616
28774
  function readGitSnapshot(worktree, commandRunner) {
27617
28775
  const comparablePath = comparableWorkspacePath(worktree.path);
27618
- if (worktree.prunable || !import_node_fs51.default.existsSync(comparablePath)) {
28776
+ if (worktree.prunable || !import_node_fs53.default.existsSync(comparablePath)) {
27619
28777
  return { worktree, checkout: "missing", head: null, committerDate: null };
27620
28778
  }
27621
28779
  const head = gitOutput(comparablePath, ["rev-parse", "--verify", "HEAD"], commandRunner);
@@ -27666,14 +28824,14 @@ function parseGitHubChanges(value, project, branch) {
27666
28824
  if (!Array.isArray(value)) return void 0;
27667
28825
  const changes = [];
27668
28826
  for (const item of value) {
27669
- if (!isRecord8(item)) return void 0;
27670
- const repository = isRecord8(item.repository) ? item.repository.nameWithOwner : void 0;
27671
- const headRepository = isRecord8(item.headRepository) ? item.headRepository.nameWithOwner : void 0;
28827
+ if (!isRecord9(item)) return void 0;
28828
+ const repository = isRecord9(item.repository) ? item.repository.nameWithOwner : void 0;
28829
+ const headRepository = isRecord9(item.headRepository) ? item.headRepository.nameWithOwner : void 0;
27672
28830
  if (repository !== project || headRepository !== project || item.headRefName !== branch || !isSha(item.headRefOid) || typeof item.baseRefName !== "string") {
27673
28831
  continue;
27674
28832
  }
27675
28833
  const merged = item.state === "MERGED" && validTimestamp(item.mergedAt);
27676
- const mergeCommit = isRecord8(item.mergeCommit) && isSha(item.mergeCommit.oid) ? item.mergeCommit.oid : void 0;
28834
+ const mergeCommit = isRecord9(item.mergeCommit) && isSha(item.mergeCommit.oid) ? item.mergeCommit.oid : void 0;
27677
28835
  const baseSha = isSha(item.baseRefOid) ? item.baseRefOid : void 0;
27678
28836
  changes.push({
27679
28837
  sourceBranch: branch,
@@ -27690,14 +28848,14 @@ function parseGitLabChanges(value, _project, branch) {
27690
28848
  if (!Array.isArray(value)) return void 0;
27691
28849
  const changes = [];
27692
28850
  for (const item of value) {
27693
- if (!isRecord8(item)) return void 0;
28851
+ if (!isRecord9(item)) return void 0;
27694
28852
  const sourceProjectId = item.source_project_id;
27695
28853
  const targetProjectId = item.target_project_id;
27696
28854
  const sameProject = typeof sourceProjectId === "number" && typeof targetProjectId === "number" && sourceProjectId === targetProjectId;
27697
28855
  if (!sameProject || item.source_branch !== branch || !isSha(item.sha) || typeof item.target_branch !== "string") {
27698
28856
  continue;
27699
28857
  }
27700
- const diffRefs = isRecord8(item.diff_refs) ? item.diff_refs : void 0;
28858
+ const diffRefs = isRecord9(item.diff_refs) ? item.diff_refs : void 0;
27701
28859
  const mergeCommitSha = isSha(item.merge_commit_sha) ? item.merge_commit_sha : void 0;
27702
28860
  const baseSha = diffRefs && isSha(diffRefs.base_sha) ? diffRefs.base_sha : void 0;
27703
28861
  changes.push({
@@ -28140,7 +29298,7 @@ function buildWorkspaceCleanupReport(options = {}, dependencies = {}) {
28140
29298
  containers2 = measureContainersFn(worktreePaths);
28141
29299
  } catch (error) {
28142
29300
  const reason = `container measurement failed: ${describeCause(error)}`;
28143
- containers2 = new Map(worktreePaths.map((path63) => [path63, unknownContainers(reason)]));
29301
+ containers2 = new Map(worktreePaths.map((path66) => [path66, unknownContainers(reason)]));
28144
29302
  }
28145
29303
  }
28146
29304
  const rows2 = records.map((record5) => {
@@ -28190,12 +29348,12 @@ function buildWorkspaceCleanupReport(options = {}, dependencies = {}) {
28190
29348
  networkCapacity: (dependencies.inspectNetworkCapacity ?? inspectNetworkCapacity)()
28191
29349
  };
28192
29350
  }
28193
- var import_node_child_process35, import_node_fs51, DEFAULT_INACTIVE_FOR, READ_ONLY_GIT_ENV2, ACTIVITY_SOURCES, defaultCommandRunner;
29351
+ var import_node_child_process35, import_node_fs53, DEFAULT_INACTIVE_FOR, READ_ONLY_GIT_ENV2, ACTIVITY_SOURCES, defaultCommandRunner;
28194
29352
  var init_workspace_cleanup = __esm({
28195
29353
  "src/core/workspace-cleanup.ts"() {
28196
29354
  "use strict";
28197
29355
  import_node_child_process35 = require("child_process");
28198
- import_node_fs51 = __toESM(require("fs"));
29356
+ import_node_fs53 = __toESM(require("fs"));
28199
29357
  init_devpod_workspaces();
28200
29358
  init_host_routes();
28201
29359
  init_network_diagnostics();
@@ -28307,7 +29465,7 @@ function warnMissingWorkspaceOwnership(repoPath) {
28307
29465
  );
28308
29466
  }
28309
29467
  function defaultWorktreePath(mainRepo, ws) {
28310
- return import_node_path63.default.join(mainRepo, "trees", ws);
29468
+ return import_node_path66.default.join(mainRepo, "trees", ws);
28311
29469
  }
28312
29470
  function assertDefaultWorktreeRootIgnored(mainRepo) {
28313
29471
  const ignored = (0, import_node_child_process36.spawnSync)("git", ["-C", mainRepo, "check-ignore", "-q", "--no-index", "trees/"], {
@@ -28315,12 +29473,12 @@ function assertDefaultWorktreeRootIgnored(mainRepo) {
28315
29473
  });
28316
29474
  if (ignored.status !== 0) {
28317
29475
  throw new Error(
28318
- `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.`
29476
+ `Default worktree root '${import_node_path66.default.join(mainRepo, "trees")}' is not ignored. Add 'trees/' to '${import_node_path66.default.join(mainRepo, ".gitignore")}' or use --path.`
28319
29477
  );
28320
29478
  }
28321
29479
  }
28322
29480
  function legacyDefaultWorktreePath(mainRepo, ws) {
28323
- return import_node_path63.default.join(import_node_path63.default.dirname(mainRepo), `${import_node_path63.default.basename(mainRepo)}-${ws}`);
29481
+ return import_node_path66.default.join(import_node_path66.default.dirname(mainRepo), `${import_node_path66.default.basename(mainRepo)}-${ws}`);
28324
29482
  }
28325
29483
  function teardownFallbackPath(mainRepo, workspace) {
28326
29484
  const candidates = [
@@ -28432,7 +29590,7 @@ function assertFullDownPreflight(mainRepo, target) {
28432
29590
  if (sameWorkspacePath(target.worktreePath, mainRepo)) {
28433
29591
  throw new Error("Refusing to remove the primary Git checkout.");
28434
29592
  }
28435
- if (!target.worktree || target.worktree.prunable || !import_node_fs52.default.existsSync(target.worktreePath)) return;
29593
+ if (!target.worktree || target.worktree.prunable || !import_node_fs54.default.existsSync(target.worktreePath)) return;
28436
29594
  if (target.worktree.locked) {
28437
29595
  throw new Error(
28438
29596
  `Worktree '${target.worktreePath}' is locked; unlock it before workspace down.`
@@ -28472,9 +29630,9 @@ async function workspaceUp(branch, opts = {}) {
28472
29630
  (candidate) => defaultWorktreePath(mainRepo, candidate)
28473
29631
  );
28474
29632
  const generatedPath = candidatePaths.find(
28475
- (candidate) => !import_node_fs52.default.existsSync(candidate) && !worktrees.some((worktree) => sameWorkspacePath(worktree.path, candidate))
29633
+ (candidate) => !import_node_fs54.default.existsSync(candidate) && !worktrees.some((worktree) => sameWorkspacePath(worktree.path, candidate))
28476
29634
  );
28477
- const selectedPath = opts.path ? import_node_path63.default.resolve(opts.path) : existingBranch?.path ?? generatedPath;
29635
+ const selectedPath = opts.path ? import_node_path66.default.resolve(opts.path) : existingBranch?.path ?? generatedPath;
28478
29636
  if (!selectedPath) {
28479
29637
  throw new Error(`Could not allocate a collision-safe worktree path for branch '${branch}'.`);
28480
29638
  }
@@ -28483,7 +29641,7 @@ async function workspaceUp(branch, opts = {}) {
28483
29641
  `Branch '${branch}' already uses worktree '${existingBranch.path}', not '${selectedPath}'.`
28484
29642
  );
28485
29643
  }
28486
- if (import_node_fs52.default.existsSync(selectedPath)) {
29644
+ if (import_node_fs54.default.existsSync(selectedPath)) {
28487
29645
  const registered = worktrees.find(
28488
29646
  (worktree) => sameWorkspacePath(worktree.path, selectedPath)
28489
29647
  );
@@ -28646,7 +29804,7 @@ async function runWorkspaceLifecycle(action2, target, opts = {}) {
28646
29804
  }
28647
29805
  const result = await mutateWorkspaceRuntime("delete", resolved, worktrees, opts.quiet);
28648
29806
  if (removeWorktree) {
28649
- if (resolved.worktree && !resolved.worktree.prunable && import_node_fs52.default.existsSync(resolved.worktreePath)) {
29807
+ if (resolved.worktree && !resolved.worktree.prunable && import_node_fs54.default.existsSync(resolved.worktreePath)) {
28650
29808
  const rm = (0, import_node_child_process36.spawnSync)("git", ["-C", mainRepo, "worktree", "remove", resolved.worktreePath], {
28651
29809
  encoding: "utf-8"
28652
29810
  });
@@ -28665,7 +29823,7 @@ async function runWorkspaceLifecycle(action2, target, opts = {}) {
28665
29823
  }
28666
29824
  return result;
28667
29825
  };
28668
- return resolved.worktree && !resolved.worktree.prunable && import_node_fs52.default.existsSync(resolved.worktreePath) ? withLifecycleOperationLock(resolved.worktreePath, operation) : operation();
29826
+ return resolved.worktree && !resolved.worktree.prunable && import_node_fs54.default.existsSync(resolved.worktreePath) ? withLifecycleOperationLock(resolved.worktreePath, operation) : operation();
28669
29827
  }
28670
29828
  async function workspaceStop(target, opts = {}) {
28671
29829
  return runWorkspaceLifecycle("stop", target, opts);
@@ -28673,13 +29831,13 @@ async function workspaceStop(target, opts = {}) {
28673
29831
  async function workspaceDown(target, opts = {}) {
28674
29832
  return runWorkspaceLifecycle("down", target, opts);
28675
29833
  }
28676
- var import_node_child_process36, import_node_fs52, import_node_path63, WORKSPACE_ALLOCATION_LOCK_WAIT_MS;
29834
+ var import_node_child_process36, import_node_fs54, import_node_path66, WORKSPACE_ALLOCATION_LOCK_WAIT_MS;
28677
29835
  var init_workspace_lifecycle = __esm({
28678
29836
  "src/core/workspace-lifecycle.ts"() {
28679
29837
  "use strict";
28680
29838
  import_node_child_process36 = require("child_process");
28681
- import_node_fs52 = __toESM(require("fs"));
28682
- import_node_path63 = __toESM(require("path"));
29839
+ import_node_fs54 = __toESM(require("fs"));
29840
+ import_node_path66 = __toESM(require("path"));
28683
29841
  init_devpod_mutation();
28684
29842
  init_devpod_workspaces();
28685
29843
  init_devsy_mutation();
@@ -28863,7 +30021,7 @@ var init_version = __esm({
28863
30021
 
28864
30022
  // src/cli.ts
28865
30023
  var import_commander = require("commander");
28866
- var CLI_VERSION = true ? "0.0.79" : "0.0.0-dev";
30024
+ var CLI_VERSION = true ? "0.1.0" : "0.0.0-dev";
28867
30025
  var VERSION_FLAGS = /* @__PURE__ */ new Set(["-V", "--version"]);
28868
30026
  function withErrorHandling(action2) {
28869
30027
  return async (...args) => {
@@ -28911,6 +30069,13 @@ controllerCommand.command("status").option("--session <id>").option("--cursor <c
28911
30069
  await runControllerCommand2("status", options);
28912
30070
  })
28913
30071
  );
30072
+ var capacityCommand = program.command("capacity").description("Inspect machines-local capacity ledger state");
30073
+ capacityCommand.command("reconcile").description("Replace a provably lost capacity ledger with a fresh empty baseline").option("--yes").option("--json").action(
30074
+ withErrorHandling(async (options) => {
30075
+ const { runCapacityCommand: runCapacityCommand2 } = await Promise.resolve().then(() => (init_capacity(), capacity_exports));
30076
+ await runCapacityCommand2("reconcile", options);
30077
+ })
30078
+ );
28914
30079
  program.command("init").description("Print an AI onboarding prompt template for adapting a repository to devrouter").option("--repo <path>", "Repository path to embed in the prompt (defaults to current directory)").option("--entries-json <json>", "Optional JSON array of app entries to embed in the prompt").option("--json", "Output prompt and command intents as JSON").option("--write-agents", "Write/update devrouter section in AGENTS.md").option("--write-skill", "Write .agents/skills/devrouter/SKILL.md").action(
28915
30080
  withErrorHandling(
28916
30081
  async (options) => {
@@ -28994,6 +30159,13 @@ program.command("status").description("Show router/container/network/TLS status
28994
30159
  await runStatusCommand2(options);
28995
30160
  })
28996
30161
  );
30162
+ var harnessCommand = program.command("harness").description("Agent harness integration for managed environments");
30163
+ harnessCommand.command("gate").description("Defer one harness tool call until this checkout's lifecycle phase settles").option("--repo <path>", "Repository path (defaults to the harness payload cwd)").option("--wait-budget-ms <ms>", "Bounded defer budget in milliseconds before one refusal").option("--json", "Print the devrouter decision envelope instead of the hook decision").action(
30164
+ withErrorHandling(async (options) => {
30165
+ const { runHarnessCommand: runHarnessCommand2 } = await Promise.resolve().then(() => (init_harness(), harness_exports));
30166
+ await runHarnessCommand2("gate", options, options.repo);
30167
+ })
30168
+ );
28997
30169
  program.command("doctor").alias("verify").description("Run global + repo diagnostics with actionable fixes for humans and AI agents").option("--repo <path>", "Repository path to validate (defaults to current directory)").option("--json", "Output JSON").action(
28998
30170
  withErrorHandling(async (options) => {
28999
30171
  const { runDoctorCommand: runDoctorCommand2 } = await Promise.resolve().then(() => (init_doctor2(), doctor_exports));
@@ -29238,10 +30410,10 @@ workspaceCommand2.command("down").description("Delete a workspace's runtime and
29238
30410
  workspaceCommand2.command("journal").description("Inspect and recover workspace lifecycle journals").command("settle").description(
29239
30411
  "Settle an interrupted lifecycle operation as unobservable so ensure and stop can proceed"
29240
30412
  ).argument("[path]", "Checkout path (defaults to current directory)").option("--json", "Output JSON").action(
29241
- withErrorHandling(async (path63, _options, command) => {
30413
+ withErrorHandling(async (path66, _options, command) => {
29242
30414
  const options = command.opts();
29243
30415
  const { runWorkspaceJournalSettleCommand: runWorkspaceJournalSettleCommand2 } = await Promise.resolve().then(() => (init_workspace2(), workspace_exports));
29244
- await runWorkspaceJournalSettleCommand2({ ...path63 ? { path: path63 } : {}, ...options });
30416
+ await runWorkspaceJournalSettleCommand2({ ...path66 ? { path: path66 } : {}, ...options });
29245
30417
  })
29246
30418
  );
29247
30419
  function parseVersionRequest(argv) {