scar-cli 0.23.0__tar.gz → 0.25.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 (165) hide show
  1. scar_cli-0.25.0/.release-please-manifest.json +3 -0
  2. {scar_cli-0.23.0 → scar_cli-0.25.0}/.scars/0001-git-grep-ere-pitfalls.landmine.md +1 -1
  3. {scar_cli-0.23.0 → scar_cli-0.25.0}/.scars/0005-history-rewrite-orphans-commit-evidence.landmine.md +5 -0
  4. {scar_cli-0.23.0 → scar_cli-0.25.0}/.scars/0006-yaml-pattern-anchor-over-escaping.landmine.md +2 -1
  5. {scar_cli-0.23.0 → scar_cli-0.25.0}/.scars/0008-rich-output-nontty-must-bypass-rich.landmine.md +2 -2
  6. {scar_cli-0.23.0 → scar_cli-0.25.0}/.scars/0011-lint-compile-check-lets-redos-anchors-hang-hotpath.landmine.md +2 -2
  7. scar_cli-0.23.0/.scars/candidates/cascade-block-exit-collides-with-argparse.md → scar_cli-0.25.0/.scars/0023-cascade-block-exit-collides-with-argparse.fence.md +5 -3
  8. scar_cli-0.25.0/.scars/0024-host-detection-must-not-read-the-process-home.deadend.md +42 -0
  9. scar_cli-0.23.0/.scars/candidates/one-plugin-hook-manifest-two-hosts-two-root-variables.md → scar_cli-0.25.0/.scars/0025-one-plugin-hook-manifest-two-hosts-two-root-variables.landmine.md +4 -2
  10. scar_cli-0.25.0/.scars/0026-path-anchor-breadth-drives-the-review-due-threshold.landmine.md +39 -0
  11. {scar_cli-0.23.0 → scar_cli-0.25.0}/AGENTS.md +5 -2
  12. {scar_cli-0.23.0 → scar_cli-0.25.0}/CHANGELOG.md +35 -0
  13. {scar_cli-0.23.0 → scar_cli-0.25.0}/PKG-INFO +3 -1
  14. {scar_cli-0.23.0 → scar_cli-0.25.0}/README.md +2 -0
  15. {scar_cli-0.23.0 → scar_cli-0.25.0}/SCAR-FORMAT.md +1 -1
  16. {scar_cli-0.23.0 → scar_cli-0.25.0}/SPEC.md +39 -11
  17. {scar_cli-0.23.0 → scar_cli-0.25.0}/plugin/plugin.json +1 -1
  18. {scar_cli-0.23.0/src/scar → scar_cli-0.25.0/plugin}/skills/scar-authoring/SKILL.md +3 -0
  19. {scar_cli-0.23.0 → scar_cli-0.25.0}/pyproject.toml +1 -1
  20. {scar_cli-0.23.0 → scar_cli-0.25.0}/src/scar/cascade.py +14 -6
  21. {scar_cli-0.23.0 → scar_cli-0.25.0}/src/scar/cli.py +276 -51
  22. {scar_cli-0.23.0 → scar_cli-0.25.0}/src/scar/codex.py +9 -3
  23. scar_cli-0.25.0/src/scar/firing_log.py +71 -0
  24. {scar_cli-0.23.0 → scar_cli-0.25.0}/src/scar/hooks.py +37 -13
  25. {scar_cli-0.23.0 → scar_cli-0.25.0}/src/scar/hosts.py +27 -16
  26. {scar_cli-0.23.0 → scar_cli-0.25.0}/src/scar/installer.py +51 -13
  27. {scar_cli-0.23.0 → scar_cli-0.25.0}/src/scar/lint.py +6 -2
  28. {scar_cli-0.23.0 → scar_cli-0.25.0}/src/scar/match.py +104 -26
  29. {scar_cli-0.23.0 → scar_cli-0.25.0}/src/scar/mcp.py +1 -1
  30. {scar_cli-0.23.0 → scar_cli-0.25.0}/src/scar/orphan.py +55 -1
  31. {scar_cli-0.23.0 → scar_cli-0.25.0}/src/scar/render.py +22 -3
  32. {scar_cli-0.23.0/plugin → scar_cli-0.25.0/src/scar}/skills/scar-authoring/SKILL.md +3 -0
  33. {scar_cli-0.23.0 → scar_cli-0.25.0}/src/scar/symbols.py +13 -2
  34. {scar_cli-0.23.0 → scar_cli-0.25.0}/tests/test_cascade.py +46 -0
  35. {scar_cli-0.23.0 → scar_cli-0.25.0}/tests/test_cli.py +654 -3
  36. {scar_cli-0.23.0 → scar_cli-0.25.0}/tests/test_codex.py +5 -1
  37. scar_cli-0.25.0/tests/test_firing_log.py +113 -0
  38. {scar_cli-0.23.0 → scar_cli-0.25.0}/tests/test_hooks.py +37 -0
  39. {scar_cli-0.23.0 → scar_cli-0.25.0}/tests/test_hosts.py +48 -3
  40. {scar_cli-0.23.0 → scar_cli-0.25.0}/tests/test_installer.py +126 -3
  41. {scar_cli-0.23.0 → scar_cli-0.25.0}/tests/test_json_contract.py +19 -1
  42. {scar_cli-0.23.0 → scar_cli-0.25.0}/tests/test_lint.py +26 -0
  43. {scar_cli-0.23.0 → scar_cli-0.25.0}/tests/test_match.py +117 -1
  44. scar_cli-0.25.0/tests/test_matched_by.py +179 -0
  45. {scar_cli-0.23.0 → scar_cli-0.25.0}/tests/test_orphan.py +197 -1
  46. {scar_cli-0.23.0 → scar_cli-0.25.0}/tests/test_render.py +35 -0
  47. scar_cli-0.25.0/tests/test_sweep.py +277 -0
  48. {scar_cli-0.23.0 → scar_cli-0.25.0}/uv.lock +2 -2
  49. {scar_cli-0.23.0 → scar_cli-0.25.0}/website/docs/agents.md +28 -0
  50. {scar_cli-0.23.0 → scar_cli-0.25.0}/website/docs/concepts.md +3 -2
  51. {scar_cli-0.23.0 → scar_cli-0.25.0}/website/docs/methodology.md +2 -0
  52. {scar_cli-0.23.0 → scar_cli-0.25.0}/website/docs/quickstart.md +1 -0
  53. {scar_cli-0.23.0 → scar_cli-0.25.0}/website/docusaurus.config.ts +2 -0
  54. scar_cli-0.25.0/website/static/img/social-card.png +0 -0
  55. {scar_cli-0.23.0 → scar_cli-0.25.0}/website/static/llms.txt +1 -1
  56. scar_cli-0.23.0/.release-please-manifest.json +0 -3
  57. {scar_cli-0.23.0 → scar_cli-0.25.0}/.claude-plugin/marketplace.json +0 -0
  58. {scar_cli-0.23.0 → scar_cli-0.25.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  59. {scar_cli-0.23.0 → scar_cli-0.25.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  60. {scar_cli-0.23.0 → scar_cli-0.25.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  61. {scar_cli-0.23.0 → scar_cli-0.25.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  62. {scar_cli-0.23.0 → scar_cli-0.25.0}/.github/workflows/ci.yml +0 -0
  63. {scar_cli-0.23.0 → scar_cli-0.25.0}/.github/workflows/docs.yml +0 -0
  64. {scar_cli-0.23.0 → scar_cli-0.25.0}/.github/workflows/pr-validation.yml +0 -0
  65. {scar_cli-0.23.0 → scar_cli-0.25.0}/.github/workflows/release.yml +0 -0
  66. {scar_cli-0.23.0 → scar_cli-0.25.0}/.gitignore +0 -0
  67. {scar_cli-0.23.0 → scar_cli-0.25.0}/.pre-commit-config.yaml +0 -0
  68. {scar_cli-0.23.0 → scar_cli-0.25.0}/.pre-commit-hooks.yaml +0 -0
  69. {scar_cli-0.23.0 → scar_cli-0.25.0}/.scars/0002-agent-direct-hook-install.deadend.md +0 -0
  70. {scar_cli-0.23.0 → scar_cli-0.25.0}/.scars/0003-installer-binds-to-active-venv-scar.landmine.md +0 -0
  71. {scar_cli-0.23.0 → scar_cli-0.25.0}/.scars/0004-promote-roundtrip-drops-expires-evidence.landmine.md +0 -0
  72. {scar_cli-0.23.0 → scar_cli-0.25.0}/.scars/0007-release-please-config-change-skips-open-pr.landmine.md +0 -0
  73. {scar_cli-0.23.0 → scar_cli-0.25.0}/.scars/0009-anchor-survival-harness-measures-symbol-not-path-anchors.deadend.md +0 -0
  74. {scar_cli-0.23.0 → scar_cli-0.25.0}/.scars/0010-git-returncode-128-vs-empty-tracked-set.landmine.md +0 -0
  75. {scar_cli-0.23.0 → scar_cli-0.25.0}/.scars/0012-firing-log-readers-need-blanket-guards.landmine.md +0 -0
  76. {scar_cli-0.23.0 → scar_cli-0.25.0}/.scars/0013-docusaurus-future-v4-requires-faster-dep.landmine.md +0 -0
  77. {scar_cli-0.23.0 → scar_cli-0.25.0}/.scars/0014-inject-flag-dest-collides-with-subcommand-dispatch.landmine.md +0 -0
  78. {scar_cli-0.23.0 → scar_cli-0.25.0}/.scars/0015-zero-checkruns-means-approval-gate-not-workflow-bug.deadend.md +0 -0
  79. {scar_cli-0.23.0 → scar_cli-0.25.0}/.scars/0016-hook-ownership-must-be-kind-scoped.landmine.md +0 -0
  80. {scar_cli-0.23.0 → scar_cli-0.25.0}/.scars/0017-codex-pretool-context-can-replay-apply-patch.landmine.md +0 -0
  81. {scar_cli-0.23.0 → scar_cli-0.25.0}/.scars/0018-cli-subparsers-register-through-_add-wrapper.landmine.md +0 -0
  82. {scar_cli-0.23.0 → scar_cli-0.25.0}/.scars/0019-token-in-scar-body-cannot-prove-hook-delivery.deadend.md +0 -0
  83. {scar_cli-0.23.0 → scar_cli-0.25.0}/.scars/0020-one-directional-health-check-leaves-flattering-direction-unwatched.landmine.md +0 -0
  84. {scar_cli-0.23.0 → scar_cli-0.25.0}/.scars/0021-posttool-fix-in-hooks-does-not-reach-codex-or-cascade.landmine.md +0 -0
  85. {scar_cli-0.23.0 → scar_cli-0.25.0}/.scars/0022-codex-hook-delivery-needs-user-file-and-trust.landmine.md +0 -0
  86. {scar_cli-0.23.0 → scar_cli-0.25.0}/.scars/README.md +0 -0
  87. {scar_cli-0.23.0 → scar_cli-0.25.0}/.scars/template.md +0 -0
  88. {scar_cli-0.23.0 → scar_cli-0.25.0}/CONTRIBUTING.md +0 -0
  89. {scar_cli-0.23.0 → scar_cli-0.25.0}/IDEA.md +0 -0
  90. {scar_cli-0.23.0 → scar_cli-0.25.0}/LICENSE +0 -0
  91. {scar_cli-0.23.0 → scar_cli-0.25.0}/ROADMAP.md +0 -0
  92. {scar_cli-0.23.0 → scar_cli-0.25.0}/STRESS-TEST.md +0 -0
  93. {scar_cli-0.23.0 → scar_cli-0.25.0}/action.yml +0 -0
  94. {scar_cli-0.23.0 → scar_cli-0.25.0}/codecov.yml +0 -0
  95. {scar_cli-0.23.0 → scar_cli-0.25.0}/experiments/anchor-survival/PROTOCOL.md +0 -0
  96. {scar_cli-0.23.0 → scar_cli-0.25.0}/experiments/anchor-survival/RESULTS.md +0 -0
  97. {scar_cli-0.23.0 → scar_cli-0.25.0}/experiments/anchor-survival/long_replay.py +0 -0
  98. {scar_cli-0.23.0 → scar_cli-0.25.0}/experiments/anchor-survival/replay.py +0 -0
  99. {scar_cli-0.23.0 → scar_cli-0.25.0}/experiments/anchor-survival/replay_shipped.py +0 -0
  100. {scar_cli-0.23.0 → scar_cli-0.25.0}/experiments/anchor-survival/test_replay_shipped.py +0 -0
  101. {scar_cli-0.23.0 → scar_cli-0.25.0}/experiments/auto-authorship/FINDINGS.md +0 -0
  102. {scar_cli-0.23.0 → scar_cli-0.25.0}/experiments/auto-authorship/PROTOCOL.md +0 -0
  103. {scar_cli-0.23.0 → scar_cli-0.25.0}/experiments/fence-honor/.gitignore +0 -0
  104. {scar_cli-0.23.0 → scar_cli-0.25.0}/experiments/fence-honor/PROTOCOL.md +0 -0
  105. {scar_cli-0.23.0 → scar_cli-0.25.0}/experiments/fence-honor/RESULTS.md +0 -0
  106. {scar_cli-0.23.0 → scar_cli-0.25.0}/experiments/fence-honor/fixture/.scars/0001-vendor-retry-window.fence.md +0 -0
  107. {scar_cli-0.23.0 → scar_cli-0.25.0}/experiments/fence-honor/fixture/.scars/0002-evicting-session-store.deadend.md +0 -0
  108. {scar_cli-0.23.0 → scar_cli-0.25.0}/experiments/fence-honor/fixture/.scars/0003-export-column-order.landmine.md +0 -0
  109. {scar_cli-0.23.0 → scar_cli-0.25.0}/experiments/fence-honor/fixture/README.md +0 -0
  110. {scar_cli-0.23.0 → scar_cli-0.25.0}/experiments/fence-honor/fixture/payments/retry.py +0 -0
  111. {scar_cli-0.23.0 → scar_cli-0.25.0}/experiments/fence-honor/fixture/reports/export.py +0 -0
  112. {scar_cli-0.23.0 → scar_cli-0.25.0}/experiments/fence-honor/fixture/services/sessions.py +0 -0
  113. {scar_cli-0.23.0 → scar_cli-0.25.0}/experiments/fence-honor/grade.py +0 -0
  114. {scar_cli-0.23.0 → scar_cli-0.25.0}/experiments/harvest/PROTOCOL.md +0 -0
  115. {scar_cli-0.23.0 → scar_cli-0.25.0}/experiments/harvest/harvest.py +0 -0
  116. {scar_cli-0.23.0 → scar_cli-0.25.0}/hook/scar-hooks.py +0 -0
  117. {scar_cli-0.23.0 → scar_cli-0.25.0}/plugin/hooks/run.sh +0 -0
  118. {scar_cli-0.23.0 → scar_cli-0.25.0}/plugin/skills/scar-authoring/assets/template.md +0 -0
  119. {scar_cli-0.23.0 → scar_cli-0.25.0}/release-please-config.json +0 -0
  120. {scar_cli-0.23.0 → scar_cli-0.25.0}/src/scar/__init__.py +0 -0
  121. {scar_cli-0.23.0 → scar_cli-0.25.0}/src/scar/agent.py +0 -0
  122. {scar_cli-0.23.0 → scar_cli-0.25.0}/src/scar/draftcheck.py +0 -0
  123. {scar_cli-0.23.0 → scar_cli-0.25.0}/src/scar/evidence.py +0 -0
  124. {scar_cli-0.23.0 → scar_cli-0.25.0}/src/scar/gc.py +0 -0
  125. {scar_cli-0.23.0 → scar_cli-0.25.0}/src/scar/harvest.py +0 -0
  126. {scar_cli-0.23.0 → scar_cli-0.25.0}/src/scar/model.py +0 -0
  127. {scar_cli-0.23.0 → scar_cli-0.25.0}/src/scar/output.py +0 -0
  128. {scar_cli-0.23.0 → scar_cli-0.25.0}/src/scar/reanchor.py +0 -0
  129. {scar_cli-0.23.0 → scar_cli-0.25.0}/src/scar/renames.py +0 -0
  130. {scar_cli-0.23.0 → scar_cli-0.25.0}/src/scar/review.py +0 -0
  131. {scar_cli-0.23.0 → scar_cli-0.25.0}/src/scar/skills/scar-authoring/assets/template.md +0 -0
  132. {scar_cli-0.23.0 → scar_cli-0.25.0}/src/scar/store.py +0 -0
  133. {scar_cli-0.23.0 → scar_cli-0.25.0}/tests/conftest.py +0 -0
  134. {scar_cli-0.23.0 → scar_cli-0.25.0}/tests/test_action_yml.py +0 -0
  135. {scar_cli-0.23.0 → scar_cli-0.25.0}/tests/test_anchor_kind.py +0 -0
  136. {scar_cli-0.23.0 → scar_cli-0.25.0}/tests/test_block_capability.py +0 -0
  137. {scar_cli-0.23.0 → scar_cli-0.25.0}/tests/test_context_size.py +0 -0
  138. {scar_cli-0.23.0 → scar_cli-0.25.0}/tests/test_docs.py +0 -0
  139. {scar_cli-0.23.0 → scar_cli-0.25.0}/tests/test_draftcheck.py +0 -0
  140. {scar_cli-0.23.0 → scar_cli-0.25.0}/tests/test_evidence.py +0 -0
  141. {scar_cli-0.23.0 → scar_cli-0.25.0}/tests/test_gc.py +0 -0
  142. {scar_cli-0.23.0 → scar_cli-0.25.0}/tests/test_git_hook.py +0 -0
  143. {scar_cli-0.23.0 → scar_cli-0.25.0}/tests/test_harvest.py +0 -0
  144. {scar_cli-0.23.0 → scar_cli-0.25.0}/tests/test_lifecycle.py +0 -0
  145. {scar_cli-0.23.0 → scar_cli-0.25.0}/tests/test_mcp.py +0 -0
  146. {scar_cli-0.23.0 → scar_cli-0.25.0}/tests/test_model.py +0 -0
  147. {scar_cli-0.23.0 → scar_cli-0.25.0}/tests/test_output.py +0 -0
  148. {scar_cli-0.23.0 → scar_cli-0.25.0}/tests/test_plugin.py +0 -0
  149. {scar_cli-0.23.0 → scar_cli-0.25.0}/tests/test_precommit_hooks.py +0 -0
  150. {scar_cli-0.23.0 → scar_cli-0.25.0}/tests/test_reanchor.py +0 -0
  151. {scar_cli-0.23.0 → scar_cli-0.25.0}/tests/test_renames.py +0 -0
  152. {scar_cli-0.23.0 → scar_cli-0.25.0}/tests/test_review.py +0 -0
  153. {scar_cli-0.23.0 → scar_cli-0.25.0}/tests/test_skill.py +0 -0
  154. {scar_cli-0.23.0 → scar_cli-0.25.0}/tests/test_store.py +0 -0
  155. {scar_cli-0.23.0 → scar_cli-0.25.0}/tests/test_symbols.py +0 -0
  156. {scar_cli-0.23.0 → scar_cli-0.25.0}/tests/test_verdict_observability.py +0 -0
  157. {scar_cli-0.23.0 → scar_cli-0.25.0}/website/.gitignore +0 -0
  158. {scar_cli-0.23.0 → scar_cli-0.25.0}/website/docs/index.md +0 -0
  159. {scar_cli-0.23.0 → scar_cli-0.25.0}/website/package-lock.json +0 -0
  160. {scar_cli-0.23.0 → scar_cli-0.25.0}/website/package.json +0 -0
  161. {scar_cli-0.23.0 → scar_cli-0.25.0}/website/scripts/sync-changelog.mjs +0 -0
  162. {scar_cli-0.23.0 → scar_cli-0.25.0}/website/sidebars.ts +0 -0
  163. {scar_cli-0.23.0 → scar_cli-0.25.0}/website/src/css/custom.css +0 -0
  164. {scar_cli-0.23.0 → scar_cli-0.25.0}/website/static/img/favicon.svg +0 -0
  165. {scar_cli-0.23.0 → scar_cli-0.25.0}/website/tsconfig.json +0 -0
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.25.0"
3
+ }
@@ -10,7 +10,7 @@ anchors:
10
10
  - path: experiments/anchor-survival/
11
11
  - path: src/scar/harvest.py
12
12
  - pattern: "git.{0,20}grep"
13
- violation: "git.{0,60}grep.{0,120}\\b"
13
+ violation: "git[^#\n]{0,60}grep[^#\n]{0,80}[\x22\x27][^\x22\x27#\n]{0,40}\\b"
14
14
  evidence:
15
15
  - note: "orphaned receipt — pre-v0.1.0 commit 5c63b14 produced a fake 0% anchor-survival run before diagnosis (gate 0.2); resolves at github.com/Daily-Nerd/Scar/commit/5c63b14 until GC, not in fresh clones (rewritten at the v0.1.0 release)"
16
16
  expires:
@@ -8,8 +8,13 @@ created: 2026-06-11
8
8
  authors: ["claude-code", "Kibukx"]
9
9
  anchors:
10
10
  - path: .scars/
11
+ - command: "git push.*--force"
12
+ - command: "git rebase"
13
+ - command: "git filter-repo|git filter-branch"
14
+ - command: "git commit.*--amend"
11
15
  violation: "commit: [0-9a-f]{7,40}"
12
16
  evidence:
17
+ - note: "2026-09-05 anchors: path .scars/ KEPT on purpose. The violation tripwire commit: [0-9a-f]{7,40} only evaluates on files an anchor matched, so dropping it to cut firings would silently disarm the scar. Command anchors added instead, so the history-rewrite half fires on the act rather than on any scar edit"
13
18
  - note: v0.1.0 public release (2026-06-11): fresh-start force-push orphaned 3 commit SHAs cited by scars 0001 and 0002; SHAs still resolve on GitHub by URL but fail in any fresh clone, and GitHub may GC them eventually
14
19
  expires:
15
20
  condition: "evidence schema gains a resolvable form (full URL or archived diff) or lint warns on bare SHAs at promotion"
@@ -9,8 +9,9 @@ authors: ["claude-code", "kibukx"]
9
9
  anchors:
10
10
  - path: src/scar/orphan.py
11
11
  - path: src/scar/match.py
12
- - path: .scars/
12
+ - pattern: "\n\s*-\s*pattern:"
13
13
  evidence:
14
+ - note: "2026-09-05 anchors: path .scars/ replaced by a pattern matching a scar frontmatter pattern: entry. The directory anchor fired on every scar edit, while the hazard only exists when a pattern anchor is being authored"
14
15
  - pr: 40
15
16
  - note: scar 1 grep pattern matched only its own body, never experiments/anchor-survival/RESULTS.md
16
17
  expires:
@@ -7,10 +7,10 @@ confidence: 0.9
7
7
  created: 2026-06-30
8
8
  authors: ["claude-code", "kibukx"]
9
9
  anchors:
10
- - path: src/scar/cli.py
11
10
  - path: src/scar/output.py
12
- - pattern: "output\.render\("
11
+ - pattern: "def plain\(\):"
13
12
  evidence:
13
+ - note: "2026-09-05 anchors: path src/scar/cli.py dropped, it is 3000+ lines and the tty gate is 17 of them. The 18 output.render( call sites only invoke the already-safe dispatcher, so that pattern is replaced by def plain():, which marks the 14 non-tty contract closures this scar actually protects"
14
14
  - issue: 78
15
15
  - note: 187-test suite asserts plain substrings like '0001-bad.deadend.md' and long anchor paths on main([...]) under capsys
16
16
  expires:
@@ -7,11 +7,11 @@ confidence: 0.9
7
7
  created: 2026-06-30
8
8
  authors: ["claude-code", "Kibukx"]
9
9
  anchors:
10
- - path: src/scar/lint.py
11
- - path: src/scar/match.py
12
10
  - pattern: "rx\.search\("
13
11
  - pattern: "MAX_ANCHOR_SCAN"
12
+ - pattern: "_is_redos_prone"
14
13
  evidence:
14
+ - note: "2026-09-05 anchors: both whole-file path anchors dropped, lint.py and match.py serve many concerns beyond ReDoS. _is_redos_prone added so the GATE itself stays anchored, since the body says the gate is the defense and the 64 KiB cap is not"
15
15
  - issue: 88
16
16
  expires:
17
17
  condition: "lint gains full regex-safety analysis OR the matcher runs on a hardened regex engine with a hard timeout"
@@ -1,22 +1,24 @@
1
1
  ---
2
- id: 0
2
+ id: 23
3
3
  type: fence
4
4
  title: Cascade's block code is 2, and so is argparse's error code — never exit 2 directly from cascade-hook
5
5
  severity: high
6
6
  confidence: 0.9
7
7
  created: 2026-08-04
8
8
  authors: ["claude-code"]
9
+ promoted_by: Kibukx
10
+ promoted_by_source: explicit
9
11
  anchors:
10
12
  - path: src/scar/cascade.py
11
13
  - path: src/scar/installer.py
12
14
  - pattern: "cascade-hook"
13
15
  evidence:
14
16
  - issue: 197
15
- - note: ".windsurf/hooks.json is committed and workspace-level, so a teammate on an older scar-cli runs whatever command string we wrote"
17
+ - note: .windsurf/hooks.json is committed and workspace-level, so a teammate on an older scar-cli runs whatever command string we wrote
16
18
  expires:
17
19
  condition: "Cascade gains a non-blocking context channel (an additionalContext equivalent), so blocking is no longer the injection path at all"
18
20
  review_after: 2027-02-04
19
- status: candidate
21
+ status: active
20
22
  ---
21
23
 
22
24
  `scar cascade-hook` signals "block this action" with sentinel exit code 20,
@@ -0,0 +1,42 @@
1
+ ---
2
+ id: 24
3
+ type: deadend
4
+ title: Host detection must take home from installer.CLAUDE_DIR, never Path.home() — a bare call passes locally and fails every other machine
5
+ severity: high
6
+ confidence: 0.9
7
+ created: 2026-09-07
8
+ authors: ["claude-code"]
9
+ promoted_by: Kibukx
10
+ promoted_by_source: explicit
11
+ anchors:
12
+ - pattern: "detect_hosts\("
13
+ - symbol: src/scar/cli.py::_detect
14
+ evidence:
15
+ - issue: 318
16
+ - note: 2026-09-07, during the multi-host skill install work: the skill-install refusal resolved home with Path.home(). Under HOME=empty the suite went 1031 passed / 2 FAILED, both PRE-EXISTING tests, printing 'claude not detected on this machine, nothing written'. Fixed by reusing _detect(). Branch SHAs deliberately omitted: scar #5 fired on the amend that wrote this, and squash-merge would orphan them.
17
+ expires:
18
+ condition: "detect_hosts takes home from one injected seam no caller can bypass"
19
+ review_after: 2027-03-08
20
+ status: active
21
+ ---
22
+
23
+ `Path.home()` in a detection path looks obviously right and is a trap. Tests
24
+ patch `installer.CLAUDE_DIR`, not the environment, and `_detect()` derives
25
+ home from `installer.CLAUDE_DIR.parent`. A bare call reads the author's real
26
+ home, finds their real `~/.claude`, and passes. On any machine without one,
27
+ every CI runner included, detection returns nothing and the caller refuses.
28
+
29
+ The failure is invisible where it is introduced: the suite was fully green on
30
+ the authoring machine, and the tests that finally failed were pre-existing
31
+ ones nobody had touched. Use `_detect(kind)`, or `installer.CLAUDE_DIR.parent`
32
+ exactly as `_detect` does. Never add a second answer to where home is.
33
+
34
+ Two anchors on purpose. `cli.py` is 143 KiB and its first `detect_hosts(` sits
35
+ at byte ~123,000, past the 64 KiB `MAX_ANCHOR_SCAN` bound, so the pattern is
36
+ blind to the very file the defect landed in. The `symbol:` anchor covers it,
37
+ at the cost of needing `scar-cli[symbols]`. Lint reports neither gap: the
38
+ pattern matches `hosts.py`, so the scar reads live while blind where it counts.
39
+
40
+ Not armed. The obvious tripwire is `Path\.home\(\)`, but `cli.py` carries that
41
+ string in a warning comment and `installer.py` uses it correctly for module
42
+ constants, so it would fire on prose and on correct code.
@@ -1,11 +1,13 @@
1
1
  ---
2
- id: 0
2
+ id: 25
3
3
  type: landmine
4
4
  title: A plugin hook manifest is read by every host that installs the plugin, and hosts expand different root variables
5
5
  severity: high
6
6
  confidence: 0.9
7
7
  created: 2026-09-03
8
8
  authors: ["claude-code"]
9
+ promoted_by: Kibukx
10
+ promoted_by_source: explicit
9
11
  anchors:
10
12
  - path: plugin/
11
13
  - path: src/scar/installer.py
@@ -16,7 +18,7 @@ evidence:
16
18
  expires:
17
19
  condition: "the plugin format gains per-host scoping for hook files"
18
20
  review_after: 2026-12-01
19
- status: candidate
21
+ status: active
20
22
  ---
21
23
 
22
24
  Claude Code merges plugin.json hooks with hooks/hooks.json and defines only
@@ -0,0 +1,39 @@
1
+ ---
2
+ id: 26
3
+ type: landmine
4
+ title: A whole-file or whole-directory path anchor makes the review-due threshold report anchor breadth, not code hazard
5
+ severity: medium
6
+ confidence: 0.8
7
+ created: 2026-09-04
8
+ authors: ["claude-code"]
9
+ promoted_by: Kibukx
10
+ promoted_by_source: explicit
11
+ anchors:
12
+ - path: SCAR-FORMAT.md
13
+ - pattern: "review_after_firings"
14
+ evidence:
15
+ - issue: 312
16
+ - note: triage of all five over-threshold scars in this repo on 2026-09-04: four were over-anchored, one was a real code fix
17
+ expires:
18
+ condition: "lint reports what share of a scar's firings touched the hazard, so the review-due warning can distinguish breadth from recurrence"
19
+ review_after: 2027-03-04
20
+ status: active
21
+ ---
22
+
23
+ `scar lint` warns past `review_after_firings` that either the guarded code
24
+ merits the fix or the scar merits revision. Read literally that sends you to
25
+ the code first. It is usually the anchor.
26
+
27
+ A `path:` anchor is a repo-relative PREFIX, so it fires on every edit beneath
28
+ it. Scar #8 guards a 17-line tty branch inside a 52-line module but anchored
29
+ the whole 3,090-line `src/scar/cli.py`; roughly nine of every ten of its 47
30
+ firings touched nothing related. Scar #5 guards against history rewrites and
31
+ anchored all of `.scars/`, so it fired on 22 routine promotions and zero
32
+ force-pushes, which is zero of its actual trigger events.
33
+
34
+ A firing count therefore measures anchor breadth first and hazard recurrence
35
+ second, and the two are not separable from the log. Before reading a
36
+ review-due warning as evidence that code needs fixing, check what share of the
37
+ firings touched the hazard at all. Then narrow the anchor to the smallest file
38
+ holding the branching, or switch to a `command:` anchor when the trigger is an
39
+ action rather than a file (#312 is the one case here that really was the code).
@@ -26,8 +26,11 @@ The full authoring contract — qualification criteria, the candidates-only writ
26
26
  path, mandatory YAML frontmatter, and the regex over-escaping trap — is packaged
27
27
  as the `scar-authoring` skill.
28
28
 
29
- - **Claude Code:** install the plugin (recommended) or `scar skill install` to
30
- drop the skill into `~/.claude/skills/`. It auto-loads on trigger.
29
+ - **Claude Code, Codex, Cursor, opencode, Windsurf:** `scar skill install`
30
+ detects which of those hosts are on the machine and writes the skill to each
31
+ one's native skills directory. On Claude Code the plugin is the recommended
32
+ route and carries the same skill. The per-host destination table is in
33
+ [website/docs/agents.md](website/docs/agents.md).
31
34
  - **MCP agents (Cursor/Windsurf/opencode):** the `scar_draft` tool enforces the
32
35
  candidates-only path and lints before writing; its description carries the
33
36
  digest.
@@ -1,5 +1,40 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.25.0](https://github.com/Daily-Nerd/Scar/compare/v0.24.0...v0.25.0) (2026-10-06)
4
+
5
+
6
+ ### Features
7
+
8
+ * **firing-log:** record which anchor kinds matched each scar ([#335](https://github.com/Daily-Nerd/Scar/issues/335)) ([#339](https://github.com/Daily-Nerd/Scar/issues/339)) ([aa6b719](https://github.com/Daily-Nerd/Scar/commit/aa6b719089d504fce045052598e8d93ef1832fd7))
9
+ * **lint:** warn on a violation that cannot arm, dead candidate anchors, and broad patterns ([#325](https://github.com/Daily-Nerd/Scar/issues/325)) ([#328](https://github.com/Daily-Nerd/Scar/issues/328)) ([b3b3224](https://github.com/Daily-Nerd/Scar/commit/b3b3224c31a6b29f9d253290dd0e7bf99e1e7e56))
10
+ * **render:** header names the pre-cap total when the cap cut scars ([#322](https://github.com/Daily-Nerd/Scar/issues/322)) ([fc3754b](https://github.com/Daily-Nerd/Scar/commit/fc3754beef137d1d51bf0079d8172765a0277c73))
11
+ * **sweep:** run every armed violation over the whole tree and report live hits ([#326](https://github.com/Daily-Nerd/Scar/issues/326)) ([#329](https://github.com/Daily-Nerd/Scar/issues/329)) ([ecbd159](https://github.com/Daily-Nerd/Scar/commit/ecbd1597db6536d801a05c80a69fa1bd3e945ccc))
12
+
13
+
14
+ ### Bug Fixes
15
+
16
+ * **lint:** report duplicate scar ids, and stop the matcher dropping one ([#327](https://github.com/Daily-Nerd/Scar/issues/327)) ([210f4c7](https://github.com/Daily-Nerd/Scar/commit/210f4c77197fa603d8659f9717ef8350277fc6f8))
17
+ * **match:** symbol anchor is a content signal only when the edit mentions the symbol ([#338](https://github.com/Daily-Nerd/Scar/issues/338)) ([0a1aac6](https://github.com/Daily-Nerd/Scar/commit/0a1aac63c8118fb3701c56fd6a9c03de08fe3581))
18
+ * **orphan:** count symbol anchors in orphan detection and report one that no longer resolves ([#337](https://github.com/Daily-Nerd/Scar/issues/337)) ([#342](https://github.com/Daily-Nerd/Scar/issues/342)) ([321f9de](https://github.com/Daily-Nerd/Scar/commit/321f9de04f18630d6354ee6cd204b0dba67f0bda))
19
+
20
+
21
+ ### Documentation
22
+
23
+ * **anchors:** say path anchors are prefixes, and teach the skill when to use symbol anchors ([#340](https://github.com/Daily-Nerd/Scar/issues/340)) ([7babb9f](https://github.com/Daily-Nerd/Scar/commit/7babb9face9116f9dfa5884a14cefdeb6b9bee90))
24
+
25
+ ## [0.24.0](https://github.com/Daily-Nerd/Scar/compare/v0.23.0...v0.24.0) (2026-09-08)
26
+
27
+
28
+ ### Features
29
+
30
+ * **firing-log:** one shared validated reader for the firing log ([#313](https://github.com/Daily-Nerd/Scar/issues/313)) ([d316922](https://github.com/Daily-Nerd/Scar/commit/d31692205715e63380b8ac104643de4ceac0338d))
31
+ * **skill:** install the authoring skill to every detected host ([#320](https://github.com/Daily-Nerd/Scar/issues/320)) ([1d9c985](https://github.com/Daily-Nerd/Scar/commit/1d9c985e3e5815ba67d222defa86d549886c7e94))
32
+
33
+
34
+ ### Bug Fixes
35
+
36
+ * **promote:** record whether a promotion happened at a terminal ([#315](https://github.com/Daily-Nerd/Scar/issues/315)) ([f28e8eb](https://github.com/Daily-Nerd/Scar/commit/f28e8eb90a5fb3161aa40565dbfb218d1b9b4160)), closes [#308](https://github.com/Daily-Nerd/Scar/issues/308)
37
+
3
38
  ## [0.23.0](https://github.com/Daily-Nerd/Scar/compare/v0.22.0...v0.23.0) (2026-09-03)
4
39
 
5
40
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: scar-cli
3
- Version: 0.23.0
3
+ Version: 0.25.0
4
4
  Summary: SCAR — version control for negative knowledge (deadends, fences, landmines)
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -80,6 +80,8 @@ scar skill install # same detection for the scar-authoring skill
80
80
  scar hook status # every host with the channel that serves it (settings, plugin, none)
81
81
  ```
82
82
 
83
+ `scar skill install` writes the scar-authoring skill to a native path for Claude Code, Codex, Cursor, opencode, or Windsurf. Claude Code is the one confirmed to load it, and the rest are written per vendor documentation but unverified: see the [per-host table](https://daily-nerd.github.io/Scar/agents#where-scar-skill-install-writes) for which is which.
84
+
83
85
  Claude Code users: the marketplace **plugin** ships the hooks and the scar-authoring skill together. `scar hook install` and `scar skill install` see the plugin and skip Claude unless you pass `--runtime claude --force`. Re-run `scar skill install` after upgrading a manual install; the installed skill is a static copy.
84
86
 
85
87
  Codex users: run `scar hook install --runtime codex`, then open `/hooks` in Codex to review and trust the entries. The hooks push matching scars before `Bash` and `apply_patch` calls and report post-patch `violation:` tripwires without blocking. The plugin no longer carries Codex hook definitions; the installer is the one path.
@@ -65,6 +65,8 @@ scar skill install # same detection for the scar-authoring skill
65
65
  scar hook status # every host with the channel that serves it (settings, plugin, none)
66
66
  ```
67
67
 
68
+ `scar skill install` writes the scar-authoring skill to a native path for Claude Code, Codex, Cursor, opencode, or Windsurf. Claude Code is the one confirmed to load it, and the rest are written per vendor documentation but unverified: see the [per-host table](https://daily-nerd.github.io/Scar/agents#where-scar-skill-install-writes) for which is which.
69
+
68
70
  Claude Code users: the marketplace **plugin** ships the hooks and the scar-authoring skill together. `scar hook install` and `scar skill install` see the plugin and skip Claude unless you pass `--runtime claude --force`. Re-run `scar skill install` after upgrading a manual install; the installed skill is a static copy.
69
71
 
70
72
  Codex users: run `scar hook install --runtime codex`, then open `/hooks` in Codex to review and trust the entries. The hooks push matching scars before `Bash` and `apply_patch` calls and report post-patch `violation:` tripwires without blocking. The plugin no longer carries Codex hook definitions; the installer is the one path.
@@ -43,7 +43,7 @@ is deliberate: consumers in hook hot-paths parse with zero dependencies.
43
43
  | `created` | yes | ISO date | |
44
44
  | `authors` | yes | inline list | who drafted it: agents as `"claude-code"` etc. The reviewer is no longer appended here |
45
45
  | `promoted_by` | active scars | string | the human who promoted it. Written only by `scar promote`, which overwrites anything a candidate carried. Empty on scars promoted before the field existed, and never inferred from `authors` |
46
- | `promoted_by_source` | optional | `explicit` \| `git-config` | whether `promoted_by` came from `--reviewer` or from `git config user.name`. Written only alongside `promoted_by`, never on its own. Absent on scars promoted before the field existed |
46
+ | `promoted_by_source` | optional | `explicit` \| `git-config-interactive` \| `git-config` | how strongly `promoted_by` is evidenced. `explicit` = a name was typed with `--reviewer`. `git-config-interactive` = no name typed, but `promote` ran at a terminal, so a person was there. `git-config` = no name typed and no terminal recorded, which is the unattended shape `lint` warns on. Written only alongside `promoted_by`, never on its own. Absent on scars promoted before the field existed, and plain `git-config` on scars promoted before interactivity was recorded, so neither absence proves an agent did it |
47
47
  | `anchors` | yes, ≥1 | list of `- path:`, `- pattern:`, `- symbol:`, and/or `- command:` | §4 |
48
48
  | `evidence` | recommended | list of `- commit:`/`- pr:`/`- issue:`/`- incident:`/`- note:`/`- url:` | absent ⇒ challengeable on sight; `issue:`/`url:` are durable forms that survive squash-merge |
49
49
  | `expires.condition` | recommended | quoted string | what change obsoletes this scar |
@@ -59,6 +59,8 @@ Do not reintroduce Redis (or any evicting store) for sessions unless sessions
59
59
  become stateless/re-derivable. Postgres-backed sessions are intentional.
60
60
  ```
61
61
 
62
+ An `id` identifies one scar. `scar lint` reports an id carried by more than one numbered scar file as an error on each of them (two branches that each promote a candidate both take the next number), naming the other files and the next free id. It does not renumber: firing-log rows and evidence notes already reference the number, so which scar keeps it is a human call. Candidates are not checked: promotion assigns their id, and they never fire.
63
+
62
64
  ### Type semantics
63
65
 
64
66
  - **`deadend`** — protects against *re-attempting an approach*. Primary anchor is often a `pattern` (the approach reappearing anywhere), not a location.
@@ -71,18 +73,20 @@ The hard technical problem. Line numbers are dead on arrival; file paths die on
71
73
 
72
74
  Four anchor classes, used in combination:
73
75
 
74
- 1. **Path anchors** — file or directory globs. Cheap, survive content change, die on rename (mitigated by git rename tracking during re-anchor).
76
+ 1. **Path anchors** — file or directory prefixes. A directory with a trailing slash or a file path both work; wildcards are not expanded. Cheap, survive content change, die on rename (mitigated by git rename tracking during re-anchor).
75
77
  2. **Symbol anchors** — function/class/method names resolved via tree-sitter. Survive moves within and across files in the same repo. Primary anchor class for fences.
78
+ For orphan detection (#337) a symbol anchor is live while its definition resolves: `path::name` in that file, a bare name in any tracked file the `[symbols]` extra can parse. Without the extra it cannot be checked, so it holds the scar alive the way a command anchor does, and `scar lint` prints one `symbols-unchecked` hint per run.
76
79
  3. **Pattern anchors** — regex/AST patterns over *new* code (diff-scoped, not whole-repo). The only anchor class that can catch a dead end being re-attempted in a brand-new file. Powers `deadend` enforcement.
77
80
  4. **Command anchors** (#175) — regexes over a *shell command about to execute* (`PreToolUse:Bash` and `scar inject --command`). The only anchor class with a firing surface for run-a-command mistakes — knowledge like "bare `uv sync` strips extras" has no edit to anchor to. Never matched against paths or content (structurally immune to self-match/partial-rot) and exempt from content liveness; `review_after` is the freshness mechanism.
78
81
 
79
- Plus a **content fingerprint** (normalized-token hash of the protected region) used not for matching but for *drift detection*: fingerprint drift is an advisory warning surfaced by `scar orphan` and `scar lint`. The `orphaned` transition itself is driven by the location anchors — a scar orphans when all of its path/pattern anchors go dead — and surfaces in `scar status` and CI as "this knowledge has come loose — re-anchor or archive." Orphaned ≠ deleted, ever.
82
+ Plus a **content fingerprint** (normalized-token hash of the protected region) used not for matching but for *drift detection*: fingerprint drift is an advisory warning surfaced by `scar orphan` and `scar lint`. The `orphaned` transition itself is driven by the location anchors (a scar orphans when all of its path/pattern/symbol anchors go dead) and surfaces in `scar status` and CI as "this knowledge has come loose: re-anchor or archive." Orphaned ≠ deleted, ever.
80
83
 
81
84
  ## 3. CLI surface (v0)
82
85
 
83
86
  ```
84
87
  scar init # create .scars/ with a seeded example candidate; installs nothing (hooks are explicit: scar hook install)
85
88
  scar check <path|diff> # scars relevant to a path or staged diff; exit code for CI
89
+ scar sweep # run every armed violation over the whole tree; read-only, exit 0 unless --exit-code
86
90
  scar why <path> # human-readable history of pain for a file/dir
87
91
  scar challenge <id> # open a challenge: contest staleness with evidence
88
92
  scar harvest # mine git history, emit candidate scars to .scars/candidates/
@@ -117,7 +121,7 @@ Also `PostToolUse`/stop-hook prompt: *"You appear to have abandoned approach X a
117
121
 
118
122
  ### 4.3 Ranking and the fatigue budget
119
123
 
120
- Hard rule: **max 3 scars injected per edit, max ~120 words each.** A scar system that warns constantly is a scar system that gets uninstalled. Ranking: `severity_weight × confidence × anchor_specificity`, sorted descending (`confidence` is the static authored weight — see §5). Everything else is reachable via `scar why` but not pushed.
124
+ Hard rule: **max 3 scars injected per edit, max ~120 words each.** A scar system that warns constantly is a scar system that gets uninstalled. Ranking: `severity_weight × confidence × anchor_specificity`, sorted descending (`confidence` is the static authored weight, see §5). Everything else is reachable via `scar why` but not pushed. The cut is never silent (#321): when more scars matched than the cap allows, the injection header reads `3 of 5 matched shown` (the count is distinct scars, taken before the cut). With no census the header makes no claim about a total, and when nothing was cut it is unchanged.
121
125
 
122
126
  ## 5. Lifecycle
123
127
 
@@ -167,8 +171,8 @@ All harvest output is `candidates/`, never active. Precision over recall: a harv
167
171
 
168
172
  ## 9. Machine-readable output contract
169
173
 
170
- Nine subcommands accept `--json`: `lint`, `status`, `check`, `why`, `stats`,
171
- `gc`, `orphan`, `reanchor`, `draft-check`. Two more are machine-mode by
174
+ Ten subcommands accept `--json`: `lint`, `status`, `check`, `sweep`, `why`,
175
+ `stats`, `gc`, `orphan`, `reanchor`, `draft-check`. Two more are machine-mode by
172
176
  design: `scar inject` (hook JSON or silence) and `scar brief --compact`.
173
177
 
174
178
  Everything in this section is a **promise to external consumers**. It exists
@@ -221,9 +225,10 @@ Top-level keys, and the keys of each object inside the listed arrays.
221
225
 
222
226
  | Command | Guaranteed top-level | Array item keys |
223
227
  |---|---|---|
224
- | `lint` | `files` int, `findings` array, `failed` int, `orphans`, `partial_rot`, `symbol_drift`, `revivals`, `reverse_hints`, `unreachable_evidence` arrays, `shallow_clone` bool | `findings[]`: `file`, `level`, `message` |
228
+ | `lint` | `files` int, `findings` array, `failed` int, `orphans`, `partial_rot`, `symbol_drift`, `revivals`, `reverse_hints`, `unreachable_evidence` arrays, `symbols_unchecked` array of int, `shallow_clone` bool | `findings[]`: `file`, `level`, `message` |
225
229
  | `status` | `scars_dir` str, `active`, `challenged`, `candidates`, `review_due`, `orphan_detected`, `orphaned`, `partial_rot`, `broken` arrays, `counts` object | `active[]`: `id`, `type`, `severity`, `title`. `candidates[]` are strings. `counts`: `active`, `candidates`, `orphan_detected`, `orphaned`, `partial_rot`, `broken` |
226
230
  | `check` | `paths` array of str, `scars` array | `scars[]`: `id`, `type`, `severity`, `confidence`, `status`, `title`, `body` |
231
+ | `sweep` | `files` int (tracked files swept), `skipped` int (tracked files with no readable content: oversized or binary), `scars` int (scars armed on at least one swept file), `hits` array | `hits[]`: `id` int, `type`, `severity`, `title`, `source` (scar file, repo-relative), `path`, `line` int (1-based), `excerpt` |
227
232
  | `why` | `path` str, `records` array | `records[]`: `id`, `type`, `status`, `title`, `file`, `body` |
228
233
  | `stats` | `repo` str, `total_firings` int, `per_scar` array, `most_fired` int, `last_fired` str, `never_fired` array of int, `demotions` int, `demotions_path_only` int, `demotions_cooldown` int, `demotions_reason_unknown` int, `census_known` int, `census_unknown` int, `cofires_per_edit` float **or null**, `edits_multi_fire` int, `path_only_ratio` float **or null**, `edits_observed` int, `injection_rate` float **or null**, `retrieval_misses` int **or null**, `instrument_disconnected` bool, `posttool_silent` bool, `last_fired_age_days` int, `armed_firings` int, `armed_unknown` int, `verdicts_expected` int, `verdicts_observed` int, `verdicts_unresolved` int, `verdicts_unplaceable` int, `firings_block_capable` int, `firings_advisory` int, `firings_block_unknown` int, `all_firings_advisory` bool, `firings_context_known` int, `firings_context_unknown` int, `command_firings` int, `firings_kind_unknown` int, `advisories` array. `window` object **only when `--since`/`--until` is given** | `per_scar[]`: `id`, `count`, `violations`. `window`: `since`, `until`, `excluded_undated` |
229
234
  | `gc` | `removed_markers` int, `dropped_firings` int, `dry_run` bool, `candidates` array, `fp_log` object | `candidates[]`: `name`, `age_days`. `fp_log`: `present`, `size`, `lines` |
@@ -373,8 +378,8 @@ CI, and never emits JSON.
373
378
  A scar rendered as a one-liner was demoted for one of two reasons, and they are
374
379
  opposite evidence. A **path-only** demotion means the anchor proved the file
375
380
  was in scope and nothing in the edit matched: a near miss. A **cooldown**
376
- demotion means the edit content matched (a pattern hit, a symbol resolved, a
377
- command matched) and the full body was withheld only because it had been shown
381
+ demotion means the edit content matched (a pattern hit, a symbol the edit
382
+ names, a command matched) and the full body was withheld only because it had been shown
378
383
  in the last four hours: a hit we chose not to repeat.
379
384
 
380
385
  `demoted_ids` lists both in one field. `demotion_reasons` records which is
@@ -402,9 +407,18 @@ never recoverable from the log.
402
407
 
403
408
  `matched` records what `_match_target` returned before `_select_top` truncated
404
409
  it: `{"total": n, "content": c, "path_only": p}` with `content + path_only ==
405
- total`. `content` matched the edit itself (a pattern hit, a symbol resolved, a
406
- command matched). `path_only` matched nothing but the file's location. The
407
- split is the same one the fatigue budget uses to tier, counted before it cuts.
410
+ total`. `content` matched the edit itself (a pattern hit, a symbol the edit
411
+ names, a command matched). `path_only` matched nothing but the file's location.
412
+ The split is the same one the fatigue budget uses to tier, counted before it
413
+ cuts.
414
+
415
+ A symbol anchor yields one of two `matched_by` kinds. `symbol` means the file
416
+ on disk defines the symbol and the edited text names it as a whole word (the
417
+ innermost name, so `src/a.py::Store.save` looks for `save`); it is a content
418
+ signal. `symbol_file` means the file defines the symbol but the edited text
419
+ never names it; it counts as `path_only`, like a path anchor. The hook sees the
420
+ new text without line numbers, so a name mention is the strongest proof
421
+ available that the edit touched the symbol.
408
422
 
409
423
  The key is written by every shipped writer (edit, command, Codex, Cascade). It
410
424
  is **omitted** by a writer that did not count, never zeroed: zeros would claim
@@ -458,6 +472,18 @@ denominators changed.
458
472
  hook recorded **anything**, so command rows and kind-unknown rows answer it
459
473
  just as well as edit rows do.
460
474
 
475
+ `matched_by` on a firing row says which anchor kinds matched each scar that
476
+ fired, keyed by the scar id as a string, kinds in rank order:
477
+ `{"24": ["path", "symbol"], "1": ["content_pattern"]}`. `scar_ids` says that a
478
+ scar fired and `matched` is a census for the whole edit; neither says what a
479
+ given scar matched on. The kinds are opaque strings copied from the matcher, so
480
+ a new anchor kind appears here without a format change.
481
+
482
+ Every scar that fired has a non-empty list. The key is **omitted**, never
483
+ emptied, when a writer has no match objects in hand, and a MISSING `matched_by`
484
+ means the row predates the field, never "no kinds". Readers must not treat its
485
+ absence as a value.
486
+
461
487
  ### 9.4 Enforcement
462
488
 
463
489
  `tests/test_json_contract.py` asserts every guaranteed key above against live
@@ -468,3 +494,5 @@ ones that reach `main` wrong.
468
494
  ### 9.10 Install detection
469
495
 
470
496
  `scar hook install` and `scar skill install` without `--runtime` detect hosts (claude, codex, windsurf, cursor, opencode) by config directory or binary on PATH, and resolve for each the channel that already serves it: `settings` (Scar's own entries in the host's hook file), `plugin` (the scar marketplace plugin installed and not disabled), or `none`. windsurf is repo-scoped: its hooks file is written into the working repo, so the binary on PATH counts only when that repo is inside a git repository. On a terminal, one question per present, wirable, unserved host, default no. Without a terminal, exactly one such host is installed; zero or several means the commands are printed and nothing is written. Zero detected hosts says so, rather than naming the plugin. `--all` wires every such host. `--runtime claude` on a Claude Code already served by the plugin refuses unless `--force`. `--all` and `--force` apply to `install` only, and `--force` applies to `--runtime claude` only. A missing or unreadable plugin registry never counts as `plugin`.
497
+
498
+ Two rules differ for the skill kind. windsurf is not repo-scoped there: its global skills live under home (`~/.codeium/windsurf/skills`), so the binary on PATH counts on its own, with no git-repo guard. And `scar skill install --runtime <host>` refuses, exit 1, when detection does not find that host on the machine, rather than creating a skill directory nothing reads. Every destination resolves through the same home the detection pass used, and the Codex destination honours `$CODEX_HOME`, so a reported success always names a directory the detected host actually reads.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scar",
3
- "version": "0.23.0",
3
+ "version": "0.25.0",
4
4
  "description": "SCAR — version control for negative knowledge (deadends, fences, landmines)",
5
5
  "hooks": {
6
6
  "PreToolUse": [
@@ -96,6 +96,9 @@ self-match).
96
96
 
97
97
  - `path:` = repo-relative prefix (file or directory). `pattern:` =
98
98
  case-insensitive regex over path + new content.
99
+ - `symbol:` = `path::name` or a bare name. Use it for code likely to move or be
100
+ renamed: it follows the definition, not the path. Needs the `[symbols]` extra
101
+ (`uv tool install "scar-cli[symbols]"`), Python, TypeScript and JavaScript.
99
102
  - Severity: `low | medium | high | critical`.
100
103
  - Injection is capped at ~3 scars / ~700 chars each — write tight: 5–15 lines,
101
104
  evidence cited inline.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "scar-cli"
3
- version = "0.23.0"
3
+ version = "0.25.0"
4
4
  description = "SCAR — version control for negative knowledge (deadends, fences, landmines)"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -38,12 +38,13 @@ from .hooks import (
38
38
  _extract_edit_content,
39
39
  _log_firing,
40
40
  _log_violation_firing,
41
+ _matched_by_of,
41
42
  _read_payload,
42
43
  _state_dir,
43
44
  )
44
45
  from .match import (MatchCensus, armed_scar_ids, find_violations,
45
46
  has_content_signal, rank_and_census_for_edit)
46
- from .render import compact_block, rule_line
47
+ from .render import compact_block, cut_total, rule_line
47
48
  from .store import ScarStore
48
49
 
49
50
  # NOT 2, which is what Cascade reads as "block". argparse also exits 2 on an
@@ -120,16 +121,19 @@ def _remember_fired(keys: list[str]) -> bool:
120
121
  return True
121
122
 
122
123
 
123
- def _block_text(scars: list) -> str:
124
+ def _block_text(scars: list, matched_total: int | None = None) -> str:
124
125
  """What the agent reads on the bounced action. Compact tier only — this
125
126
  renders as an error in the Cascade UI, and the agent needs the constraint,
126
- not the essay."""
127
+ not the essay. When the fatigue cap cut matches, the closing line says so
128
+ in a clause (#321); no census, or one that shows no cut, changes nothing."""
129
+ cut = cut_total(len(scars), matched_total)
130
+ shown = "" if cut is None else f"{len(scars)} of {cut} matched shown. "
127
131
  return (
128
132
  f"SCAR blocked this action once — {len(scars)} scar(s) record why it "
129
133
  "went wrong before. Read them, then retry: the identical action goes "
130
134
  "through, informed.\n\n"
131
135
  + compact_block(scars)
132
- + "\n\nFull records in .scars/ (`scar why <path>`).")
136
+ + f"\n\n{shown}Full records in .scars/ (`scar why <path>`).")
133
137
 
134
138
 
135
139
  def _partition(matches: list, fired: dict[str, float], trajectory: str,
@@ -173,7 +177,10 @@ def _respond(store: ScarStore, trajectory: str, target: str,
173
177
  if not to_block:
174
178
  return 0
175
179
  scars = [m.scar for m in to_block]
176
- print(_block_text(scars), file=sys.stderr)
180
+ # Only a cap cut counts: matches the partition above sent to stdout instead
181
+ # of the block were not cut, so the census must exceed the whole list.
182
+ capped = census.total if census and census.total > len(matches) else None
183
+ print(_block_text(scars, capped), file=sys.stderr)
177
184
  # Firing log parity (#106) records what the AGENT saw, and here only a
178
185
  # block reaches it — a stdout line went to the human. So the surface-only
179
186
  # matches are NOT logged, and in particular not as `demoted_ids`: that
@@ -184,7 +191,8 @@ def _respond(store: ScarStore, trajectory: str, target: str,
184
191
  # refuse the action. Surface-only matches never reach the log at all
185
192
  # (see the comment above), so every windsurf row is block-capable.
186
193
  _log_firing(store, target, scars, runtime=RUNTIME, block_capable=True,
187
- matched=census, anchor_kind=anchor_kind)
194
+ matched=census, anchor_kind=anchor_kind,
195
+ matched_by=_matched_by_of(to_block))
188
196
  return BLOCK_EXIT
189
197
 
190
198