scar-cli 0.26.0__tar.gz → 0.27.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 (169) hide show
  1. scar_cli-0.27.0/.release-please-manifest.json +3 -0
  2. {scar_cli-0.26.0 → scar_cli-0.27.0}/CHANGELOG.md +7 -0
  3. {scar_cli-0.26.0 → scar_cli-0.27.0}/PKG-INFO +6 -1
  4. {scar_cli-0.26.0 → scar_cli-0.27.0}/README.md +5 -0
  5. {scar_cli-0.26.0 → scar_cli-0.27.0}/plugin/plugin.json +1 -1
  6. {scar_cli-0.26.0 → scar_cli-0.27.0}/pyproject.toml +1 -1
  7. {scar_cli-0.26.0 → scar_cli-0.27.0}/src/scar/cli.py +84 -6
  8. {scar_cli-0.26.0 → scar_cli-0.27.0}/tests/test_docs.py +12 -0
  9. {scar_cli-0.26.0 → scar_cli-0.27.0}/tests/test_json_contract.py +2 -1
  10. scar_cli-0.27.0/tests/test_review_queue.py +173 -0
  11. {scar_cli-0.26.0 → scar_cli-0.27.0}/uv.lock +1 -1
  12. {scar_cli-0.26.0 → scar_cli-0.27.0}/website/docs/agents.md +54 -0
  13. scar_cli-0.26.0/.release-please-manifest.json +0 -3
  14. {scar_cli-0.26.0 → scar_cli-0.27.0}/.claude-plugin/marketplace.json +0 -0
  15. {scar_cli-0.26.0 → scar_cli-0.27.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  16. {scar_cli-0.26.0 → scar_cli-0.27.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  17. {scar_cli-0.26.0 → scar_cli-0.27.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  18. {scar_cli-0.26.0 → scar_cli-0.27.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  19. {scar_cli-0.26.0 → scar_cli-0.27.0}/.github/workflows/ci.yml +0 -0
  20. {scar_cli-0.26.0 → scar_cli-0.27.0}/.github/workflows/docs.yml +0 -0
  21. {scar_cli-0.26.0 → scar_cli-0.27.0}/.github/workflows/pr-validation.yml +0 -0
  22. {scar_cli-0.26.0 → scar_cli-0.27.0}/.github/workflows/release.yml +0 -0
  23. {scar_cli-0.26.0 → scar_cli-0.27.0}/.gitignore +0 -0
  24. {scar_cli-0.26.0 → scar_cli-0.27.0}/.pre-commit-config.yaml +0 -0
  25. {scar_cli-0.26.0 → scar_cli-0.27.0}/.pre-commit-hooks.yaml +0 -0
  26. {scar_cli-0.26.0 → scar_cli-0.27.0}/.scars/0001-git-grep-ere-pitfalls.landmine.md +0 -0
  27. {scar_cli-0.26.0 → scar_cli-0.27.0}/.scars/0002-agent-direct-hook-install.deadend.md +0 -0
  28. {scar_cli-0.26.0 → scar_cli-0.27.0}/.scars/0003-installer-binds-to-active-venv-scar.landmine.md +0 -0
  29. {scar_cli-0.26.0 → scar_cli-0.27.0}/.scars/0004-promote-roundtrip-drops-expires-evidence.landmine.md +0 -0
  30. {scar_cli-0.26.0 → scar_cli-0.27.0}/.scars/0005-history-rewrite-orphans-commit-evidence.landmine.md +0 -0
  31. {scar_cli-0.26.0 → scar_cli-0.27.0}/.scars/0006-yaml-pattern-anchor-over-escaping.landmine.md +0 -0
  32. {scar_cli-0.26.0 → scar_cli-0.27.0}/.scars/0007-release-please-config-change-skips-open-pr.landmine.md +0 -0
  33. {scar_cli-0.26.0 → scar_cli-0.27.0}/.scars/0008-rich-output-nontty-must-bypass-rich.landmine.md +0 -0
  34. {scar_cli-0.26.0 → scar_cli-0.27.0}/.scars/0009-anchor-survival-harness-measures-symbol-not-path-anchors.deadend.md +0 -0
  35. {scar_cli-0.26.0 → scar_cli-0.27.0}/.scars/0010-git-returncode-128-vs-empty-tracked-set.landmine.md +0 -0
  36. {scar_cli-0.26.0 → scar_cli-0.27.0}/.scars/0011-lint-compile-check-lets-redos-anchors-hang-hotpath.landmine.md +0 -0
  37. {scar_cli-0.26.0 → scar_cli-0.27.0}/.scars/0012-firing-log-readers-need-blanket-guards.landmine.md +0 -0
  38. {scar_cli-0.26.0 → scar_cli-0.27.0}/.scars/0013-docusaurus-future-v4-requires-faster-dep.landmine.md +0 -0
  39. {scar_cli-0.26.0 → scar_cli-0.27.0}/.scars/0014-inject-flag-dest-collides-with-subcommand-dispatch.landmine.md +0 -0
  40. {scar_cli-0.26.0 → scar_cli-0.27.0}/.scars/0015-zero-checkruns-means-approval-gate-not-workflow-bug.deadend.md +0 -0
  41. {scar_cli-0.26.0 → scar_cli-0.27.0}/.scars/0016-hook-ownership-must-be-kind-scoped.landmine.md +0 -0
  42. {scar_cli-0.26.0 → scar_cli-0.27.0}/.scars/0017-codex-pretool-context-can-replay-apply-patch.landmine.md +0 -0
  43. {scar_cli-0.26.0 → scar_cli-0.27.0}/.scars/0018-cli-subparsers-register-through-_add-wrapper.landmine.md +0 -0
  44. {scar_cli-0.26.0 → scar_cli-0.27.0}/.scars/0019-token-in-scar-body-cannot-prove-hook-delivery.deadend.md +0 -0
  45. {scar_cli-0.26.0 → scar_cli-0.27.0}/.scars/0020-one-directional-health-check-leaves-flattering-direction-unwatched.landmine.md +0 -0
  46. {scar_cli-0.26.0 → scar_cli-0.27.0}/.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.0}/.scars/0022-codex-hook-delivery-needs-user-file-and-trust.landmine.md +0 -0
  48. {scar_cli-0.26.0 → scar_cli-0.27.0}/.scars/0023-cascade-block-exit-collides-with-argparse.fence.md +0 -0
  49. {scar_cli-0.26.0 → scar_cli-0.27.0}/.scars/0024-host-detection-must-not-read-the-process-home.deadend.md +0 -0
  50. {scar_cli-0.26.0 → scar_cli-0.27.0}/.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.0}/.scars/0026-path-anchor-breadth-drives-the-review-due-threshold.landmine.md +0 -0
  52. {scar_cli-0.26.0 → scar_cli-0.27.0}/.scars/README.md +0 -0
  53. {scar_cli-0.26.0 → scar_cli-0.27.0}/.scars/candidates/violation-evidence-must-not-persist-raw-edits.md +0 -0
  54. {scar_cli-0.26.0 → scar_cli-0.27.0}/.scars/template.md +0 -0
  55. {scar_cli-0.26.0 → scar_cli-0.27.0}/AGENTS.md +0 -0
  56. {scar_cli-0.26.0 → scar_cli-0.27.0}/CONTRIBUTING.md +0 -0
  57. {scar_cli-0.26.0 → scar_cli-0.27.0}/IDEA.md +0 -0
  58. {scar_cli-0.26.0 → scar_cli-0.27.0}/LICENSE +0 -0
  59. {scar_cli-0.26.0 → scar_cli-0.27.0}/ROADMAP.md +0 -0
  60. {scar_cli-0.26.0 → scar_cli-0.27.0}/SCAR-FORMAT.md +0 -0
  61. {scar_cli-0.26.0 → scar_cli-0.27.0}/SPEC.md +0 -0
  62. {scar_cli-0.26.0 → scar_cli-0.27.0}/STRESS-TEST.md +0 -0
  63. {scar_cli-0.26.0 → scar_cli-0.27.0}/action.yml +0 -0
  64. {scar_cli-0.26.0 → scar_cli-0.27.0}/codecov.yml +0 -0
  65. {scar_cli-0.26.0 → scar_cli-0.27.0}/experiments/anchor-survival/PROTOCOL.md +0 -0
  66. {scar_cli-0.26.0 → scar_cli-0.27.0}/experiments/anchor-survival/RESULTS.md +0 -0
  67. {scar_cli-0.26.0 → scar_cli-0.27.0}/experiments/anchor-survival/long_replay.py +0 -0
  68. {scar_cli-0.26.0 → scar_cli-0.27.0}/experiments/anchor-survival/replay.py +0 -0
  69. {scar_cli-0.26.0 → scar_cli-0.27.0}/experiments/anchor-survival/replay_shipped.py +0 -0
  70. {scar_cli-0.26.0 → scar_cli-0.27.0}/experiments/anchor-survival/test_replay_shipped.py +0 -0
  71. {scar_cli-0.26.0 → scar_cli-0.27.0}/experiments/auto-authorship/FINDINGS.md +0 -0
  72. {scar_cli-0.26.0 → scar_cli-0.27.0}/experiments/auto-authorship/PROTOCOL.md +0 -0
  73. {scar_cli-0.26.0 → scar_cli-0.27.0}/experiments/fence-honor/.gitignore +0 -0
  74. {scar_cli-0.26.0 → scar_cli-0.27.0}/experiments/fence-honor/PROTOCOL.md +0 -0
  75. {scar_cli-0.26.0 → scar_cli-0.27.0}/experiments/fence-honor/RESULTS.md +0 -0
  76. {scar_cli-0.26.0 → scar_cli-0.27.0}/experiments/fence-honor/fixture/.scars/0001-vendor-retry-window.fence.md +0 -0
  77. {scar_cli-0.26.0 → scar_cli-0.27.0}/experiments/fence-honor/fixture/.scars/0002-evicting-session-store.deadend.md +0 -0
  78. {scar_cli-0.26.0 → scar_cli-0.27.0}/experiments/fence-honor/fixture/.scars/0003-export-column-order.landmine.md +0 -0
  79. {scar_cli-0.26.0 → scar_cli-0.27.0}/experiments/fence-honor/fixture/README.md +0 -0
  80. {scar_cli-0.26.0 → scar_cli-0.27.0}/experiments/fence-honor/fixture/payments/retry.py +0 -0
  81. {scar_cli-0.26.0 → scar_cli-0.27.0}/experiments/fence-honor/fixture/reports/export.py +0 -0
  82. {scar_cli-0.26.0 → scar_cli-0.27.0}/experiments/fence-honor/fixture/services/sessions.py +0 -0
  83. {scar_cli-0.26.0 → scar_cli-0.27.0}/experiments/fence-honor/grade.py +0 -0
  84. {scar_cli-0.26.0 → scar_cli-0.27.0}/experiments/harvest/PROTOCOL.md +0 -0
  85. {scar_cli-0.26.0 → scar_cli-0.27.0}/experiments/harvest/harvest.py +0 -0
  86. {scar_cli-0.26.0 → scar_cli-0.27.0}/hook/scar-hooks.py +0 -0
  87. {scar_cli-0.26.0 → scar_cli-0.27.0}/plugin/hooks/run.sh +0 -0
  88. {scar_cli-0.26.0 → scar_cli-0.27.0}/plugin/skills/scar-authoring/SKILL.md +0 -0
  89. {scar_cli-0.26.0 → scar_cli-0.27.0}/plugin/skills/scar-authoring/assets/template.md +0 -0
  90. {scar_cli-0.26.0 → scar_cli-0.27.0}/release-please-config.json +0 -0
  91. {scar_cli-0.26.0 → scar_cli-0.27.0}/src/scar/__init__.py +0 -0
  92. {scar_cli-0.26.0 → scar_cli-0.27.0}/src/scar/agent.py +0 -0
  93. {scar_cli-0.26.0 → scar_cli-0.27.0}/src/scar/cascade.py +0 -0
  94. {scar_cli-0.26.0 → scar_cli-0.27.0}/src/scar/codex.py +0 -0
  95. {scar_cli-0.26.0 → scar_cli-0.27.0}/src/scar/draftcheck.py +0 -0
  96. {scar_cli-0.26.0 → scar_cli-0.27.0}/src/scar/evidence.py +0 -0
  97. {scar_cli-0.26.0 → scar_cli-0.27.0}/src/scar/firing_log.py +0 -0
  98. {scar_cli-0.26.0 → scar_cli-0.27.0}/src/scar/gc.py +0 -0
  99. {scar_cli-0.26.0 → scar_cli-0.27.0}/src/scar/harvest.py +0 -0
  100. {scar_cli-0.26.0 → scar_cli-0.27.0}/src/scar/hooks.py +0 -0
  101. {scar_cli-0.26.0 → scar_cli-0.27.0}/src/scar/hosts.py +0 -0
  102. {scar_cli-0.26.0 → scar_cli-0.27.0}/src/scar/installer.py +0 -0
  103. {scar_cli-0.26.0 → scar_cli-0.27.0}/src/scar/lint.py +0 -0
  104. {scar_cli-0.26.0 → scar_cli-0.27.0}/src/scar/match.py +0 -0
  105. {scar_cli-0.26.0 → scar_cli-0.27.0}/src/scar/mcp.py +0 -0
  106. {scar_cli-0.26.0 → scar_cli-0.27.0}/src/scar/model.py +0 -0
  107. {scar_cli-0.26.0 → scar_cli-0.27.0}/src/scar/orphan.py +0 -0
  108. {scar_cli-0.26.0 → scar_cli-0.27.0}/src/scar/output.py +0 -0
  109. {scar_cli-0.26.0 → scar_cli-0.27.0}/src/scar/reanchor.py +0 -0
  110. {scar_cli-0.26.0 → scar_cli-0.27.0}/src/scar/renames.py +0 -0
  111. {scar_cli-0.26.0 → scar_cli-0.27.0}/src/scar/render.py +0 -0
  112. {scar_cli-0.26.0 → scar_cli-0.27.0}/src/scar/review.py +0 -0
  113. {scar_cli-0.26.0 → scar_cli-0.27.0}/src/scar/skills/scar-authoring/SKILL.md +0 -0
  114. {scar_cli-0.26.0 → scar_cli-0.27.0}/src/scar/skills/scar-authoring/assets/template.md +0 -0
  115. {scar_cli-0.26.0 → scar_cli-0.27.0}/src/scar/store.py +0 -0
  116. {scar_cli-0.26.0 → scar_cli-0.27.0}/src/scar/symbols.py +0 -0
  117. {scar_cli-0.26.0 → scar_cli-0.27.0}/src/scar/violation_evidence.py +0 -0
  118. {scar_cli-0.26.0 → scar_cli-0.27.0}/tests/conftest.py +0 -0
  119. {scar_cli-0.26.0 → scar_cli-0.27.0}/tests/test_action_yml.py +0 -0
  120. {scar_cli-0.26.0 → scar_cli-0.27.0}/tests/test_anchor_kind.py +0 -0
  121. {scar_cli-0.26.0 → scar_cli-0.27.0}/tests/test_block_capability.py +0 -0
  122. {scar_cli-0.26.0 → scar_cli-0.27.0}/tests/test_cascade.py +0 -0
  123. {scar_cli-0.26.0 → scar_cli-0.27.0}/tests/test_cli.py +0 -0
  124. {scar_cli-0.26.0 → scar_cli-0.27.0}/tests/test_codex.py +0 -0
  125. {scar_cli-0.26.0 → scar_cli-0.27.0}/tests/test_context_size.py +0 -0
  126. {scar_cli-0.26.0 → scar_cli-0.27.0}/tests/test_draftcheck.py +0 -0
  127. {scar_cli-0.26.0 → scar_cli-0.27.0}/tests/test_evidence.py +0 -0
  128. {scar_cli-0.26.0 → scar_cli-0.27.0}/tests/test_firing_log.py +0 -0
  129. {scar_cli-0.26.0 → scar_cli-0.27.0}/tests/test_gc.py +0 -0
  130. {scar_cli-0.26.0 → scar_cli-0.27.0}/tests/test_git_hook.py +0 -0
  131. {scar_cli-0.26.0 → scar_cli-0.27.0}/tests/test_harvest.py +0 -0
  132. {scar_cli-0.26.0 → scar_cli-0.27.0}/tests/test_hooks.py +0 -0
  133. {scar_cli-0.26.0 → scar_cli-0.27.0}/tests/test_hosts.py +0 -0
  134. {scar_cli-0.26.0 → scar_cli-0.27.0}/tests/test_installer.py +0 -0
  135. {scar_cli-0.26.0 → scar_cli-0.27.0}/tests/test_lifecycle.py +0 -0
  136. {scar_cli-0.26.0 → scar_cli-0.27.0}/tests/test_lint.py +0 -0
  137. {scar_cli-0.26.0 → scar_cli-0.27.0}/tests/test_match.py +0 -0
  138. {scar_cli-0.26.0 → scar_cli-0.27.0}/tests/test_matched_by.py +0 -0
  139. {scar_cli-0.26.0 → scar_cli-0.27.0}/tests/test_mcp.py +0 -0
  140. {scar_cli-0.26.0 → scar_cli-0.27.0}/tests/test_model.py +0 -0
  141. {scar_cli-0.26.0 → scar_cli-0.27.0}/tests/test_orphan.py +0 -0
  142. {scar_cli-0.26.0 → scar_cli-0.27.0}/tests/test_output.py +0 -0
  143. {scar_cli-0.26.0 → scar_cli-0.27.0}/tests/test_plugin.py +0 -0
  144. {scar_cli-0.26.0 → scar_cli-0.27.0}/tests/test_precommit_hooks.py +0 -0
  145. {scar_cli-0.26.0 → scar_cli-0.27.0}/tests/test_reanchor.py +0 -0
  146. {scar_cli-0.26.0 → scar_cli-0.27.0}/tests/test_renames.py +0 -0
  147. {scar_cli-0.26.0 → scar_cli-0.27.0}/tests/test_render.py +0 -0
  148. {scar_cli-0.26.0 → scar_cli-0.27.0}/tests/test_review.py +0 -0
  149. {scar_cli-0.26.0 → scar_cli-0.27.0}/tests/test_skill.py +0 -0
  150. {scar_cli-0.26.0 → scar_cli-0.27.0}/tests/test_store.py +0 -0
  151. {scar_cli-0.26.0 → scar_cli-0.27.0}/tests/test_sweep.py +0 -0
  152. {scar_cli-0.26.0 → scar_cli-0.27.0}/tests/test_symbols.py +0 -0
  153. {scar_cli-0.26.0 → scar_cli-0.27.0}/tests/test_verdict_observability.py +0 -0
  154. {scar_cli-0.26.0 → scar_cli-0.27.0}/tests/test_violation_evidence.py +0 -0
  155. {scar_cli-0.26.0 → scar_cli-0.27.0}/website/.gitignore +0 -0
  156. {scar_cli-0.26.0 → scar_cli-0.27.0}/website/docs/concepts.md +0 -0
  157. {scar_cli-0.26.0 → scar_cli-0.27.0}/website/docs/index.md +0 -0
  158. {scar_cli-0.26.0 → scar_cli-0.27.0}/website/docs/methodology.md +0 -0
  159. {scar_cli-0.26.0 → scar_cli-0.27.0}/website/docs/quickstart.md +0 -0
  160. {scar_cli-0.26.0 → scar_cli-0.27.0}/website/docusaurus.config.ts +0 -0
  161. {scar_cli-0.26.0 → scar_cli-0.27.0}/website/package-lock.json +0 -0
  162. {scar_cli-0.26.0 → scar_cli-0.27.0}/website/package.json +0 -0
  163. {scar_cli-0.26.0 → scar_cli-0.27.0}/website/scripts/sync-changelog.mjs +0 -0
  164. {scar_cli-0.26.0 → scar_cli-0.27.0}/website/sidebars.ts +0 -0
  165. {scar_cli-0.26.0 → scar_cli-0.27.0}/website/src/css/custom.css +0 -0
  166. {scar_cli-0.26.0 → scar_cli-0.27.0}/website/static/img/favicon.svg +0 -0
  167. {scar_cli-0.26.0 → scar_cli-0.27.0}/website/static/img/social-card.png +0 -0
  168. {scar_cli-0.26.0 → scar_cli-0.27.0}/website/static/llms.txt +0 -0
  169. {scar_cli-0.26.0 → scar_cli-0.27.0}/website/tsconfig.json +0 -0
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.27.0"
3
+ }
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.27.0](https://github.com/Daily-Nerd/Scar/compare/v0.26.0...v0.27.0) (2026-11-06)
4
+
5
+
6
+ ### Features
7
+
8
+ * **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))
9
+
3
10
  ## [0.26.0](https://github.com/Daily-Nerd/Scar/compare/v0.25.1...v0.26.0) (2026-11-06)
