Skip to content

feat: approximate Compose's restart: policy with a foreground wip up --watch poll loop - #73

Merged
abechan1 merged 3 commits into
mainfrom
feat/watch-restart-policy
Aug 10, 2026
Merged

feat: approximate Compose's restart: policy with a foreground wip up --watch poll loop#73
abechan1 merged 3 commits into
mainfrom
feat/watch-restart-policy

Conversation

@abechan1

@abechan1 abechan1 commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Summary

  • restart: in compose.yml was silently dropped by mode: compose-nativewslc run/exec has no restart-policy flag. A crashed sidecar (e.g. a MySQL dependency) just stayed down until someone manually reran wip up.
  • Investigated whether wslc itself (now open-sourced in microsoft/WSL) could support this some other way: it can't, today. No RestartPolicy concept anywhere in its engine, and no public wslc events CLI command (only an unmerged, SDK/COM-level PR — Wslc events microsoft/WSL#40971 — not consumable from wip's shell-out-to-CLI architecture even once merged). The only viable mechanism is polling wslc list --all --format json, which wip already calls.
  • wip up --watch adds that polling as a foreground, opt-in loop — the same shape as the already-shipped wip sync --watch — not a background daemon (which the README explicitly rules out as a non-goal; see the updated Roadmap section for how this stays inside that boundary).
  • restart: is now parsed (compose-native) and accepted (mode: container) instead of ignored, defaulting to "no". Handles a real gotcha: YAML coerces an unquoted restart: no to the boolean false, not the string "no" — both parsing paths correct this back.
  • Deliberately conservative scope, consistent with how this codebase already treats compose-native mode ("a deliberately minimal subset"): always/unless-stopped/on-failure are all treated identically (restart on exited/dead, no exit-code gating for on-failure), and the loop is status-based rather than transition-based, so it can race with a concurrent manual wip stop/down in another terminal — documented as a known limitation, not solved.

Known unverified assumption (flagged prominently in code + README)

The exited/dead detection assumes wslc list --all --format json reports a lowercase State field matching docker ps --format json's own shape. No fixture or sample output exists anywhere in this repo to confirm the exact field name against a real wslc install — isolated to one method (container_status in cli.rb) and logged under --debug so a wrong guess is immediately visible rather than silently inert. Needs a follow-up check against a real WSLC install (see README's new "Restarting exited dependencies" section for exactly what to check).

Test plan

  • bundle exec rspec (278 examples, 0 failures)
  • bundle exec rubocop (46 files, no offenses)
  • Manual smoke test: Config#dependency merges restart: "no" default correctly for both mode: container and mode: compose-native; unquoted restart: no normalizes correctly in both; wip init's scaffolded template parses with the new key.
  • Cannot be verified from this environment (no real wslc/WSL2/Windows access) — needs manual verification on a real machine:
    • Confirm wslc list --all --format json's actual field name/values for container status.
    • Run wip up --watch --debug against a real stack with a restart: always sidecar, kill it externally, confirm it gets restarted.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added wip up --watch to automatically restart exited dependency services based on configured restart policies.
    • Added configurable polling intervals with --interval.
    • Watch mode runs detached and continues until interrupted.
  • Bug Fixes

    • Restart settings are now preserved and normalized consistently across dependency configurations.
    • Added validation and clear handling for unsupported watch mode in Compose workflows.
  • Documentation

    • Documented watch mode, polling, restart policies, detached execution, and known limitations.

