scar-cli 0.2.0__tar.gz → 0.4.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.2.0 → scar_cli-0.4.0}/.scars/0003-installer-binds-to-active-venv-scar.landmine.md +2 -1
- {scar_cli-0.2.0 → scar_cli-0.4.0}/.scars/0004-promote-roundtrip-drops-expires-evidence.landmine.md +2 -1
- scar_cli-0.2.0/.scars/candidates/history-rewrite-orphans-commit-evidence.md → scar_cli-0.4.0/.scars/0005-history-rewrite-orphans-commit-evidence.landmine.md +4 -3
- scar_cli-0.4.0/AGENTS.md +43 -0
- scar_cli-0.4.0/CHANGELOG.md +44 -0
- {scar_cli-0.2.0 → scar_cli-0.4.0}/PKG-INFO +27 -2
- {scar_cli-0.2.0 → scar_cli-0.4.0}/README.md +26 -1
- scar_cli-0.4.0/ROADMAP.md +48 -0
- {scar_cli-0.2.0 → scar_cli-0.4.0}/SCAR-FORMAT.md +11 -0
- {scar_cli-0.2.0 → scar_cli-0.4.0}/SPEC.md +5 -2
- scar_cli-0.4.0/hook/scar-hooks.py +15 -0
- {scar_cli-0.2.0 → scar_cli-0.4.0}/pyproject.toml +1 -1
- scar_cli-0.4.0/src/scar/agent.py +66 -0
- {scar_cli-0.2.0 → scar_cli-0.4.0}/src/scar/cli.py +71 -46
- {scar_cli-0.2.0 → scar_cli-0.4.0}/src/scar/hooks.py +4 -19
- scar_cli-0.2.0/hook/scar-hooks.py → scar_cli-0.4.0/src/scar/installer.py +33 -46
- scar_cli-0.4.0/src/scar/match.py +147 -0
- scar_cli-0.4.0/src/scar/mcp.py +235 -0
- scar_cli-0.4.0/src/scar/render.py +38 -0
- {scar_cli-0.2.0 → scar_cli-0.4.0}/src/scar/store.py +9 -0
- {scar_cli-0.2.0 → scar_cli-0.4.0}/tests/test_cli.py +36 -0
- {scar_cli-0.2.0 → scar_cli-0.4.0}/tests/test_installer.py +38 -0
- {scar_cli-0.2.0 → scar_cli-0.4.0}/tests/test_match.py +64 -1
- scar_cli-0.4.0/tests/test_mcp.py +92 -0
- {scar_cli-0.2.0 → scar_cli-0.4.0}/tests/test_store.py +12 -0
- {scar_cli-0.2.0 → scar_cli-0.4.0}/uv.lock +1 -1
- scar_cli-0.2.0/CHANGELOG.md +0 -20
- scar_cli-0.2.0/ROADMAP.md +0 -46
- scar_cli-0.2.0/src/scar/match.py +0 -51
- {scar_cli-0.2.0 → scar_cli-0.4.0}/.github/workflows/ci.yml +0 -0
- {scar_cli-0.2.0 → scar_cli-0.4.0}/.github/workflows/pr-validation.yml +0 -0
- {scar_cli-0.2.0 → scar_cli-0.4.0}/.github/workflows/release.yml +0 -0
- {scar_cli-0.2.0 → scar_cli-0.4.0}/.gitignore +0 -0
- {scar_cli-0.2.0 → scar_cli-0.4.0}/.scars/0001-git-grep-ere-pitfalls.landmine.md +0 -0
- {scar_cli-0.2.0 → scar_cli-0.4.0}/.scars/0002-agent-direct-hook-install.deadend.md +0 -0
- {scar_cli-0.2.0 → scar_cli-0.4.0}/.scars/README.md +0 -0
- {scar_cli-0.2.0 → scar_cli-0.4.0}/.scars/candidates/fp-log.txt +0 -0
- {scar_cli-0.2.0 → scar_cli-0.4.0}/.scars/template.md +0 -0
- {scar_cli-0.2.0 → scar_cli-0.4.0}/CONTRIBUTING.md +0 -0
- {scar_cli-0.2.0 → scar_cli-0.4.0}/IDEA.md +0 -0
- {scar_cli-0.2.0 → scar_cli-0.4.0}/LICENSE +0 -0
- {scar_cli-0.2.0 → scar_cli-0.4.0}/STRESS-TEST.md +0 -0
- {scar_cli-0.2.0 → scar_cli-0.4.0}/experiments/anchor-survival/PROTOCOL.md +0 -0
- {scar_cli-0.2.0 → scar_cli-0.4.0}/experiments/anchor-survival/RESULTS.md +0 -0
- {scar_cli-0.2.0 → scar_cli-0.4.0}/experiments/anchor-survival/long_replay.py +0 -0
- {scar_cli-0.2.0 → scar_cli-0.4.0}/experiments/anchor-survival/replay.py +0 -0
- {scar_cli-0.2.0 → scar_cli-0.4.0}/experiments/auto-authorship/FINDINGS.md +0 -0
- {scar_cli-0.2.0 → scar_cli-0.4.0}/experiments/auto-authorship/PROTOCOL.md +0 -0
- {scar_cli-0.2.0 → scar_cli-0.4.0}/experiments/fence-honor/.gitignore +0 -0
- {scar_cli-0.2.0 → scar_cli-0.4.0}/experiments/fence-honor/PROTOCOL.md +0 -0
- {scar_cli-0.2.0 → scar_cli-0.4.0}/experiments/fence-honor/RESULTS.md +0 -0
- {scar_cli-0.2.0 → scar_cli-0.4.0}/experiments/fence-honor/fixture/.scars/0001-vendor-retry-window.fence.md +0 -0
- {scar_cli-0.2.0 → scar_cli-0.4.0}/experiments/fence-honor/fixture/.scars/0002-evicting-session-store.deadend.md +0 -0
- {scar_cli-0.2.0 → scar_cli-0.4.0}/experiments/fence-honor/fixture/.scars/0003-export-column-order.landmine.md +0 -0
- {scar_cli-0.2.0 → scar_cli-0.4.0}/experiments/fence-honor/fixture/README.md +0 -0
- {scar_cli-0.2.0 → scar_cli-0.4.0}/experiments/fence-honor/fixture/payments/retry.py +0 -0
- {scar_cli-0.2.0 → scar_cli-0.4.0}/experiments/fence-honor/fixture/reports/export.py +0 -0
- {scar_cli-0.2.0 → scar_cli-0.4.0}/experiments/fence-honor/fixture/services/sessions.py +0 -0
- {scar_cli-0.2.0 → scar_cli-0.4.0}/experiments/fence-honor/grade.py +0 -0
- {scar_cli-0.2.0 → scar_cli-0.4.0}/experiments/harvest/harvest.py +0 -0
- {scar_cli-0.2.0 → scar_cli-0.4.0}/src/scar/__init__.py +0 -0
- {scar_cli-0.2.0 → scar_cli-0.4.0}/src/scar/harvest.py +0 -0
- {scar_cli-0.2.0 → scar_cli-0.4.0}/src/scar/lint.py +0 -0
- {scar_cli-0.2.0 → scar_cli-0.4.0}/src/scar/model.py +0 -0
- {scar_cli-0.2.0 → scar_cli-0.4.0}/tests/test_harvest.py +0 -0
- {scar_cli-0.2.0 → scar_cli-0.4.0}/tests/test_hooks.py +0 -0
- {scar_cli-0.2.0 → scar_cli-0.4.0}/tests/test_lifecycle.py +0 -0
- {scar_cli-0.2.0 → scar_cli-0.4.0}/tests/test_lint.py +0 -0
- {scar_cli-0.2.0 → scar_cli-0.4.0}/tests/test_model.py +0 -0
{scar_cli-0.2.0 → scar_cli-0.4.0}/.scars/0003-installer-binds-to-active-venv-scar.landmine.md
RENAMED
|
@@ -11,7 +11,8 @@ anchors:
|
|
|
11
11
|
- pattern: "shutil\\.which\\([\"']scar[\"']\\)"
|
|
12
12
|
evidence:
|
|
13
13
|
- note: 2026-06-11 session: user ran `source .venv/bin/activate` then `python3 fabcap/hook/scar-hooks.py install` intending to rebind global hooks to ~/.local/bin/scar; installer reported 'up-to-date' and left all 3 hooks on fabcap/.venv/bin/scar
|
|
14
|
-
|
|
14
|
+
- note: archived 2026-06-11: fix shipped in #8 with 4 installer tests guarding regression
|
|
15
|
+
status: archived
|
|
15
16
|
---
|
|
16
17
|
|
|
17
18
|
`install()` resolves the scar binary with `shutil.which("scar")` and writes that
|
{scar_cli-0.2.0 → scar_cli-0.4.0}/.scars/0004-promote-roundtrip-drops-expires-evidence.landmine.md
RENAMED
|
@@ -11,7 +11,8 @@ anchors:
|
|
|
11
11
|
- pattern: "_field\(front"
|
|
12
12
|
evidence:
|
|
13
13
|
- note: Observed 2026-06-11 promoting 5 candidates in context-as-program (PR 3 there restores the data by hand). All 5 lost their expires block; one also lost evidence because its note contained escaped quotes.
|
|
14
|
-
|
|
14
|
+
- note: archived 2026-06-11: expires.condition met by #10: parser reads nested keys and quote evidence, roundtrip test guards it
|
|
15
|
+
status: archived
|
|
15
16
|
---
|
|
16
17
|
|
|
17
18
|
promote() does parse -> mutate -> to_text. Two asymmetries make that lossy.
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
---
|
|
2
|
+
id: 5
|
|
2
3
|
type: landmine
|
|
3
4
|
title: rewriting git history orphans commit-SHA evidence in scars — receipts break in fresh clones
|
|
4
5
|
severity: medium
|
|
5
6
|
confidence: 0.9
|
|
6
7
|
created: 2026-06-11
|
|
7
|
-
authors: ["claude-code"]
|
|
8
|
+
authors: ["claude-code", "Kibukx"]
|
|
8
9
|
anchors:
|
|
9
10
|
- path: .scars/
|
|
10
11
|
- pattern: "push.{0,30}(--force|\\+[a-zA-Z]).{0,40}main|filter-repo|checkout --orphan"
|
|
11
12
|
evidence:
|
|
12
|
-
- note:
|
|
13
|
+
- 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
|
|
13
14
|
expires:
|
|
14
15
|
condition: "evidence schema gains a resolvable form (full URL or archived diff) or lint warns on bare SHAs at promotion"
|
|
15
16
|
review_after: 2027-06-11
|
|
16
|
-
status:
|
|
17
|
+
status: active
|
|
17
18
|
---
|
|
18
19
|
|
|
19
20
|
Scars cite commit SHAs as evidence receipts. Those receipts implicitly assume
|
scar_cli-0.4.0/AGENTS.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
## Project Overview
|
|
4
|
+
|
|
5
|
+
SCAR records negative knowledge for a repository: failed approaches
|
|
6
|
+
(`deadend`), intentional weirdness (`fence`), and non-obvious coupling
|
|
7
|
+
(`landmine`). Scars live in `.scars/` as Markdown files with mandatory YAML
|
|
8
|
+
frontmatter.
|
|
9
|
+
|
|
10
|
+
## Agent Contract
|
|
11
|
+
|
|
12
|
+
- Before editing anchored code, query SCAR with `scar inject --path <path>` or,
|
|
13
|
+
when you have a diff, `scar inject --diff <unified-diff>`.
|
|
14
|
+
- Honor injected scars unless the user explicitly overrides them.
|
|
15
|
+
- New scars always start in `.scars/candidates/`; never write directly to
|
|
16
|
+
active `.scars/*.md` files.
|
|
17
|
+
- A human promotes candidates with `scar promote`.
|
|
18
|
+
- Do not silently ignore broken scar files. Run `scar lint` when changing scar
|
|
19
|
+
format, parsing, promotion, lifecycle, or candidate-writing behavior.
|
|
20
|
+
|
|
21
|
+
## Agent Integrations
|
|
22
|
+
|
|
23
|
+
- MCP-capable agents can launch the local server with `scar mcp`.
|
|
24
|
+
- Integration snippets are available with:
|
|
25
|
+
- `scar agent config codex`
|
|
26
|
+
- `scar agent config cursor`
|
|
27
|
+
- `scar agent config opencode`
|
|
28
|
+
- `scar agent config windsurf`
|
|
29
|
+
- Check local readiness with `scar agent doctor`.
|
|
30
|
+
|
|
31
|
+
## Development Commands
|
|
32
|
+
|
|
33
|
+
- Run tests: `uv run pytest`
|
|
34
|
+
- Run focused tests: `uv run pytest tests/test_cli.py tests/test_match.py`
|
|
35
|
+
- Lint scars: `uv run scar lint`
|
|
36
|
+
|
|
37
|
+
## Repository Rules
|
|
38
|
+
|
|
39
|
+
- Do not add AI attribution or `Co-Authored-By` lines to commits.
|
|
40
|
+
- Use conventional commit messages.
|
|
41
|
+
- Do not build after changes.
|
|
42
|
+
- Keep runtime dependencies at zero unless there is a strong architectural
|
|
43
|
+
reason and tests/docs are updated with the tradeoff.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.4.0](https://github.com/Daily-Nerd/Scar/compare/v0.3.0...v0.4.0) (2026-06-12)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **format:** reserve optional receipt_id field ([#29](https://github.com/Daily-Nerd/Scar/issues/29)) ([47ce933](https://github.com/Daily-Nerd/Scar/commit/47ce933cde02fa1155d0474e98101804cb7b1a80))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **hooks:** expose lifecycle commands ([#31](https://github.com/Daily-Nerd/Scar/issues/31)) ([dba2c0d](https://github.com/Daily-Nerd/Scar/commit/dba2c0d1c1bd8a0f73880bfab0ff17187eec2fb9)), closes [#30](https://github.com/Daily-Nerd/Scar/issues/30)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Documentation
|
|
17
|
+
|
|
18
|
+
* **roadmap:** truth pass — gates resolved, Phase 1 shipped, Phase 2 in progress ([#26](https://github.com/Daily-Nerd/Scar/issues/26)) ([7701a97](https://github.com/Daily-Nerd/Scar/commit/7701a97610f470e7726e7f5fc86932a5101eb255))
|
|
19
|
+
|
|
20
|
+
## [0.3.0](https://github.com/Daily-Nerd/Scar/compare/v0.2.0...v0.3.0) (2026-06-12)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Features
|
|
24
|
+
|
|
25
|
+
* **agents:** multi-agent scar integration — AGENTS.md, MCP server, agent helpers ([#21](https://github.com/Daily-Nerd/Scar/issues/21)) ([52c817f](https://github.com/Daily-Nerd/Scar/commit/52c817fc963f8f829b70de60b772c1097c6f0334))
|
|
26
|
+
|
|
27
|
+
## [0.2.0](https://github.com/Daily-Nerd/Scar/compare/v0.1.1...v0.2.0) (2026-06-12)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
### Features
|
|
31
|
+
|
|
32
|
+
* **cli:** lifecycle v0 — challenge, archive, review_after surfacing ([#16](https://github.com/Daily-Nerd/Scar/issues/16)) ([0c6fb05](https://github.com/Daily-Nerd/Scar/commit/0c6fb05fbdbb57f8ac9b2a5b558e4cf121c3d5c0)), closes [#14](https://github.com/Daily-Nerd/Scar/issues/14)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
### Documentation
|
|
36
|
+
|
|
37
|
+
* **readme:** scar challenge is planned, not shipped — point to lifecycle issue ([8c6b021](https://github.com/Daily-Nerd/Scar/commit/8c6b021c95299cf40bf6c2d978a0421bb9705cb6))
|
|
38
|
+
|
|
39
|
+
## [0.1.1](https://github.com/Daily-Nerd/Scar/compare/v0.1.0...v0.1.1) (2026-06-12)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
### Bug Fixes
|
|
43
|
+
|
|
44
|
+
* **hooks:** drafter triggers on revert language only ([#12](https://github.com/Daily-Nerd/Scar/issues/12)) ([547c4bb](https://github.com/Daily-Nerd/Scar/commit/547c4bb21e3521682b6a4046602d6703d88c2cf1)), closes [#11](https://github.com/Daily-Nerd/Scar/issues/11)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scar-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: SCAR — version control for negative knowledge (deadends, fences, landmines)
|
|
5
5
|
License: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -49,7 +49,7 @@ The flip side: agents also solve the historically fatal flaw of every knowledge-
|
|
|
49
49
|
- Enforcement happens **at the moment of action**:
|
|
50
50
|
- `scar check <path>` — CLI gate for humans and CI
|
|
51
51
|
- Agent hook (Claude Code `PreToolUse`, etc.) — injects relevant scars into the agent's context *before* it edits the file
|
|
52
|
-
-
|
|
52
|
+
- `scar mcp` — local MCP server, so MCP-capable agents can query and draft scars
|
|
53
53
|
- `scar harvest` — mines git history (reverts, add-then-remove dependencies, reopened issues) to propose candidate scars for codebases starting from zero.
|
|
54
54
|
- Scars are **advisory, never blocking, by default** — and stale knowledge has a lifecycle: `scar challenge <id> --reason` disputes a scar (it still fires, marked as disputed), `scar archive <id> --reason` retires it (never fires again; `scar why` keeps the history), and `scar lint`/`scar status` surface any scar whose `review_after` date has passed. Nothing expires automatically — archiving is a human decision, same governance as promotion.
|
|
55
55
|
|
|
@@ -84,8 +84,33 @@ Wiring the Claude Code hook (auto-injects scars before any agent edit):
|
|
|
84
84
|
|
|
85
85
|
```bash
|
|
86
86
|
scar hook install
|
|
87
|
+
scar hook status
|
|
87
88
|
```
|
|
88
89
|
|
|
90
|
+
Hooks are advisory and are installed only by this explicit user command. To
|
|
91
|
+
stop all automatic injection and drafting while keeping the repository's
|
|
92
|
+
`.scars/` records:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
scar hook uninstall
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Wiring MCP-capable agents:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
scar agent doctor
|
|
102
|
+
scar agent config opencode # or: codex, cursor, windsurf
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The MCP server runs as:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
scar mcp
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
It exposes `scar_query`, `scar_why`, and `scar_draft`. Drafting writes only to
|
|
112
|
+
`.scars/candidates/`; active enforcement still requires human promotion.
|
|
113
|
+
|
|
89
114
|
## Quality discipline
|
|
90
115
|
|
|
91
116
|
- **Candidates vs active:** agents and `scar harvest` only ever write to `.scars/candidates/`. A human promotes (`scar promote`) — nothing enters active enforcement without review.
|
|
@@ -40,7 +40,7 @@ The flip side: agents also solve the historically fatal flaw of every knowledge-
|
|
|
40
40
|
- Enforcement happens **at the moment of action**:
|
|
41
41
|
- `scar check <path>` — CLI gate for humans and CI
|
|
42
42
|
- Agent hook (Claude Code `PreToolUse`, etc.) — injects relevant scars into the agent's context *before* it edits the file
|
|
43
|
-
-
|
|
43
|
+
- `scar mcp` — local MCP server, so MCP-capable agents can query and draft scars
|
|
44
44
|
- `scar harvest` — mines git history (reverts, add-then-remove dependencies, reopened issues) to propose candidate scars for codebases starting from zero.
|
|
45
45
|
- Scars are **advisory, never blocking, by default** — and stale knowledge has a lifecycle: `scar challenge <id> --reason` disputes a scar (it still fires, marked as disputed), `scar archive <id> --reason` retires it (never fires again; `scar why` keeps the history), and `scar lint`/`scar status` surface any scar whose `review_after` date has passed. Nothing expires automatically — archiving is a human decision, same governance as promotion.
|
|
46
46
|
|
|
@@ -75,8 +75,33 @@ Wiring the Claude Code hook (auto-injects scars before any agent edit):
|
|
|
75
75
|
|
|
76
76
|
```bash
|
|
77
77
|
scar hook install
|
|
78
|
+
scar hook status
|
|
78
79
|
```
|
|
79
80
|
|
|
81
|
+
Hooks are advisory and are installed only by this explicit user command. To
|
|
82
|
+
stop all automatic injection and drafting while keeping the repository's
|
|
83
|
+
`.scars/` records:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
scar hook uninstall
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Wiring MCP-capable agents:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
scar agent doctor
|
|
93
|
+
scar agent config opencode # or: codex, cursor, windsurf
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The MCP server runs as:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
scar mcp
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
It exposes `scar_query`, `scar_why`, and `scar_draft`. Drafting writes only to
|
|
103
|
+
`.scars/candidates/`; active enforcement still requires human promotion.
|
|
104
|
+
|
|
80
105
|
## Quality discipline
|
|
81
106
|
|
|
82
107
|
- **Candidates vs active:** agents and `scar harvest` only ever write to `.scars/candidates/`. A human promotes (`scar promote`) — nothing enters active enforcement without review.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# SCAR — Roadmap
|
|
2
|
+
|
|
3
|
+
Restructured after the adversarial review (see [STRESS-TEST.md](STRESS-TEST.md)): **validation before construction**. Every phase had a kill gate decided in advance. Phase 0 and Phase 1 are complete; Phase 2 is in progress. Strategy note (2026-06-11): SCAR is published as **OSS-as-gift** — personal infrastructure shared openly, no product promises. Product work only happens if organic stranger traction appears; that decision retired gate 0.5.
|
|
4
|
+
|
|
5
|
+
## Phase 0 — Kill-gate experiments ✅ complete
|
|
6
|
+
|
|
7
|
+
| # | Experiment | Gate (pass/kill) | Result |
|
|
8
|
+
|---|-----------|------------------|--------|
|
|
9
|
+
| 0.1 | Prototype `scar harvest` (reverts, add-then-remove deps, reopened issues, comment archaeology); run on 5 real aging repos | ≥1 "I'd forgotten that" reaction per repo from someone who knows it; harvest precision subjectively >50% | ✅ **PASSED 2026-06-09** on a 7-year-old personal infra repo — owner had forgotten a service-placement deadend the harvest resurfaced; 12/12 curated candidates correct. Raw precision 13% → ranking layer remains a requirement (Phase 2). |
|
|
10
|
+
| 0.2 | Prototype anchors (tree-sitter symbols + content fingerprints); replay real historical refactor commits against them | ≥80% anchor survival across rename + file-split refactors | ✅ **PASSED 2026-06-09** — 94.8% single-commit, 88.0% at 200-commit zero-maintenance horizon; naive path+line baseline 0.0% ([results](experiments/anchor-survival/RESULTS.md)) |
|
|
11
|
+
| 0.3 | **Fence honor test**: hand-write fences on a real repo, wire a PreToolUse hook, A/B agent sessions hook-on vs hook-off on tasks that tempt fence-bulldozing | Hook measurably reduces fence violations without degrading task completion | ✅ **PASSED 2026-06-09** — 6/6 control violations vs 0/6 treatment ([results](experiments/fence-honor/RESULTS.md)) |
|
|
12
|
+
| 0.4 | Auto-authorship trial: 2 weeks of normal agent-assisted work with the stop-hook drafting `deadend` candidates | ≥5 human-kept scars; false-positive rate <15% (skeptic's bar, adopted) | ✅ **PASSED 2026-06-11**, closed day 3 of 14 — 13 keepable agent-authored scars across 3 repos, 0% rejected. One agent-authored scar caught a real parser bug and fired on the exact edit that fixed it. Drafter *trigger* precision was tuned separately (revert-language-only) after 3 of 6 firings proved false. |
|
|
13
|
+
| 0.5 | Survey 50 Claude Code / Cursor users re: Copilot Memory | Meaningful segment says no + wants repo-resident, reviewable knowledge | ⛔ **RETIRED 2026-06-11** — the OSS-as-gift decision removed the product hypothesis this gate validated. If stranger traction ever reopens the product question, this survey reopens with it. |
|
|
14
|
+
| 0.6 | Implement Lore trailers on the same repo as 0.3; compare agent behavior vs scar injection | SCAR injection outperforms history-walk over trailers on latency and compliance | ⏸ **Deprioritized** — no longer gating anything; optional research. |
|
|
15
|
+
|
|
16
|
+
## Phase 1 — Format + CLI (OSS) ✅ shipped
|
|
17
|
+
|
|
18
|
+
Public at [github.com/Daily-Nerd/Scar](https://github.com/Daily-Nerd/Scar), `scar-cli` on PyPI. Honest deltas from the original plan:
|
|
19
|
+
|
|
20
|
+
- ✅ `SCAR-FORMAT.md` v0.1 published; one parser/serializer (`model.py`), one renderer (`render.py`)
|
|
21
|
+
- ✅ CLI: `init, lint, status, promote, check, why, challenge, archive, harvest, hook, mcp, agent, inject` — lifecycle commands (`challenge`/`archive`, expiry review surfacing) shipped beyond the original plan
|
|
22
|
+
- ✅ Claude Code plugin: PreToolUse injection + stop-hook candidate drafting, both field-validated (gates 0.3, 0.4)
|
|
23
|
+
- ⚠️ **Python, not Go/Rust** — zero-dependency stdlib hits the goal the compiled binary was chasing (~20ms hook startup, trivial install via `uv tool install scar-cli`); a rewrite is not planned unless profiling says otherwise
|
|
24
|
+
- ❌ No `add` command — copy `template.md` + `scar promote` covers authoring; revisit only on user friction
|
|
25
|
+
- ❌ Lore trailer ingestion in `harvest` — moved to Phase 2, optional
|
|
26
|
+
- ✅ Dogfooding: 6 repos, including this one (the repo's own scars caught its own release-process bug)
|
|
27
|
+
|
|
28
|
+
## Phase 2 — Ecosystem 🔄 in progress
|
|
29
|
+
|
|
30
|
+
- ✅ **MCP server** (`scar_query`, `scar_why`, `scar_draft`) — shipped v0.3.0, dependency-free stdio, drafts gated to candidates/. First non-Claude agent (Codex) arrived and contributed the implementation — the deferral condition resolving itself.
|
|
31
|
+
- ✅ Multi-agent surface: committed `AGENTS.md`, `scar inject --diff`, `scar agent doctor/config` for Codex, Cursor, Windsurf, opencode (v0.3.0)
|
|
32
|
+
- 🔶 CI surface: expiry warnings shipped (`lint`/`status`, v0.2.0); **orphan detection is the next milestone** — content-fingerprint drift → `orphaned` status, loud in CI (principle 3 is not yet enforced by code)
|
|
33
|
+
- ⬜ Harvest ranking layer (gate 0.1 verdict: required — raw precision 13% without it)
|
|
34
|
+
- ⬜ Re-anchoring agent workflow: orphaned scar + orphaning diff → proposed new anchors as a PR
|
|
35
|
+
- ⬜ Editor surfaces (VS Code gutter marks, LSP code lens) — fences visible to humans, not only agents
|
|
36
|
+
- ⬜ Lint warning on evidence commit SHAs unreachable from HEAD (scar #5's expiry condition)
|
|
37
|
+
|
|
38
|
+
## Phase 3 — The org graph ⏸ parked by design
|
|
39
|
+
|
|
40
|
+
The commercial hypothesis (cross-repo aggregation, recurrence analytics, policy, managed harvest) is **parked under the OSS-as-gift decision**, not killed: it reopens only on organic adoption signal — external repos with active scars, inbound interest from strangers. "SCAR remains a free standard" was declared in advance as an acceptable ending, and it is the current operating assumption.
|
|
41
|
+
|
|
42
|
+
## Non-negotiable principles carried from the stress test
|
|
43
|
+
|
|
44
|
+
1. Advisory by default, forever. Blocking is opt-in, per-scar-severity, in CI only.
|
|
45
|
+
2. Max 3 scars / ~120 words each injected per edit. The fatigue budget is a format-level guarantee, not a tuning knob (enforced in `render.py`).
|
|
46
|
+
3. Rot must be loud. No scar ever disappears silently; orphaning is a visible state. *(Lifecycle transitions enforce this for human decisions; orphan detection — the code-drift half — is Phase 2's next milestone.)*
|
|
47
|
+
4. Assume zero ongoing human maintenance; design for graceful visible decay.
|
|
48
|
+
5. The format stays open and vendor-neutral even if a company forms. Platform absorption of the format = success, not failure.
|
|
@@ -47,6 +47,17 @@ is deliberate: consumers in hook hot-paths parse with zero dependencies.
|
|
|
47
47
|
| `expires.condition` | recommended | quoted string | what change obsoletes this scar |
|
|
48
48
|
| `expires.review_after` | recommended | ISO date | forces periodic freshness contact |
|
|
49
49
|
| `status` | yes | `candidate` \| `active` \| `challenged` \| `archived` \| `orphaned` \| `template` | lifecycle §5 |
|
|
50
|
+
| `receipt_id` | reserved | string ref | **reserved — not yet parsed.** Optional pointer to a cryptographic provenance receipt; see note below. |
|
|
51
|
+
|
|
52
|
+
`receipt_id` is a forward-compatibility reservation, not a live field. Scar's
|
|
53
|
+
trust model is social by design (git history, `authors`, evidence-by-reference);
|
|
54
|
+
that is sufficient within one repo or org. It does **not** carry across orgs that
|
|
55
|
+
share no git history — the future cross-org / org-graph layer where "this dead end
|
|
56
|
+
hit N teams" must be attributable. `receipt_id` reserves the slot for a signed,
|
|
57
|
+
content-addressed receipt (e.g. [veritrail](https://github.com/Daily-Nerd/veritrail))
|
|
58
|
+
bound to an authorship (`scar_draft`) or promotion event. No tool emits, requires,
|
|
59
|
+
or validates it today, and the line-wise parser ignores it like any unknown key —
|
|
60
|
+
so existing scars are unaffected. It will not become required in v0.x.
|
|
50
61
|
|
|
51
62
|
Body: prose after the frontmatter, 5–15 lines. What happened, why, what a
|
|
52
63
|
future editor must do instead — written for a reader with zero context.
|
|
@@ -81,7 +81,10 @@ scar why <path> # human-readable history of pain for a file/dir
|
|
|
81
81
|
scar challenge <id> # open a challenge: contest staleness with evidence
|
|
82
82
|
scar harvest # mine git history, emit candidate scars to .scars/candidates/
|
|
83
83
|
scar status # active/orphaned/challenged/expiring counts; repo health
|
|
84
|
-
scar inject --
|
|
84
|
+
scar inject --path <p> # machine mode: top-k scars for one edit as hook JSON
|
|
85
|
+
scar inject --diff <d> # machine mode: top-k scars for a unified diff as hook JSON
|
|
86
|
+
scar mcp # stdio MCP server for MCP-capable agents
|
|
87
|
+
scar agent config <name> # print setup snippets for supported agent runtimes
|
|
85
88
|
```
|
|
86
89
|
|
|
87
90
|
## 4. Agent integration
|
|
@@ -98,7 +101,7 @@ Also `PostToolUse`/stop-hook prompt: *"You appear to have abandoned approach X a
|
|
|
98
101
|
|
|
99
102
|
### 4.2 MCP server
|
|
100
103
|
|
|
101
|
-
`scar
|
|
104
|
+
`scar mcp` exposes: `scar_query(paths|content|diff)`, `scar_why(path)`, `scar_draft(type, title, body, anchors, evidence)` (writes to `candidates/`, never directly to active). Works for any MCP-capable agent — Codex, Cursor, Windsurf, opencode, custom.
|
|
102
105
|
|
|
103
106
|
### 4.3 Ranking and the fatigue budget
|
|
104
107
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Backward-compatible wrapper for the packaged hook lifecycle commands."""
|
|
3
|
+
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from scar.installer import find_scar, install, status, uninstall
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
if __name__ == "__main__":
|
|
10
|
+
args = sys.argv[1:]
|
|
11
|
+
dry = "--dry-run" in args
|
|
12
|
+
cmd = next((a for a in args if not a.startswith("-")), "status")
|
|
13
|
+
sys.exit({"install": lambda: install(dry=dry),
|
|
14
|
+
"uninstall": lambda: uninstall(dry=dry),
|
|
15
|
+
"status": status}.get(cmd, status)())
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Agent integration helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
_MCP_SERVERS_SNIPPET = """\
|
|
9
|
+
Configure a local MCP server named "scar" with:
|
|
10
|
+
|
|
11
|
+
{
|
|
12
|
+
"mcpServers": {
|
|
13
|
+
"scar": {
|
|
14
|
+
"command": "scar",
|
|
15
|
+
"args": ["mcp"]
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
# target -> setup text; adding a runtime is one entry here, no logic change
|
|
22
|
+
CONFIGS = {
|
|
23
|
+
"codex": """\
|
|
24
|
+
Codex-compatible setup:
|
|
25
|
+
|
|
26
|
+
1. Keep AGENTS.md committed at the repository root.
|
|
27
|
+
2. Expose SCAR through MCP with command: scar mcp
|
|
28
|
+
3. For direct shell use, ask the agent to run:
|
|
29
|
+
scar inject --path <path> --content <new-content>
|
|
30
|
+
scar inject --diff <unified-diff>
|
|
31
|
+
""",
|
|
32
|
+
"cursor": _MCP_SERVERS_SNIPPET,
|
|
33
|
+
"opencode": """\
|
|
34
|
+
Add this to opencode.jsonc:
|
|
35
|
+
|
|
36
|
+
{
|
|
37
|
+
"$schema": "https://opencode.ai/config.json",
|
|
38
|
+
"mcp": {
|
|
39
|
+
"scar": {
|
|
40
|
+
"type": "local",
|
|
41
|
+
"command": ["scar", "mcp"],
|
|
42
|
+
"enabled": true
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
""",
|
|
47
|
+
"windsurf": "Cascade: " + _MCP_SERVERS_SNIPPET,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
TARGETS = tuple(sorted(CONFIGS))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def doctor(repo: Path) -> list[str]:
|
|
54
|
+
root = repo.resolve()
|
|
55
|
+
findings = []
|
|
56
|
+
findings.append(f"AGENTS.md: {'present' if (root / 'AGENTS.md').exists() else 'missing'}")
|
|
57
|
+
findings.append(f".scars/: {'present' if (root / '.scars').is_dir() else 'missing'}")
|
|
58
|
+
findings.append(f"scar binary: {shutil.which('scar') or 'not found on PATH'}")
|
|
59
|
+
findings.append("MCP command: scar mcp")
|
|
60
|
+
return findings
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def config(target: str) -> str:
|
|
64
|
+
if target not in CONFIGS:
|
|
65
|
+
raise ValueError(f"unknown target '{target}' (expected: {', '.join(TARGETS)})")
|
|
66
|
+
return CONFIGS[target]
|
|
@@ -14,12 +14,10 @@ import time
|
|
|
14
14
|
from pathlib import Path
|
|
15
15
|
|
|
16
16
|
from .lint import lint_text
|
|
17
|
-
from .match import rank_for_edit
|
|
18
|
-
from .
|
|
17
|
+
from .match import rank_for_edit, rank_matches_for_diff, rank_matches_for_edit
|
|
18
|
+
from .render import injection_context, label_line
|
|
19
19
|
from .store import ScarStore, init_scars
|
|
20
20
|
|
|
21
|
-
MAX_BODY_CHARS = 700 # ~120 words — the fatigue budget is a format guarantee
|
|
22
|
-
|
|
23
21
|
|
|
24
22
|
def _require_store(start: Path | None = None) -> ScarStore | None:
|
|
25
23
|
store = ScarStore.discover(start or Path.cwd())
|
|
@@ -103,8 +101,7 @@ def _cmd_check(args) -> int:
|
|
|
103
101
|
print(f"no scars anchored to {args.path}")
|
|
104
102
|
return 0
|
|
105
103
|
for s in hits:
|
|
106
|
-
|
|
107
|
-
print(f"[{label} #{s.id} | severity: {s.severity} | confidence: {s.confidence}] {s.title}")
|
|
104
|
+
print(label_line(s))
|
|
108
105
|
print(" " + s.body[:200].replace("\n", "\n "))
|
|
109
106
|
return 0
|
|
110
107
|
|
|
@@ -132,52 +129,39 @@ def _cmd_why(args) -> int:
|
|
|
132
129
|
if store is None:
|
|
133
130
|
return 1
|
|
134
131
|
rel = str(Path(args.path).resolve().relative_to(store.root))
|
|
135
|
-
|
|
136
|
-
for f in
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
continue
|
|
141
|
-
# bidirectional: query under an anchor (editing inside protected dir)
|
|
142
|
-
# OR anchor under the query (asking a parent dir for its history)
|
|
143
|
-
if any(rel.startswith(p.rstrip("/")) or p.rstrip("/").startswith(rel)
|
|
144
|
-
for p in s.path_anchors):
|
|
145
|
-
found += 1
|
|
146
|
-
print(f"[{s.status} {s.type} #{s.id}] {s.title} ({f.name})")
|
|
147
|
-
print(" " + s.body[:300].replace("\n", "\n ") + "\n")
|
|
148
|
-
if not found:
|
|
132
|
+
records = store.scars_for_path(rel)
|
|
133
|
+
for f, s in records:
|
|
134
|
+
print(f"[{s.status} {s.type} #{s.id}] {s.title} ({f.name})")
|
|
135
|
+
print(" " + s.body[:300].replace("\n", "\n ") + "\n")
|
|
136
|
+
if not records:
|
|
149
137
|
print(f"no recorded pain for {rel}")
|
|
150
138
|
return 0
|
|
151
139
|
|
|
152
140
|
|
|
153
141
|
def _cmd_inject(args) -> int:
|
|
154
142
|
"""Machine mode for hooks: JSON additionalContext or silence."""
|
|
155
|
-
|
|
143
|
+
start = Path(args.path).resolve() if args.path else Path.cwd()
|
|
144
|
+
store = ScarStore.discover(start)
|
|
156
145
|
if store is None:
|
|
157
146
|
return 0 # hooks must never fail the edit
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
if
|
|
174
|
-
parts.append(
|
|
175
|
-
f"SCAR warning: {len(broken)} scar file(s) unparseable and can NEVER "
|
|
176
|
-
f"fire: {', '.join(b.name for b in broken)}. Fix frontmatter "
|
|
177
|
-
f"(copy {store.scars_dir}/template.md).")
|
|
178
|
-
if parts:
|
|
147
|
+
if args.diff:
|
|
148
|
+
try:
|
|
149
|
+
diff_text = Path(args.diff).read_text(encoding="utf-8")
|
|
150
|
+
except (OSError, UnicodeDecodeError, ValueError):
|
|
151
|
+
# ValueError covers NUL-byte paths; a hook must never crash on
|
|
152
|
+
# whatever lands in --diff — fall back to treating it as text
|
|
153
|
+
diff_text = args.diff
|
|
154
|
+
matches = rank_matches_for_diff(store, diff_text, top_k=args.top_k)
|
|
155
|
+
elif args.path:
|
|
156
|
+
matches = rank_matches_for_edit(store, Path(args.path).resolve(),
|
|
157
|
+
args.content or "", top_k=args.top_k)
|
|
158
|
+
else:
|
|
159
|
+
matches = []
|
|
160
|
+
context = injection_context([m.scar for m in matches], store.broken(),
|
|
161
|
+
store.scars_dir)
|
|
162
|
+
if context:
|
|
179
163
|
print(json.dumps({"hookSpecificOutput": {
|
|
180
|
-
"hookEventName": args.hook_event, "additionalContext":
|
|
164
|
+
"hookEventName": args.hook_event, "additionalContext": context}}))
|
|
181
165
|
return 0
|
|
182
166
|
|
|
183
167
|
|
|
@@ -203,6 +187,29 @@ def _cmd_harvest(args) -> int:
|
|
|
203
187
|
return 0
|
|
204
188
|
|
|
205
189
|
|
|
190
|
+
def _cmd_agent(args) -> int:
|
|
191
|
+
from .agent import config, doctor
|
|
192
|
+
if args.agent_command == "doctor":
|
|
193
|
+
for line in doctor(Path.cwd()):
|
|
194
|
+
print(line)
|
|
195
|
+
return 0
|
|
196
|
+
try:
|
|
197
|
+
print(config(args.target))
|
|
198
|
+
except ValueError as exc:
|
|
199
|
+
print(str(exc))
|
|
200
|
+
return 1
|
|
201
|
+
return 0
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _cmd_hook_lifecycle(args) -> int:
|
|
205
|
+
from .installer import install, status, uninstall
|
|
206
|
+
if args.kind == "install":
|
|
207
|
+
return install(dry=args.dry_run)
|
|
208
|
+
if args.kind == "uninstall":
|
|
209
|
+
return uninstall(dry=args.dry_run)
|
|
210
|
+
return status()
|
|
211
|
+
|
|
212
|
+
|
|
206
213
|
def main(argv: list[str] | None = None) -> int:
|
|
207
214
|
parser = argparse.ArgumentParser(prog="scar",
|
|
208
215
|
description="version control for negative knowledge")
|
|
@@ -235,17 +242,34 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
235
242
|
p = sub.add_parser("harvest", help="mine git history for candidate scars")
|
|
236
243
|
p.add_argument("repo", nargs="?", default=".")
|
|
237
244
|
|
|
238
|
-
p = sub.add_parser("hook", help="
|
|
239
|
-
p.add_argument("kind", choices=["
|
|
245
|
+
p = sub.add_parser("hook", help="install, remove, inspect, or run Claude Code hooks")
|
|
246
|
+
p.add_argument("kind", choices=["install", "uninstall", "status",
|
|
247
|
+
"precheck", "session-notice", "stop-drafter"])
|
|
248
|
+
p.add_argument("--dry-run", action="store_true",
|
|
249
|
+
help="show lifecycle changes without writing settings")
|
|
250
|
+
|
|
251
|
+
sub.add_parser("mcp", help="run the SCAR MCP stdio server")
|
|
252
|
+
|
|
253
|
+
p = sub.add_parser("agent", help="agent integration helpers")
|
|
254
|
+
agent_sub = p.add_subparsers(dest="agent_command", required=True)
|
|
255
|
+
agent_sub.add_parser("doctor", help="show local agent integration readiness")
|
|
256
|
+
cfg = agent_sub.add_parser("config", help="print config for an agent runtime")
|
|
257
|
+
cfg.add_argument("target", choices=["codex", "cursor", "opencode", "windsurf"])
|
|
240
258
|
|
|
241
259
|
p = sub.add_parser("inject", help="machine mode for hooks: JSON or silence")
|
|
242
|
-
p.add_argument("--path"
|
|
260
|
+
p.add_argument("--path")
|
|
243
261
|
p.add_argument("--content", default="")
|
|
262
|
+
p.add_argument("--diff", help="unified diff text, or path to a diff file")
|
|
244
263
|
p.add_argument("--top-k", type=int, default=3)
|
|
245
264
|
p.add_argument("--hook-event", default="PreToolUse")
|
|
246
265
|
|
|
247
266
|
args = parser.parse_args(argv)
|
|
267
|
+
if args.command == "mcp":
|
|
268
|
+
from .mcp import serve
|
|
269
|
+
return serve()
|
|
248
270
|
if args.command == "hook":
|
|
271
|
+
if args.kind in ("install", "uninstall", "status"):
|
|
272
|
+
return _cmd_hook_lifecycle(args)
|
|
249
273
|
from .hooks import HANDLERS # hot path: imports nothing beyond library
|
|
250
274
|
return HANDLERS[args.kind]()
|
|
251
275
|
if args.command in ("challenge", "archive"):
|
|
@@ -255,6 +279,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
255
279
|
"init": _cmd_init, "lint": _cmd_lint, "status": _cmd_status,
|
|
256
280
|
"promote": _cmd_promote, "check": _cmd_check, "why": _cmd_why,
|
|
257
281
|
"inject": _cmd_inject, "harvest": _cmd_harvest,
|
|
282
|
+
"agent": _cmd_agent,
|
|
258
283
|
}[args.command]
|
|
259
284
|
return handler(args)
|
|
260
285
|
|
|
@@ -18,10 +18,9 @@ import time
|
|
|
18
18
|
from pathlib import Path
|
|
19
19
|
|
|
20
20
|
from .match import rank_for_edit
|
|
21
|
+
from .render import injection_context
|
|
21
22
|
from .store import ScarStore
|
|
22
23
|
|
|
23
|
-
MAX_BODY_CHARS = 700
|
|
24
|
-
|
|
25
24
|
REVERT_RE = re.compile(
|
|
26
25
|
r"revert(ing|ed)?\b|roll(ing|ed)? back|undo(ing)? (the|that|my)|"
|
|
27
26
|
r"abandon(ing|ed)? (the|this|that|this approach)|scrap(ping)? (the|this|that)|"
|
|
@@ -71,23 +70,9 @@ def precheck() -> int:
|
|
|
71
70
|
new_content = " ".join(str(tool_input.get(k, ""))
|
|
72
71
|
for k in ("content", "new_string", "new_source"))
|
|
73
72
|
hits = rank_for_edit(store, Path(target), new_content)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
blocks = [f"[{'challenged ' if s.status == 'challenged' else ''}{s.type} "
|
|
78
|
-
f"#{s.id} | severity: {s.severity} | confidence: "
|
|
79
|
-
f"{s.confidence}] {s.title}\n{s.body[:MAX_BODY_CHARS]}" for s in hits]
|
|
80
|
-
parts.append(
|
|
81
|
-
"SCAR pre-edit check — negative knowledge anchored to code you are "
|
|
82
|
-
f"about to modify ({len(hits)} match(es)). Honor these unless the "
|
|
83
|
-
"user explicitly overrides; full records in .scars/.\n\n" + "\n\n".join(blocks))
|
|
84
|
-
if broken:
|
|
85
|
-
parts.append(
|
|
86
|
-
f"SCAR warning: {len(broken)} scar file(s) unparseable and can NEVER "
|
|
87
|
-
f"fire: {', '.join(b.name for b in broken)}. Their knowledge is "
|
|
88
|
-
f"silently dead. Fix the frontmatter (copy {store.scars_dir}/template.md).")
|
|
89
|
-
if parts:
|
|
90
|
-
_emit("PreToolUse", "\n\n".join(parts))
|
|
73
|
+
context = injection_context(hits, store.broken(), store.scars_dir)
|
|
74
|
+
if context:
|
|
75
|
+
_emit("PreToolUse", context)
|
|
91
76
|
return 0
|
|
92
77
|
|
|
93
78
|
|