@devrouter/cli 0.0.74 → 0.0.76

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
@@ -4256,7 +4256,7 @@ function loadRepoConfig(repoPath, read2 = (file) => import_node_fs7.default.read
4256
4256
  const config = parseConfig(parsed ?? {}, configPath);
4257
4257
  const requiredVersion = config.devrouter?.version;
4258
4258
  if (requiredVersion && !hasWarnedVersionMismatch) {
4259
- const cliVersion = true ? "0.0.74" : "0.0.0-dev";
4259
+ const cliVersion = true ? "0.0.76" : "0.0.0-dev";
4260
4260
  if (cliVersion !== "0.0.0-dev" && compareSemver(requiredVersion, cliVersion) > 0) {
4261
4261
  hasWarnedVersionMismatch = true;
4262
4262
  process.stderr.write(
@@ -5509,7 +5509,7 @@ var init_reliability_contract = __esm({
5509
5509
 
5510
5510
  // src/core/reliability-operation-store.ts
5511
5511
  function installedCliVersion() {
5512
- return true ? "0.0.74" : "0.0.0-dev";
5512
+ return true ? "0.0.76" : "0.0.0-dev";
5513
5513
  }
5514
5514
  function compareCliVersions(left, right) {
5515
5515
  const parts = (value) => value.split(".").map((part) => Number.parseInt(part, 10) || 0);
@@ -8413,6 +8413,12 @@ function isWildcard(values) {
8413
8413
  function sha256(contents) {
8414
8414
  return (0, import_node_crypto11.createHash)("sha256").update(contents, "utf-8").digest("hex");
8415
8415
  }
8416
+ function safeComposeProject(value) {
8417
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(value)) {
8418
+ throw new Error(`Docker Compose project '${value}' is not a safe exact identifier.`);
8419
+ }
8420
+ return value;
8421
+ }
8416
8422
  function inspectManagedDevcontainerConfig(options) {
8417
8423
  const managedRuntime = options.config.managedRuntime;
8418
8424
  if (!managedRuntime) {
@@ -8531,6 +8537,91 @@ function managedComposeEnvironment(workspace) {
8531
8537
  }
8532
8538
  return env;
8533
8539
  }
8540
+ function assertReadableComposeFile(file) {
8541
+ try {
8542
+ if (!import_node_fs18.default.statSync(file).isFile()) throw new Error("not a regular file");
8543
+ import_node_fs18.default.accessSync(file, import_node_fs18.default.constants.R_OK);
8544
+ } catch {
8545
+ throw new Error("Managed container Compose configuration contains an unreadable file.");
8546
+ }
8547
+ }
8548
+ function assertManagedContainerConfigUnchanged(options) {
8549
+ if (options.containers.length === 0) {
8550
+ throw new Error("Managed container configuration cannot be verified without containers.");
8551
+ }
8552
+ for (const container of options.containers) {
8553
+ const project = container.labels["com.docker.compose.project"];
8554
+ const service = container.labels["com.docker.compose.service"];
8555
+ const workingDirectory = container.labels["com.docker.compose.project.working_dir"];
8556
+ const recordedConfigFiles = container.labels["com.docker.compose.project.config_files"];
8557
+ const expectedHash = container.labels["com.docker.compose.config-hash"];
8558
+ if (!project || !service || !workingDirectory || !recordedConfigFiles || !expectedHash || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(service) || !/^[a-f0-9]{64}$/.test(expectedHash)) {
8559
+ throw new Error("Managed container is missing valid Compose identity and hash labels.");
8560
+ }
8561
+ const composeProject = safeComposeProject(project);
8562
+ const configFiles2 = recordedConfigFiles.split(",").map((file) => file.trim());
8563
+ if (configFiles2.some((file) => file.length === 0)) {
8564
+ throw new Error("Managed container has an invalid recorded Compose file list.");
8565
+ }
8566
+ if (options.plan.composeFiles.some(
8567
+ (expected) => !configFiles2.some((actual) => sameWorkspacePath(actual, expected))
8568
+ )) {
8569
+ throw new Error("Managed container Compose files do not include the managed plan files.");
8570
+ }
8571
+ for (const file of configFiles2) assertReadableComposeFile(file);
8572
+ const fileArgs = configFiles2.flatMap((file) => ["-f", file]);
8573
+ const result = (() => {
8574
+ try {
8575
+ const composeArgs = [
8576
+ "compose",
8577
+ "--project-name",
8578
+ composeProject,
8579
+ "--project-directory",
8580
+ workingDirectory
8581
+ ];
8582
+ const env = managedComposeEnvironment(options.workspace);
8583
+ const rendered = (0, import_node_child_process8.spawnSync)(
8584
+ "docker",
8585
+ [...composeArgs, ...fileArgs, "config", "--format", "json"],
8586
+ {
8587
+ cwd: workingDirectory,
8588
+ encoding: "utf-8",
8589
+ env,
8590
+ stdio: ["ignore", "pipe", "ignore"],
8591
+ timeout: COMPOSE_HASH_TIMEOUT_MS,
8592
+ maxBuffer: 1024 * 1024
8593
+ }
8594
+ );
8595
+ if (rendered.status !== 0 || rendered.error || !rendered.stdout?.trim()) {
8596
+ throw new Error("Compose configuration rendering failed.");
8597
+ }
8598
+ return (0, import_node_child_process8.spawnSync)(
8599
+ "docker",
8600
+ [...composeArgs, "-f", "-", "config", "--no-interpolate", "--hash", service],
8601
+ {
8602
+ cwd: workingDirectory,
8603
+ encoding: "utf-8",
8604
+ env,
8605
+ input: rendered.stdout,
8606
+ stdio: ["pipe", "pipe", "ignore"],
8607
+ timeout: COMPOSE_HASH_TIMEOUT_MS,
8608
+ maxBuffer: 1024 * 1024
8609
+ }
8610
+ );
8611
+ } catch {
8612
+ throw new Error(`Could not verify managed Compose configuration for service '${service}'.`);
8613
+ }
8614
+ })();
8615
+ if (result.status !== 0 || result.error) {
8616
+ throw new Error(`Could not verify managed Compose configuration for service '${service}'.`);
8617
+ }
8618
+ const output2 = typeof result.stdout === "string" ? result.stdout.trim() : "";
8619
+ const match = /^(\S+)\s+([a-f0-9]{64})$/.exec(output2);
8620
+ if (!match || match[1] !== service || match[2] !== expectedHash) {
8621
+ throw new Error(`Managed Compose configuration changed for service '${service}'.`);
8622
+ }
8623
+ }
8624
+ }
8534
8625
  function stopExactManagedService(containerId, service, options) {
8535
8626
  assertSafeContainerId(containerId);
8536
8627
  const result = (0, import_node_child_process8.spawnSync)("docker", ["stop", containerId], {
@@ -8543,7 +8634,7 @@ function stopExactManagedService(containerId, service, options) {
8543
8634
  throw new Error(`Could not stop exact managed service '${service}' (${containerId}).`);
8544
8635
  }
8545
8636
  }
8546
- var import_node_child_process8, import_node_crypto11, import_node_fs18, import_node_path25, import_yaml3, MANAGED_DEVCONTAINER_PATH, MANAGED_DEVCONTAINER_MARKER;
8637
+ var import_node_child_process8, import_node_crypto11, import_node_fs18, import_node_path25, import_yaml3, MANAGED_DEVCONTAINER_PATH, MANAGED_DEVCONTAINER_MARKER, COMPOSE_HASH_TIMEOUT_MS;
8547
8638
  var init_devcontainer_profile = __esm({
8548
8639
  "src/core/devcontainer-profile.ts"() {
8549
8640
  "use strict";
@@ -8559,6 +8650,7 @@ var init_devcontainer_profile = __esm({
8559
8650
  init_workspace();
8560
8651
  MANAGED_DEVCONTAINER_PATH = ".devcontainer/devcontainer.devrouter.json";
8561
8652
  MANAGED_DEVCONTAINER_MARKER = "// devrouter:managed devcontainer profile";
8653
+ COMPOSE_HASH_TIMEOUT_MS = 1e4;
8562
8654
  }
8563
8655
  });
8564
8656
 
@@ -10529,8 +10621,30 @@ function proveInitialManagedDevsyAbsence(repoPath, expectedId) {
10529
10621
  }
10530
10622
  function stopInitialManagedDevsyWorkspace(repoPath, devsyId) {
10531
10623
  if (!import_node_fs25.default.existsSync(import_node_path33.default.join(repoPath, ".devrouter.yml"))) return false;
10624
+ if (!loadRepoConfig(repoPath).managedRuntime) return false;
10532
10625
  const linked = isLinkedWorktree(repoPath);
10533
10626
  const workspace = linked ? resolveWorktreeWorkspace(repoPath) : void 0;
10627
+ const identity2 = { repoPath, workspace: workspace ?? null, provider: "devsy" };
10628
+ const readAuthority = () => {
10629
+ const record5 = readReliabilityOperation(identity2);
10630
+ const operation = record5?.state.operation;
10631
+ const entries = record5?.state.operationHistory.filter((entry2) => entry2.id === operation?.id);
10632
+ const entry = entries?.[0];
10633
+ if (!record5 || !(0, import_node_util9.isDeepStrictEqual)(record5.identity, identity2) || record5.worker || !operation || operation.kind !== "ensure" || !operation.drained || !["COMPLETED", "INTERRUPTED", "COMPLETION_UNKNOWN"].includes(operation.status) || record5.state.phase !== "stopping" || record5.state.desired !== "stopped-by-user" || entries?.length !== 1 || !entry || entry.kind !== "ensure" || !entry.profile || !entry.drained || entry.status !== operation.status)
10634
+ throw new Error("Initial managed stop requires the drained ensure's recorded profile.");
10635
+ return {
10636
+ identity: record5.identity,
10637
+ fence: reliabilityFence(record5.state),
10638
+ operation,
10639
+ entry
10640
+ };
10641
+ };
10642
+ const authority = readAuthority();
10643
+ const gitCommonDir = linked ? resolveGitCommonDir(repoPath) : void 0;
10644
+ const workspaceEnv = workspace && gitCommonDir ? { token: workspace, gitCommonDir } : void 0;
10645
+ function canonicalProfile(recorded) {
10646
+ return resolveProfile(loadRepoConfig(repoPath), recorded).name;
10647
+ }
10534
10648
  const registration2 = () => {
10535
10649
  resetWorkspaceRuntimeCaches();
10536
10650
  if (resolveWorkspaceRuntimeOrDefault(repoPath) !== "devsy")
@@ -10540,14 +10654,21 @@ function stopInitialManagedDevsyWorkspace(repoPath, devsyId) {
10540
10654
  throw new Error("Initial managed stop requires one exact Devsy registration.");
10541
10655
  if (linked) {
10542
10656
  const record5 = workspace ? readWorkspaceOwnership(repoPath, workspace) : void 0;
10543
- if (!record5 || record5.devpodId !== devsyId || !sameWorkspacePath(record5.worktreePath, repoPath) || resolveWorktreeWorkspace(repoPath) !== workspace)
10657
+ if (!record5 || record5.devpodId !== devsyId || !sameWorkspacePath(record5.worktreePath, repoPath) || resolveWorktreeWorkspace(repoPath) !== workspace || resolveGitCommonDir(repoPath) !== gitCommonDir || inspectWorkspaceOwnership(record5, listGitWorktrees(repoPath), void 0).ownerStatus !== "present")
10544
10658
  throw new Error("Initial managed stop workspace ownership changed.");
10545
10659
  }
10546
10660
  return owner2.workspace;
10547
10661
  };
10548
- const runtime = loadRuntimeConfig(repoPath, workspace ?? "");
10549
- if (!runtime.config.managedRuntime) return false;
10662
+ const canonicalName = canonicalProfile(authority.entry.profile);
10663
+ const runtime = loadRuntimeConfig(repoPath, workspace ?? "", canonicalName);
10664
+ if (!runtime.config.managedRuntime || runtime.profile !== canonicalName)
10665
+ throw new Error("Initial managed stop requires the recorded managed profile.");
10550
10666
  const owner = registration2();
10667
+ proveLocalDockerSelection(owner);
10668
+ const endpoint = resolveManagedStopEndpoint();
10669
+ if (!supportsManagedStopBaseline(endpoint))
10670
+ throw new Error("Initial managed stop requires a pinned local Docker endpoint.");
10671
+ const daemon = inspectManagedStopDaemon(endpoint);
10551
10672
  if (!owner.context || !/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(owner.context) || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(devsyId))
10552
10673
  throw new Error("Initial managed stop requires exact provider context.");
10553
10674
  const plan = inspectManagedDevcontainerConfig({
@@ -10567,27 +10688,53 @@ function stopInitialManagedDevsyWorkspace(repoPath, devsyId) {
10567
10688
  ".docker-compose"
10568
10689
  );
10569
10690
  const observe = () => {
10570
- if (!(0, import_node_util9.isDeepStrictEqual)(registration2(), owner) || readManagedRuntimeState(repoPath, workspace))
10571
- throw new Error("Initial managed stop ownership or runtime state changed.");
10691
+ if (!(0, import_node_util9.isDeepStrictEqual)(readAuthority(), authority) || !(0, import_node_util9.isDeepStrictEqual)(registration2(), owner) || readManagedRuntimeState(repoPath, workspace) || resolveManagedStopEndpoint() !== endpoint || inspectManagedStopDaemon(endpoint) !== daemon)
10692
+ throw new Error("Initial managed stop authority or runtime state changed.");
10693
+ proveLocalDockerSelection(owner);
10694
+ if (listDevpodWorkspacesRaw({ readLocalWhenMissing: true }).some(
10695
+ (entry) => entry.id === devsyId || entry.source.localFolder !== "" && sameWorkspacePath(entry.source.localFolder, repoPath)
10696
+ ))
10697
+ throw new Error("Initial managed stop found conflicting provider ownership.");
10698
+ if (canonicalProfile(authority.entry.profile) !== canonicalName)
10699
+ throw new Error("Initial managed stop configuration changed.");
10700
+ const currentRuntime = loadRuntimeConfig(repoPath, workspace ?? "", canonicalName);
10701
+ const currentPlan = inspectManagedDevcontainerConfig({
10702
+ repoPath,
10703
+ config: currentRuntime.config,
10704
+ profile: currentRuntime.resolvedProfile,
10705
+ linked
10706
+ });
10707
+ if (currentRuntime.profile !== canonicalName || !(0, import_node_util9.isDeepStrictEqual)(currentRuntime.resolvedProfile, runtime.resolvedProfile) || !(0, import_node_util9.isDeepStrictEqual)(currentPlan, plan) || inspectManagedDevcontainerGeneratedConfig(currentPlan).status !== "valid")
10708
+ throw new Error("Initial managed stop configuration changed.");
10572
10709
  const matches = inspectWorkspaceContainers().filter(
10573
10710
  (c) => sameWorkspacePath(
10574
10711
  c.labels["com.docker.compose.project.working_dir"] ?? "",
10575
10712
  plan.composeDirectory
10576
10713
  )
10577
10714
  );
10578
- if (!matches.length) return [];
10715
+ if (!matches.length)
10716
+ throw new Error("Initial managed stop requires its complete selected population.");
10579
10717
  const projects = new Set(matches.map((c) => c.labels["com.docker.compose.project"]));
10580
10718
  if (projects.size !== 1 || !matches[0]?.labels["com.docker.compose.project"])
10581
10719
  throw new Error("Initial managed stop requires one exact Compose project.");
10582
10720
  const population = inspectManagedStopContainers(
10583
- matches[0].labels["com.docker.compose.project"]
10721
+ matches[0].labels["com.docker.compose.project"],
10722
+ endpoint
10584
10723
  );
10585
10724
  if (!sameSet(
10725
+ inspectManagedStopWorkspaceIds(endpoint, plan.composeDirectory),
10726
+ population.map((c) => c.id)
10727
+ ) || !sameSet(
10586
10728
  matches.map((c) => c.id),
10587
10729
  population.map((c) => c.id)
10588
10730
  ))
10589
10731
  throw new Error("Initial managed stop population changed.");
10590
- proveManagedComposePopulation({
10732
+ if (!sameSet(
10733
+ population.map((c) => c.labels["com.docker.compose.service"] ?? ""),
10734
+ plan.desiredServices
10735
+ ))
10736
+ throw new Error("Initial managed stop requires its complete selected population.");
10737
+ const primary = proveManagedComposePopulation({
10591
10738
  plan,
10592
10739
  repoPath,
10593
10740
  composeProject: matches[0].labels["com.docker.compose.project"],
@@ -10595,9 +10742,23 @@ function stopInitialManagedDevsyWorkspace(repoPath, devsyId) {
10595
10742
  featureDirectory,
10596
10743
  containers: population
10597
10744
  });
10745
+ const uidBytes = Buffer.byteLength(owner.uid ?? "");
10746
+ const runnerId = (uidBytes === 16 || uidBytes === 40 ? owner.uid : void 0) ?? devsyId;
10747
+ if (owner.source.container && owner.source.container !== primary.id || !owner.source.container && inspectManagedStopRunnerId(endpoint, primary.id) !== runnerId)
10748
+ throw new Error("Initial managed stop provider-to-container binding changed.");
10749
+ if (!owner.source.container && !sameSet(inspectProviderRunnerContainers(endpoint, runnerId), [primary.id]))
10750
+ throw new Error("Initial managed stop provider runner population changed.");
10751
+ assertManagedContainerConfigUnchanged({
10752
+ plan,
10753
+ containers: population,
10754
+ workspace: workspaceEnv
10755
+ });
10756
+ if (!(0, import_node_util9.isDeepStrictEqual)(readAuthority(), authority) || !(0, import_node_util9.isDeepStrictEqual)(registration2(), owner) || resolveManagedStopEndpoint() !== endpoint || inspectManagedStopDaemon(endpoint) !== daemon)
10757
+ throw new Error("Initial managed stop authority changed during inspection.");
10598
10758
  return population;
10599
10759
  };
10600
10760
  const initial = observe();
10761
+ const stopped = new Set(initial.filter((c) => !c.state.Running).map((c) => c.id));
10601
10762
  const stable = () => {
10602
10763
  const current = observe();
10603
10764
  if (!sameSet(
@@ -10605,19 +10766,21 @@ function stopInitialManagedDevsyWorkspace(repoPath, devsyId) {
10605
10766
  current.map((c) => c.id)
10606
10767
  ) || current.some((c) => {
10607
10768
  const prior = initial.find((p) => p.id === c.id);
10608
- return !prior || containerIdentity(prior) !== containerIdentity(c) || !prior.state.Running && c.state.Running;
10769
+ return !prior || containerIdentity(prior) !== containerIdentity(c) || stopped.has(c.id) && c.state.Running;
10609
10770
  }))
10610
10771
  throw new Error("Initial managed stop population or identity changed.");
10772
+ for (const c of current) if (!c.state.Running) stopped.add(c.id);
10611
10773
  return current;
10612
10774
  };
10613
10775
  stable();
10614
- if (!initial.length) return false;
10615
10776
  for (const container of initial) {
10616
10777
  const current = stable().find((c) => c.id === container.id);
10617
- if (current?.state.Running)
10618
- stopExactManagedService(current.id, current.labels["com.docker.compose.service"] ?? "", {
10619
- timeoutMs: 3e4
10620
- });
10778
+ if (current?.state.Running) {
10779
+ claimLifecycleEffect();
10780
+ stopPinnedManagedContainer(endpoint, current.id);
10781
+ if (stable().find((c) => c.id === current.id)?.state.Running)
10782
+ throw new Error("Initial managed stop container cessation is not proven.");
10783
+ }
10621
10784
  }
10622
10785
  if (stable().some((c) => c.state.Running))
10623
10786
  throw new Error("Initial managed stop left a workload running.");
@@ -10837,11 +11000,15 @@ var init_managed_devsy_stop = __esm({
10837
11000
  init_devcontainer_profile();
10838
11001
  init_devpod_environment();
10839
11002
  init_devpod_registry();
11003
+ init_devsy_exec_proof();
10840
11004
  init_devsy_workspaces();
10841
11005
  init_host_routes();
10842
11006
  init_managed_compose_population();
10843
11007
  init_managed_runtime_state();
10844
11008
  init_managed_stop_recovery();
11009
+ init_reliability_context();
11010
+ init_reliability_contract();
11011
+ init_reliability_operation_store();
10845
11012
  init_repo_config();
10846
11013
  init_route_publication();
10847
11014
  init_traefik_route_health();
@@ -20955,7 +21122,7 @@ async function buildDoctorReport(options = {}) {
20955
21122
  const config = runtimeConfig.config;
20956
21123
  loadedConfig = config;
20957
21124
  loadedWorkspace = runtimeConfig.workspace;
20958
- const cliVersion = true ? "0.0.74" : "0.0.0-dev";
21125
+ const cliVersion = true ? "0.0.76" : "0.0.0-dev";
20959
21126
  const configVersion = config.devrouter?.version;
20960
21127
  if (configVersion && cliVersion !== "0.0.0-dev" && compareSemver(configVersion, cliVersion) > 0) {
20961
21128
  addCheck(checks, {
@@ -26396,7 +26563,7 @@ var init_version = __esm({
26396
26563
 
26397
26564
  // src/cli.ts
26398
26565
  var import_commander = require("commander");
26399
- var CLI_VERSION = true ? "0.0.74" : "0.0.0-dev";
26566
+ var CLI_VERSION = true ? "0.0.76" : "0.0.0-dev";
26400
26567
  var VERSION_FLAGS = /* @__PURE__ */ new Set(["-V", "--version"]);
26401
26568
  function withErrorHandling(action2) {
26402
26569
  return async (...args) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devrouter/cli",
3
- "version": "0.0.74",
3
+ "version": "0.0.76",
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,35 @@
1
+ # Upgrade to devrouter 0.0.75
2
+
3
+ This patch fixes canonical stop after initial managed Devsy startup is cancelled
4
+ before managed runtime state is saved. Stop uses the exact drained ensure's
5
+ recorded profile and validates complete live ownership and Compose configuration
6
+ hashes. No configuration schema, command, flag or runtime-provider migration is
7
+ required.
8
+
9
+ ## Task
10
+
11
+ Use the published 0.0.75 CLI. Update `.devrouter.yml` devrouter.version to 0.0.75
12
+ only when consumer source changes are authorized. Preserve staged work owned by
13
+ another task. Refresh generated agent guidance only with the repository owner's
14
+ authority. No DevPod installation is needed.
15
+
16
+ For an affected exact checkout, use `devrouter stop <path> --json` with the same
17
+ non-secret Compose inputs used at startup. Existing interrupted workspaces can
18
+ recover when the exact operation history and all live configuration and ownership
19
+ proofs remain available. Missing, unreadable, conflicting or changed evidence
20
+ remains a refusal; never fabricate journal state or replace the selected profile
21
+ with the default to bypass it. Stop is non-destructive and retains volumes.
22
+
23
+ ## Validation
24
+
25
+ Verify the released CLI revision and exact checkout ownership. Let the checkout's
26
+ runtime owner perform the canonical stop, require provider cessation and route
27
+ removal proof, then verify stopped lifecycle state. Revalidate port conflicts
28
+ before a new ensure and require its own provider, application and route proof.
29
+ A successful stop or source test does not prove successful application startup.
30
+
31
+ ## Report
32
+
33
+ Report release version/revision, exact checkout, producing stop result and
34
+ lifecycle state, subsequent ensure evidence or precise refusal, and source/index
35
+ preservation. Separate released source from live consumer acceptance.
@@ -0,0 +1,33 @@
1
+ # Upgrade to devrouter 0.0.76
2
+
3
+ This patch accepts canonical-equivalent historical profile selections during
4
+ initial managed stop and prevents first-transition rollback from contradicting
5
+ captured degraded configuration. No schema, command or flag migration is needed.
6
+
7
+ ## Task
8
+
9
+ Use the published 0.0.76 CLI. Update `.devrouter.yml` devrouter.version only when
10
+ consumer source changes are authorized. Preserve another task's staged work.
11
+ Refresh generated agent guidance only with the repository owner's authority.
12
+ No generated runtime configuration or journal rewrite is required or authorized.
13
+
14
+ For an affected exact checkout, its runtime owner uses `devrouter stop <path>
15
+ --json` with the original Compose inputs. Canonical-equivalent selections pass
16
+ the profile check; every ownership and configuration proof still applies. A
17
+ Compose hash refusal must be investigated rather than bypassed. Historical
18
+ contradictory generated configuration remains unproved without writer evidence;
19
+ a valid retained stop baseline can support containment, not automatic resume.
20
+
21
+ ## Validation
22
+
23
+ Verify the installed version and exact checkout ownership. Require live provider,
24
+ container and route cessation proof for stop. A later ensure needs its own runtime
25
+ and readiness proof. New compatible failed-first-transition retries retain matching
26
+ configuration; extra resources and manual drift remain refusals. Source tests or a
27
+ passed profile check do not establish full consumer recovery.
28
+
29
+ ## Report
30
+
31
+ Report release revision, installed executable, exact checkout, stop/ensure result
32
+ or precise refusal, and source/index preservation. Separate release/install proof
33
+ from live consumer acceptance.