4
11
 
5
12
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: scar-cli
3
- Version: 0.26.0
3
+ Version: 0.27.0
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.0",
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.0"
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,74 @@ 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:
598
+ return ""
599
+ lines = []
600
+ for scar_id in sorted(states):
601
+ found = by_id.get(scar_id)
602
+ if found:
603
+ f, s = found
604
+ where = s.path_anchors[0] if s.path_anchors else f.name
605
+ title = s.title
606
+ else:
607
+ where, title = "unknown file", "(scar not found)"
608
+ cmd = "; ".join(cmds[scar_id]).replace("<path>", where)
609
+ lines.append(f"- [ ] #{scar_id} {title} ({where}): "
610
+ f"{'; '.join(states[scar_id])}. Suggested: {cmd}")
611
+ lines.append("")
612
+ lines.append("Firing-log signals (firing review, never fired) are not included: "
613
+ "the firing log is machine-local and empty in CI.")
614
+ return "\n".join(lines) + "\n"
615
+
616
+
549
617
  def _cmd_lint(args) -> int:
550
618
  store = _require_store()
551
619
  if store is None:
@@ -733,6 +801,7 @@ def _cmd_lint(args) -> int:
733
801
  # revised. Advisory like every other lifecycle signal here — only the
734
802
  # opt-in --fail-firing-review turns it into an exit code.
