clauster 0.2.2__tar.gz → 0.3.0__tar.gz

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.
Files changed (137) hide show
  1. {clauster-0.2.2 → clauster-0.3.0}/.bestpractices.json +1 -1
  2. clauster-0.3.0/.coderabbit.yaml +54 -0
  3. clauster-0.3.0/.github/ISSUE_TEMPLATE/bug_report.yml +56 -0
  4. clauster-0.3.0/.github/ISSUE_TEMPLATE/config.yml +5 -0
  5. clauster-0.3.0/.github/ISSUE_TEMPLATE/feature_request.yml +21 -0
  6. clauster-0.3.0/.github/PULL_REQUEST_TEMPLATE.md +14 -0
  7. clauster-0.3.0/.github/repo-config/labels.json +15 -0
  8. clauster-0.3.0/.github/rulesets/README.md +63 -0
  9. clauster-0.3.0/.github/rulesets/main.json +53 -0
  10. clauster-0.3.0/.github/rulesets/tags.json +16 -0
  11. {clauster-0.2.2 → clauster-0.3.0}/.github/workflows/lint.yml +2 -1
  12. clauster-0.3.0/.release-please-manifest.json +3 -0
  13. {clauster-0.2.2 → clauster-0.3.0}/.yamllint.yaml +1 -1
  14. {clauster-0.2.2 → clauster-0.3.0}/CHANGELOG.md +15 -0
  15. {clauster-0.2.2 → clauster-0.3.0}/PKG-INFO +44 -3
  16. {clauster-0.2.2 → clauster-0.3.0}/README.md +43 -2
  17. clauster-0.3.0/compose.yaml +38 -0
  18. {clauster-0.2.2 → clauster-0.3.0}/pyproject.toml +1 -1
  19. {clauster-0.2.2 → clauster-0.3.0}/renovate.json +16 -6
  20. {clauster-0.2.2 → clauster-0.3.0}/src/clauster/__init__.py +1 -1
  21. {clauster-0.2.2 → clauster-0.3.0}/src/clauster/config.py +1 -4
  22. {clauster-0.2.2 → clauster-0.3.0}/src/clauster/ops.py +49 -1
  23. {clauster-0.2.2 → clauster-0.3.0}/src/clauster/redact.py +8 -0
  24. {clauster-0.2.2 → clauster-0.3.0}/src/clauster/runner.py +50 -5
  25. {clauster-0.2.2 → clauster-0.3.0}/src/clauster/templates/dashboard.html +7 -1
  26. {clauster-0.2.2 → clauster-0.3.0}/tests/E2E_CHECKLIST.md +9 -3
  27. {clauster-0.2.2 → clauster-0.3.0}/tests/test_app.py +12 -0
  28. {clauster-0.2.2 → clauster-0.3.0}/tests/test_clone_jobs.py +2 -1
  29. {clauster-0.2.2 → clauster-0.3.0}/tests/test_ops.py +48 -0
  30. {clauster-0.2.2 → clauster-0.3.0}/tests/test_redact.py +39 -4
  31. {clauster-0.2.2 → clauster-0.3.0}/tests/test_runner.py +56 -0
  32. {clauster-0.2.2 → clauster-0.3.0}/uv.lock +1 -1
  33. clauster-0.2.2/.coderabbit.yaml +0 -17
  34. clauster-0.2.2/.github/settings.yml +0 -143
  35. clauster-0.2.2/.release-please-manifest.json +0 -3
  36. {clauster-0.2.2 → clauster-0.3.0}/.dockerignore +0 -0
  37. {clauster-0.2.2 → clauster-0.3.0}/.github/CODEOWNERS +0 -0
  38. {clauster-0.2.2 → clauster-0.3.0}/.github/workflows/ci.yml +0 -0
  39. {clauster-0.2.2 → clauster-0.3.0}/.github/workflows/pr-title.yml +0 -0
  40. {clauster-0.2.2 → clauster-0.3.0}/.github/workflows/release-please.yml +0 -0
  41. {clauster-0.2.2 → clauster-0.3.0}/.github/workflows/release.yml +0 -0
  42. {clauster-0.2.2 → clauster-0.3.0}/.github/workflows/scorecard.yml +0 -0
  43. {clauster-0.2.2 → clauster-0.3.0}/.github/workflows/security.yml +0 -0
  44. {clauster-0.2.2 → clauster-0.3.0}/.gitignore +0 -0
  45. {clauster-0.2.2 → clauster-0.3.0}/.markdownlint-cli2.yaml +0 -0
  46. {clauster-0.2.2 → clauster-0.3.0}/.pre-commit-config.yaml +0 -0
  47. {clauster-0.2.2 → clauster-0.3.0}/CONTRIBUTING.md +0 -0
  48. {clauster-0.2.2 → clauster-0.3.0}/Dockerfile +0 -0
  49. {clauster-0.2.2 → clauster-0.3.0}/LICENSE +0 -0
  50. {clauster-0.2.2 → clauster-0.3.0}/SECURITY.md +0 -0
  51. {clauster-0.2.2 → clauster-0.3.0}/THIRD_PARTY_NOTICES.md +0 -0
  52. {clauster-0.2.2 → clauster-0.3.0}/UPGRADING.md +0 -0
  53. {clauster-0.2.2 → clauster-0.3.0}/clauster.spec +0 -0
  54. {clauster-0.2.2 → clauster-0.3.0}/clauster.yml.example +0 -0
  55. {clauster-0.2.2 → clauster-0.3.0}/codecov.yml +0 -0
  56. {clauster-0.2.2 → clauster-0.3.0}/docker/entrypoint.sh +0 -0
  57. {clauster-0.2.2 → clauster-0.3.0}/docs/screenshots/dashboard-dark.png +0 -0
  58. {clauster-0.2.2 → clauster-0.3.0}/docs/screenshots/dashboard-light.png +0 -0
  59. {clauster-0.2.2 → clauster-0.3.0}/docs/screenshots/login-dark.png +0 -0
  60. {clauster-0.2.2 → clauster-0.3.0}/docs/screenshots/new-project-clone.png +0 -0
  61. {clauster-0.2.2 → clauster-0.3.0}/package-lock.json +0 -0
  62. {clauster-0.2.2 → clauster-0.3.0}/package.json +0 -0
  63. {clauster-0.2.2 → clauster-0.3.0}/release-please-config.json +0 -0
  64. {clauster-0.2.2 → clauster-0.3.0}/scripts/build-binary.sh +0 -0
  65. {clauster-0.2.2 → clauster-0.3.0}/scripts/lint-docs.sh +0 -0
  66. {clauster-0.2.2 → clauster-0.3.0}/src/clauster/__main__.py +0 -0
  67. {clauster-0.2.2 → clauster-0.3.0}/src/clauster/app.py +0 -0
  68. {clauster-0.2.2 → clauster-0.3.0}/src/clauster/auth.py +0 -0
  69. {clauster-0.2.2 → clauster-0.3.0}/src/clauster/bridge_log.py +0 -0
  70. {clauster-0.2.2 → clauster-0.3.0}/src/clauster/claude_cli.py +0 -0
  71. {clauster-0.2.2 → clauster-0.3.0}/src/clauster/claude_md.py +0 -0
  72. {clauster-0.2.2 → clauster-0.3.0}/src/clauster/clone_jobs.py +0 -0
  73. {clauster-0.2.2 → clauster-0.3.0}/src/clauster/discovery.py +0 -0
  74. {clauster-0.2.2 → clauster-0.3.0}/src/clauster/environments.py +0 -0
  75. {clauster-0.2.2 → clauster-0.3.0}/src/clauster/hooks/__init__.py +0 -0
  76. {clauster-0.2.2 → clauster-0.3.0}/src/clauster/hooks/resume_recap.py +0 -0
  77. {clauster-0.2.2 → clauster-0.3.0}/src/clauster/inspector.py +0 -0
  78. {clauster-0.2.2 → clauster-0.3.0}/src/clauster/logstream.py +0 -0
  79. {clauster-0.2.2 → clauster-0.3.0}/src/clauster/models.py +0 -0
  80. {clauster-0.2.2 → clauster-0.3.0}/src/clauster/pointers.py +0 -0
  81. {clauster-0.2.2 → clauster-0.3.0}/src/clauster/procutil.py +0 -0
  82. {clauster-0.2.2 → clauster-0.3.0}/src/clauster/provisioning.py +0 -0
  83. {clauster-0.2.2 → clauster-0.3.0}/src/clauster/pty_keeper.py +0 -0
  84. {clauster-0.2.2 → clauster-0.3.0}/src/clauster/recap.py +0 -0
  85. {clauster-0.2.2 → clauster-0.3.0}/src/clauster/state.py +0 -0
  86. {clauster-0.2.2 → clauster-0.3.0}/src/clauster/static/alpine.LICENSE +0 -0
  87. {clauster-0.2.2 → clauster-0.3.0}/src/clauster/static/alpine.min.js +0 -0
  88. {clauster-0.2.2 → clauster-0.3.0}/src/clauster/static/clauster.css +0 -0
  89. {clauster-0.2.2 → clauster-0.3.0}/src/clauster/static/favicon.svg +0 -0
  90. {clauster-0.2.2 → clauster-0.3.0}/src/clauster/static/vendor/iconoir/LICENSE +0 -0
  91. {clauster-0.2.2 → clauster-0.3.0}/src/clauster/static/vendor/iconoir/README.md +0 -0
  92. {clauster-0.2.2 → clauster-0.3.0}/src/clauster/static/vendor/tabler/LICENSE +0 -0
  93. {clauster-0.2.2 → clauster-0.3.0}/src/clauster/static/vendor/tabler/README.md +0 -0
  94. {clauster-0.2.2 → clauster-0.3.0}/src/clauster/static/vendor/tabler/css/tabler.min.css +0 -0
  95. {clauster-0.2.2 → clauster-0.3.0}/src/clauster/static/vendor/tabler/js/tabler.min.js +0 -0
  96. {clauster-0.2.2 → clauster-0.3.0}/src/clauster/static/vendor/versions.txt +0 -0
  97. {clauster-0.2.2 → clauster-0.3.0}/src/clauster/templates/_iconoir_sprite.html +0 -0
  98. {clauster-0.2.2 → clauster-0.3.0}/src/clauster/templates/_project_card.html +0 -0
  99. {clauster-0.2.2 → clauster-0.3.0}/src/clauster/templates/login.html +0 -0
  100. {clauster-0.2.2 → clauster-0.3.0}/src/clauster/trust.py +0 -0
  101. {clauster-0.2.2 → clauster-0.3.0}/src/clauster/usage.py +0 -0
  102. {clauster-0.2.2 → clauster-0.3.0}/tests/conftest.py +0 -0
  103. {clauster-0.2.2 → clauster-0.3.0}/tests/fixtures/bridge-logs/test1-bridge-debug.log +0 -0
  104. {clauster-0.2.2 → clauster-0.3.0}/tests/fixtures/fake_claude/claude +0 -0
  105. {clauster-0.2.2 → clauster-0.3.0}/tests/fixtures/fake_claude/claude.cmd +0 -0
  106. {clauster-0.2.2 → clauster-0.3.0}/tests/fixtures/fake_git/git +0 -0
  107. {clauster-0.2.2 → clauster-0.3.0}/tests/fixtures/fake_git/git.cmd +0 -0
  108. {clauster-0.2.2 → clauster-0.3.0}/tests/fixtures/pointers/dockerize2.bridge-pointer.json +0 -0
  109. {clauster-0.2.2 → clauster-0.3.0}/tests/fixtures/pointers/test1.bridge-pointer.json +0 -0
  110. {clauster-0.2.2 → clauster-0.3.0}/tests/fixtures/pointers/test2.bridge-pointer.json +0 -0
  111. {clauster-0.2.2 → clauster-0.3.0}/tests/fixtures/transcripts/test1-session.jsonl +0 -0
  112. {clauster-0.2.2 → clauster-0.3.0}/tests/test_app_auth.py +0 -0
  113. {clauster-0.2.2 → clauster-0.3.0}/tests/test_app_instances.py +0 -0
  114. {clauster-0.2.2 → clauster-0.3.0}/tests/test_app_routes.py +0 -0
  115. {clauster-0.2.2 → clauster-0.3.0}/tests/test_auth.py +0 -0
  116. {clauster-0.2.2 → clauster-0.3.0}/tests/test_bridge_log.py +0 -0
  117. {clauster-0.2.2 → clauster-0.3.0}/tests/test_claude_md.py +0 -0
  118. {clauster-0.2.2 → clauster-0.3.0}/tests/test_config.py +0 -0
  119. {clauster-0.2.2 → clauster-0.3.0}/tests/test_discovery.py +0 -0
  120. {clauster-0.2.2 → clauster-0.3.0}/tests/test_environments.py +0 -0
  121. {clauster-0.2.2 → clauster-0.3.0}/tests/test_fixtures.py +0 -0
  122. {clauster-0.2.2 → clauster-0.3.0}/tests/test_inspector.py +0 -0
  123. {clauster-0.2.2 → clauster-0.3.0}/tests/test_logstream.py +0 -0
  124. {clauster-0.2.2 → clauster-0.3.0}/tests/test_logtail.py +0 -0
  125. {clauster-0.2.2 → clauster-0.3.0}/tests/test_main.py +0 -0
  126. {clauster-0.2.2 → clauster-0.3.0}/tests/test_pointers.py +0 -0
  127. {clauster-0.2.2 → clauster-0.3.0}/tests/test_procutil.py +0 -0
  128. {clauster-0.2.2 → clauster-0.3.0}/tests/test_provisioning.py +0 -0
  129. {clauster-0.2.2 → clauster-0.3.0}/tests/test_pty_keeper.py +0 -0
  130. {clauster-0.2.2 → clauster-0.3.0}/tests/test_recap.py +0 -0
  131. {clauster-0.2.2 → clauster-0.3.0}/tests/test_runner_pty.py +0 -0
  132. {clauster-0.2.2 → clauster-0.3.0}/tests/test_runner_recap.py +0 -0
  133. {clauster-0.2.2 → clauster-0.3.0}/tests/test_spawn_controls.py +0 -0
  134. {clauster-0.2.2 → clauster-0.3.0}/tests/test_state.py +0 -0
  135. {clauster-0.2.2 → clauster-0.3.0}/tests/test_trust.py +0 -0
  136. {clauster-0.2.2 → clauster-0.3.0}/tests/test_urls.py +0 -0
  137. {clauster-0.2.2 → clauster-0.3.0}/tests/test_usage.py +0 -0