… --watch` poll loop

wslc has no restart-policy support and no public event-stream CLI (confirmed
against the now-public microsoft/WSL source: no RestartPolicy concept anywhere
in the engine, and the one in-progress events PR — microsoft/WSL#40971 — is
SDK/COM-level only, not something wip's shell-out-to-CLI architecture could
consume even once merged). Polling `wslc list --all --format json` is the only
viable mechanism, so `compose.yml`'s `restart:` has been silently dropped by
mode: compose-native until now — a crashed sidecar (e.g. a MySQL dependency)
just stays down until someone manually reruns `wip up`.

`wip up --watch` closes that gap without adding a background daemon (which
the README explicitly rules out): it's a foreground, opt-in poll loop the
user keeps a terminal open for, the same shape as the already-shipped
`wip sync --watch`. `restart:` is now parsed (compose-native) and accepted
(mode: container) instead of ignored, defaulting to "no" — including handling
YAML's boolean coercion of an unquoted `restart: no` into `false`. The loop
treats always/unless-stopped/on-failure identically (restart on exited/dead,
no exit-code gating) and is deliberately status-based rather than
transition-based, so it can race with a concurrent manual `wip stop`/`down`
in another terminal — documented as a known limitation rather than solved.

The exited/dead status read from `wslc list --all --format json` assumes a
`State` field matching `docker ps --format json`'s own shape; unconfirmed
against a real wslc install (no fixture exists anywhere in this repo for that
output today), so it's isolated to one method and logged under --debug to
make a wrong guess immediately visible rather than silently inert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 10, 2026 09:08
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 44 minutes

This review is too large to run within your organization's remaining usage spending cap. Raise or remove your spending cap in the billing tab, then retry.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c5d2712a-3112-4853-b186-856ccb3a6e07

📥 Commits

Reviewing files that changed from the base of the PR and between 224948f and 8994b24.

📒 Files selected for processing (6)
  • .rubocop.yml
  • README.md
  • lib/wip/cli.rb
  • lib/wip/config.rb
  • spec/wip/cli_spec.rb
  • spec/wip/config_spec.rb
📝 Walkthrough

Walkthrough

This change adds restart-policy support for dependencies and Compose services. wip up --watch starts containers in detached mode, polls dependency status, and restarts eligible exited or dead containers. Configuration, initializer templates, documentation, and tests cover the new behavior.

Changes

Dependency restart watching

Layer / File(s) Summary
Restart policy contract and normalization
lib/wip/compose_file.rb, lib/wip/config.rb, lib/wip/initializer.rb, spec/wip/compose_file_spec.rb, spec/wip/config_spec.rb, spec/wip/initializer_spec.rb, README.md
Restart policies are preserved in service and dependency data. Missing, empty, and boolean false values become "no". Templates, documentation, and specifications describe the supported policies.
Watch-mode startup and polling
lib/wip/cli.rb, spec/wip/cli_spec.rb, README.md
wip up accepts --watch and --interval. Watch mode forces detached startup, rejects Compose mode, validates the interval, polls container status, logs raw status in debug mode, and restarts eligible exited or dead dependencies.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant WipCLI
  participant ContainerRuntime
  User->>WipCLI: run wip up --watch
  WipCLI->>ContainerRuntime: start containers detached
  loop configured interval
    WipCLI->>ContainerRuntime: query dependency status
    ContainerRuntime-->>WipCLI: return JSON status
    WipCLI->>ContainerRuntime: restart eligible exited or dead containers
  end
Loading

Possibly related PRs

  • slidict/wip#13: Introduces dependency configuration and wip up logic extended by this change.
  • slidict/wip#19: Introduces Compose-mode handling extended to reject --watch.
  • slidict/wip#31: Covers Compose service and dependency parsing extended to preserve restart policies.

Suggested reviewers: claude

Poem

A rabbit watches containers at night,
Restarting the sleepy ones just right.
Policies guide each hop and run,
Detached polling follows the sun.
“No” means rest; eligible friends restart—
A tidy loop with a careful heart.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: Compose restart policy support through a foreground wip up --watch polling loop.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/watch-restart-policy

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds an opt-in wip up --watch foreground poll loop to approximate Docker Compose restart: behavior when running under mode: compose-native, and ensures restart: is parsed/retained (instead of ignored) across config sources.

Changes:

  • Add wip up --watch + --interval to poll wslc list --all --format json and restart exited/dead dependencies whose restart: policy allows it.
  • Parse and default restart: to "no" in both wip.yml dependencies and compose-native compose.yml parsing, including normalization of YAML’s unquoted restart: no (boolean false).
  • Update docs and templates to expose the new restart: key and describe the --watch behavior/limitations.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
spec/wip/initializer_spec.rb Updates initializer template expectations to include restart.
spec/wip/config_spec.rb Asserts default restart: "no" and YAML-boolean normalization in config dependencies.
spec/wip/compose_file_spec.rb Covers compose-native parsing/defaulting of restart: and normalization of unquoted no.
spec/wip/cli_spec.rb Adds coverage for up --watch behavior (detach implication, restart loop behavior, validation, debug logging, compose-mode rejection).
README.md Documents restart: semantics and the wip up --watch poll loop behavior/limitations.
lib/wip/initializer.rb Adds restart: "no" to the generated container-mode template.
lib/wip/config.rb Adds default restart: "no" and normalizes YAML false to "no" for wip.yml dependencies.
lib/wip/compose_file.rb Parses restart: from compose files, defaults it to "no", and normalizes YAML false to "no".
lib/wip/cli.rb Implements up --watch polling + restart logic and enforces watch incompatibility with mode: compose.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread README.md Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/wip/cli.rb`:
- Around line 85-88: Update the restart-policy matching logic that consumes
AUTO_RESTART_POLICIES so only exact “always” and “unless-stopped” values, plus
valid “on-failure” values with an optional numeric retry suffix, trigger
restarts; reject prefixed invalid values such as “always-invalid”,
“unless-stopped-extra”, and “on-failurex”. Add coverage for these invalid
prefixed policies.
- Around line 97-105: Validate the watch interval at the start of `up`, before
`ensure_compose_images`, `ensure_network`, dependency startup, sync, or
container creation; reuse the existing validation around the interval handling
near `watch_restarts`/`ConfigError`. Add a spec asserting that an invalid
`--watch --interval` causes `ConfigError` and no startup command is executed.
- Around line 480-494: Update container_status to convert WSLC’s integer State
enum into the string states consumed by watch-mode comparisons, mapping Exited
(3) to exited and Deleted (4) to the appropriate terminal state while leaving
other states handled consistently. Add a fixture covering the WSLC integer State
payload and verify exited containers trigger the existing restart behavior.

