@devrouter/cli 0.0.80 → 0.1.1

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.
@@ -5347,7 +5347,7 @@ function retain(session, previous) {
5347
5347
 
5348
5348
  // src/core/reliability-operation-store.ts
5349
5349
  function installedCliVersion() {
5350
- return true ? "0.0.80" : "0.0.0-dev";
5350
+ return true ? "0.1.1" : "0.0.0-dev";
5351
5351
  }
5352
5352
  function compareCliVersions(left, right) {
5353
5353
  const parts = (value) => value.split(".").map((part) => Number.parseInt(part, 10) || 0);
@@ -7163,7 +7163,7 @@ function loadRepoConfig(repoPath, read2 = (file) => import_node_fs18.default.rea
7163
7163
  const config = parseConfig(parsed ?? {}, configPath);
7164
7164
  const requiredVersion = config.devrouter?.version;
7165
7165
  if (requiredVersion && !hasWarnedVersionMismatch) {
7166
- const cliVersion = true ? "0.0.80" : "0.0.0-dev";
7166
+ const cliVersion = true ? "0.1.1" : "0.0.0-dev";
7167
7167
  if (cliVersion !== "0.0.0-dev" && compareSemver2(requiredVersion, cliVersion) > 0) {
7168
7168
  hasWarnedVersionMismatch = true;
7169
7169
  process.stderr.write(
@@ -11957,6 +11957,11 @@ function reliabilityRecovery(reason2, repoPath) {
11957
11957
  case "stop-incomplete":
11958
11958
  case "operation-unknown":
11959
11959
  return [`Run: devrouter stop ${repoPath}`, `Run: devrouter ensure ${repoPath} --repair`];
11960
+ case "start-refused":
11961
+ return [
11962
+ `Run: devrouter doctor ${repoPath} to read the refused fixed host-port claim and its holder`,
11963
+ `Stop the holding workspace or change this repository's own published binding, then run: devrouter ensure ${repoPath}`
11964
+ ];
11960
11965
  case "unadmittable":
11961
11966
  return [
11962
11967
  `Run: devrouter stop ${repoPath}`,
@@ -11970,6 +11975,13 @@ function reliabilityRecovery(reason2, repoPath) {
11970
11975
  return [];
11971
11976
  }
11972
11977
  }
11978
+ function refusedManagedStart(result) {
11979
+ if (result?.ok !== true) return false;
11980
+ const value = result.value;
11981
+ if (typeof value !== "object" || value === null) return false;
11982
+ const conflicts = value.hostPortConflicts;
11983
+ return Array.isArray(conflicts) && conflicts.length > 0;
11984
+ }
11973
11985
  function readReliabilityStatus(options) {
11974
11986
  let provider;
11975
11987
  try {
@@ -11989,7 +12001,7 @@ function readReliabilityStatus(options) {
11989
12001
  return void 0;
11990
12002
  }
11991
12003
  if (!record5) return void 0;
11992
- const reason2 = reliabilityAttention(record5.state);
12004
+ const reason2 = reliabilityAttention(record5.state) ?? (record5.state.desired === "running" && refusedManagedStart(record5.result) ? "start-refused" : void 0);
11993
12005
  return {
11994
12006
  desired: record5.state.desired,
11995
12007
  phase: record5.state.phase,
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.80" : "0.0.0-dev";
4278
+ const cliVersion = true ? "0.1.1" : "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(
@@ -6259,7 +6259,7 @@ var init_reliability_contract = __esm({
6259
6259
 
6260
6260
  // src/core/reliability-operation-store.ts
6261
6261
  function installedCliVersion() {
6262
- return true ? "0.0.80" : "0.0.0-dev";
6262
+ return true ? "0.1.1" : "0.0.0-dev";
6263
6263
  }
6264
6264
  function compareCliVersions(left, right) {
6265
6265
  const parts = (value) => value.split(".").map((part) => Number.parseInt(part, 10) || 0);
@@ -15902,6 +15902,11 @@ function reliabilityRecovery(reason, repoPath) {
15902
15902
  case "stop-incomplete":
15903
15903
  case "operation-unknown":
15904
15904
  return [`Run: devrouter stop ${repoPath}`, `Run: devrouter ensure ${repoPath} --repair`];
15905
+ case "start-refused":
15906
+ return [
15907
+ `Run: devrouter doctor ${repoPath} to read the refused fixed host-port claim and its holder`,
15908
+ `Stop the holding workspace or change this repository's own published binding, then run: devrouter ensure ${repoPath}`
15909
+ ];
15905
15910
  case "unadmittable":
15906
15911
  return [
15907
15912
  `Run: devrouter stop ${repoPath}`,
@@ -15915,6 +15920,13 @@ function reliabilityRecovery(reason, repoPath) {
15915
15920
  return [];
15916
15921
  }
15917
15922
  }
15923
+ function refusedManagedStart(result) {
15924
+ if (result?.ok !== true) return false;
15925
+ const value = result.value;
15926
+ if (typeof value !== "object" || value === null) return false;
15927
+ const conflicts = value.hostPortConflicts;
15928
+ return Array.isArray(conflicts) && conflicts.length > 0;
15929
+ }
15918
15930
  function readReliabilityStatus(options) {
15919
15931
  let provider;
15920
15932
  try {
@@ -15934,7 +15946,7 @@ function readReliabilityStatus(options) {
15934
15946
  return void 0;
15935
15947
  }
15936
15948
  if (!record5) return void 0;
15937
- const reason = reliabilityAttention(record5.state);
15949
+ const reason = reliabilityAttention(record5.state) ?? (record5.state.desired === "running" && refusedManagedStart(record5.result) ? "start-refused" : void 0);
15938
15950
  return {
15939
15951
  desired: record5.state.desired,
15940
15952
  phase: record5.state.phase,
@@ -21024,7 +21036,7 @@ Run several worktrees of one repo in parallel without host/route collisions. A *
21024
21036
  - **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.
21025
21037
  - **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>\`.
21026
21038
  - **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.
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.
21039
+ - **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. A start refused before any provider mutation \u2014 a fixed published host port already held by a running container \u2014 stays visible in \`status\` as a \`start-refused\` attention reason naming the read-only \`doctor\` check and the consumer-side fix; an explicit \`stop\` releases the intent and the reason, and a later admitted ensure clears it. Preserve journals and retained configuration when evidence is unavailable. This does not provide OOM prevention or capacity-managed automatic recovery.
21028
21040
  - **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.
21029
21041
  - **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.
21030
21042
  - **Cleanup**: \`workspace cleanup --repo . --inactive-for 30d --json\` is a report-only, no-\`--yes\` report for managed linked workspaces. It joins ownership (\`present|missing|locked|conflict\`), workspace runtime registration, runtime state (\`running|stopped|busy|not-found|absent|unknown\`), checkout, route, advisory activity, and integration evidence without mutating the workspace runtime, routes, ownership, Git, Docker, applications, worktrees, or branches. Local DevPod/Devsy list/status checks always run; \`--check-merged\` alone enables read-only origin and matching GitHub/GitLab checks. Treat \`not-found\` as stale runtime after Docker pruning; busy, unavailable, or conflicting evidence suppresses destructive suggestions. Explicit \`gc\`/\`down\` can remove exact stale registration only after expected-ID \`NotFound\` proof and ownership revalidation. GC never removes Git worktrees, branches, or prune state. Git has no worktree-removal hook.
@@ -21117,7 +21129,7 @@ Run several worktrees of one repo in parallel without host/route collisions. A *
21117
21129
 
21118
21130
  A managed consumer can gate agent tool calls so a command never starts while the
21119
21131
  checkout's environment is mid-transition. Wire \`devrouter harness gate\` as a
21120
- Claude Code \`PreToolUse\` hook:
21132
+ \`PreToolUse\` hook. Claude Code reads it from a settings file:
21121
21133
 
21122
21134
  {
21123
21135
  "hooks": {
@@ -21129,7 +21141,26 @@ Claude Code \`PreToolUse\` hook:
21129
21141
  }
21130
21142
  }
21131
21143
 
21132
- The hook reads the harness payload on stdin, defers inside the hook process while
21144
+ The Codex CLI reads the same hook from \`$CODEX_HOME/hooks.json\`, where the shell
21145
+ tool is named \`exec_command\`:
21146
+
21147
+ {
21148
+ "hooks": {
21149
+ "PreToolUse": [
21150
+ { "matcher": "exec_command|Bash|shell", "hooks": [
21151
+ { "type": "command", "command": "devrouter harness gate", "timeout": 180 }
21152
+ ] }
21153
+ ]
21154
+ }
21155
+ }
21156
+
21157
+ The gate identifies the requesting harness from its payload and answers in that
21158
+ harness's accepted shape: a refusal is a \`deny\` in both, while an allowed call is
21159
+ \`permissionDecision: allow\` for Claude Code and Codex receives a bare completion
21160
+ carrying the same guidance as \`additionalContext\`, because Codex rejects an
21161
+ \`allow\` decision as unsupported hook output.
21162
+
21163
+
21133
21164
  the checkout's durable phase is \`queued\`, \`starting\`, \`verifying\`, \`recovering\`
21134
21165
  or \`stopping\`, and prints the harness permission decision. The deferral consumes
21135
21166
  no model turns because the harness is blocked on the hook. \`--wait-budget-ms\`
@@ -21141,7 +21172,7 @@ the wait budget: an overrunning hook is not honored, and the tool proceeds under
21141
21172
  the harness's normal permission rules. Use \`--json\` for the devrouter decision
21142
21173
  envelope with wait metrics.
21143
21174
 
21144
- +Gated calls are keyed by the harness \`tool_use_id\` and recorded durably. If the
21175
+ Gated calls are keyed by the harness \`tool_use_id\` and recorded durably. If the
21145
21176
  harness re-delivers a call it already granted or cancelled, the gate returns one
21146
21177
  refusal naming the earlier decision instead of waiting again, because that call
21147
21178
  may already have run; the agent should verify it and issue a new call. Entries
@@ -21280,7 +21311,7 @@ function buildOnboardingPrompt(options = {}) {
21280
21311
  "- if kind=app and runtime=proxy:",
21281
21312
  " - HTTP-only optional readiness: { path: /api/health, statuses: [200], contentType: application/json }; statuses default to [200], explicit lists allow unique 2xx/4xx codes, never redirects. Paths stay on the configured host with no query, fragment, percent escapes, backslash or dot segments. Media types match their case-insensitive base type.",
21282
21313
  " - Declared application readiness failures return applicationReadiness.status=application-error and a nonzero ensure exit, while retaining tooling and routes for application fixes; do not reset caches or recreate the provider. Absent readiness means legacy route liveness, not full application readiness.",
21283
- " - A configured FIXED published host binding already held by a running container refuses managed ensure before any provider start (hostPortConflicts in --json, nonzero exit) naming the port, holder container, compose project, and owning workspace when attributable. Ephemeral host::container bindings are exempt. Never rewrite consumer ports to resolve a conflict: stop the holding workspace or use the consumer's own override, then ensure again. Unverifiable evidence also refuses.",
21314
+ " - A configured FIXED published host binding already held by a running container refuses managed ensure before any provider start (hostPortConflicts in --json, nonzero exit) naming the port, holder container, compose project, and owning workspace when attributable. Ephemeral host::container bindings are exempt. Never rewrite consumer ports to resolve a conflict: stop the holding workspace or use the consumer's own override, then ensure again. The refused start stays visible in `devrouter status` as a `start-refused` attention reason that names the read-only `doctor` check and the consumer-side fix; an explicit `stop` releases the intent and the reason, and a later admitted ensure clears it. Unverifiable evidence also refuses.",
21284
21315
  ' - upstream: "host:port" (an already-running port, e.g. a devcontainer published on 127.0.0.1:3000, or a container reachable by name on a shared Docker network such as `derivatives-db:5432`)',
21285
21316
  ` - upstream may use the \`${WORKSPACE_PLACEHOLDER}\` placeholder (e.g. \`${WORKSPACE_PLACEHOLDER}-app:3000\`) to target a per-workspace devcontainer alias; it is substituted with the resolved workspace token at runtime and re-validated. Do NOT put \`${WORKSPACE_PLACEHOLDER}\` in \`host\` (rejected) \u2014 the host is auto-namespaced.`,
21286
21317
  " - managed `devrouter ensure` requires every HTTP/TCP proxy upstream to begin with the exact resolved workspace/project alias prefix before it mutates DevPod or routes",
@@ -21354,7 +21385,7 @@ function buildOnboardingPrompt(options = {}) {
21354
21385
  `- 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.`,
21355
21386
  "- 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.",
21356
21387
  "- 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.",
21357
- "- Managed agent harnesses can defer tool calls through `devrouter harness gate` wired as a Claude Code `PreToolUse` hook: 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 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.",
21388
+ "- 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.",
21358
21389
  "- 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.",
21359
21390
  "- 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.",
21360
21391
  "- 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.",
@@ -23174,7 +23205,7 @@ function probeInstalledCliVersion(executable) {
23174
23205
  }
23175
23206
  function buildCliPathCheck(options = {}) {
23176
23207
  const id5 = "global.cli-path";
23177
- const runningVersion = options.runningVersion ?? (true ? "0.0.80" : "0.0.0-dev");
23208
+ const runningVersion = options.runningVersion ?? (true ? "0.1.1" : "0.0.0-dev");
23178
23209
  if (runningVersion === "0.0.0-dev") {
23179
23210
  return {
23180
23211
  id: id5,
@@ -24022,7 +24053,7 @@ async function buildDoctorReport(options = {}) {
24022
24053
  suggestion: `Remedy ${expansionNotice.remedy.code}: set a named default profile other than '${expansionNotice.profile}'${remedyCandidates}.`
24023
24054
  });
24024
24055
  }
24025
- const cliVersion = true ? "0.0.80" : "0.0.0-dev";
24056
+ const cliVersion = true ? "0.1.1" : "0.0.0-dev";
24026
24057
  const configVersion = config.devrouter?.version;
24027
24058
  const installedKnown = cliVersion !== "0.0.0-dev";
24028
24059
  const versionComparison = installedKnown && configVersion ? compareSemver(configVersion, cliVersion) : void 0;
@@ -25128,8 +25159,13 @@ var init_harness_gate = __esm({
25128
25159
  // src/commands/harness.ts
25129
25160
  var harness_exports = {};
25130
25161
  __export(harness_exports, {
25162
+ detectHarnessHook: () => detectHarnessHook,
25131
25163
  runHarnessCommand: () => runHarnessCommand
25132
25164
  });
25165
+ function detectHarnessHook(payload) {
25166
+ const turnId = payload.turn_id;
25167
+ return typeof turnId === "string" && turnId.length > 0 ? "codex" : "claude";
25168
+ }
25133
25169
  function readStdin() {
25134
25170
  if (process.stdin.isTTY) {
25135
25171
  return Promise.resolve("");
@@ -25181,11 +25217,28 @@ function permitReason(decision) {
25181
25217
  return "devrouter: environment settled.";
25182
25218
  }
25183
25219
  }
25184
- function hookOutput(decision) {
25220
+ function hookOutput(decision, kind) {
25221
+ if (decision.decision === "refuse") {
25222
+ return JSON.stringify({
25223
+ hookSpecificOutput: {
25224
+ hookEventName: "PreToolUse",
25225
+ permissionDecision: "deny",
25226
+ permissionDecisionReason: permitReason(decision)
25227
+ }
25228
+ });
25229
+ }
25230
+ if (kind === "codex") {
25231
+ return JSON.stringify({
25232
+ hookSpecificOutput: {
25233
+ hookEventName: "PreToolUse",
25234
+ additionalContext: permitReason(decision)
25235
+ }
25236
+ });
25237
+ }
25185
25238
  return JSON.stringify({
25186
25239
  hookSpecificOutput: {
25187
25240
  hookEventName: "PreToolUse",
25188
- permissionDecision: decision.decision === "refuse" ? "deny" : "allow",
25241
+ permissionDecision: "allow",
25189
25242
  permissionDecisionReason: permitReason(decision)
25190
25243
  }
25191
25244
  });
@@ -25209,6 +25262,7 @@ async function runHarnessCommand(method, options, repoPath, dependencies = {}) {
25209
25262
  `));
25210
25263
  const observe = dependencies.observe ?? readHarnessGateObservation;
25211
25264
  const raw = await (dependencies.stdin ?? readStdin)();
25265
+ let kind = "claude";
25212
25266
  let payload;
25213
25267
  try {
25214
25268
  const parsed = JSON.parse(raw);
@@ -25218,28 +25272,35 @@ async function runHarnessCommand(method, options, repoPath, dependencies = {}) {
25218
25272
  }
25219
25273
  if (!payload) {
25220
25274
  const decision2 = gateDecision("allow", "hook-payload-invalid");
25221
- process.stdout.write(json ? `${JSON.stringify(decision2)}
25222
- ` : `${hookOutput(decision2)}
25223
- `);
25275
+ process.stdout.write(
25276
+ json ? `${JSON.stringify(decision2)}
25277
+ ` : `${hookOutput(decision2, kind)}
25278
+ `
25279
+ );
25224
25280
  return;
25225
25281
  }
25282
+ kind = detectHarnessHook(payload);
25226
25283
  const toolName = typeof payload.tool_name === "string" ? payload.tool_name : "";
25227
25284
  const toolInput = isRecord7(payload.tool_input) ? payload.tool_input : {};
25228
25285
  const command = typeof toolInput.command === "string" ? toolInput.command : "";
25229
25286
  if (toolName === "Bash" && DEVROUTER_COMMAND_RE.test(command)) {
25230
25287
  const decision2 = gateDecision("allow", "devrouter-command");
25231
- process.stdout.write(json ? `${JSON.stringify(decision2)}
25232
- ` : `${hookOutput(decision2)}
25233
- `);
25288
+ process.stdout.write(
25289
+ json ? `${JSON.stringify(decision2)}
25290
+ ` : `${hookOutput(decision2, kind)}
25291
+ `
25292
+ );
25234
25293
  return;
25235
25294
  }
25236
25295
  const sessionCwd = repoPath ?? (typeof payload.cwd === "string" && payload.cwd.length > 0 ? payload.cwd : void 0);
25237
25296
  const repoRoot = sessionCwd ? findHarnessGateRepoRoot(sessionCwd) : void 0;
25238
25297
  if (!repoRoot) {
25239
25298
  const decision2 = gateDecision("allow", "unmanaged-checkout");
25240
- process.stdout.write(json ? `${JSON.stringify(decision2)}
25241
- ` : `${hookOutput(decision2)}
25242
- `);
25299
+ process.stdout.write(
25300
+ json ? `${JSON.stringify(decision2)}
25301
+ ` : `${hookOutput(decision2, kind)}
25302
+ `
25303
+ );
25243
25304
  return;
25244
25305
  }
25245
25306
  const budgetMs = parseHarnessGateBudget(options.waitBudgetMs);
@@ -25290,7 +25351,7 @@ async function runHarnessCommand(method, options, repoPath, dependencies = {}) {
25290
25351
  };
25291
25352
  process.stdout.write(
25292
25353
  json ? `${JSON.stringify({ ...decision2, checkout: repoRoot })}
25293
- ` : `${hookOutput(decision2)}
25354
+ ` : `${hookOutput(decision2, kind)}
25294
25355
  `
25295
25356
  );
25296
25357
  return;
@@ -25348,7 +25409,7 @@ async function runHarnessCommand(method, options, repoPath, dependencies = {}) {
25348
25409
  }
25349
25410
  process.stdout.write(
25350
25411
  json ? `${JSON.stringify({ ...decision, checkout: repoRoot })}
25351
- ` : `${hookOutput(decision)}
25412
+ ` : `${hookOutput(decision, kind)}
25352
25413
  `
25353
25414
  );
25354
25415
  }
@@ -29972,7 +30033,7 @@ var init_version = __esm({
29972
30033
 
29973
30034
  // src/cli.ts
29974
30035
  var import_commander = require("commander");
29975
- var CLI_VERSION = true ? "0.0.80" : "0.0.0-dev";
30036
+ var CLI_VERSION = true ? "0.1.1" : "0.0.0-dev";
29976
30037
  var VERSION_FLAGS = /* @__PURE__ */ new Set(["-V", "--version"]);
29977
30038
  function withErrorHandling(action2) {
29978
30039
  return async (...args) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devrouter/cli",
3
- "version": "0.0.80",
3
+ "version": "0.1.1",
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",
@@ -52,6 +52,9 @@
52
52
  "qualify:controller": "node --import tsx scripts/qualify-controller.ts",
53
53
  "qualify:stop-drift": "node --import tsx scripts/qualify-config-drift-stop.ts",
54
54
  "qualify:harness-journey": "./scripts/qualify-harness-journey.sh",
55
+ "qualify:codex-journey": "DR_JOURNEY_HARNESS=codex ./scripts/qualify-harness-journey.sh",
56
+ "qualify:non-node": "./scripts/qualify-non-node-consumer.sh",
57
+ "qualify:slow-dependency": "./scripts/qualify-slow-dependency-recovery.sh",
55
58
  "test:process": "bash scripts/test-devrouter-process.sh",
56
59
  "check": "biome check .",
57
60
  "check:fix": "biome check --write .",
@@ -0,0 +1,69 @@
1
+ # Upgrade to devrouter 0.1.0
2
+
3
+ This release is the first stable cut of the reliability program. The managed
4
+ lifecycle is now driven by durable intent instead of a live CLI process, capacity
5
+ admission refuses instead of guessing when its evidence is incomplete, and the
6
+ agent harness gate serves Claude Code and the Codex CLI in the shape each of them
7
+ accepts.
8
+
9
+ ## What changed for a consumer
10
+
11
+ - A managed `stop` whose physical cessation is proven against a positively
12
+ absent capacity ledger now completes instead of staying in `stopping`
13
+ forever. An intact ledger keeps today's settlement and revision fencing, and
14
+ every other ledger classification still refuses.
15
+ - `devrouter capacity reconcile --yes [--json]` is the explicit forward
16
+ recovery for a provably absent ledger. It refuses without `--yes`, while any
17
+ journal binding remains, and when a declared runtime domain cannot be observed.
18
+ - `devrouter doctor` reports `global.cli-path` when another `devrouter` on
19
+ PATH is newer than the running CLI or when the shell resolves a different
20
+ install. Keep one install authoritative before trusting lifecycle state.
21
+ - `devrouter harness gate` defers one agent tool call while the checkout's
22
+ durable phase is `queued`, `starting`, `verifying`, `recovering` or
23
+ `stopping`, waits inside the hook process with no model turns, and refuses
24
+ once with recovery guidance when the budget runs out. It identifies the
25
+ requesting harness from its payload: Claude Code receives
26
+ `permissionDecision: allow`, the Codex CLI receives the same guidance as
27
+ `additionalContext` because it reports an `allow` decision as unsupported
28
+ hook output. A re-delivered call returns one refusal that names the earlier
29
+ decision instead of repeating a command that may already have run.
30
+ - A fixed host-port conflict whose holder is the shared `devrouter-traefik`
31
+ router names the consumer's own published binding as the side to change instead
32
+ of telling the agent to stop the router.
33
+ - Managed `ensure` accepts a devnet upstream served by any Compose project owned
34
+ by the exact worktree, while a container owned by a different worktree or by a
35
+ shared project outside the checkout is still refused.
36
+
37
+ ## Task
38
+
39
+ Use the published 0.1.0 CLI. Update the repository `devrouter.version` metadata
40
+ only when consumer source changes are authorized, and refresh generated agent
41
+ guidance only with its owner's authority. No schema, command, flag, environment
42
+ or journal migration is needed, and the upgrade rewrites no runtime state.
43
+
44
+ Two operating notes matter for scripts and agents:
45
+
46
+ - `ensure` and `stop` write bounded progress lines to stderr. Parse JSON from
47
+ stdout only; do not merge the streams before parsing.
48
+ - Capacity admission, parking consent and automatic recovery stay inert unless an
49
+ operator enables them. Nothing in this release starts, stops or parks a
50
+ consumer environment on its own.
51
+
52
+ ## Validation
53
+
54
+ Verify the resolved executable and installed version, then run `devrouter
55
+ doctor` and confirm `global.cli-path` reports one authoritative install and no
56
+ newer shadow copy. For an authorized managed checkout, run `devrouter status`
57
+ and confirm the reported intent, phase, capacity state and attention reason agree
58
+ with the runtime you observe. Require each lifecycle operation's own terminal
59
+ evidence: a status block or a progress line is not stop or ensure success. When
60
+ the gate is wired, prove it once in the harness you use by gating a harmless
61
+ command against a settled checkout.
62
+
63
+ ## Report
64
+
65
+ Report the release revision, the executable and installed version, the
66
+ CLI-path diagnostics, the status block for any affected checkout, and the
67
+ terminal result of any authorized lifecycle operation. Keep source and package
68
+ validation separate from consumer readiness or recovery, and record unknown
69
+ ownership or surviving resources as unresolved rather than as recovered.
@@ -0,0 +1,48 @@
1
+ # Upgrade to devrouter 0.1.1
2
+
3
+ This is a patch release on top of 0.1.0. It closes one way a managed environment
4
+ could sit blocked without saying so: a start that admission refused on a fixed
5
+ published host-port claim left the journal recording the intent as `running`
6
+ while no runtime existed, so `devrouter status` reported a stable, drifting-free
7
+ environment that could not progress.
8
+
9
+ ## What changed for a consumer
10
+
11
+ - A managed `ensure` refused because a running container already holds a fixed
12
+ published host port now stays visible after the command exits. `devrouter
13
+ status` reports a `start-refused` attention reason with two recovery
14
+ commands: the read-only `devrouter doctor <repo>` check that names the
15
+ conflicting consumer binding and its holder, and the consumer-side fix
16
+ followed by `devrouter ensure <repo>`.
17
+ - An explicit `devrouter stop <repo>` releases that intent and the reason, and a
18
+ later admitted ensure clears it. Nothing else about the refusal changed: the
19
+ start is still refused before any provider mutation, and live-worker,
20
+ unknown-ownership and surviving-resource checks still refuse fail-closed.
21
+ - The release also carries `pnpm qualify:slow-dependency`, the developer
22
+ qualifier behind the Q06 dependency-recovery evidence. It is a repository
23
+ script and has no consumer-facing effect.
24
+
25
+ ## Task
26
+
27
+ Use the published 0.1.1 CLI. Update the repository `devrouter.version` metadata
28
+ only when consumer source changes are authorized, and refresh generated agent
29
+ guidance only with its owner's authority. No schema, command, flag, environment
30
+ or journal migration is needed, and the upgrade rewrites no runtime state.
31
+
32
+ ## Validation
33
+
34
+ Verify the resolved executable and installed version, then run `devrouter
35
+ doctor` for the checkout and confirm `global.cli-path` reports one
36
+ authoritative install. Run `devrouter status` and confirm the reported intent,
37
+ phase and attention reason agree with the runtime you observe. When the reported
38
+ reason is `start-refused`, resolve the named binding as the owner of that
39
+ port — the shared router is never the side to stop — and keep each lifecycle
40
+ operation's own terminal evidence separate from a status block or progress line.
41
+
42
+ ## Report
43
+
44
+ Report the release revision, the executable and installed version, the status
45
+ block for any affected checkout, and the terminal result of any authorized
46
+ lifecycle operation. Keep source and package validation separate from consumer
47
+ readiness or recovery, and record unknown ownership or surviving resources as
48
+ unresolved rather than as recovered.