scar-cli 0.26.0__tar.gz → 0.27.1__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 (169) hide show
  1. scar_cli-0.27.1/.release-please-manifest.json +3 -0
  2. {scar_cli-0.26.0 → scar_cli-0.27.1}/CHANGELOG.md +14 -0
  3. {scar_cli-0.26.0 → scar_cli-0.27.1}/PKG-INFO +6 -1
  4. {scar_cli-0.26.0 → scar_cli-0.27.1}/README.md +5 -0
  5. {scar_cli-0.26.0 → scar_cli-0.27.1}/plugin/plugin.json +1 -1
  6. {scar_cli-0.26.0 → scar_cli-0.27.1}/pyproject.toml +1 -1
  7. {scar_cli-0.26.0 → scar_cli-0.27.1}/src/scar/cli.py +105 -6
  8. {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_docs.py +18 -0
  9. {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_json_contract.py +2 -1
  10. scar_cli-0.27.1/tests/test_review_queue.py +243 -0
  11. {scar_cli-0.26.0 → scar_cli-0.27.1}/uv.lock +1 -1
  12. {scar_cli-0.26.0 → scar_cli-0.27.1}/website/docs/agents.md +65 -0
  13. scar_cli-0.26.0/.release-please-manifest.json +0 -3
  14. {scar_cli-0.26.0 → scar_cli-0.27.1}/.claude-plugin/marketplace.json +0 -0
  15. {scar_cli-0.26.0 → scar_cli-0.27.1}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  16. {scar_cli-0.26.0 → scar_cli-0.27.1}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  17. {scar_cli-0.26.0 → scar_cli-0.27.1}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  18. {scar_cli-0.26.0 → scar_cli-0.27.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  19. {scar_cli-0.26.0 → scar_cli-0.27.1}/.github/workflows/ci.yml +0 -0
  20. {scar_cli-0.26.0 → scar_cli-0.27.1}/.github/workflows/docs.yml +0 -0
  21. {scar_cli-0.26.0 → scar_cli-0.27.1}/.github/workflows/pr-validation.yml +0 -0
  22. {scar_cli-0.26.0 → scar_cli-0.27.1}/.github/workflows/release.yml +0 -0
  23. {scar_cli-0.26.0 → scar_cli-0.27.1}/.gitignore +0 -0
  24. {scar_cli-0.26.0 → scar_cli-0.27.1}/.pre-commit-config.yaml +0 -0
  25. {scar_cli-0.26.0 → scar_cli-0.27.1}/.pre-commit-hooks.yaml +0 -0
  26. {scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/0001-git-grep-ere-pitfalls.landmine.md +0 -0
  27. {scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/0002-agent-direct-hook-install.deadend.md +0 -0
  28. {scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/0003-installer-binds-to-active-venv-scar.landmine.md +0 -0
  29. {scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/0004-promote-roundtrip-drops-expires-evidence.landmine.md +0 -0
  30. {scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/0005-history-rewrite-orphans-commit-evidence.landmine.md +0 -0
  31. {scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/0006-yaml-pattern-anchor-over-escaping.landmine.md +0 -0
  32. {scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/0007-release-please-config-change-skips-open-pr.landmine.md +0 -0
  33. {scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/0008-rich-output-nontty-must-bypass-rich.landmine.md +0 -0
  34. {scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/0009-anchor-survival-harness-measures-symbol-not-path-anchors.deadend.md +0 -0
  35. {scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/0010-git-returncode-128-vs-empty-tracked-set.landmine.md +0 -0
  36. {scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/0011-lint-compile-check-lets-redos-anchors-hang-hotpath.landmine.md +0 -0
  37. {scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/0012-firing-log-readers-need-blanket-guards.landmine.md +0 -0
  38. {scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/0013-docusaurus-future-v4-requires-faster-dep.landmine.md +0 -0
  39. {scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/0014-inject-flag-dest-collides-with-subcommand-dispatch.landmine.md +0 -0
  40. {scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/0015-zero-checkruns-means-approval-gate-not-workflow-bug.deadend.md +0 -0
  41. {scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/0016-hook-ownership-must-be-kind-scoped.landmine.md +0 -0
  42. {scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/0017-codex-pretool-context-can-replay-apply-patch.landmine.md +0 -0
  43. {scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/0018-cli-subparsers-register-through-_add-wrapper.landmine.md +0 -0
  44. {scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/0019-token-in-scar-body-cannot-prove-hook-delivery.deadend.md +0 -0
  45. {scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/0020-one-directional-health-check-leaves-flattering-direction-unwatched.landmine.md +0 -0
  46. {scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/0021-posttool-fix-in-hooks-does-not-reach-codex-or-cascade.landmine.md +0 -0
  47. {scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/0022-codex-hook-delivery-needs-user-file-and-trust.landmine.md +0 -0
  48. {scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/0023-cascade-block-exit-collides-with-argparse.fence.md +0 -0
  49. {scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/0024-host-detection-must-not-read-the-process-home.deadend.md +0 -0
  50. {scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/0025-one-plugin-hook-manifest-two-hosts-two-root-variables.landmine.md +0 -0
  51. {scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/0026-path-anchor-breadth-drives-the-review-due-threshold.landmine.md +0 -0
  52. {scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/README.md +0 -0
  53. {scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/candidates/violation-evidence-must-not-persist-raw-edits.md +0 -0
  54. {scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/template.md +0 -0
  55. {scar_cli-0.26.0 → scar_cli-0.27.1}/AGENTS.md +0 -0
  56. {scar_cli-0.26.0 → scar_cli-0.27.1}/CONTRIBUTING.md +0 -0
  57. {scar_cli-0.26.0 → scar_cli-0.27.1}/IDEA.md +0 -0
  58. {scar_cli-0.26.0 → scar_cli-0.27.1}/LICENSE +0 -0
  59. {scar_cli-0.26.0 → scar_cli-0.27.1}/ROADMAP.md +0 -0
  60. {scar_cli-0.26.0 → scar_cli-0.27.1}/SCAR-FORMAT.md +0 -0
  61. {scar_cli-0.26.0 → scar_cli-0.27.1}/SPEC.md +0 -0
  62. {scar_cli-0.26.0 → scar_cli-0.27.1}/STRESS-TEST.md +0 -0
  63. {scar_cli-0.26.0 → scar_cli-0.27.1}/action.yml +0 -0
  64. {scar_cli-0.26.0 → scar_cli-0.27.1}/codecov.yml +0 -0
  65. {scar_cli-0.26.0 → scar_cli-0.27.1}/experiments/anchor-survival/PROTOCOL.md +0 -0
  66. {scar_cli-0.26.0 → scar_cli-0.27.1}/experiments/anchor-survival/RESULTS.md +0 -0
  67. {scar_cli-0.26.0 → scar_cli-0.27.1}/experiments/anchor-survival/long_replay.py +0 -0
  68. {scar_cli-0.26.0 → scar_cli-0.27.1}/experiments/anchor-survival/replay.py +0 -0
  69. {scar_cli-0.26.0 → scar_cli-0.27.1}/experiments/anchor-survival/replay_shipped.py +0 -0
  70. {scar_cli-0.26.0 → scar_cli-0.27.1}/experiments/anchor-survival/test_replay_shipped.py +0 -0
  71. {scar_cli-0.26.0 → scar_cli-0.27.1}/experiments/auto-authorship/FINDINGS.md +0 -0
  72. {scar_cli-0.26.0 → scar_cli-0.27.1}/experiments/auto-authorship/PROTOCOL.md +0 -0
  73. {scar_cli-0.26.0 → scar_cli-0.27.1}/experiments/fence-honor/.gitignore +0 -0
  74. {scar_cli-0.26.0 → scar_cli-0.27.1}/experiments/fence-honor/PROTOCOL.md +0 -0
  75. {scar_cli-0.26.0 → scar_cli-0.27.1}/experiments/fence-honor/RESULTS.md +0 -0
  76. {scar_cli-0.26.0 → scar_cli-0.27.1}/experiments/fence-honor/fixture/.scars/0001-vendor-retry-window.fence.md +0 -0
  77. {scar_cli-0.26.0 → scar_cli-0.27.1}/experiments/fence-honor/fixture/.scars/0002-evicting-session-store.deadend.md +0 -0
  78. {scar_cli-0.26.0 → scar_cli-0.27.1}/experiments/fence-honor/fixture/.scars/0003-export-column-order.landmine.md +0 -0
  79. {scar_cli-0.26.0 → scar_cli-0.27.1}/experiments/fence-honor/fixture/README.md +0 -0
  80. {scar_cli-0.26.0 → scar_cli-0.27.1}/experiments/fence-honor/fixture/payments/retry.py +0 -0
  81. {scar_cli-0.26.0 → scar_cli-0.27.1}/experiments/fence-honor/fixture/reports/export.py +0 -0
  82. {scar_cli-0.26.0 → scar_cli-0.27.1}/experiments/fence-honor/fixture/services/sessions.py +0 -0
  83. {scar_cli-0.26.0 → scar_cli-0.27.1}/experiments/fence-honor/grade.py +0 -0
  84. {scar_cli-0.26.0 → scar_cli-0.27.1}/experiments/harvest/PROTOCOL.md +0 -0
  85. {scar_cli-0.26.0 → scar_cli-0.27.1}/experiments/harvest/harvest.py +0 -0
  86. {scar_cli-0.26.0 → scar_cli-0.27.1}/hook/scar-hooks.py +0 -0
  87. {scar_cli-0.26.0 → scar_cli-0.27.1}/plugin/hooks/run.sh +0 -0
  88. {scar_cli-0.26.0 → scar_cli-0.27.1}/plugin/skills/scar-authoring/SKILL.md +0 -0
  89. {scar_cli-0.26.0 → scar_cli-0.27.1}/plugin/skills/scar-authoring/assets/template.md +0 -0
  90. {scar_cli-0.26.0 → scar_cli-0.27.1}/release-please-config.json +0 -0
  91. {scar_cli-0.26.0 → scar_cli-0.27.1}/src/scar/__init__.py +0 -0
  92. {scar_cli-0.26.0 → scar_cli-0.27.1}/src/scar/agent.py +0 -0
  93. {scar_cli-0.26.0 → scar_cli-0.27.1}/src/scar/cascade.py +0 -0
  94. {scar_cli-0.26.0 → scar_cli-0.27.1}/src/scar/codex.py +0 -0
  95. {scar_cli-0.26.0 → scar_cli-0.27.1}/src/scar/draftcheck.py +0 -0
  96. {scar_cli-0.26.0 → scar_cli-0.27.1}/src/scar/evidence.py +0 -0
  97. {scar_cli-0.26.0 → scar_cli-0.27.1}/src/scar/firing_log.py +0 -0
  98. {scar_cli-0.26.0 → scar_cli-0.27.1}/src/scar/gc.py +0 -0
  99. {scar_cli-0.26.0 → scar_cli-0.27.1}/src/scar/harvest.py +0 -0
  100. {scar_cli-0.26.0 → scar_cli-0.27.1}/src/scar/hooks.py +0 -0
  101. {scar_cli-0.26.0 → scar_cli-0.27.1}/src/scar/hosts.py +0 -0
  102. {scar_cli-0.26.0 → scar_cli-0.27.1}/src/scar/installer.py +0 -0
  103. {scar_cli-0.26.0 → scar_cli-0.27.1}/src/scar/lint.py +0 -0
  104. {scar_cli-0.26.0 → scar_cli-0.27.1}/src/scar/match.py +0 -0
  105. {scar_cli-0.26.0 → scar_cli-0.27.1}/src/scar/mcp.py +0 -0
  106. {scar_cli-0.26.0 → scar_cli-0.27.1}/src/scar/model.py +0 -0
  107. {scar_cli-0.26.0 → scar_cli-0.27.1}/src/scar/orphan.py +0 -0
  108. {scar_cli-0.26.0 → scar_cli-0.27.1}/src/scar/output.py +0 -0
  109. {scar_cli-0.26.0 → scar_cli-0.27.1}/src/scar/reanchor.py +0 -0
  110. {scar_cli-0.26.0 → scar_cli-0.27.1}/src/scar/renames.py +0 -0
  111. {scar_cli-0.26.0 → scar_cli-0.27.1}/src/scar/render.py +0 -0
  112. {scar_cli-0.26.0 → scar_cli-0.27.1}/src/scar/review.py +0 -0
  113. {scar_cli-0.26.0 → scar_cli-0.27.1}/src/scar/skills/scar-authoring/SKILL.md +0 -0
  114. {scar_cli-0.26.0 → scar_cli-0.27.1}/src/scar/skills/scar-authoring/assets/template.md +0 -0
  115. {scar_cli-0.26.0 → scar_cli-0.27.1}/src/scar/store.py +0 -0
  116. {scar_cli-0.26.0 → scar_cli-0.27.1}/src/scar/symbols.py +0 -0
  117. {scar_cli-0.26.0 → scar_cli-0.27.1}/src/scar/violation_evidence.py +0 -0
  118. {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/conftest.py +0 -0
  119. {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_action_yml.py +0 -0
  120. {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_anchor_kind.py +0 -0
  121. {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_block_capability.py +0 -0
  122. {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_cascade.py +0 -0
  123. {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_cli.py +0 -0
  124. {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_codex.py +0 -0
  125. {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_context_size.py +0 -0
  126. {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_draftcheck.py +0 -0
  127. {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_evidence.py +0 -0
  128. {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_firing_log.py +0 -0
  129. {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_gc.py +0 -0
  130. {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_git_hook.py +0 -0
  131. {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_harvest.py +0 -0
  132. {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_hooks.py +0 -0
  133. {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_hosts.py +0 -0
  134. {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_installer.py +0 -0
  135. {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_lifecycle.py +0 -0
  136. {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_lint.py +0 -0
  137. {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_match.py +0 -0
  138. {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_matched_by.py +0 -0
  139. {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_mcp.py +0 -0
  140. {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_model.py +0 -0
  141. {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_orphan.py +0 -0
  142. {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_output.py +0 -0
  143. {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_plugin.py +0 -0
  144. {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_precommit_hooks.py +0 -0
  145. {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_reanchor.py +0 -0
  146. {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_renames.py +0 -0
  147. {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_render.py +0 -0
  148. {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_review.py +0 -0
  149. {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_skill.py +0 -0
  150. {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_store.py +0 -0
  151. {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_sweep.py +0 -0
  152. {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_symbols.py +0 -0
  153. {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_verdict_observability.py +0 -0
  154. {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_violation_evidence.py +0 -0
  155. {scar_cli-0.26.0 → scar_cli-0.27.1}/website/.gitignore +0 -0
  156. {scar_cli-0.26.0 → scar_cli-0.27.1}/website/docs/concepts.md +0 -0
  157. {scar_cli-0.26.0 → scar_cli-0.27.1}/website/docs/index.md +0 -0
  158. {scar_cli-0.26.0 → scar_cli-0.27.1}/website/docs/methodology.md +0 -0
  159. {scar_cli-0.26.0 → scar_cli-0.27.1}/website/docs/quickstart.md +0 -0
  160. {scar_cli-0.26.0 → scar_cli-0.27.1}/website/docusaurus.config.ts +0 -0
  161. {scar_cli-0.26.0 → scar_cli-0.27.1}/website/package-lock.json +0 -0
  162. {scar_cli-0.26.0 → scar_cli-0.27.1}/website/package.json +0 -0
  163. {scar_cli-0.26.0 → scar_cli-0.27.1}/website/scripts/sync-changelog.mjs +0 -0
  164. {scar_cli-0.26.0 → scar_cli-0.27.1}/website/sidebars.ts +0 -0
  165. {scar_cli-0.26.0 → scar_cli-0.27.1}/website/src/css/custom.css +0 -0
  166. {scar_cli-0.26.0 → scar_cli-0.27.1}/website/static/img/favicon.svg +0 -0
  167. {scar_cli-0.26.0 → scar_cli-0.27.1}/website/static/img/social-card.png +0 -0
  168. {scar_cli-0.26.0 → scar_cli-0.27.1}/website/static/llms.txt +0 -0
  169. {scar_cli-0.26.0 → scar_cli-0.27.1}/website/tsconfig.json +0 -0
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.27.1"
3
+ }
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.27.1](https://github.com/Daily-Nerd/Scar/compare/v0.27.0...v0.27.1) (2026-11-07)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **lint:** list scars with lint errors in the review queue and sync before failing ([#360](https://github.com/Daily-Nerd/Scar/issues/360)) ([cc23232](https://github.com/Daily-Nerd/Scar/commit/cc23232194e1e1b7daa319a5b73a35e4eba8c801))
9
+
10
+ ## [0.27.0](https://github.com/Daily-Nerd/Scar/compare/v0.26.0...v0.27.0) (2026-11-06)
11
+
12
+
13
+ ### Features
14
+
15
+ * **lint:** rolling review queue from scar lint (--review-queue) ([#357](https://github.com/Daily-Nerd/Scar/issues/357)) ([8136513](https://github.com/Daily-Nerd/Scar/commit/8136513ef1c7cbba2ac2d7e1b4f0b68baf6544c0))
16
+
3
17
  ## [0.26.0](https://github.com/Daily-Nerd/Scar/compare/v0.25.1...v0.26.0) (2026-11-06)
4
18
 
5
19
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: scar-cli
3
- Version: 0.26.0
3
+ Version: 0.27.1
4
4
  Summary: SCAR — version control for negative knowledge (deadends, fences, landmines)
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -126,6 +126,11 @@ CI without pre-commit:
126
126
  `@main` always tracks the latest commit; releases are tagged `vX.Y.Z` via
127
127
  release-please, so pin a tag for reproducible CI.
128
128
 
129
+ `scar lint --review-queue` prints a markdown checklist of scars that need a human
130
+ (orphans, partial rot, symbol drift, unreachable evidence, passed `review_after`),
131
+ empty when there are none. The docs have a scheduled-workflow recipe that keeps
132
+ one rolling issue in sync with it: see [CI](https://daily-nerd.github.io/Scar/docs/agents#review-queue).
133
+
129
134
  ## Quality discipline
130
135
 
131
136
  - **Candidates vs active:** agents and `scar harvest` only ever write to `.scars/candidates/`. A human promotes (`scar promote`) — nothing enters active enforcement without review.
@@ -111,6 +111,11 @@ CI without pre-commit:
111
111
  `@main` always tracks the latest commit; releases are tagged `vX.Y.Z` via
112
112
  release-please, so pin a tag for reproducible CI.
113
113
 
114
+ `scar lint --review-queue` prints a markdown checklist of scars that need a human
115
+ (orphans, partial rot, symbol drift, unreachable evidence, passed `review_after`),
116
+ empty when there are none. The docs have a scheduled-workflow recipe that keeps
117
+ one rolling issue in sync with it: see [CI](https://daily-nerd.github.io/Scar/docs/agents#review-queue).
118
+
114
119
  ## Quality discipline
115
120
 
116
121
  - **Candidates vs active:** agents and `scar harvest` only ever write to `.scars/candidates/`. A human promotes (`scar promote`) — nothing enters active enforcement without review.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scar",
3
- "version": "0.26.0",
3
+ "version": "0.27.1",
4
4
  "description": "SCAR — version control for negative knowledge (deadends, fences, landmines)",
5
5
  "hooks": {
6
6
  "PreToolUse": [
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "scar-cli"
3
- version = "0.26.0"
3
+ version = "0.27.1"
4
4
  description = "SCAR — version control for negative knowledge (deadends, fences, landmines)"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -546,6 +546,94 @@ def _firing_reviews(store):
546
546
  return []
547
547
 
548
548
 
549
+ def _review_due(store) -> list[Scar]:
550
+ """Firing scars whose review_after date has passed. The one definition
551
+ shared by `status` and `lint` so the two JSON surfaces cannot disagree."""
552
+ today = time.strftime("%Y-%m-%d")
553
+ return [s for _f, s in store.firing() if s.review_after and s.review_after < today]
554
+
555
+
556
+ def _review_due_json(due: list[Scar]) -> list[dict]:
557
+ return [{"type": s.type, "id": s.id, "review_after": s.review_after} for s in due]
558
+
559
+
560
+ def _review_queue_markdown(store, data: dict) -> str:
561
+ """Markdown checklist of scars a human should look at, built from the SAME
562
+ dict `lint --json` emits (#354). One row per scar, states merged. Only
563
+ CI-usable signals: the firing log is machine-local and empty on a fresh
564
+ checkout, so firing_review and never_fired are deliberately absent.
565
+
566
+ Only the id, title, a path and fixed state wording are printed. The
567
+ `reason` strings in `data` embed anchor text and violation hints, so they
568
+ are never used here, and the body and evidence are never read."""
569
+ by_id: dict[int, tuple[Path, Scar]] = {}
570
+ for f, s in store.parsed():
571
+ if s.id is not None:
572
+ by_id.setdefault(s.id, (f, s))
573
+
574
+ states: dict[int, list[str]] = {}
575
+ cmds: dict[int, list[str]] = {}
576
+
577
+ def add(scar_id: int, state: str, cmd: str) -> None:
578
+ if state not in states.setdefault(scar_id, []):
579
+ states[scar_id].append(state)
580
+ if cmd not in cmds.setdefault(scar_id, []):
581
+ cmds[scar_id].append(cmd)
582
+
583
+ for o in data["orphans"]:
584
+ add(o["scar_id"], "orphan (all anchors dead)", "`scar reanchor`")
585
+ for pr in data["partial_rot"]:
586
+ add(pr["scar_id"], "partial rot (dead anchor among live ones)", "`scar reanchor`")
587
+ for d in data["symbol_drift"]:
588
+ add(d["scar_id"], "symbol drift", "`scar why <path>`")
589
+ for ue in data["unreachable_evidence"]:
590
+ add(ue["scar_id"], "evidence commit unreachable", "`scar why <path>`")
591
+ for r in data["revivals"]:
592
+ add(r["scar_id"], "archived but revives_if matches again", "`scar why <path>`")
593
+ for rd in data["review_due"]:
594
+ add(rd["id"], f"review_after {rd['review_after']} passed",
595
+ f"`scar archive {rd['id']} --reason \"...\"` or bump review_after")
596
+
597
+ if not states and not data["failed_files"]:
598
+ return ""
599
+ lines = []
600
+ # Files with lint errors (unparseable, or parseable but failing an error
601
+ # rule such as duplicate ids) come first and are keyed by relative path (a
602
+ # broken file may have no usable id or title). Only
603
+ # the path and fixed wording are printed, never lint messages or the body.
604
+ for rel in sorted(data["failed_files"]):
605
+ lines.append(f"- [ ] `{rel}`: file has lint errors. "
606
+ "Suggested: run `scar lint` locally and fix them")
607
+ for scar_id in sorted(states):
608
+ found = by_id.get(scar_id)
609
+ if found:
610
+ f, s = found
611
+ where = s.path_anchors[0] if s.path_anchors else f.name
612
+ title = s.title
613
+ else:
614
+ where, title = "unknown file", "(scar not found)"
615
+ cmd = "; ".join(cmds[scar_id]).replace("<path>", where)
616
+ lines.append(f"- [ ] #{scar_id} {title} ({where}): "
617
+ f"{'; '.join(states[scar_id])}. Suggested: {cmd}")
618
+ tail = ["", "Firing-log signals (firing review, never fired) are not included: "
619
+ "the firing log is machine-local and empty in CI."]
620
+ # GitHub rejects issue bodies over 65536 characters. Scar owns the cap so
621
+ # no consumer has to: drop trailing rows and say how many were omitted.
622
+ def omit_line(n: int) -> list[str]:
623
+ return [f"... {n} more row(s) omitted to fit GitHub's issue body limit. "
624
+ "Run `scar lint` locally for the full list."] if n else []
625
+
626
+ kept = len(lines)
627
+ while kept and len("\n".join(
628
+ lines[:kept] + omit_line(len(lines) - kept) + tail)) + 1 > _QUEUE_BODY_LIMIT:
629
+ kept -= 1
630
+ return "\n".join(lines[:kept] + omit_line(len(lines) - kept) + tail) + "\n"
631
+
632
+
633
+ # Under GitHub's 65536-character issue body cap, with headroom.
634
+ _QUEUE_BODY_LIMIT = 65000
635
+
636
+
549
637
  def _cmd_lint(args) -> int:
550
638
  store = _require_store()
551
639
  if store is None:
@@ -733,6 +821,7 @@ def _cmd_lint(args) -> int:
733
821
  # revised. Advisory like every other lifecycle signal here — only the
734
822
  # opt-in --fail-firing-review turns it into an exit code.
735
823
  reviews = _firing_reviews(store)
824
+ review_due = _review_due(store)
736
825
 
737
826
  # evidence reachability (#43, scar #5): commit-SHA receipts that no longer
738
827
  # resolve from HEAD. None = shallow clone, reachability indeterminate → skip.
@@ -762,6 +851,8 @@ def _cmd_lint(args) -> int:
762
851
  "unreachable_evidence": [{"scar_id": ue.scar_id, "sha": ue.sha, "reason": ue.reason}
763
852
  for ue in unreachable],
764
853
  "failed": failed,
854
+ "failed_files": sorted(failed_files),
855
+ "review_due": _review_due_json(review_due),
765
856
  }
766
857
 
767
858
  def plain():
@@ -809,8 +900,13 @@ def _cmd_lint(args) -> int:
809
900
  f"{len(unreachable)} unreachable-evidence, "
810
901
  f"{len(reviews)} firing-review")
811
902
 
812
- output.render(data=data, json_flag=getattr(args, "json", False),
813
- tty=lambda: _lint_rich(data), plain=plain)
903
+ if getattr(args, "review_queue", False):
904
+ # Machine path: plain markdown, never rich, and empty stdout when the
905
+ # queue is empty so a workflow can close its issue on `-s queue.md`.
906
+ sys.stdout.write(_review_queue_markdown(store, data))
907
+ else:
908
+ output.render(data=data, json_flag=getattr(args, "json", False),
909
+ tty=lambda: _lint_rich(data), plain=plain)
814
910
 
815
911
  if failed:
816
912
  return 1
@@ -829,8 +925,7 @@ def _cmd_status(args) -> int:
829
925
  return 1
830
926
  active, broken, cands = store.active(), store.broken(), store.candidates()
831
927
  challenged = [(f, s) for f, s in store.parsed() if s.status == "challenged"]
832
- today = time.strftime("%Y-%m-%d")
833
- due = [s for _, s in store.firing() if s.review_after and s.review_after < today]
928
+ due = _review_due(store)
834
929
 
835
930
  # Orphans: detected (firing scars whose anchors all died — not yet persisted)
836
931
  # and persisted (already flipped to status: orphaned, invisible until now).
@@ -849,7 +944,7 @@ def _cmd_status(args) -> int:
849
944
  for _f, s in active],
850
945
  "challenged": [{"type": s.type, "id": s.id, "title": s.title} for _f, s in challenged],
851
946
  "candidates": [c.name for c in cands],
852
- "review_due": [{"type": s.type, "id": s.id, "review_after": s.review_after} for s in due],
947
+ "review_due": _review_due_json(due),
853
948
  # Count-based review trigger (#274), kept SEPARATE from review_due:
854
949
  # one is a date passing, the other is accumulated firings, and a
855
950
  # consumer that merges them cannot tell which obligation it is looking
@@ -3116,7 +3211,11 @@ def build_parser() -> argparse.ArgumentParser:
3116
3211
  p.add_argument("--fail-firing-review", action="store_true",
3117
3212
  help="exit non-zero when any scar has crossed its "
3118
3213
  "review_after_firings threshold")
3119
- p.add_argument("--json", action="store_true", help="emit machine-readable JSON")
3214
+ fmt = p.add_mutually_exclusive_group()
3215
+ fmt.add_argument("--json", action="store_true", help="emit machine-readable JSON")
3216
+ fmt.add_argument("--review-queue", action="store_true",
3217
+ help="print a markdown checklist of scars needing a human "
3218
+ "(CI-usable signals only); empty output when none")
3120
3219
  p = _add(sub, "status", _cmd_status, help="counts, titles, broken-file warnings")
3121
3220
  p.add_argument("--json", action="store_true", help="emit machine-readable JSON")
3122
3221
 
@@ -36,3 +36,21 @@ def test_agents_doc_documents_native_codex_hooks_and_trust():
36
36
  assert "Bash" in text and "apply_patch" in text
37
37
  assert "/hooks" in text
38
38
  assert "silently" in text
39
+
40
+
41
+ def test_agents_doc_documents_review_queue_recipe():
42
+ """#354: the rolling review issue is a documented recipe over
43
+ `scar lint --review-queue`, not a subcommand."""
44
+ text = (ROOT / "website" / "docs" / "agents.md").read_text(encoding="utf-8")
45
+ assert "### Review queue" in text
46
+ assert "scar lint --review-queue" in text
47
+ assert "scar-review-queue" in text
48
+ assert "issues: write" in text
49
+ assert "fetch-depth: 0" in text
50
+ # #359: the sync must not be skipped by lint's exit code, and failures of
51
+ # the gh calls must not be swallowed.
52
+ recipe = text.split("### Review queue")[1].split("## Machine-readable")[0]
53
+ assert "set +e" in recipe and "status=$?" in recipe
54
+ assert "2>/dev/null" not in recipe and "|| true" not in recipe
55
+ assert "lint errors" in recipe
56
+ assert "\u2014" not in text.split("### Review queue")[1].split("## Machine-readable")[0]
@@ -36,7 +36,8 @@ ROOT = Path(__file__).resolve().parent.parent
36
36
  GUARANTEED: dict[str, set[str]] = {
37
37
  "lint": {"files", "findings", "failed", "orphans", "partial_rot",
38
38
  "symbol_drift", "revivals", "reverse_hints",
39
- "unreachable_evidence", "shallow_clone", "symbols_unchecked"},
39
+ "unreachable_evidence", "shallow_clone", "symbols_unchecked",
40
+ "review_due"},
40
41
  "status": {"scars_dir", "active", "challenged", "candidates", "review_due",
41
42
  "orphan_detected", "orphaned", "partial_rot", "broken", "counts"},
42
43
  "check": {"paths", "scars"},
@@ -0,0 +1,243 @@
1
+ """Review queue (#354): lint --json carries review_due, and
2
+ `lint --review-queue` renders the CI-usable review signals as markdown."""
3
+ import json
4
+ import subprocess
5
+
6
+ from scar.cli import main
7
+ from scar.store import init_scars
8
+
9
+ SCAR = """\
10
+ ---
11
+ id: {id}
12
+ type: landmine
13
+ title: {title}
14
+ severity: medium
15
+ confidence: 0.8
16
+ created: 2026-06-10
17
+ authors: ["claude-code"]
18
+ anchors:
19
+ {anchors}
20
+ evidence:
21
+ - commit: abc1234
22
+ {extra}status: active
23
+ ---
24
+
25
+ {body}
26
+ """
27
+
28
+
29
+ def _write(root, name, *, id, title, anchors, extra="", body="Plain body."):
30
+ text = SCAR.format(id=id, title=title, anchors="\n".join(
31
+ f" - path: {a}" for a in anchors), extra=extra, body=body)
32
+ (root / ".scars" / name).write_text(text)
33
+
34
+
35
+ def _repo(tmp_path, monkeypatch):
36
+ subprocess.run(["git", "init", "-q"], cwd=tmp_path, check=True)
37
+ subprocess.run(["git", "config", "user.email", "t@t.t"], cwd=tmp_path, check=True)
38
+ subprocess.run(["git", "config", "user.name", "t"], cwd=tmp_path, check=True)
39
+ init_scars(tmp_path)
40
+ (tmp_path / "src" / "live").mkdir(parents=True)
41
+ (tmp_path / "src" / "live" / "mod.py").write_text("x = 1\n")
42
+ monkeypatch.chdir(tmp_path)
43
+
44
+
45
+ def _commit(root):
46
+ """Commit, then point every scar's evidence at that real commit so the
47
+ fixtures are not accidentally flagged as unreachable-evidence."""
48
+ subprocess.run(["git", "add", "-A"], cwd=root, check=True)
49
+ subprocess.run(["git", "commit", "-q", "-m", "init"], cwd=root, check=True)
50
+ sha = subprocess.run(["git", "rev-parse", "HEAD"], cwd=root, check=True,
51
+ capture_output=True, text=True).stdout.strip()
52
+ for f in (root / ".scars").glob("0*.md"):
53
+ f.write_text(f.read_text().replace("abc1234", sha))
54
+ subprocess.run(["git", "add", "-A"], cwd=root, check=True)
55
+ subprocess.run(["git", "commit", "-q", "-m", "evidence"], cwd=root, check=True)
56
+
57
+
58
+ def _due_and_rot(tmp_path, monkeypatch):
59
+ _repo(tmp_path, monkeypatch)
60
+ _write(tmp_path, "0001-due.landmine.md", id=1, title="Due scar",
61
+ anchors=["src/live/"], extra="review_after: 2020-01-01\n")
62
+ _write(tmp_path, "0002-rot.landmine.md", id=2, title="Rotted scar",
63
+ anchors=["src/live/", "src/gone/"], body="SECRET-BODY-TEXT")
64
+ _commit(tmp_path)
65
+
66
+
67
+ def test_lint_json_review_due_matches_status(tmp_path, monkeypatch, capsys):
68
+ _due_and_rot(tmp_path, monkeypatch)
69
+ main(["lint", "--json"])
70
+ lint_due = json.loads(capsys.readouterr().out)["review_due"]
71
+ main(["status", "--json"])
72
+ status_due = json.loads(capsys.readouterr().out)["review_due"]
73
+ assert lint_due == status_due
74
+ assert [d["id"] for d in lint_due] == [1]
75
+ assert lint_due[0]["review_after"] == "2020-01-01"
76
+
77
+
78
+ def test_lint_json_review_due_empty_list_when_none(tmp_path, monkeypatch, capsys):
79
+ _repo(tmp_path, monkeypatch)
80
+ main(["lint", "--json"])
81
+ assert json.loads(capsys.readouterr().out)["review_due"] == []
82
+
83
+
84
+ # --- lint --review-queue ---
85
+
86
+ def _queue(capsys, *extra):
87
+ rc = main(["lint", "--review-queue", *extra])
88
+ return rc, capsys.readouterr().out
89
+
90
+
91
+ def test_review_queue_empty_store_prints_nothing(tmp_path, monkeypatch, capsys):
92
+ _repo(tmp_path, monkeypatch)
93
+ rc, out = _queue(capsys)
94
+ assert rc == 0
95
+ assert out == ""
96
+
97
+
98
+ def test_review_queue_one_row_per_flagged_scar(tmp_path, monkeypatch, capsys):
99
+ _due_and_rot(tmp_path, monkeypatch)
100
+ rc, out = _queue(capsys)
101
+ assert rc == 0
102
+ rows = [ln for ln in out.splitlines() if ln.startswith("- [ ] ")]
103
+ assert len(rows) == 2
104
+ due = next(r for r in rows if "#1 " in r)
105
+ rot = next(r for r in rows if "#2 " in r)
106
+ assert "Due scar" in due and "src/live/" in due
107
+ assert "review_after 2020-01-01" in due
108
+ assert "Suggested:" in due and "scar archive 1" in due
109
+ assert "Rotted scar" in rot and "partial rot" in rot
110
+ assert "Suggested: `scar reanchor`" in rot
111
+
112
+
113
+ def test_review_queue_merges_states_into_one_row(tmp_path, monkeypatch, capsys):
114
+ _repo(tmp_path, monkeypatch)
115
+ _write(tmp_path, "0003-both.landmine.md", id=3, title="Both",
116
+ anchors=["src/live/", "src/gone/"], extra="review_after: 2020-01-01\n")
117
+ _commit(tmp_path)
118
+ _, out = _queue(capsys)
119
+ rows = [ln for ln in out.splitlines() if "#3 " in ln]
120
+ assert len(rows) == 1
121
+ assert "partial rot" in rows[0] and "review_after 2020-01-01" in rows[0]
122
+
123
+
124
+ def test_review_queue_never_prints_body_or_violation(tmp_path, monkeypatch, capsys):
125
+ _repo(tmp_path, monkeypatch)
126
+ _write(tmp_path, "0004-v.landmine.md", id=4, title="Violating",
127
+ anchors=["src/live/", "src/gone/"],
128
+ extra="violation: 'LEAKY_REGEX_\\d+'\n",
129
+ body="SECRET-BODY-TEXT")
130
+ _commit(tmp_path)
131
+ _, out = _queue(capsys)
132
+ assert "#4" in out
133
+ assert "SECRET-BODY-TEXT" not in out
134
+ assert "LEAKY_REGEX" not in out
135
+ assert "evidence" not in out.lower()
136
+
137
+
138
+ def test_review_queue_excludes_firing_log_signals(tmp_path, monkeypatch, capsys):
139
+ _repo(tmp_path, monkeypatch)
140
+ _write(tmp_path, "0005-n.landmine.md", id=5, title="Healthy",
141
+ anchors=["src/live/"], extra="review_after_firings: 1\n")
142
+ _commit(tmp_path)
143
+ _, out = _queue(capsys)
144
+ assert out == ""
145
+
146
+
147
+ def test_review_queue_notes_firing_log_exclusion_when_non_empty(tmp_path, monkeypatch, capsys):
148
+ _due_and_rot(tmp_path, monkeypatch)
149
+ _, out = _queue(capsys)
150
+ last = out.rstrip("\n").splitlines()[-1]
151
+ assert "firing" in last.lower()
152
+
153
+
154
+ def test_review_queue_plain_even_on_tty(tmp_path, monkeypatch, capsys):
155
+ from scar import output
156
+ _due_and_rot(tmp_path, monkeypatch)
157
+ monkeypatch.setattr(output, "is_tty", lambda: True)
158
+ _, out = _queue(capsys)
159
+ assert "\x1b[" not in out
160
+ assert "- [ ] #1 " in out
161
+
162
+
163
+ def test_review_queue_does_not_change_exit_codes(tmp_path, monkeypatch, capsys):
164
+ _due_and_rot(tmp_path, monkeypatch)
165
+ rc, _ = _queue(capsys, "--fail-partial-rot")
166
+ assert rc == 1
167
+ assert main(["lint", "--fail-partial-rot"]) == 1
168
+
169
+
170
+ def test_review_queue_has_no_em_dash(tmp_path, monkeypatch, capsys):
171
+ _due_and_rot(tmp_path, monkeypatch)
172
+ _, out = _queue(capsys)
173
+ assert "\u2014" not in out
174
+
175
+
176
+ def _broken(root, name="0009-broken.landmine.md", marker="BROKEN-BODY-TEXT"):
177
+ # No frontmatter terminator and no required fields: lint_text errors.
178
+ (root / ".scars" / name).write_text(f"---\nid: 9\ntitle: {marker}\n\n{marker}\n")
179
+
180
+
181
+ def test_review_queue_lists_broken_file_and_still_exits_1(tmp_path, monkeypatch, capsys):
182
+ """#359: a file with lint errors must be in the queue."""
183
+ _due_and_rot(tmp_path, monkeypatch)
184
+ _broken(tmp_path)
185
+ rc, out = _queue(capsys)
186
+ assert rc == 1
187
+ assert "- [ ] `.scars/0009-broken.landmine.md`: file has lint errors." in out
188
+ assert "scar lint" in out
189
+
190
+
191
+ def test_review_queue_only_broken_file_is_non_empty(tmp_path, monkeypatch, capsys):
192
+ _repo(tmp_path, monkeypatch)
193
+ _broken(tmp_path)
194
+ rc, out = _queue(capsys)
195
+ assert rc == 1
196
+ assert out.strip() != ""
197
+ assert len([r for r in out.splitlines() if r.startswith("- [ ]")]) == 1
198
+
199
+
200
+ def test_review_queue_broken_row_never_leaks_body_or_message(tmp_path, monkeypatch, capsys):
201
+ _repo(tmp_path, monkeypatch)
202
+ _broken(tmp_path)
203
+ data_rc = main(["lint", "--json"])
204
+ msgs = [f["message"] for f in json.loads(capsys.readouterr().out)["findings"]]
205
+ assert data_rc == 1 and msgs
206
+ _, out = _queue(capsys)
207
+ assert "BROKEN-BODY-TEXT" not in out
208
+ for m in msgs:
209
+ assert m not in out
210
+
211
+
212
+ def test_review_queue_broken_rows_are_sorted_by_path(tmp_path, monkeypatch, capsys):
213
+ _repo(tmp_path, monkeypatch)
214
+ _broken(tmp_path, "0012-b.landmine.md")
215
+ _broken(tmp_path, "0010-a.landmine.md")
216
+ _, out = _queue(capsys)
217
+ rows = [r for r in out.splitlines() if r.startswith("- [ ]")]
218
+ assert "0010-a" in rows[0] and "0012-b" in rows[1]
219
+
220
+
221
+ def test_review_queue_truncates_to_issue_body_limit(tmp_path, monkeypatch, capsys):
222
+ """GitHub rejects issue bodies over 65536 characters (#359)."""
223
+ _repo(tmp_path, monkeypatch)
224
+ for i in range(1200):
225
+ _broken(tmp_path, f"{3000 + i}-{'x' * 40}.landmine.md")
226
+ rc, out = _queue(capsys)
227
+ assert rc == 1
228
+ assert len(out) <= 65536
229
+ assert "more row(s) omitted" in out
230
+ assert out.count("- [ ]") < 1200
231
+
232
+
233
+ def test_review_queue_lists_duplicate_id_scars_that_parse(tmp_path, monkeypatch, capsys):
234
+ """Duplicate ids parse fine (and can fire) but are lint errors: both get rows."""
235
+ _repo(tmp_path, monkeypatch)
236
+ _write(tmp_path, "0001-a.landmine.md", id=1, title="A", anchors=["src/live/"])
237
+ _write(tmp_path, "0002-b.landmine.md", id=1, title="B", anchors=["src/live/"])
238
+ _commit(tmp_path)
239
+ rc, out = _queue(capsys)
240
+ assert rc == 1
241
+ assert "`.scars/0001-a.landmine.md`: file has lint errors." in out
242
+ assert "`.scars/0002-b.landmine.md`: file has lint errors." in out
243
+ assert "duplicate id" not in out
@@ -297,7 +297,7 @@ wheels = [
297
297
 
298
298
  [[package]]
299
299
  name = "scar-cli"
300
- version = "0.26.0"
300
+ version = "0.27.1"
301
301
  source = { editable = "." }
302
302
  dependencies = [
303
303
  { name = "rich" },
@@ -183,6 +183,71 @@ scar lint # format, dead tripwires, overdue
183
183
 
184
184
  Advisory by default everywhere; CI opts into strictness explicitly.
185
185
 
186
+ ### Review queue
187
+
188
+ Orphans, partial rot, symbol drift, unreachable evidence, revivals and passed `review_after` dates all end in a red job or a terminal warning that nobody owns. `scar lint --review-queue` turns them into one markdown checklist, one row per scar, and a scheduled workflow keeps a single rolling issue in sync with it:
189
+
190
+ ```bash
191
+ scar lint --review-queue
192
+ # - [ ] #7 One anchor alive, one rotted (src/live/): partial rot (dead anchor among live ones). Suggested: `scar reanchor`
193
+ ```
194
+
195
+ The output is plain text with no scar bodies, evidence or violation patterns: only id, title, path, state and a suggested command. When nothing needs review it prints nothing and exits 0. Scar files with lint errors are listed too, by path, with no lint message text. Exit codes are unchanged: lint still exits 1 when any file has an error, and `--fail-orphans` and friends still apply. The output is capped below GitHub's issue body limit, with a final line saying how many rows were omitted. The firing-log signals (firing review, never fired) are left out on purpose: that log lives on each developer's machine and is empty on a CI checkout.
196
+
197
+ ```yaml
198
+ # .github/workflows/scar-review-queue.yml
199
+ name: Scar review queue
200
+ on:
201
+ schedule:
202
+ - cron: '0 7 * * 1'
203
+ workflow_dispatch:
204
+
205
+ permissions:
206
+ contents: read
207
+ issues: write
208
+
209
+ jobs:
210
+ queue:
211
+ runs-on: ubuntu-latest
212
+ steps:
213
+ - uses: actions/checkout@v4
214
+ with:
215
+ fetch-depth: 0 # evidence-reachability checks need full history
216
+ - uses: astral-sh/setup-uv@v5
217
+ - name: Build the queue
218
+ # Keep lint's exit code, but let the next step run first: each run
219
+ # step is bash -e, so a bare failure would skip the issue sync.
220
+ run: |
221
+ set +e
222
+ uvx --from scar-cli scar lint --review-queue > queue.md
223
+ echo "status=$?" >> "$GITHUB_ENV"
224
+ exit 0
225
+ - name: Sync the rolling issue
226
+ env:
227
+ GH_TOKEN: ${{ github.token }}
228
+ run: |
229
+ label=scar-review-queue
230
+ gh label create "$label" --color BFD4F2 --description "Rolling Scar review queue" --force
231
+ issue=$(gh issue list --label "$label" --state all --limit 1 --json number,state)
232
+ number=$(jq -r '.[0].number // empty' <<<"$issue")
233
+ state=$(jq -r '.[0].state // empty' <<<"$issue")
234
+ if [ -s queue.md ]; then
235
+ if [ -z "$number" ]; then
236
+ gh issue create --title "Scar review queue" --label "$label" --body-file queue.md
237
+ else
238
+ gh issue edit "$number" --body-file queue.md
239
+ if [ "$state" = "CLOSED" ]; then gh issue reopen "$number"; fi
240
+ fi
241
+ elif [ -n "$number" ] && [ "$state" = "OPEN" ]; then
242
+ gh issue close "$number" --comment "Review queue is empty."
243
+ fi
244
+ - name: Fail the job if lint failed
245
+ if: env.status != '0'
246
+ run: exit 1
247
+ ```
248
+
249
+ The queue step records lint's exit code instead of failing, the issue is synced, and the last step fails the job when lint did, so CI stays red and the issue stays current. The issue is rewritten from scar's output on every run, never duplicated, closed when the queue is empty and reopened when it is not. Its visibility follows the repository's: on a public repo the titles and file paths in the checklist are public too.
250
+
186
251
  ## Machine-readable docs
187
252
 
188
253
  Agents reading these docs programmatically: fetch [`/llms.txt`](pathname:///Scar/llms.txt) for a plain-text index of every page with stable URLs, plus the changelog.
@@ -1,3 +0,0 @@
1
- {
2
- ".": "0.26.0"
3
- }
File without changes