@@ -114,7 +114,7 @@
114
114
  "vulnerabilities_fixed_60_days_status": "Met",
115
115
  "vulnerabilities_fixed_60_days_justification": "Renovate and GitHub security alerts surface dependency vulnerabilities, which are patched promptly (well within 60 days).",
116
116
  "vulnerabilities_critical_fixed_status": "Met",
117
- "vulnerabilities_critical_fixed_justification": "No known unpatched vulnerabilities exist (OpenSSF Scorecard Vulnerabilities check scores 10/10).",
117
+ "vulnerabilities_critical_fixed_justification": "No known unpatched vulnerabilities exist; the OpenSSF Scorecard Vulnerabilities check runs on pushes to main and on a weekly schedule, and dependency alerts (Renovate + GitHub) surface any new ones for prompt patching.",
118
118
  "no_leaked_credentials_status": "Met",
119
119
  "no_leaked_credentials_justification": "No credentials are stored in the repository; gitleaks and GitHub secret scanning run in CI, sensitive config is gitignored, and session URLs/tokens are redacted in logs.",
120
120
  "static_analysis_status": "Met",
@@ -0,0 +1,54 @@
1
+ # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
2
+ # CodeRabbit configuration. Schema: https://www.coderabbit.ai/integrations/schema.v2.json
3
+ #
4
+ # Docstrings: our gate is ruff's pydocstyle (D) rules, enforced on EVERY PR by the
5
+ # "lint" CI job (.github/workflows/lint.yml, `on: pull_request`). It requires
6
+ # docstrings on all public modules/classes/functions (pep257 convention; D105/D107
7
+ # and tests/** are intentionally exempt — see pyproject.toml).
8
+ #
9
+ # CodeRabbit's separate docstring-coverage pre-merge check computes a raw
10
+ # percentage that counts inner/nested helpers (preexec closures, signal handlers,
11
+ # test fixtures) which our gate deliberately exempts, so it reports a misleadingly
12
+ # low number against its 80% default. Defer to ruff D as the single source of
13
+ # truth and turn the redundant check off rather than run two disagreeing gates.
14
+ language: "en-US"
15
+ reviews:
16
+ profile: "chill"
17
+ # Don't gate merges on a CodeRabbit "request changes" state — it isn't a
18
+ # required check and we don't want it blocking the solo merge flow.
19
+ request_changes_workflow: false
20
+ high_level_summary: true
21
+ poem: false
22
+ review_status: true
23
+ pre_merge_checks:
24
+ docstrings:
25
+ mode: "off"
26
+ auto_review:
27
+ enabled: true
28
+ drafts: false
29
+ # Skip bot-authored PRs (Renovate, release-please). They only touch deps /
30
+ # CHANGELOG / version files, and skipping them lets auto-merge proceed without
31
+ # waiting on review-thread resolution (the `main` ruleset requires it).
32
+ ignore_usernames:
33
+ - "renovate[bot]"
34
+ - "clauster-release-bot[bot]"
35
+ # Don't spend review effort on vendored / generated / lockfile content.
36
+ path_filters:
37
+ - "!src/clauster/static/vendor/**"
38
+ - "!**/*.min.js"
39
+ - "!**/*.min.css"
40
+ - "!uv.lock"
41
+ - "!package-lock.json"
42
+ path_instructions:
43
+ - path: "src/clauster/templates/**"
44
+ instructions: >-
45
+ These are Jinja2 templates (autoescape on) driving Alpine.js. Values
46
+ rendered with {{ }} are HTML-escaped by default — don't flag ordinary
47
+ interpolation as XSS. Only raw / `| safe` output, or interpolation into
48
+ event-handler attributes / inline script, is a genuine concern.
49
+ - path: "tests/**"
50
+ instructions: >-
51
+ Tests use FastAPI's TestClient. WebSocket and background-task tests must
52
+ wrap the client in `with _client(...)` so the app lifespan/startup runs.
53
+ chat:
54
+ auto_reply: true
@@ -0,0 +1,56 @@
1
+ name: Bug report
2
+ description: Something in Clauster isn't working as expected
3
+ labels: ["bug"]
4
+ body:
5
+ - type: markdown
6
+ attributes:
7
+ value: >
8
+ Thanks for taking the time to file a bug. **For security vulnerabilities,
9
+ do not open an issue** — report it privately via a
10
+ [GitHub Security Advisory](https://github.com/schubydoo/clauster/security/advisories/new)
11
+ (see [SECURITY.md](https://github.com/schubydoo/clauster/blob/main/SECURITY.md)).
12
+ - type: textarea
13
+ id: what-happened
14
+ attributes:
15
+ label: What happened?
16
+ description: A clear description of the bug, including what you expected instead.
17
+ validations:
18
+ required: true
19
+ - type: textarea
20
+ id: repro
21
+ attributes:
22
+ label: Steps to reproduce
23
+ placeholder: |
24
+ 1. ...
25
+ 2. ...
26
+ 3. ...
27
+ validations:
28
+ required: true
29
+ - type: input
30
+ id: version
31
+ attributes:
32
+ label: Clauster version
33
+ description: Output of `clauster --version`, or the Docker image tag.
34
+ validations:
35
+ required: true
36
+ - type: dropdown
37
+ id: install
38
+ attributes:
39
+ label: Install method
40
+ options:
41
+ - pip / uv (PyPI)
42
+ - Docker (GHCR)
43
+ - from source
44
+ validations:
45
+ required: true
46
+ - type: textarea
47
+ id: env
48
+ attributes:
49
+ label: Environment
50
+ description: OS, Python version, browser (for a UI bug), reverse proxy (if any).
51
+ - type: textarea
52
+ id: logs
53
+ attributes:
54
+ label: Relevant logs
55
+ description: Paste `clauster` output or bridge logs. Redact any tokens or secrets.
56
+ render: shell
@@ -0,0 +1,5 @@
1
+ blank_issues_enabled: false
2
+ contact_links:
3
+ - name: Security vulnerability (report privately)
4
+ url: https://github.com/schubydoo/clauster/security/advisories/new
5
+ about: Please report security issues privately via a GitHub Security Advisory — do not open a public issue.
@@ -0,0 +1,21 @@
1
+ name: Feature request
2
+ description: Suggest an idea or improvement
3
+ labels: ["enhancement"]
4
+ body:
5
+ - type: textarea
6
+ id: problem
7
+ attributes:
8
+ label: Problem / motivation
9
+ description: What are you trying to do that Clauster doesn't support today?
10
+ validations:
11
+ required: true
12
+ - type: textarea
13
+ id: proposal
14
+ attributes:
15
+ label: Proposed solution
16
+ description: What would you like to see happen?
17
+ - type: textarea
18
+ id: alternatives
19
+ attributes:
20
+ label: Alternatives considered
21
+ description: Other approaches or workarounds you've tried or thought about.
@@ -0,0 +1,14 @@
1
+ <!-- markdownlint-disable-file -->
2
+
3
+ ## What
4
+
5
+ <!-- What does this PR change, and why? Link any related issue. -->
6
+
7
+ ## Checklist
8
+
9
+ - [ ] PR title follows [Conventional Commits](https://www.conventionalcommits.org/) — it becomes the squashed commit subject that release-please parses (and the required `conventional PR title` check gates it).
10
+ - [ ] Tests added/updated for the change (the `tests` check enforces a 96% coverage gate).
11
+ - [ ] Docs / README updated where user-facing behaviour or config changed.
12
+ - [ ] `uv run pytest` and `uv run pre-commit run -a` pass locally.
13
+
14
+ <!-- The CHANGELOG is generated from commit history by release-please — no manual CHANGELOG edits are needed. -->
@@ -0,0 +1,15 @@
1
+ [
2
+ { "name": "bug", "color": "d73a4a", "description": "Something isn't working" },
3
+ { "name": "enhancement", "color": "a2eeef", "description": "New feature or request" },
4
+ { "name": "documentation", "color": "0075ca", "description": "Improvements or additions to documentation" },
5
+ { "name": "question", "color": "d876e3", "description": "Further information is requested" },
6
+ { "name": "good first issue", "color": "7057ff", "description": "Good for newcomers" },
7
+ { "name": "help wanted", "color": "008672", "description": "Extra attention is needed" },
8
+ { "name": "breaking", "color": "b60205", "description": "Backwards-incompatible change" },
9
+ { "name": "security", "color": "d93f0b", "description": "Security-related — Renovate vulnerability alerts use this" },
10
+ { "name": "dependencies", "color": "ededed", "description": "Renovate-opened dependency update PRs (and the dashboard issue)" },
11
+ { "name": "vendored-assets", "color": "ededed", "description": "Renovate heads-up to re-vendor self-hosted front-end assets (manual dist update)" },
12
+ { "name": "autorelease: pending", "color": "ededed", "description": "Release Please — release PR is open, awaiting merge" },
13
+ { "name": "autorelease: tagged", "color": "0e8a16", "description": "Release Please — release PR is merged and the tag has been pushed" },
14
+ { "name": "autorelease: snapshot", "color": "5319e7", "description": "Release Please — snapshot/prerelease (rare)" }
15
+ ]
@@ -0,0 +1,63 @@
1
+ # Repository rulesets
2
+
3
+ `main.json` is the source-of-truth for the **`main` branch ruleset** — the
4
+ replacement for the legacy branch-protection rules previously declared in
5
+ `.github/settings.yml` and reconciled by the [`repository-settings`][app] Probot
6
+ app. That config file is removed in this PR, and the app itself has been
7
+ uninstalled from the repository — the ruleset is now the sole source of branch
8
+ protection.
9
+
10
+ It encodes the same contract the project has always enforced:
11
+
12
+ - required status checks (`ci required checks passed`, `security required checks
13
+ passed`, `conventional PR title`, `lint`) with strict / up-to-date branches,
14
+ - linear history, conversation resolution, a pull request required (0 approvals —
15
+ solo maintainer), and blocked deletions / force-pushes,
16
+ - `enforcement: active` with an empty `bypass_actors` list — i.e. admins are
17
+ enforced too (the old `enforce_admins: true`).
18
+
19
+ ## Tag protection + immutable releases
20
+
21
+ `tags.json` is a second ruleset (`target: tag`) over `refs/tags/v*` that blocks
22
+ **deletion** and **non-fast-forward** (moving) of release tags, with no bypass —
23
+ so a published version tag can't be repointed or removed. Tag *creation* is not
24
+ blocked, so release-please can still cut new `vX.Y.Z` tags.
25
+
26
+ This pairs with **immutable releases** (enabled on the repo via
27
+ `PUT /repos/{owner}/{repo}/immutable-releases`): once a release is published its
28
+ tag is locked to its commit and its assets can't be changed, and a release
29
+ attestation is generated. Immutability applies to *future* releases only; the
30
+ release **body/notes remain editable**, so the post-publish
31
+ `gh release edit --notes-file` step (when needed) still works.
32
+
33
+ ## Applying
34
+
35
+ The rulesets are currently applied/maintained manually via the API. Each file
36
+ maps to one ruleset: `main.json` → ruleset **`main`** (branch), `tags.json` →
37
+ ruleset **`protect-version-tags`** (tag).
38
+
39
+ ```sh
40
+ # create (first time)
41
+ gh api -X POST repos/<owner>/<repo>/rulesets --input .github/rulesets/main.json
42
+ gh api -X POST repos/<owner>/<repo>/rulesets --input .github/rulesets/tags.json
43
+
44
+ # update an existing ruleset (look up its id by name first)
45
+ id=$(gh api repos/<owner>/<repo>/rulesets --jq '.[]|select(.name=="main")|.id')
46
+ gh api -X PUT repos/<owner>/<repo>/rulesets/"$id" --input .github/rulesets/main.json
47
+ id=$(gh api repos/<owner>/<repo>/rulesets --jq '.[]|select(.name=="protect-version-tags")|.id')
48
+ gh api -X PUT repos/<owner>/<repo>/rulesets/"$id" --input .github/rulesets/tags.json
49
+
50
+ # verify: effective branch rules, every ruleset, and immutable releases
51
+ gh api repos/<owner>/<repo>/rules/branches/main
52
+ gh api repos/<owner>/<repo>/rulesets --jq '.[]|{id,name,target,enforcement}'
53
+ gh api repos/<owner>/<repo>/immutable-releases # expect {"enabled": true, ...}
54
+ ```
55
+
56
+ Immutable releases is a repo setting, not a ruleset file: enable with
57
+ `gh api -X PUT repos/<owner>/<repo>/immutable-releases` (disable with `-X DELETE`).
58
+
59
+ A small GitHub Action to reconcile these files (plus repo settings and labels)
60
+ automatically — applying on merge and re-applying on a schedule to revert
61
+ out-of-band drift — is planned. Until then, edit the JSON and re-apply by hand.
62
+
63
+ [app]: https://github.com/repository-settings/app
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "main",
3
+ "target": "branch",
4
+ "enforcement": "active",
5
+ "bypass_actors": [],
6
+ "conditions": {
7
+ "ref_name": {
8
+ "include": ["~DEFAULT_BRANCH"],
9
+ "exclude": []
10
+ }
11
+ },
12
+ "rules": [
13
+ { "type": "deletion" },
14
+ { "type": "non_fast_forward" },
15
+ { "type": "required_linear_history" },
16
+ {
17
+ "type": "pull_request",
18
+ "parameters": {
19
+ "required_approving_review_count": 0,
20
+ "dismiss_stale_reviews_on_push": true,
21
+ "require_code_owner_review": false,
22
+ "require_last_push_approval": false,
23
+ "required_review_thread_resolution": true,
24
+ "allowed_merge_methods": ["squash"]
25
+ }
26
+ },
27
+ {
28
+ "type": "required_status_checks",
29
+ "parameters": {
30
+ "strict_required_status_checks_policy": true,
31
+ "do_not_enforce_on_create": false,
32
+ "required_status_checks": [
33
+ { "context": "ci required checks passed", "integration_id": 15368 },
34
+ { "context": "security required checks passed", "integration_id": 15368 },
35
+ { "context": "conventional PR title", "integration_id": 15368 },
36
+ { "context": "lint", "integration_id": 15368 }
37
+ ]
38
+ }
39
+ },
40
+ {
41
+ "type": "code_scanning",
42
+ "parameters": {
43
+ "code_scanning_tools": [
44
+ {
45
+ "tool": "CodeQL",
46
+ "security_alerts_threshold": "high_or_higher",
47
+ "alerts_threshold": "errors"
48
+ }
49
+ ]
50
+ }
51
+ }
52
+ ]
53
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "protect-version-tags",
3
+ "target": "tag",
4
+ "enforcement": "active",
5
+ "bypass_actors": [],
6
+ "conditions": {
7
+ "ref_name": {
8
+ "include": ["refs/tags/v*"],
9
+ "exclude": []
10
+ }
11
+ },
12
+ "rules": [
13
+ { "type": "deletion" },
14
+ { "type": "non_fast_forward" }
15
+ ]
16
+ }
@@ -15,7 +15,8 @@ concurrency:
15
15
  jobs:
16
16
  lint:
17
17
  # This job name is a REQUIRED status check — it must match the context list
18
- # in .github/settings.yml exactly (renaming means updating that list too).
18
+ # in the `main` ruleset (.github/rulesets/main.json) exactly (renaming means
19
+ # updating that ruleset too).
19
20
  # Runs ruff (check + format), pyright, and the Markdown + YAML doc-lint
20
21
  # (scripts/lint-docs.sh).
21
22
  name: lint
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.3.0"
3
+ }
@@ -29,5 +29,5 @@ rules:
29
29
  level: warning
30
30
  # Workflow/config files routinely omit the `---` start marker.
31
31
  document-start: disable
32
- # Probot Settings uses comments aligned under keys; don't fight that.
32
+ # Some config files align comments under keys; don't fight that.
33
33
  comments-indentation: disable
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.0](https://github.com/schubydoo/clauster/compare/v0.2.2...v0.3.0) (2026-06-03)
4
+
5
+
6
+ ### Features
7
+
8
+ * **docker:** add a Docker Compose quickstart ([#97](https://github.com/schubydoo/clauster/issues/97)) ([e6c914d](https://github.com/schubydoo/clauster/commit/e6c914d22e648ede2dd3c0285a717fdae5f1bef2))
9
+ * **doctor:** check that the claude CLI is logged in ([#84](https://github.com/schubydoo/clauster/issues/84)) ([f902f23](https://github.com/schubydoo/clauster/commit/f902f23af6876f1cc95e450c0d4f1447c5e71cfe))
10
+
11
+
12
+ ### Bug Fixes
13
+
14
+ * **runner:** serialize concurrent spawns of the same project ([#91](https://github.com/schubydoo/clauster/issues/91)) ([2dc8eb0](https://github.com/schubydoo/clauster/commit/2dc8eb044aaed3e735627f2feedbce8281a6b298))
15
+ * **runner:** stop wiping persisted metadata for untracked projects ([#92](https://github.com/schubydoo/clauster/issues/92)) ([cca1c69](https://github.com/schubydoo/clauster/commit/cca1c69ec29a48c2d034d64cdf5a23e4dca1383a))
16
+ * show Restart for stopped pty bridges so true-resume is reachable ([#99](https://github.com/schubydoo/clauster/issues/99)) ([5ea38aa](https://github.com/schubydoo/clauster/commit/5ea38aaa0b2ed92039681be4babe32c7ba9ad465))
17
+
3
18
  ## [0.2.2](https://github.com/schubydoo/clauster/compare/v0.2.1...v0.2.2) (2026-06-03)
4
19
 
5
20
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clauster
3
- Version: 0.2.2
3
+ Version: 0.3.0
4
4
  Summary: Self-hosted web UI for spawning and managing Claude Code remote-control bridges on a remote host.
5
5
  Project-URL: Homepage, https://github.com/schubydoo/clauster
6
6
  Project-URL: Repository, https://github.com/schubydoo/clauster
@@ -59,7 +59,8 @@ Description-Content-Type: text/markdown
59
59
  </p>
60
60
 
61
61
  <p align="center">
62
- <img alt="Python 3.11+" src="https://img.shields.io/badge/python-3.11%2B-blue.svg">
62
+ <a href="https://pypi.org/project/clauster/"><img alt="PyPI" src="https://img.shields.io/pypi/v/clauster?logo=pypi&logoColor=white"></a>
63
+ <a href="https://pypi.org/project/clauster/"><img alt="Python versions" src="https://img.shields.io/pypi/pyversions/clauster"></a>
63
64
  <a href="https://github.com/schubydoo/clauster/blob/main/LICENSE"><img alt="License: Apache-2.0" src="https://img.shields.io/badge/license-Apache--2.0-blue.svg"></a>
64
65
  <a href="https://github.com/schubydoo/clauster/pkgs/container/clauster"><img alt="GHCR" src="https://img.shields.io/badge/ghcr.io-clauster-2496ED?logo=docker&logoColor=white"></a>
65
66
  <a href="https://github.com/astral-sh/ruff"><img alt="Ruff" src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json"></a>
@@ -121,7 +122,7 @@ it in [`clauster.yml.example`](https://github.com/schubydoo/clauster/blob/main/c
121
122
  permission mode per launch. `bypassPermissions` is double-gated: a per-project
122
123
  config ceiling (`projects.<name>.allow_bypass_permissions`) **and** a
123
124
  type-the-project-name confirm in the UI.
124
- - **Open in Claude** — a deep link to the primary session plus a scannable QR code
125
+ - **Open session in Claude** — a deep link to the primary session plus a scannable QR code
125
126
  that opens it in the Claude app (claude.ai/code or mobile), attached to the
126
127
  running bridge.
127
128
  - **External session surfacing** — sessions you started from a terminal or Desktop
@@ -183,6 +184,33 @@ uv run clauster
183
184
  Then open <http://127.0.0.1:7621>. `claude` must be on your `PATH` (Clauster spawns
184
185
  it; it isn't vendored).
185
186
 
187
+ ## First bridge in 60 seconds
188
+
189
+ With the server running (above) and `claude` on your `PATH`, spawning your first
190
+ bridge is a handful of clicks — no terminal needed once it's started:
191
+
192
+ 1. **Point Clauster at your code.** Set `projects_root` in `clauster.yml` to a
193
+ directory whose subfolders are projects (e.g. `~/code`); each child directory
194
+ becomes a card.
195
+ 2. **Sanity-check the host (optional).** `clauster doctor` confirms `claude` is found
196
+ and new enough and that `projects_root` / the state dir are usable — fix any ✗
197
+ before spawning.
198
+ 3. **Open the dashboard** at <http://127.0.0.1:7621>. You'll see one card per project.
199
+ 4. **Start a bridge.** On a project's card, click **Start**. Clauster launches
200
+ `claude remote-control` in that directory and the card flips to *Running* with a
201
+ live status badge. (Pick a spawn / permission mode first if you like — the
202
+ defaults are safe.)
203
+ 5. **Attach from anywhere.** Use the card's **Open session in Claude** link — or scan its
204
+ **QR code** — to pick the bridge up in `claude.ai/code` or the Claude mobile app.
205
+ No SSH session.
206
+ 6. **Stop or restart.** **Stop** signals the bridge; **Restart** relaunches it (with
207
+ `claude.resume_recap` or `resume_mode: pty` it can carry the prior conversation
208
+ forward — see [Opt-in extras](#opt-in-extras)).
209
+
210
+ > Exposing this beyond loopback (e.g. on your LAN)? Read
211
+ > [Auth & networking](#auth--networking) first — a non-loopback bind requires
212
+ > authentication.
213
+
186
214
  ## Docker
187
215
 
188
216
  Multi-arch images (`linux/amd64`, `linux/arm64`) are published to GHCR on each release.
@@ -223,6 +251,19 @@ docker run -d --name clauster \
223
251
  - Logs are JSON by default (`CLAUSTER_LOG_FORMAT`); health is at `/healthz`. Images
224
252
  are cosign-signed with build provenance + SBOM attestations.
225
253
 
254
+ ### Docker Compose
255
+
256
+ A ready-to-edit [`compose.yaml`](https://github.com/schubydoo/clauster/blob/main/compose.yaml) is included:
257
+
258
+ ```sh
259
+ # 1. generate a password hash (runs inside the image)
260
+ docker compose run --rm clauster clauster hash-password
261
+ # 2. export it single-quoted, then edit the projects/claude volumes in compose.yaml
262
+ export CLAUSTER_AUTH_PASSWORD_HASH='$argon2id$v=19$...'
263
+ # 3. start (the image's HEALTHCHECK is inherited)
264
+ docker compose up -d
265
+ ```
266
+
226
267
  ## Auth & networking
227
268
 
228
269
  Loopback (`127.0.0.1`) needs no auth. Binding to a non-loopback address is refused
@@ -16,7 +16,8 @@
16
16
  </p>
17
17
 
18
18
  <p align="center">
19
- <img alt="Python 3.11+" src="https://img.shields.io/badge/python-3.11%2B-blue.svg">
19
+ <a href="https://pypi.org/project/clauster/"><img alt="PyPI" src="https://img.shields.io/pypi/v/clauster?logo=pypi&logoColor=white"></a>
20
+ <a href="https://pypi.org/project/clauster/"><img alt="Python versions" src="https://img.shields.io/pypi/pyversions/clauster"></a>
20
21
  <a href="https://github.com/schubydoo/clauster/blob/main/LICENSE"><img alt="License: Apache-2.0" src="https://img.shields.io/badge/license-Apache--2.0-blue.svg"></a>
21
22
  <a href="https://github.com/schubydoo/clauster/pkgs/container/clauster"><img alt="GHCR" src="https://img.shields.io/badge/ghcr.io-clauster-2496ED?logo=docker&logoColor=white"></a>
22
23
  <a href="https://github.com/astral-sh/ruff"><img alt="Ruff" src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json"></a>
@@ -78,7 +79,7 @@ it in [`clauster.yml.example`](https://github.com/schubydoo/clauster/blob/main/c
78
79
  permission mode per launch. `bypassPermissions` is double-gated: a per-project
79
80
  config ceiling (`projects.<name>.allow_bypass_permissions`) **and** a
80
81
  type-the-project-name confirm in the UI.
81
- - **Open in Claude** — a deep link to the primary session plus a scannable QR code
82
+ - **Open session in Claude** — a deep link to the primary session plus a scannable QR code
82
83
  that opens it in the Claude app (claude.ai/code or mobile), attached to the
83
84
  running bridge.
84
85
  - **External session surfacing** — sessions you started from a terminal or Desktop
@@ -140,6 +141,33 @@ uv run clauster
140
141
  Then open <http://127.0.0.1:7621>. `claude` must be on your `PATH` (Clauster spawns
141
142
  it; it isn't vendored).
142
143
 
144
+ ## First bridge in 60 seconds
145
+
146
+ With the server running (above) and `claude` on your `PATH`, spawning your first
147
+ bridge is a handful of clicks — no terminal needed once it's started:
148
+
149
+ 1. **Point Clauster at your code.** Set `projects_root` in `clauster.yml` to a
150
+ directory whose subfolders are projects (e.g. `~/code`); each child directory
151
+ becomes a card.
152
+ 2. **Sanity-check the host (optional).** `clauster doctor` confirms `claude` is found
153
+ and new enough and that `projects_root` / the state dir are usable — fix any ✗
154
+ before spawning.
155
+ 3. **Open the dashboard** at <http://127.0.0.1:7621>. You'll see one card per project.
156
+ 4. **Start a bridge.** On a project's card, click **Start**. Clauster launches
157
+ `claude remote-control` in that directory and the card flips to *Running* with a
158
+ live status badge. (Pick a spawn / permission mode first if you like — the
159
+ defaults are safe.)
160
+ 5. **Attach from anywhere.** Use the card's **Open session in Claude** link — or scan its
161
+ **QR code** — to pick the bridge up in `claude.ai/code` or the Claude mobile app.
162
+ No SSH session.
163
+ 6. **Stop or restart.** **Stop** signals the bridge; **Restart** relaunches it (with
164
+ `claude.resume_recap` or `resume_mode: pty` it can carry the prior conversation
165
+ forward — see [Opt-in extras](#opt-in-extras)).
166
+
167
+ > Exposing this beyond loopback (e.g. on your LAN)? Read
168
+ > [Auth & networking](#auth--networking) first — a non-loopback bind requires
169
+ > authentication.
170
+
143
171
  ## Docker
144
172
 
145
173
  Multi-arch images (`linux/amd64`, `linux/arm64`) are published to GHCR on each release.
@@ -180,6 +208,19 @@ docker run -d --name clauster \
180
208
  - Logs are JSON by default (`CLAUSTER_LOG_FORMAT`); health is at `/healthz`. Images
181
209
  are cosign-signed with build provenance + SBOM attestations.
182
210
 
211
+ ### Docker Compose
212
+
213
+ A ready-to-edit [`compose.yaml`](https://github.com/schubydoo/clauster/blob/main/compose.yaml) is included:
214
+
215
+ ```sh
216
+ # 1. generate a password hash (runs inside the image)
217
+ docker compose run --rm clauster clauster hash-password
218
+ # 2. export it single-quoted, then edit the projects/claude volumes in compose.yaml
219
+ export CLAUSTER_AUTH_PASSWORD_HASH='$argon2id$v=19$...'
220
+ # 3. start (the image's HEALTHCHECK is inherited)
221
+ docker compose up -d
222
+ ```
223
+
183
224
  ## Auth & networking
184
225
 
185
226
  Loopback (`127.0.0.1`) needs no auth. Binding to a non-loopback address is refused
@@ -0,0 +1,38 @@
1
+ # Clauster — Docker Compose quickstart. See the README "Docker" section for the full rundown.
2
+ #
3
+ # Clauster binds 0.0.0.0 inside the container, so it REQUIRES enforced auth to start.
4
+ #
5
+ # 1. Generate a password hash (runs inside the image — nothing needed on the host):
6
+ # docker compose run --rm clauster clauster hash-password
7
+ # 2. Provide the printed `$argon2id$…` hash. Easiest + safest is to export it
8
+ # single-quoted (single quotes stop your shell expanding the `$`):
9
+ # export CLAUSTER_AUTH_PASSWORD_HASH='$argon2id$v=19$...'
10
+ # (Or put it in a .env file beside this one — but then double every `$` to `$$`
11
+ # so Compose doesn't try to interpolate it.)
12
+ # 3. Point the `projects` volume at your projects_root, and mount the `claude` CLI +
13
+ # credentials (Clauster spawns `claude remote-control`; the CLI is NOT in the image).
14
+ # 4. docker compose up -d
15
+
16
+ services:
17
+ clauster:
18
+ image: ghcr.io/schubydoo/clauster:latest
19
+ container_name: clauster
20
+ restart: unless-stopped
21
+ ports:
22
+ - "7621:7621"
23
+ environment:
24
+ # Non-loopback bind => enforced auth is mandatory (the container exits otherwise).
25
+ CLAUSTER_AUTH_ENABLED: "true"
26
+ CLAUSTER_AUTH_PASSWORD_REQUIRED: "true"
27
+ CLAUSTER_AUTH_PASSWORD_HASH: ${CLAUSTER_AUTH_PASSWORD_HASH:?run clauster hash-password and export it (see header)}
28
+ # Remap the runtime user to own the bind-mounts (match your host UID/GID).
29
+ PUID: "1000"
30
+ PGID: "1000"
31
+ volumes:
32
+ - ./config:/config # clauster.yml + state_dir
33
+ - ./projects:/projects # the projects_root to manage (point at your repos)
34
+ # `claude` is NOT baked into the image. Mount it onto PATH + mount the runtime
35
+ # user's ~/.claude credentials (or build a derived image that installs claude):
36
+ # - /usr/local/bin/claude:/usr/local/bin/claude:ro
37
+ # - ~/.claude:/config/.claude
38
+ # The image already defines a HEALTHCHECK against /healthz — Compose inherits it.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "clauster"
3
- version = "0.2.2"
3
+ version = "0.3.0"
4
4
  description = "Self-hosted web UI for spawning and managing Claude Code remote-control bridges on a remote host."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -9,12 +9,16 @@
9
9
  "timezone": "Etc/UTC",
10
10
  "schedule": ["before 7am on monday"],
11
11
  "labels": ["dependencies"],
12
+ "description": "Auto-merge uses GitHub native auto-merge + squash (a real PR satisfying the main ruleset), NOT branch automerge — the ruleset requires a PR, so branch automerge would be blocked.",
13
+ "platformAutomerge": true,
14
+ "automergeStrategy": "squash",
12
15
  "vulnerabilityAlerts": {
13
16
  "enabled": true,
14
17
  "labels": ["security"]
15
18
  },
16
19
  "lockFileMaintenance": {
17
20
  "enabled": true,
21
+ "automerge": true,
18
22
  "schedule": ["before 7am on monday"]
19
23
  },
20
24
  "ignorePaths": [
@@ -47,11 +51,10 @@
47
51
  "labels": ["vendored-assets"]
48
52
  },
49
53
  {
50
- "description": "Auto-merge patch updates of dev tooling.",
54
+ "description": "Auto-merge patch + minor updates of dev/test tooling once CI passes (a failing check blocks the merge).",
51
55
  "matchPackageNames": ["ruff", "pyright", "pytest", "pytest-cov", "pytest-asyncio"],
52
- "matchUpdateTypes": ["patch"],
53
- "automerge": true,
54
- "automergeType": "branch"
56
+ "matchUpdateTypes": ["patch", "minor"],
57
+ "automerge": true
55
58
  },
56
59
  {
57
60
  "description": "Group all pytest-related updates.",
@@ -64,11 +67,18 @@
64
67
  "groupName": "ruff"
65
68
  },
66
69
  {
67
- "description": "Roll all non-major GitHub Actions updates into one weekly PR.",
70
+ "description": "Roll non-major GitHub Actions version updates into one weekly PR (reviewed).",
68
71
  "matchManagers": ["github-actions"],
69
- "matchUpdateTypes": ["minor", "patch", "digest"],
72
+ "matchUpdateTypes": ["minor", "patch"],
70
73
  "groupName": "github-actions (non-major)"
71
74
  },
75
+ {
76
+ "description": "Auto-merge GitHub Actions digest re-pins (same version, new SHA) once CI passes.",
77
+ "matchManagers": ["github-actions"],
78
+ "matchUpdateTypes": ["digest"],
79
+ "groupName": "github-actions digests",
80
+ "automerge": true
81
+ },
72
82
  {
73
83
  "description": "Group security-tooling actions — same security.yml scanners, useful to review together.",
74
84
  "matchManagers": ["github-actions"],
@@ -1,3 +1,3 @@
1
1
  """Clauster — self-hosted dispatcher for Claude Code remote-control bridges."""
2
2
 
3
- __version__ = "0.2.2" # x-release-please-version
3
+ __version__ = "0.3.0" # x-release-please-version
@@ -10,7 +10,7 @@ from __future__ import annotations
10
10
  import ipaddress
11
11
  import os
12
12
  from pathlib import Path
13
- from typing import Annotated, Literal
13
+ from typing import Literal
14
14
 
15
15
  import yaml
16
16
  from pydantic import BaseModel, Field, PrivateAttr, field_validator, model_validator
@@ -255,9 +255,6 @@ class ClausterConfig(BaseModel):
255
255
  return self
256
256
 
257
257
 
258
- ConfigPath = Annotated[Path, "resolved path the config was loaded from, or None"]
259
-
260
-
261
258
  def _candidate_paths(explicit: Path | None) -> list[Path]:
262
259
  if explicit is not None:
263
260
  return [explicit]