735
803
  reviews = _firing_reviews(store)
804
+ review_due = _review_due(store)
736
805
 
737
806
  # evidence reachability (#43, scar #5): commit-SHA receipts that no longer
738
807
  # resolve from HEAD. None = shallow clone, reachability indeterminate → skip.
@@ -762,6 +831,7 @@ def _cmd_lint(args) -> int:
762
831
  "unreachable_evidence": [{"scar_id": ue.scar_id, "sha": ue.sha, "reason": ue.reason}
763
832
  for ue in unreachable],
764
833
  "failed": failed,
834
+ "review_due": _review_due_json(review_due),
765
835
  }
766
836
 
767
837
  def plain():
@@ -809,8 +879,13 @@ def _cmd_lint(args) -> int:
809
879
  f"{len(unreachable)} unreachable-evidence, "
810
880
  f"{len(reviews)} firing-review")
811
881
 
812
- output.render(data=data, json_flag=getattr(args, "json", False),
813
- tty=lambda: _lint_rich(data), plain=plain)
882
+ if getattr(args, "review_queue", False):
883
+ # Machine path: plain markdown, never rich, and empty stdout when the
884
+ # queue is empty so a workflow can close its issue on `-s queue.md`.
885
+ sys.stdout.write(_review_queue_markdown(store, data))
886
+ else:
887
+ output.render(data=data, json_flag=getattr(args, "json", False),
888
+ tty=lambda: _lint_rich(data), plain=plain)
814
889
 
815
890
  if failed:
816
891
  return 1
@@ -829,8 +904,7 @@ def _cmd_status(args) -> int:
829
904
  return 1
830
905
  active, broken, cands = store.active(), store.broken(), store.candidates()
831
906
  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]
907
+ due = _review_due(store)
834
908
 
835
909
  # Orphans: detected (firing scars whose anchors all died — not yet persisted)
836
910
  # and persisted (already flipped to status: orphaned, invisible until now).
@@ -849,7 +923,7 @@ def _cmd_status(args) -> int:
849
923
  for _f, s in active],
850
924
  "challenged": [{"type": s.type, "id": s.id, "title": s.title} for _f, s in challenged],
851
925
  "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],
926
+ "review_due": _review_due_json(due),
853
927
  # Count-based review trigger (#274), kept SEPARATE from review_due:
854
928
  # one is a date passing, the other is accumulated firings, and a
855
929
  # consumer that merges them cannot tell which obligation it is looking
@@ -3116,7 +3190,11 @@ def build_parser() -> argparse.ArgumentParser:
3116
3190
  p.add_argument("--fail-firing-review", action="store_true",
3117
3191
  help="exit non-zero when any scar has crossed its "
3118
3192
  "review_after_firings threshold")
3119
- p.add_argument("--json", action="store_true", help="emit machine-readable JSON")
3193
+ fmt = p.add_mutually_exclusive_group()
3194
+ fmt.add_argument("--json", action="store_true", help="emit machine-readable JSON")
3195
+ fmt.add_argument("--review-queue", action="store_true",
3196
+ help="print a markdown checklist of scars needing a human "
3197
+ "(CI-usable signals only); empty output when none")
3120
3198
  p = _add(sub, "status", _cmd_status, help="counts, titles, broken-file warnings")
3121
3199
  p.add_argument("--json", action="store_true", help="emit machine-readable JSON")
3122
3200
 
@@ -36,3 +36,15 @@ 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
+ 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,173 @@
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
@@ -297,7 +297,7 @@ wheels = [
297
297
 
298
298
  [[package]]
299
299
  name = "scar-cli"
300
- version = "0.26.0"
300
+ version = "0.27.0"
301
301
  source = { editable = "." }
302
302
  dependencies = [
303
303
  { name = "rich" },
@@ -183,6 +183,60 @@ 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. Exit codes follow the usual gates (`--fail-orphans` and friends), the flag only changes what is printed. 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
+ run: uvx --from scar-cli scar lint --review-queue > queue.md
219
+ - name: Sync the rolling issue
220
+ env:
221
+ GH_TOKEN: ${{ github.token }}
222
+ run: |
223
+ label=scar-review-queue
224
+ gh label create "$label" --color BFD4F2 --description "Rolling Scar review queue" 2>/dev/null || true
225
+ number=$(gh issue list --label "$label" --state all --limit 1 --json number --jq '.[0].number // empty')
226
+ if [ -s queue.md ]; then
227
+ if [ -z "$number" ]; then
228
+ gh issue create --title "Scar review queue" --label "$label" --body-file queue.md
229
+ else
230
+ gh issue edit "$number" --body-file queue.md
231
+ gh issue reopen "$number" 2>/dev/null || true
232
+ fi
233
+ elif [ -n "$number" ]; then
234
+ gh issue close "$number" --comment "Review queue is empty." 2>/dev/null || true
235
+ fi
236
+ ```
237
+
238
+ 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.
239
+
186
240
  ## Machine-readable docs
187
241
 
188
242
  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
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes