@devrouter/cli 0.0.66 → 0.0.67

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.
@@ -1188,6 +1188,17 @@ function inspectDevsyRuntimeStatus(devsyId) {
1188
1188
  if (result.status !== 0 || result.error) return "unknown";
1189
1189
  return parseDevsyRuntimeStatus(result.stdout, devsyId);
1190
1190
  }
1191
+ function inspectDevsyRuntimeAbsence(devsyId) {
1192
+ const result = (0, import_node_child_process5.spawnSync)("devsy", ["workspace", "status", devsyId, "--result-format", "json"], {
1193
+ encoding: "utf-8",
1194
+ timeout: 1e4
1195
+ });
1196
+ if (!result.error && result.status === 0) {
1197
+ return parseDevsyRuntimeStatus(result.stdout, devsyId) === "not-found";
1198
+ }
1199
+ const output = [result.error?.message, result.stdout, result.stderr].filter(Boolean).join("\n");
1200
+ return /workspace not found/i.test(output);
1201
+ }
1191
1202
  function inspectDevsyWorkspaceOwnership(workspaces, devsyId, worktreePath) {
1192
1203
  const idOwners = workspaces.filter((workspace) => workspace.id === devsyId);
1193
1204
  const pathOwners = workspaces.filter(
@@ -1987,6 +1998,7 @@ function requireReadyDevsyAgent(options = {}) {
1987
1998
  // src/core/managed-devsy-stop.ts
1988
1999
  var import_node_os4 = __toESM(require("os"));
1989
2000
  var import_node_path20 = __toESM(require("path"));
2001
+ var import_node_util4 = require("util");
1990
2002
 
1991
2003
  // src/core/devcontainer-profile.ts
1992
2004
  var import_node_child_process10 = require("child_process");
@@ -4863,7 +4875,7 @@ function loadRepoConfig(repoPath, read2 = (file) => import_node_fs14.default.rea
4863
4875
  const config = parseConfig(parsed ?? {}, configPath);
4864
4876
  const requiredVersion = config.devrouter?.version;
4865
4877
  if (requiredVersion && !hasWarnedVersionMismatch) {
4866
- const cliVersion = true ? "0.0.66" : "0.0.0-dev";
4878
+ const cliVersion = true ? "0.0.67" : "0.0.0-dev";
4867
4879
  if (cliVersion !== "0.0.0-dev" && compareSemver(requiredVersion, cliVersion) > 0) {
4868
4880
  hasWarnedVersionMismatch = true;
4869
4881
  process.stderr.write(
@@ -5418,6 +5430,66 @@ function stopRetainedManagedDevsyWorkspace(options) {
5418
5430
  }
5419
5431
  return owner.workspace;
5420
5432
  }
5433
+ function registrationStatus() {
5434
+ resetWorkspaceRuntimeCaches();
5435
+ if (resolveWorkspaceRuntimeOrDefault(repoPath) !== "devsy") {
5436
+ throw new Error("Managed stop provider selection changed.");
5437
+ }
5438
+ return inspectDevsyWorkspaceOwnership(listDevsyWorkspaces(), devsyId, repoPath);
5439
+ }
5440
+ function proveAbsentRegistrationStop() {
5441
+ const observe = () => {
5442
+ const owner = registrationStatus();
5443
+ if (owner.status === "conflict") throw new Error(owner.reason);
5444
+ if (owner.status !== "absent") {
5445
+ throw new Error("Managed stop absence proof requires the registration to remain absent.");
5446
+ }
5447
+ if (listDevpodWorkspacesRaw({ allowMissingExecutable: true }).some(
5448
+ (entry) => entry.id === devsyId || sameWorkspacePath(entry.source.localFolder, repoPath)
5449
+ )) {
5450
+ throw new Error(
5451
+ "Managed stop absence proof requires both provider registrations to remain absent."
5452
+ );
5453
+ }
5454
+ if (!inspectDevsyRuntimeAbsence(devsyId)) {
5455
+ throw new Error(
5456
+ "Managed stop absence proof requires Devsy to report the runtime not-found."
5457
+ );
5458
+ }
5459
+ if (inspectManagedStopContainers(state.composeProject).length !== 0) {
5460
+ throw new Error("Managed stop absence proof observed a remaining compose population.");
5461
+ }
5462
+ if (inspectProviderRunnerContainers(resolveManagedStopEndpoint(), devsyId).length !== 0) {
5463
+ throw new Error("Managed stop absence proof observed a remaining runner population.");
5464
+ }
5465
+ const owned = inspectWorkspaceContainers().filter(
5466
+ (container) => sameWorkspacePath(
5467
+ container.labels["com.docker.compose.project.working_dir"] ?? "",
5468
+ import_node_path20.default.join(repoPath, ".devcontainer")
5469
+ )
5470
+ );
5471
+ if (owned.length !== 0) {
5472
+ throw new Error("Managed stop absence proof observed a remaining workspace container.");
5473
+ }
5474
+ if (workspace) {
5475
+ const record5 = readWorkspaceOwnership(repoPath, workspace);
5476
+ if (!record5 || record5.devpodId !== devsyId || !sameWorkspacePath(record5.worktreePath, repoPath) || resolveWorktreeWorkspace(repoPath) !== workspace || resolveGitCommonDir(repoPath) !== workspaceEnv?.gitCommonDir || inspectWorkspaceOwnership(record5, listGitWorktrees(repoPath), void 0).ownerStatus !== "present") {
5477
+ throw new Error("Managed stop workspace ownership changed during absence proof.");
5478
+ }
5479
+ }
5480
+ if (!(0, import_node_util4.isDeepStrictEqual)(readManagedRuntimeState(repoPath, workspace), state)) {
5481
+ throw new Error("Managed stop retained state changed during absence proof.");
5482
+ }
5483
+ };
5484
+ observe();
5485
+ observe();
5486
+ }
5487
+ const initialOwner = registrationStatus();
5488
+ if (initialOwner.status === "conflict") throw new Error(initialOwner.reason);
5489
+ if (initialOwner.status === "absent") {
5490
+ proveAbsentRegistrationStop();
5491
+ return "proven-absent";
5492
+ }
5421
5493
  const context = registration2().context;
5422
5494
  if (!context || !/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(context) || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(devsyId)) {
5423
5495
  throw new Error("Managed stop requires a valid exact Devsy context and workspace ID.");
@@ -7396,6 +7468,7 @@ function assertReliabilityEvent(value) {
7396
7468
  case "launched":
7397
7469
  case "interrupted":
7398
7470
  case "not-started":
7471
+ case "settle":
7399
7472
  valid = isReliabilityId(value.operationId);
7400
7473
  break;
7401
7474
  case "completion":
@@ -7465,8 +7538,13 @@ function transition(state, outcome, effects = []) {
7465
7538
  assertReliabilityState(state);
7466
7539
  return { state, effects, outcome };
7467
7540
  }
7468
- function unchanged(state, outcome) {
7469
- return transition(cloneState(state), outcome);
7541
+ function unchanged(state, outcome, reason2) {
7542
+ const result = transition(cloneState(state), outcome);
7543
+ if (reason2) result.reason = reason2;
7544
+ return result;
7545
+ }
7546
+ function blocked(state, reason2) {
7547
+ return unchanged(state, "blocked", reason2);
7470
7548
  }
7471
7549
  function advance(value) {
7472
7550
  return value === Number.MAX_SAFE_INTEGER ? null : value + 1;
@@ -7491,7 +7569,10 @@ function updateRunningPhase(state) {
7491
7569
  }
7492
7570
  function handleRequest(state, event) {
7493
7571
  if (state.executionPolicy === "manual" && event.mode === "start")
7494
- return unchanged(state, "blocked");
7572
+ return blocked(
7573
+ state,
7574
+ "start requests are not admitted under manual execution policy; use the ensure/stop commands."
7575
+ );
7495
7576
  const existing = state.requests.find((request) => request.key === event.key);
7496
7577
  if (existing) {
7497
7578
  const consumer = state.consumers.find((candidate) => candidate.id === existing.consumerId);
@@ -7558,7 +7639,8 @@ function handleRequest(state, event) {
7558
7639
  }
7559
7640
  function canExecAfterInterruptedEnsure(state) {
7560
7641
  const latestEnsure = [...state.operationHistory].reverse().find((operation) => operation.kind === "ensure");
7561
- return state.executionPolicy === "manual" && state.desired === "running" && latestEnsure?.status === "INTERRUPTED" && latestEnsure.drained && state.operation?.drained === true && (state.operation.kind === "ensure" && state.phase === "recovering" || state.operation.kind === "exec" && ["COMPLETED", "NOT_LAUNCHED", "NOT_STARTED"].includes(state.operation.status));
7642
+ const quiescent = state.stopProof.workloadsStopped && state.stopProof.routesRemoved;
7643
+ return state.executionPolicy === "manual" && (state.desired === "running" || quiescent && state.desired === "stopped-by-user") && latestEnsure?.status === "INTERRUPTED" && latestEnsure.drained && state.operation?.drained === true && (state.operation.kind === "ensure" && (state.phase === "recovering" || quiescent) || state.operation.kind === "exec" && ["COMPLETED", "NOT_LAUNCHED", "NOT_STARTED"].includes(state.operation.status));
7562
7644
  }
7563
7645
  function handleOperationRequest(state, event) {
7564
7646
  if (state.executionPolicy !== "manual") return unchanged(state, "blocked");
@@ -7571,25 +7653,39 @@ function handleOperationRequest(state, event) {
7571
7653
  if (state.operationHistory.some((entry) => entry.id === event.operationId))
7572
7654
  return unchanged(state, "conflict");
7573
7655
  if (event.kind === "exec" && canExecAfterInterruptedEnsure(state) && event.recoverInterruptedEnsure !== true)
7574
- return unchanged(state, "blocked");
7656
+ return blocked(
7657
+ state,
7658
+ "the latest ensure is INTERRUPTED and drained; exec requires recovering it first (run ensure, or exec with recovery proof)."
7659
+ );
7575
7660
  const fullyStopped = state.stopProof.workloadsStopped && state.stopProof.routesRemoved;
7576
7661
  const reconcileEnsure = event.kind === "ensure" && state.operation?.kind === "ensure" && ["NOT_STARTED", "INTERRUPTED"].includes(state.operation.status);
7577
7662
  if (state.operation && (!state.operation.drained || !["COMPLETED", "NOT_LAUNCHED", "NOT_STARTED"].includes(state.operation.status) && !fullyStopped && !reconcileEnsure && !(event.kind === "exec" && event.recoverInterruptedEnsure === true && canExecAfterInterruptedEnsure(state))))
7578
- return unchanged(state, "blocked");
7663
+ return blocked(
7664
+ state,
7665
+ `operation ${state.operation.id} is ${state.operation.status} (drained=${state.operation.drained}) and no stop proof, ensure reconciliation, or exec recovery supersedes it.`
7666
+ );
7579
7667
  if (state.phase === "stopping" || state.desired === "parked-for-capacity")
7580
- return unchanged(state, "blocked");
7668
+ return blocked(
7669
+ state,
7670
+ `phase is '${state.phase}' with desired '${state.desired}'; wait for the running stop to finish.`
7671
+ );
7581
7672
  if (event.kind === "exec" && (!event.runtimeRunning || state.desired !== "running" && state.intentRevision !== 0))
7582
- return unchanged(state, "blocked");
7673
+ return blocked(
7674
+ state,
7675
+ `exec requires a running workspace (runtimeRunning=${event.runtimeRunning}, desired='${state.desired}').`
7676
+ );
7583
7677
  const rollover = state.operationHistory.length >= RELIABILITY_MAX_ITEMS;
7584
7678
  let retired = -1;
7585
7679
  if (rollover) {
7586
- if (state.operation && (!state.operation.drained || !["COMPLETED", "NOT_LAUNCHED"].includes(state.operation.status)))
7587
- return unchanged(state, "blocked");
7588
7680
  const latestEnsureId = [...state.operationHistory].reverse().find((entry) => entry.kind === "ensure")?.id;
7589
7681
  retired = state.operationHistory.findIndex(
7590
- (entry) => entry.id !== state.operation?.id && entry.id !== latestEnsureId && entry.drained && ["COMPLETED", "NOT_LAUNCHED"].includes(entry.status)
7682
+ (entry) => entry.id !== state.operation?.id && entry.id !== latestEnsureId && entry.drained && ["COMPLETED", "NOT_LAUNCHED", "INTERRUPTED"].includes(entry.status)
7591
7683
  );
7592
- if (retired === -1) return unchanged(state, "blocked");
7684
+ if (retired === -1)
7685
+ return blocked(
7686
+ state,
7687
+ `journal holds ${state.operationHistory.length} entries and every retirable entry is the current operation or the latest ensure.`
7688
+ );
7593
7689
  }
7594
7690
  if (state.desired !== "running" || rollover) {
7595
7691
  const revision = advance(state.intentRevision);
@@ -7644,7 +7740,10 @@ function handleDispatch(state) {
7644
7740
  if (state.operation?.drained || state.operation?.status !== "NOT_STARTED")
7645
7741
  return unchanged(state, "stale");
7646
7742
  if (state.desired !== "running" || state.executionPolicy === "capacity-managed" && state.admission !== "admitted" || state.consumers.length === 0) {
7647
- return unchanged(state, "blocked");
7743
+ return blocked(
7744
+ state,
7745
+ `dispatch requires desired 'running' with a registered consumer (desired='${state.desired}', consumers=${state.consumers.length}).`
7746
+ );
7648
7747
  }
7649
7748
  const corrective = state.phase === "recovering" && state.incident !== null;
7650
7749
  state.operation = { ...state.operation, status: "DISPATCH_PENDING", exitCode: null };
@@ -7659,7 +7758,10 @@ function handleDispatchPersisted(state, event) {
7659
7758
  if (state.desired !== "running" || state.executionPolicy === "capacity-managed" && (state.admission !== "admitted" || !state.chargeHeld)) {
7660
7759
  state.operation = setUnknown(state.operation);
7661
7760
  state.chargeHeld = state.executionPolicy === "capacity-managed";
7662
- return transition(state, "blocked");
7761
+ return blocked(
7762
+ state,
7763
+ `dispatch persistence requires desired 'running' (desired='${state.desired}'); the operation outcome is unknown.`
7764
+ );
7663
7765
  }
7664
7766
  if (state.phase === "recovering" && state.incident !== null && state.incident.correctiveActionsTaken >= state.incident.actionLimit) {
7665
7767
  return unchanged(state, "blocked");
@@ -7707,6 +7809,22 @@ function handleInterrupted(state, event) {
7707
7809
  if (state.desired === "running") state.phase = "recovering";
7708
7810
  return transition(state, "accepted");
7709
7811
  }
7812
+ function handleSettle(state, event) {
7813
+ if (state.executionPolicy !== "manual") return unchanged(state, "stale");
7814
+ if (!state.operation || state.operation.id !== event.operationId)
7815
+ return unchanged(state, "stale");
7816
+ if (state.operation.drained && state.operation.status === "INTERRUPTED")
7817
+ return unchanged(state, "joined");
7818
+ if (state.operation.status === "COMPLETED") return unchanged(state, "joined");
7819
+ state.operation = {
7820
+ ...state.operation,
7821
+ status: "INTERRUPTED",
7822
+ drained: true,
7823
+ exitCode: null
7824
+ };
7825
+ if (state.desired === "running") state.phase = "recovering";
7826
+ return transition(state, "accepted");
7827
+ }
7710
7828
  function handleObservation(state, event, nowMs) {
7711
7829
  if (state.desired !== "running") return unchanged(state, "stale");
7712
7830
  const previous = state.observations.find(
@@ -7716,7 +7834,10 @@ function handleObservation(state, event, nowMs) {
7716
7834
  return unchanged(state, "stale");
7717
7835
  }
7718
7836
  if (!previous && state.observations.length >= RELIABILITY_MAX_ITEMS)
7719
- return unchanged(state, "blocked");
7837
+ return blocked(
7838
+ state,
7839
+ `observation set holds ${state.observations.length} distinct capabilities; observations repeat per capability and never retire.`
7840
+ );
7720
7841
  state.observations = state.observations.filter(
7721
7842
  (observation) => observation.capability !== event.observation.capability
7722
7843
  );
@@ -7744,7 +7865,10 @@ function handleStop(state) {
7744
7865
  function handleStopProof(state, event) {
7745
7866
  if (state.desired === "running") return unchanged(state, "stale");
7746
7867
  if (state.executionPolicy === "manual" && state.operation && !state.operation.drained && event.workloadsStopped && event.routesRemoved)
7747
- return unchanged(state, "blocked");
7868
+ return blocked(
7869
+ state,
7870
+ `stop proof cannot settle while operation ${state.operation.id} is not drained; wait for its worker or settle the journal.`
7871
+ );
7748
7872
  const previous = { ...state.stopProof };
7749
7873
  if (previous.workloadsStopped === event.workloadsStopped && previous.routesRemoved === event.routesRemoved)
7750
7874
  return unchanged(state, "joined");
@@ -7923,6 +8047,8 @@ function applyReliabilityEvent(state, event, nowMs) {
7923
8047
  return handleCompletion(next, event);
7924
8048
  case "interrupted":
7925
8049
  return handleInterrupted(next, event);
8050
+ case "settle":
8051
+ return handleSettle(next, event);
7926
8052
  case "observation":
7927
8053
  return handleObservation(next, event, nowMs);
7928
8054
  case "stop-proof":
@@ -7958,6 +8084,19 @@ function stepReliability(state, event, nowMs) {
7958
8084
  var import_node_crypto11 = require("crypto");
7959
8085
  var import_node_fs20 = __toESM(require("fs"));
7960
8086
  var import_node_path25 = __toESM(require("path"));
8087
+ function installedCliVersion() {
8088
+ return true ? "0.0.67" : "0.0.0-dev";
8089
+ }
8090
+ function compareCliVersions(left, right) {
8091
+ const parts = (value) => value.split(".").map((part) => Number.parseInt(part, 10) || 0);
8092
+ const leftParts = parts(left);
8093
+ const rightParts = parts(right);
8094
+ for (let index = 0; index < 3; index += 1) {
8095
+ const difference = (leftParts[index] ?? 0) - (rightParts[index] ?? 0);
8096
+ if (difference !== 0) return difference;
8097
+ }
8098
+ return 0;
8099
+ }
7961
8100
  var MAX_RECORD_BYTES = 1048576;
7962
8101
  function identityKey(identity2) {
7963
8102
  if (!import_node_path25.default.isAbsolute(identity2.repoPath) || import_node_path25.default.resolve(identity2.repoPath) !== identity2.repoPath || identity2.repoPath.includes("\0") || identity2.workspace !== null && !isReliabilityId(identity2.workspace) || !["devpod", "devsy"].includes(identity2.provider)) {
@@ -7974,7 +8113,21 @@ function keys(value, expected) {
7974
8113
  }
7975
8114
  }
7976
8115
  function validate(record5, identity2) {
7977
- keys(record5, ["version", "identity", "revision", "state", "worker", "effectSequence", "outcome"]);
8116
+ keys(record5, [
8117
+ "version",
8118
+ "identity",
8119
+ "revision",
8120
+ "state",
8121
+ "worker",
8122
+ "effectSequence",
8123
+ "outcome",
8124
+ "writtenByVersion"
8125
+ ]);
8126
+ if (record5.writtenByVersion !== void 0 && compareCliVersions(record5.writtenByVersion, installedCliVersion()) > 0) {
8127
+ throw new Error(
8128
+ `Reliability record was last written by devrouter ${record5.writtenByVersion}, which is newer than the installed ${installedCliVersion()}; upgrade devrouter (devrouter upgrade) before running lifecycle commands.`
8129
+ );
8130
+ }
7978
8131
  keys(record5.identity, ["repoPath", "workspace", "provider"]);
7979
8132
  if (record5.version !== 1 || record5.identity.repoPath !== identity2.repoPath || record5.identity.workspace !== identity2.workspace || record5.identity.provider !== identity2.provider || !isReliabilityCounter(record5.revision) || !isReliabilityCounter(record5.effectSequence)) {
7980
8133
  throw new Error("Reliability record has invalid version, ownership, or counters.");
@@ -8115,6 +8268,7 @@ function updateReliabilityOperation(identity2, operation) {
8115
8268
  if (result && typeof result.then === "function") {
8116
8269
  throw new Error("Reliability record updates must be synchronous.");
8117
8270
  }
8271
+ record5.writtenByVersion = installedCliVersion();
8118
8272
  validate(record5, identity2);
8119
8273
  record5.revision += 1;
8120
8274
  persist(record5);
@@ -12861,7 +13015,9 @@ async function withLifecycleOperationLock(repoPath, operation) {
12861
13015
  function stepRecord(record5, event) {
12862
13016
  const transition2 = stepReliability(record5.state, event, Date.now());
12863
13017
  if (!["accepted", "joined"].includes(transition2.outcome))
12864
- throw new Error(`Lifecycle transition is ${transition2.outcome}.`);
13018
+ throw new Error(
13019
+ `Lifecycle transition is ${transition2.outcome}.${transition2.reason ? ` ${transition2.reason}` : ""}`
13020
+ );
12865
13021
  record5.state = transition2.state;
12866
13022
  }
12867
13023
  function reconcileDrained(record5) {
package/dist/devrouter.js CHANGED
@@ -2520,7 +2520,7 @@ function loadRepoConfig(repoPath, read2 = (file) => import_node_fs6.default.read
2520
2520
  const config = parseConfig(parsed ?? {}, configPath);
2521
2521
  const requiredVersion = config.devrouter?.version;
2522
2522
  if (requiredVersion && !hasWarnedVersionMismatch) {
2523
- const cliVersion = true ? "0.0.66" : "0.0.0-dev";
2523
+ const cliVersion = true ? "0.0.67" : "0.0.0-dev";
2524
2524
  if (cliVersion !== "0.0.0-dev" && compareSemver(requiredVersion, cliVersion) > 0) {
2525
2525
  hasWarnedVersionMismatch = true;
2526
2526
  process.stderr.write(
@@ -3494,6 +3494,19 @@ var init_reliability_contract = __esm({
3494
3494
  });
3495
3495
 
3496
3496
  // src/core/reliability-operation-store.ts
3497
+ function installedCliVersion() {
3498
+ return true ? "0.0.67" : "0.0.0-dev";
3499
+ }
3500
+ function compareCliVersions(left, right) {
3501
+ const parts = (value) => value.split(".").map((part) => Number.parseInt(part, 10) || 0);
3502
+ const leftParts = parts(left);
3503
+ const rightParts = parts(right);
3504
+ for (let index = 0; index < 3; index += 1) {
3505
+ const difference = (leftParts[index] ?? 0) - (rightParts[index] ?? 0);
3506
+ if (difference !== 0) return difference;
3507
+ }
3508
+ return 0;
3509
+ }
3497
3510
  function identityKey(identity2) {
3498
3511
  if (!import_node_path10.default.isAbsolute(identity2.repoPath) || import_node_path10.default.resolve(identity2.repoPath) !== identity2.repoPath || identity2.repoPath.includes("\0") || identity2.workspace !== null && !isReliabilityId(identity2.workspace) || !["devpod", "devsy"].includes(identity2.provider)) {
3499
3512
  throw new Error("Invalid reliability workspace identity.");
@@ -3509,7 +3522,21 @@ function keys(value, expected) {
3509
3522
  }
3510
3523
  }
3511
3524
  function validate(record5, identity2) {
3512
- keys(record5, ["version", "identity", "revision", "state", "worker", "effectSequence", "outcome"]);
3525
+ keys(record5, [
3526
+ "version",
3527
+ "identity",
3528
+ "revision",
3529
+ "state",
3530
+ "worker",
3531
+ "effectSequence",
3532
+ "outcome",
3533
+ "writtenByVersion"
3534
+ ]);
3535
+ if (record5.writtenByVersion !== void 0 && compareCliVersions(record5.writtenByVersion, installedCliVersion()) > 0) {
3536
+ throw new Error(
3537
+ `Reliability record was last written by devrouter ${record5.writtenByVersion}, which is newer than the installed ${installedCliVersion()}; upgrade devrouter (devrouter upgrade) before running lifecycle commands.`
3538
+ );
3539
+ }
3513
3540
  keys(record5.identity, ["repoPath", "workspace", "provider"]);
3514
3541
  if (record5.version !== 1 || record5.identity.repoPath !== identity2.repoPath || record5.identity.workspace !== identity2.workspace || record5.identity.provider !== identity2.provider || !isReliabilityCounter(record5.revision) || !isReliabilityCounter(record5.effectSequence)) {
3515
3542
  throw new Error("Reliability record has invalid version, ownership, or counters.");
@@ -3650,6 +3677,7 @@ function updateReliabilityOperation(identity2, operation) {
3650
3677
  if (result && typeof result.then === "function") {
3651
3678
  throw new Error("Reliability record updates must be synchronous.");
3652
3679
  }
3680
+ record5.writtenByVersion = installedCliVersion();
3653
3681
  validate(record5, identity2);
3654
3682
  record5.revision += 1;
3655
3683
  persist(record5);
@@ -7462,6 +7490,10 @@ var init_ai_prompt = __esm({
7462
7490
  {
7463
7491
  command: "devrouter workspace gc [--json] [--yes]",
7464
7492
  purpose: "Report missing owners without mutation by default; --yes deletes only exact eligible ledger-owned resources and records, never Git worktrees."
7493
+ },
7494
+ {
7495
+ command: "devrouter workspace journal settle [path] [--json]",
7496
+ purpose: "Settle a lifecycle operation whose worker is provably gone as unobservable so ensure/stop can proceed; never hand-edit the devrouter state directory."
7465
7497
  }
7466
7498
  ];
7467
7499
  }
@@ -11467,6 +11499,17 @@ function inspectDevsyRuntimeStatus(devsyId) {
11467
11499
  if (result.status !== 0 || result.error) return "unknown";
11468
11500
  return parseDevsyRuntimeStatus(result.stdout, devsyId);
11469
11501
  }
11502
+ function inspectDevsyRuntimeAbsence(devsyId) {
11503
+ const result = (0, import_node_child_process16.spawnSync)("devsy", ["workspace", "status", devsyId, "--result-format", "json"], {
11504
+ encoding: "utf-8",
11505
+ timeout: 1e4
11506
+ });
11507
+ if (!result.error && result.status === 0) {
11508
+ return parseDevsyRuntimeStatus(result.stdout, devsyId) === "not-found";
11509
+ }
11510
+ const output2 = [result.error?.message, result.stdout, result.stderr].filter(Boolean).join("\n");
11511
+ return /workspace not found/i.test(output2);
11512
+ }
11470
11513
  function inspectDevsyWorkspaceOwnership(workspaces, devsyId, worktreePath) {
11471
11514
  const idOwners = workspaces.filter((workspace) => workspace.id === devsyId);
11472
11515
  const pathOwners = workspaces.filter(
@@ -13092,6 +13135,66 @@ function stopRetainedManagedDevsyWorkspace(options) {
13092
13135
  }
13093
13136
  return owner.workspace;
13094
13137
  }
13138
+ function registrationStatus() {
13139
+ resetWorkspaceRuntimeCaches();
13140
+ if (resolveWorkspaceRuntimeOrDefault(repoPath) !== "devsy") {
13141
+ throw new Error("Managed stop provider selection changed.");
13142
+ }
13143
+ return inspectDevsyWorkspaceOwnership(listDevsyWorkspaces(), devsyId, repoPath);
13144
+ }
13145
+ function proveAbsentRegistrationStop() {
13146
+ const observe = () => {
13147
+ const owner = registrationStatus();
13148
+ if (owner.status === "conflict") throw new Error(owner.reason);
13149
+ if (owner.status !== "absent") {
13150
+ throw new Error("Managed stop absence proof requires the registration to remain absent.");
13151
+ }
13152
+ if (listDevpodWorkspacesRaw({ allowMissingExecutable: true }).some(
13153
+ (entry) => entry.id === devsyId || sameWorkspacePath(entry.source.localFolder, repoPath)
13154
+ )) {
13155
+ throw new Error(
13156
+ "Managed stop absence proof requires both provider registrations to remain absent."
13157
+ );
13158
+ }
13159
+ if (!inspectDevsyRuntimeAbsence(devsyId)) {
13160
+ throw new Error(
13161
+ "Managed stop absence proof requires Devsy to report the runtime not-found."
13162
+ );
13163
+ }
13164
+ if (inspectManagedStopContainers(state.composeProject).length !== 0) {
13165
+ throw new Error("Managed stop absence proof observed a remaining compose population.");
13166
+ }
13167
+ if (inspectProviderRunnerContainers(resolveManagedStopEndpoint(), devsyId).length !== 0) {
13168
+ throw new Error("Managed stop absence proof observed a remaining runner population.");
13169
+ }
13170
+ const owned = inspectWorkspaceContainers().filter(
13171
+ (container) => sameWorkspacePath(
13172
+ container.labels["com.docker.compose.project.working_dir"] ?? "",
13173
+ import_node_path36.default.join(repoPath, ".devcontainer")
13174
+ )
13175
+ );
13176
+ if (owned.length !== 0) {
13177
+ throw new Error("Managed stop absence proof observed a remaining workspace container.");
13178
+ }
13179
+ if (workspace) {
13180
+ const record5 = readWorkspaceOwnership(repoPath, workspace);
13181
+ if (!record5 || record5.devpodId !== devsyId || !sameWorkspacePath(record5.worktreePath, repoPath) || resolveWorktreeWorkspace(repoPath) !== workspace || resolveGitCommonDir(repoPath) !== workspaceEnv?.gitCommonDir || inspectWorkspaceOwnership(record5, listGitWorktrees(repoPath), void 0).ownerStatus !== "present") {
13182
+ throw new Error("Managed stop workspace ownership changed during absence proof.");
13183
+ }
13184
+ }
13185
+ if (!(0, import_node_util4.isDeepStrictEqual)(readManagedRuntimeState(repoPath, workspace), state)) {
13186
+ throw new Error("Managed stop retained state changed during absence proof.");
13187
+ }
13188
+ };
13189
+ observe();
13190
+ observe();
13191
+ }
13192
+ const initialOwner = registrationStatus();
13193
+ if (initialOwner.status === "conflict") throw new Error(initialOwner.reason);
13194
+ if (initialOwner.status === "absent") {
13195
+ proveAbsentRegistrationStop();
13196
+ return "proven-absent";
13197
+ }
13095
13198
  const context = registration2().context;
13096
13199
  if (!context || !/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(context) || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(devsyId)) {
13097
13200
  throw new Error("Managed stop requires a valid exact Devsy context and workspace ID.");
@@ -13201,14 +13304,16 @@ function stopRetainedManagedDevsyWorkspace(options) {
13201
13304
  if (providerFailed) throw providerError;
13202
13305
  return true;
13203
13306
  }
13204
- var import_node_os4, import_node_path36;
13307
+ var import_node_os4, import_node_path36, import_node_util4;
13205
13308
  var init_managed_devsy_stop = __esm({
13206
13309
  "src/core/managed-devsy-stop.ts"() {
13207
13310
  "use strict";
13208
13311
  import_node_os4 = __toESM(require("os"));
13209
13312
  import_node_path36 = __toESM(require("path"));
13313
+ import_node_util4 = require("util");
13210
13314
  init_devcontainer_profile();
13211
13315
  init_devpod_environment();
13316
+ init_devpod_registry();
13212
13317
  init_devsy_workspaces();
13213
13318
  init_managed_compose_population();
13214
13319
  init_managed_runtime_state();
@@ -14185,7 +14290,7 @@ async function buildDoctorReport(options = {}) {
14185
14290
  const config = runtimeConfig.config;
14186
14291
  loadedConfig = config;
14187
14292
  loadedWorkspace = runtimeConfig.workspace;
14188
- const cliVersion = true ? "0.0.66" : "0.0.0-dev";
14293
+ const cliVersion = true ? "0.0.67" : "0.0.0-dev";
14189
14294
  const configVersion = config.devrouter?.version;
14190
14295
  if (configVersion && cliVersion !== "0.0.0-dev" && compareSemver(configVersion, cliVersion) > 0) {
14191
14296
  addCheck(checks, {
@@ -14871,6 +14976,7 @@ function assertReliabilityEvent(value) {
14871
14976
  case "launched":
14872
14977
  case "interrupted":
14873
14978
  case "not-started":
14979
+ case "settle":
14874
14980
  valid = isReliabilityId(value.operationId);
14875
14981
  break;
14876
14982
  case "completion":
@@ -14940,8 +15046,13 @@ function transition(state, outcome, effects = []) {
14940
15046
  assertReliabilityState(state);
14941
15047
  return { state, effects, outcome };
14942
15048
  }
14943
- function unchanged(state, outcome) {
14944
- return transition(cloneState(state), outcome);
15049
+ function unchanged(state, outcome, reason) {
15050
+ const result = transition(cloneState(state), outcome);
15051
+ if (reason) result.reason = reason;
15052
+ return result;
15053
+ }
15054
+ function blocked2(state, reason) {
15055
+ return unchanged(state, "blocked", reason);
14945
15056
  }
14946
15057
  function advance(value) {
14947
15058
  return value === Number.MAX_SAFE_INTEGER ? null : value + 1;
@@ -14966,7 +15077,10 @@ function updateRunningPhase(state) {
14966
15077
  }
14967
15078
  function handleRequest(state, event) {
14968
15079
  if (state.executionPolicy === "manual" && event.mode === "start")
14969
- return unchanged(state, "blocked");
15080
+ return blocked2(
15081
+ state,
15082
+ "start requests are not admitted under manual execution policy; use the ensure/stop commands."
15083
+ );
14970
15084
  const existing = state.requests.find((request) => request.key === event.key);
14971
15085
  if (existing) {
14972
15086
  const consumer = state.consumers.find((candidate) => candidate.id === existing.consumerId);
@@ -15033,7 +15147,8 @@ function handleRequest(state, event) {
15033
15147
  }
15034
15148
  function canExecAfterInterruptedEnsure(state) {
15035
15149
  const latestEnsure = [...state.operationHistory].reverse().find((operation) => operation.kind === "ensure");
15036
- return state.executionPolicy === "manual" && state.desired === "running" && latestEnsure?.status === "INTERRUPTED" && latestEnsure.drained && state.operation?.drained === true && (state.operation.kind === "ensure" && state.phase === "recovering" || state.operation.kind === "exec" && ["COMPLETED", "NOT_LAUNCHED", "NOT_STARTED"].includes(state.operation.status));
15150
+ const quiescent = state.stopProof.workloadsStopped && state.stopProof.routesRemoved;
15151
+ return state.executionPolicy === "manual" && (state.desired === "running" || quiescent && state.desired === "stopped-by-user") && latestEnsure?.status === "INTERRUPTED" && latestEnsure.drained && state.operation?.drained === true && (state.operation.kind === "ensure" && (state.phase === "recovering" || quiescent) || state.operation.kind === "exec" && ["COMPLETED", "NOT_LAUNCHED", "NOT_STARTED"].includes(state.operation.status));
15037
15152
  }
15038
15153
  function handleOperationRequest(state, event) {
15039
15154
  if (state.executionPolicy !== "manual") return unchanged(state, "blocked");
@@ -15046,25 +15161,39 @@ function handleOperationRequest(state, event) {
15046
15161
  if (state.operationHistory.some((entry) => entry.id === event.operationId))
15047
15162
  return unchanged(state, "conflict");
15048
15163
  if (event.kind === "exec" && canExecAfterInterruptedEnsure(state) && event.recoverInterruptedEnsure !== true)
15049
- return unchanged(state, "blocked");
15164
+ return blocked2(
15165
+ state,
15166
+ "the latest ensure is INTERRUPTED and drained; exec requires recovering it first (run ensure, or exec with recovery proof)."
15167
+ );
15050
15168
  const fullyStopped = state.stopProof.workloadsStopped && state.stopProof.routesRemoved;
15051
15169
  const reconcileEnsure = event.kind === "ensure" && state.operation?.kind === "ensure" && ["NOT_STARTED", "INTERRUPTED"].includes(state.operation.status);
15052
15170
  if (state.operation && (!state.operation.drained || !["COMPLETED", "NOT_LAUNCHED", "NOT_STARTED"].includes(state.operation.status) && !fullyStopped && !reconcileEnsure && !(event.kind === "exec" && event.recoverInterruptedEnsure === true && canExecAfterInterruptedEnsure(state))))
15053
- return unchanged(state, "blocked");
15171
+ return blocked2(
15172
+ state,
15173
+ `operation ${state.operation.id} is ${state.operation.status} (drained=${state.operation.drained}) and no stop proof, ensure reconciliation, or exec recovery supersedes it.`
15174
+ );
15054
15175
  if (state.phase === "stopping" || state.desired === "parked-for-capacity")
15055
- return unchanged(state, "blocked");
15176
+ return blocked2(
15177
+ state,
15178
+ `phase is '${state.phase}' with desired '${state.desired}'; wait for the running stop to finish.`
15179
+ );
15056
15180
  if (event.kind === "exec" && (!event.runtimeRunning || state.desired !== "running" && state.intentRevision !== 0))
15057
- return unchanged(state, "blocked");
15181
+ return blocked2(
15182
+ state,
15183
+ `exec requires a running workspace (runtimeRunning=${event.runtimeRunning}, desired='${state.desired}').`
15184
+ );
15058
15185
  const rollover = state.operationHistory.length >= RELIABILITY_MAX_ITEMS;
15059
15186
  let retired = -1;
15060
15187
  if (rollover) {
15061
- if (state.operation && (!state.operation.drained || !["COMPLETED", "NOT_LAUNCHED"].includes(state.operation.status)))
15062
- return unchanged(state, "blocked");
15063
15188
  const latestEnsureId = [...state.operationHistory].reverse().find((entry) => entry.kind === "ensure")?.id;
15064
15189
  retired = state.operationHistory.findIndex(
15065
- (entry) => entry.id !== state.operation?.id && entry.id !== latestEnsureId && entry.drained && ["COMPLETED", "NOT_LAUNCHED"].includes(entry.status)
15190
+ (entry) => entry.id !== state.operation?.id && entry.id !== latestEnsureId && entry.drained && ["COMPLETED", "NOT_LAUNCHED", "INTERRUPTED"].includes(entry.status)
15066
15191
  );
15067
- if (retired === -1) return unchanged(state, "blocked");
15192
+ if (retired === -1)
15193
+ return blocked2(
15194
+ state,
15195
+ `journal holds ${state.operationHistory.length} entries and every retirable entry is the current operation or the latest ensure.`
15196
+ );
15068
15197
  }
15069
15198
  if (state.desired !== "running" || rollover) {
15070
15199
  const revision = advance(state.intentRevision);
@@ -15119,7 +15248,10 @@ function handleDispatch(state) {
15119
15248
  if (state.operation?.drained || state.operation?.status !== "NOT_STARTED")
15120
15249
  return unchanged(state, "stale");
15121
15250
  if (state.desired !== "running" || state.executionPolicy === "capacity-managed" && state.admission !== "admitted" || state.consumers.length === 0) {
15122
- return unchanged(state, "blocked");
15251
+ return blocked2(
15252
+ state,
15253
+ `dispatch requires desired 'running' with a registered consumer (desired='${state.desired}', consumers=${state.consumers.length}).`
15254
+ );
15123
15255
  }
15124
15256
  const corrective = state.phase === "recovering" && state.incident !== null;
15125
15257
  state.operation = { ...state.operation, status: "DISPATCH_PENDING", exitCode: null };
@@ -15134,7 +15266,10 @@ function handleDispatchPersisted(state, event) {
15134
15266
  if (state.desired !== "running" || state.executionPolicy === "capacity-managed" && (state.admission !== "admitted" || !state.chargeHeld)) {
15135
15267
  state.operation = setUnknown(state.operation);
15136
15268
  state.chargeHeld = state.executionPolicy === "capacity-managed";
15137
- return transition(state, "blocked");
15269
+ return blocked2(
15270
+ state,
15271
+ `dispatch persistence requires desired 'running' (desired='${state.desired}'); the operation outcome is unknown.`
15272
+ );
15138
15273
  }
15139
15274
  if (state.phase === "recovering" && state.incident !== null && state.incident.correctiveActionsTaken >= state.incident.actionLimit) {
15140
15275
  return unchanged(state, "blocked");
@@ -15182,6 +15317,22 @@ function handleInterrupted(state, event) {
15182
15317
  if (state.desired === "running") state.phase = "recovering";
15183
15318
  return transition(state, "accepted");
15184
15319
  }
15320
+ function handleSettle(state, event) {
15321
+ if (state.executionPolicy !== "manual") return unchanged(state, "stale");
15322
+ if (!state.operation || state.operation.id !== event.operationId)
15323
+ return unchanged(state, "stale");
15324
+ if (state.operation.drained && state.operation.status === "INTERRUPTED")
15325
+ return unchanged(state, "joined");
15326
+ if (state.operation.status === "COMPLETED") return unchanged(state, "joined");
15327
+ state.operation = {
15328
+ ...state.operation,
15329
+ status: "INTERRUPTED",
15330
+ drained: true,
15331
+ exitCode: null
15332
+ };
15333
+ if (state.desired === "running") state.phase = "recovering";
15334
+ return transition(state, "accepted");
15335
+ }
15185
15336
  function handleObservation(state, event, nowMs) {
15186
15337
  if (state.desired !== "running") return unchanged(state, "stale");
15187
15338
  const previous = state.observations.find(
@@ -15191,7 +15342,10 @@ function handleObservation(state, event, nowMs) {
15191
15342
  return unchanged(state, "stale");
15192
15343
  }
15193
15344
  if (!previous && state.observations.length >= RELIABILITY_MAX_ITEMS)
15194
- return unchanged(state, "blocked");
15345
+ return blocked2(
15346
+ state,
15347
+ `observation set holds ${state.observations.length} distinct capabilities; observations repeat per capability and never retire.`
15348
+ );
15195
15349
  state.observations = state.observations.filter(
15196
15350
  (observation) => observation.capability !== event.observation.capability
15197
15351
  );
@@ -15219,7 +15373,10 @@ function handleStop(state) {
15219
15373
  function handleStopProof(state, event) {
15220
15374
  if (state.desired === "running") return unchanged(state, "stale");
15221
15375
  if (state.executionPolicy === "manual" && state.operation && !state.operation.drained && event.workloadsStopped && event.routesRemoved)
15222
- return unchanged(state, "blocked");
15376
+ return blocked2(
15377
+ state,
15378
+ `stop proof cannot settle while operation ${state.operation.id} is not drained; wait for its worker or settle the journal.`
15379
+ );
15223
15380
  const previous = { ...state.stopProof };
15224
15381
  if (previous.workloadsStopped === event.workloadsStopped && previous.routesRemoved === event.routesRemoved)
15225
15382
  return unchanged(state, "joined");
@@ -15398,6 +15555,8 @@ function applyReliabilityEvent(state, event, nowMs) {
15398
15555
  return handleCompletion(next, event);
15399
15556
  case "interrupted":
15400
15557
  return handleInterrupted(next, event);
15558
+ case "settle":
15559
+ return handleSettle(next, event);
15401
15560
  case "observation":
15402
15561
  return handleObservation(next, event, nowMs);
15403
15562
  case "stop-proof":
@@ -15558,7 +15717,9 @@ async function runLifecycleWorker(request, beforeAdmission) {
15558
15717
  Date.now()
15559
15718
  );
15560
15719
  if (admitted.outcome !== "accepted")
15561
- throw new Error(`Lifecycle admission is ${admitted.outcome}.`);
15720
+ throw new Error(
15721
+ `Lifecycle admission is ${admitted.outcome}.${admitted.reason ? ` ${admitted.reason}` : ""}`
15722
+ );
15562
15723
  record5.state = admitted.state;
15563
15724
  record5.outcome = null;
15564
15725
  request.fence = reliabilityFence(record5.state);
@@ -16048,7 +16209,9 @@ async function withLifecycleOperationLock(repoPath, operation) {
16048
16209
  function stepRecord(record5, event) {
16049
16210
  const transition2 = stepReliability(record5.state, event, Date.now());
16050
16211
  if (!["accepted", "joined"].includes(transition2.outcome))
16051
- throw new Error(`Lifecycle transition is ${transition2.outcome}.`);
16212
+ throw new Error(
16213
+ `Lifecycle transition is ${transition2.outcome}.${transition2.reason ? ` ${transition2.reason}` : ""}`
16214
+ );
16052
16215
  record5.state = transition2.state;
16053
16216
  }
16054
16217
  function reconcileDrained(record5) {
@@ -20397,6 +20560,70 @@ var init_workspace_cleanup = __esm({
20397
20560
  }
20398
20561
  });
20399
20562
 
20563
+ // src/core/workspace-journal-settle.ts
20564
+ async function settleWorkspaceJournal(repoPath) {
20565
+ repoPath = comparableWorkspacePath(repoPath);
20566
+ if (isLinkedWorktree(repoPath) && !readPersistedWorkspace(repoPath)) {
20567
+ throw new Error("Settle requires the existing linked workspace identity.");
20568
+ }
20569
+ const identity2 = {
20570
+ repoPath,
20571
+ workspace: resolveWorktreeWorkspace(repoPath) ?? null,
20572
+ provider: resolveWorkspaceRuntimeOrDefault(repoPath)
20573
+ };
20574
+ if (!readReliabilityOperation(identity2)) {
20575
+ throw new Error(`No reliability journal exists for '${repoPath}'; nothing to settle.`);
20576
+ }
20577
+ const settled = await withWorkspaceLifecycleLock(
20578
+ repoPath,
20579
+ async () => updateReliabilityOperation(identity2, (record5) => settleRecord(record5))
20580
+ );
20581
+ return { repoPath, workspace: identity2.workspace, ...settled };
20582
+ }
20583
+ function settleRecord(record5) {
20584
+ if (record5.worker) {
20585
+ const birth = processBirthIdentity(record5.worker.pid);
20586
+ if (birth === record5.worker.birth || !workerGroupAbsent(record5.worker.pid)) {
20587
+ throw new Error(
20588
+ `Lifecycle worker '${record5.worker.id}' is still running; settle is refused while its outcome is observable.`
20589
+ );
20590
+ }
20591
+ reconcileDrained(record5);
20592
+ if (record5.worker) throw new Error("Dead lifecycle worker could not be drained.");
20593
+ }
20594
+ const operation = record5.state.operation;
20595
+ if (!operation) throw new Error("No lifecycle operation is recorded; nothing to settle.");
20596
+ const priorStatus = operation.status;
20597
+ const transition2 = stepReliability(
20598
+ record5.state,
20599
+ { ...reliabilityFence(record5.state), type: "settle", operationId: operation.id },
20600
+ Date.now()
20601
+ );
20602
+ if (!["accepted", "joined"].includes(transition2.outcome))
20603
+ throw new Error(
20604
+ `Lifecycle settlement is ${transition2.outcome}.${transition2.reason ? ` ${transition2.reason}` : ""}`
20605
+ );
20606
+ record5.state = transition2.state;
20607
+ return {
20608
+ operationId: operation.id,
20609
+ status: transition2.outcome === "joined" ? "already-settled" : "settled",
20610
+ priorStatus
20611
+ };
20612
+ }
20613
+ var init_workspace_journal_settle = __esm({
20614
+ "src/core/workspace-journal-settle.ts"() {
20615
+ "use strict";
20616
+ init_file_lock();
20617
+ init_reliability_contract();
20618
+ init_reliability_lifecycle();
20619
+ init_reliability_model();
20620
+ init_reliability_operation_store();
20621
+ init_reliability_worker();
20622
+ init_workspace();
20623
+ init_workspace_runtime();
20624
+ }
20625
+ });
20626
+
20400
20627
  // src/core/workspace-lifecycle.ts
20401
20628
  function withWorkspaceAllocationTransaction(repoPath, operation) {
20402
20629
  return withWorkspaceOwnershipTransaction(repoPath, operation, {
@@ -20800,6 +21027,7 @@ __export(workspace_exports, {
20800
21027
  runWorkspaceCleanupCommand: () => runWorkspaceCleanupCommand,
20801
21028
  runWorkspaceDownCommand: () => runWorkspaceDownCommand,
20802
21029
  runWorkspaceGcCommand: () => runWorkspaceGcCommand,
21030
+ runWorkspaceJournalSettleCommand: () => runWorkspaceJournalSettleCommand,
20803
21031
  runWorkspaceLsCommand: () => runWorkspaceLsCommand,
20804
21032
  runWorkspaceStopCommand: () => runWorkspaceStopCommand,
20805
21033
  runWorkspaceUpCommand: () => runWorkspaceUpCommand
@@ -20889,6 +21117,26 @@ function runWorkspaceGcCommand(options) {
20889
21117
  }
20890
21118
  if (report.summary.errors > 0) process.exitCode = 1;
20891
21119
  }
21120
+ async function runWorkspaceJournalSettleCommand(options) {
21121
+ const repoPath = resolveGitCheckoutPath(options.path);
21122
+ const result = await settleWorkspaceJournal(repoPath);
21123
+ if (options.json) {
21124
+ printJSON(result);
21125
+ return;
21126
+ }
21127
+ const workspaceLabel = result.workspace ? ` (workspace '${result.workspace}')` : "";
21128
+ if (result.status === "already-settled") {
21129
+ process.stdout.write(
21130
+ `Journal operation ${result.operationId} is already settled${workspaceLabel}.
21131
+ `
21132
+ );
21133
+ return;
21134
+ }
21135
+ process.stdout.write(
21136
+ `Settled journal operation ${result.operationId} (was ${result.priorStatus})${workspaceLabel}; ensure and stop may proceed.
21137
+ `
21138
+ );
21139
+ }
20892
21140
  var init_workspace2 = __esm({
20893
21141
  "src/commands/workspace.ts"() {
20894
21142
  "use strict";
@@ -20896,8 +21144,10 @@ var init_workspace2 = __esm({
20896
21144
  init_repo_config();
20897
21145
  init_workspace_cleanup();
20898
21146
  init_workspace_gc();
21147
+ init_workspace_journal_settle();
20899
21148
  init_workspace_lifecycle();
20900
21149
  init_workspace_ownership();
21150
+ init_environment_path();
20901
21151
  }
20902
21152
  });
20903
21153
 
@@ -20939,7 +21189,7 @@ var init_version = __esm({
20939
21189
 
20940
21190
  // src/cli.ts
20941
21191
  var import_commander = require("commander");
20942
- var CLI_VERSION = true ? "0.0.66" : "0.0.0-dev";
21192
+ var CLI_VERSION = true ? "0.0.67" : "0.0.0-dev";
20943
21193
  var VERSION_FLAGS = /* @__PURE__ */ new Set(["-V", "--version"]);
20944
21194
  function withErrorHandling(action2) {
20945
21195
  return async (...args) => {
@@ -21309,6 +21559,15 @@ workspaceCommand2.command("down").description("Delete a workspace's runtime and
21309
21559
  await runWorkspaceDownCommand2(target, options);
21310
21560
  })
21311
21561
  );
21562
+ workspaceCommand2.command("journal settle").description(
21563
+ "Settle an interrupted lifecycle operation as unobservable so ensure and stop can proceed"
21564
+ ).argument("[path]", "Checkout path (defaults to current directory)").option("--json", "Output JSON").action(
21565
+ withErrorHandling(async (path54, _options, command) => {
21566
+ const options = command.opts();
21567
+ const { runWorkspaceJournalSettleCommand: runWorkspaceJournalSettleCommand2 } = await Promise.resolve().then(() => (init_workspace2(), workspace_exports));
21568
+ await runWorkspaceJournalSettleCommand2({ ...path54 ? { path: path54 } : {}, ...options });
21569
+ })
21570
+ );
21312
21571
  function parseVersionRequest(argv) {
21313
21572
  let requested = false;
21314
21573
  let repo;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devrouter/cli",
3
- "version": "0.0.66",
3
+ "version": "0.0.67",
4
4
  "description": "Local dev routing CLI with shared Traefik reverse proxy",
5
5
  "author": "Roland Schlaefli",
6
6
  "homepage": "https://github.com/rschlaefli/devrouter#readme",
@@ -0,0 +1,41 @@
1
+ # Upgrade to devrouter 0.0.67
2
+
3
+ This patch release makes the reliability journal incapable of deadlocking
4
+ lifecycle commands and completes managed stop when a registration was deleted
5
+ rather than replaced. It restores workspaces that every canonical command
6
+ previously refused with "Lifecycle admission is blocked." after a saturated
7
+ journal held a drained `INTERRUPTED` ensure.
8
+
9
+ ## Task
10
+
11
+ Install the published release and update `.devrouter.yml` devrouter.version to
12
+ 0.0.67. No schema migration or configuration change is required. Do not change
13
+ the committed configuration, delete registrations, containers, or source files,
14
+ and do not reset lifecycle state manually. Recovery runs inside the existing
15
+ canonical commands under their normal locks, ownership proofs, and journal
16
+ accounting. If a command reports a live worker, wait for it or re-run later;
17
+ `workspace journal settle` refuses while a worker outcome is still observable.
18
+
19
+ ## Validation
20
+
21
+ On a workspace whose commands were refused with "Lifecycle admission is
22
+ blocked." (saturated journal with a drained interrupted ensure), run
23
+ `devrouter stop <path>` and confirm it completes — including the case where the
24
+ Devsy registration was already deleted and every workload is positively absent
25
+ — then run `devrouter ensure <path> --json` and confirm admission succeeds with
26
+ routes published. On a record whose worker is provably gone and that still
27
+ refuses progress, run `devrouter workspace journal settle <path> --json` and
28
+ confirm it reports the settled operation, then retry `ensure`. Confirm refusal
29
+ messages now name the blocking field and remediation. For workspaces that never
30
+ hit the deadlock, confirm existing stop, ensure, exec, and `devrouter doctor`
31
+ behavior is unchanged, and confirm a downgrade attempt (record written by a
32
+ newer CLI than the installed one) fails with an explicit upgrade instruction
33
+ instead of a mid-flight refusal.
34
+
35
+ ## Report
36
+
37
+ Report the tested CLI version, the exact workspace path or synthetic fixture,
38
+ each command result (stop, ensure, settle if used) with route state after the
39
+ run, any diagnostic failure output verbatim, and whether the
40
+ deleted-registration case, the saturated-journal case, or only the unchanged
41
+ retained path was exercised.