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