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.
- scar_cli-0.27.1/.release-please-manifest.json +3 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/CHANGELOG.md +14 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/PKG-INFO +6 -1
- {scar_cli-0.26.0 → scar_cli-0.27.1}/README.md +5 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/plugin/plugin.json +1 -1
- {scar_cli-0.26.0 → scar_cli-0.27.1}/pyproject.toml +1 -1
- {scar_cli-0.26.0 → scar_cli-0.27.1}/src/scar/cli.py +105 -6
- {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_docs.py +18 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_json_contract.py +2 -1
- scar_cli-0.27.1/tests/test_review_queue.py +243 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/uv.lock +1 -1
- {scar_cli-0.26.0 → scar_cli-0.27.1}/website/docs/agents.md +65 -0
- scar_cli-0.26.0/.release-please-manifest.json +0 -3
- {scar_cli-0.26.0 → scar_cli-0.27.1}/.claude-plugin/marketplace.json +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/.github/workflows/ci.yml +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/.github/workflows/docs.yml +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/.github/workflows/pr-validation.yml +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/.github/workflows/release.yml +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/.gitignore +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/.pre-commit-config.yaml +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/.pre-commit-hooks.yaml +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/0001-git-grep-ere-pitfalls.landmine.md +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/0002-agent-direct-hook-install.deadend.md +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/0003-installer-binds-to-active-venv-scar.landmine.md +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/0004-promote-roundtrip-drops-expires-evidence.landmine.md +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/0005-history-rewrite-orphans-commit-evidence.landmine.md +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/0006-yaml-pattern-anchor-over-escaping.landmine.md +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/0007-release-please-config-change-skips-open-pr.landmine.md +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/0008-rich-output-nontty-must-bypass-rich.landmine.md +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/0009-anchor-survival-harness-measures-symbol-not-path-anchors.deadend.md +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/0010-git-returncode-128-vs-empty-tracked-set.landmine.md +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/0011-lint-compile-check-lets-redos-anchors-hang-hotpath.landmine.md +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/0012-firing-log-readers-need-blanket-guards.landmine.md +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/0013-docusaurus-future-v4-requires-faster-dep.landmine.md +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/0014-inject-flag-dest-collides-with-subcommand-dispatch.landmine.md +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/0015-zero-checkruns-means-approval-gate-not-workflow-bug.deadend.md +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/0016-hook-ownership-must-be-kind-scoped.landmine.md +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/0017-codex-pretool-context-can-replay-apply-patch.landmine.md +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/0018-cli-subparsers-register-through-_add-wrapper.landmine.md +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/0019-token-in-scar-body-cannot-prove-hook-delivery.deadend.md +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/0020-one-directional-health-check-leaves-flattering-direction-unwatched.landmine.md +0 -0
- {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
- {scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/0022-codex-hook-delivery-needs-user-file-and-trust.landmine.md +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/0023-cascade-block-exit-collides-with-argparse.fence.md +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/0024-host-detection-must-not-read-the-process-home.deadend.md +0 -0
- {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
- {scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/0026-path-anchor-breadth-drives-the-review-due-threshold.landmine.md +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/README.md +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/candidates/violation-evidence-must-not-persist-raw-edits.md +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/template.md +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/AGENTS.md +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/CONTRIBUTING.md +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/IDEA.md +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/LICENSE +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/ROADMAP.md +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/SCAR-FORMAT.md +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/SPEC.md +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/STRESS-TEST.md +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/action.yml +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/codecov.yml +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/experiments/anchor-survival/PROTOCOL.md +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/experiments/anchor-survival/RESULTS.md +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/experiments/anchor-survival/long_replay.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/experiments/anchor-survival/replay.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/experiments/anchor-survival/replay_shipped.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/experiments/anchor-survival/test_replay_shipped.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/experiments/auto-authorship/FINDINGS.md +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/experiments/auto-authorship/PROTOCOL.md +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/experiments/fence-honor/.gitignore +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/experiments/fence-honor/PROTOCOL.md +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/experiments/fence-honor/RESULTS.md +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/experiments/fence-honor/fixture/.scars/0001-vendor-retry-window.fence.md +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/experiments/fence-honor/fixture/.scars/0002-evicting-session-store.deadend.md +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/experiments/fence-honor/fixture/.scars/0003-export-column-order.landmine.md +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/experiments/fence-honor/fixture/README.md +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/experiments/fence-honor/fixture/payments/retry.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/experiments/fence-honor/fixture/reports/export.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/experiments/fence-honor/fixture/services/sessions.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/experiments/fence-honor/grade.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/experiments/harvest/PROTOCOL.md +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/experiments/harvest/harvest.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/hook/scar-hooks.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/plugin/hooks/run.sh +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/plugin/skills/scar-authoring/SKILL.md +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/plugin/skills/scar-authoring/assets/template.md +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/release-please-config.json +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/src/scar/__init__.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/src/scar/agent.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/src/scar/cascade.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/src/scar/codex.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/src/scar/draftcheck.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/src/scar/evidence.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/src/scar/firing_log.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/src/scar/gc.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/src/scar/harvest.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/src/scar/hooks.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/src/scar/hosts.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/src/scar/installer.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/src/scar/lint.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/src/scar/match.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/src/scar/mcp.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/src/scar/model.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/src/scar/orphan.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/src/scar/output.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/src/scar/reanchor.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/src/scar/renames.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/src/scar/render.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/src/scar/review.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/src/scar/skills/scar-authoring/SKILL.md +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/src/scar/skills/scar-authoring/assets/template.md +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/src/scar/store.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/src/scar/symbols.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/src/scar/violation_evidence.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/conftest.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_action_yml.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_anchor_kind.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_block_capability.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_cascade.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_cli.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_codex.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_context_size.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_draftcheck.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_evidence.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_firing_log.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_gc.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_git_hook.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_harvest.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_hooks.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_hosts.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_installer.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_lifecycle.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_lint.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_match.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_matched_by.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_mcp.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_model.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_orphan.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_output.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_plugin.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_precommit_hooks.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_reanchor.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_renames.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_render.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_review.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_skill.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_store.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_sweep.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_symbols.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_verdict_observability.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/tests/test_violation_evidence.py +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/website/.gitignore +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/website/docs/concepts.md +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/website/docs/index.md +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/website/docs/methodology.md +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/website/docs/quickstart.md +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/website/docusaurus.config.ts +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/website/package-lock.json +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/website/package.json +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/website/scripts/sync-changelog.mjs +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/website/sidebars.ts +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/website/src/css/custom.css +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/website/static/img/favicon.svg +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/website/static/img/social-card.png +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/website/static/llms.txt +0 -0
- {scar_cli-0.26.0 → scar_cli-0.27.1}/website/tsconfig.json +0 -0
|
@@ -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.
|
|
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.
|
|
@@ -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
|
-
|
|
813
|
-
|
|
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
|
-
|
|
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":
|
|
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.
|
|
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
|
|
@@ -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.
|
|
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
|
{scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/0003-installer-binds-to-active-venv-scar.landmine.md
RENAMED
|
File without changes
|
{scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/0004-promote-roundtrip-drops-expires-evidence.landmine.md
RENAMED
|
File without changes
|
{scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/0005-history-rewrite-orphans-commit-evidence.landmine.md
RENAMED
|
File without changes
|
{scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/0006-yaml-pattern-anchor-over-escaping.landmine.md
RENAMED
|
File without changes
|
|
File without changes
|
{scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/0008-rich-output-nontty-must-bypass-rich.landmine.md
RENAMED
|
File without changes
|
|
File without changes
|
{scar_cli-0.26.0 → scar_cli-0.27.1}/.scars/0010-git-returncode-128-vs-empty-tracked-set.landmine.md
RENAMED
|
File without changes
|
|
File without changes
|