In `@lib/wip/config.rb`:
- Around line 212-214: Update the restart normalization logic in the surrounding
configuration processing to convert explicit nil and empty-string restart values
to "no", matching ComposeFile#normalize_restart and preserving the existing
false-to-"no" conversion. Add examples covering both restart: and restart: ""
inputs.

In `@README.md`:
- Around line 211-214: Update the README guidance describing the JSON response
to distinguish the capitalized `State` field name from its lowercase values,
including `exited` and `dead`. Preserve the existing diagnostic instructions for
checking the logged `list` entry when watch mode does not restart an exited
service.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 63f82a67-15cc-4fce-9fe4-5e7eda5e0661

📥 Commits

Reviewing files that changed from the base of the PR and between ad40b37 and 224948f.

📒 Files selected for processing (9)
  • README.md
  • lib/wip/cli.rb
  • lib/wip/compose_file.rb
  • lib/wip/config.rb
  • lib/wip/initializer.rb
  • spec/wip/cli_spec.rb
  • spec/wip/compose_file_spec.rb
  • spec/wip/config_spec.rb
  • spec/wip/initializer_spec.rb

Comment thread lib/wip/cli.rb Outdated
Comment thread lib/wip/cli.rb Outdated
Comment thread lib/wip/cli.rb Outdated
Comment thread lib/wip/config.rb Outdated
Comment thread README.md Outdated
…s review fixes

CodeRabbit flagged that container_status assumed a lowercase string State
field (docker ps-style) with no way to confirm it against a real wslc
install. Verified directly against microsoft/WSL's own docs and source
(ContainerModel.h's ContainerInformation#State has no custom JSON enum
serializer, so nlohmann::json emits WslcContainerState's raw ordinal):
State is an integer — 0 invalid, 1 created, 2 running, 3 exited, 4
deleted — with no separate "dead" state. `%w[exited dead].include?(status)`
against that integer could never match, so the whole restart mechanism
would have silently never fired against a real wslc install. Fixed to
compare against the confirmed WSLC_CONTAINER_STATE_EXITED (3) directly.

Also addresses the rest of CodeRabbit's review:
- auto_restart? used start_with?, so "always-invalid" or "on-failurex"
  would incorrectly trigger a restart; now exact-matches always/
  unless-stopped and regex-matches on-failure[:N] only.
- --interval is now validated before any startup side effect (image
  build, network/dependency/container creation), not after — threaded
  through to watch_restarts instead of re-read from inside the loop.
- config.rb's validate_dependency! now normalizes an explicit nil/""
  restart: to "no" too, not just the YAML `false` case, matching
  ComposeFile#normalize_restart's equivalent handling.
- README's restart-detection bullet rewritten with the verified enum
  facts instead of the old, unconfirmed docker ps-shaped guess.

lib/wip/config.rb added to Metrics/ClassLength's exclude list (joining
cli.rb and compose_file.rb, already excluded there) rather than cramming
the new normalization logic to dodge a 3-line overage.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 10, 2026 10:01
Copilot caught that the README's sample transposed "every 5s" and "for
exited restart: containers" relative to what cli.rb's watch_restarts
actually prints.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.

Suppressed comments (2)

lib/wip/cli.rb:466

  • watch_restarts logs that it is watching all dependencies, but the loop only performs status checks/restarts for dependencies whose restart: policy matches auto_restart?. This makes the startup message misleading and also needlessly iterates over entries that will never be polled.
      names = load_config.dependencies.keys
      warn "wip: watching #{names.join(', ')} for exited restart: containers every #{interval}s " \
           '(running detached; Ctrl-C to stop)'
      loop do
        names.each { |name| restart_if_exited(name) }

lib/wip/cli.rb:502

  • container_status assumes wslc list --format json returns an array of hashes. If it ever returns a non-array JSON value (e.g., a hash), JSON.parse(output).first will yield a non-hash and fetch('State') can raise (e.g., TypeError for Array#fetch with a string key), which would crash the --watch loop. This method is intended to fail closed (return nil), so it should validate the parsed JSON shape before indexing.
      entry = JSON.parse(output).first
      warn "wip: [debug] '#{name}': #{entry.inspect}" if debug?
      entry&.fetch('State', nil)
    rescue JSON::ParserError
      nil

@abechan1
abechan1 merged commit bac54d2 into main Aug 10, 2026
5 checks passed
@abechan1
abechan1 deleted the feat/watch-restart-policy branch August 10, 2026 10:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants