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.
Files changed (69) hide show
  1. {scar_cli-0.2.0 → scar_cli-0.4.0}/.scars/0003-installer-binds-to-active-venv-scar.landmine.md +2 -1
  2. {scar_cli-0.2.0 → scar_cli-0.4.0}/.scars/0004-promote-roundtrip-drops-expires-evidence.landmine.md +2 -1
  3. 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
  4. scar_cli-0.4.0/AGENTS.md +43 -0
  5. scar_cli-0.4.0/CHANGELOG.md +44 -0
  6. {scar_cli-0.2.0 → scar_cli-0.4.0}/PKG-INFO +27 -2
  7. {scar_cli-0.2.0 → scar_cli-0.4.0}/README.md +26 -1
  8. scar_cli-0.4.0/ROADMAP.md +48 -0
  9. {scar_cli-0.2.0 → scar_cli-0.4.0}/SCAR-FORMAT.md +11 -0
  10. {scar_cli-0.2.0 → scar_cli-0.4.0}/SPEC.md +5 -2
  11. scar_cli-0.4.0/hook/scar-hooks.py +15 -0
  12. {scar_cli-0.2.0 → scar_cli-0.4.0}/pyproject.toml +1 -1
  13. scar_cli-0.4.0/src/scar/agent.py +66 -0
  14. {scar_cli-0.2.0 → scar_cli-0.4.0}/src/scar/cli.py +71 -46
  15. {scar_cli-0.2.0 → scar_cli-0.4.0}/src/scar/hooks.py +4 -19
  16. scar_cli-0.2.0/hook/scar-hooks.py → scar_cli-0.4.0/src/scar/installer.py +33 -46
  17. scar_cli-0.4.0/src/scar/match.py +147 -0
  18. scar_cli-0.4.0/src/scar/mcp.py +235 -0
  19. scar_cli-0.4.0/src/scar/render.py +38 -0
  20. {scar_cli-0.2.0 → scar_cli-0.4.0}/src/scar/store.py +9 -0
  21. {scar_cli-0.2.0 → scar_cli-0.4.0}/tests/test_cli.py +36 -0
  22. {scar_cli-0.2.0 → scar_cli-0.4.0}/tests/test_installer.py +38 -0
  23. {scar_cli-0.2.0 → scar_cli-0.4.0}/tests/test_match.py +64 -1
  24. scar_cli-0.4.0/tests/test_mcp.py +92 -0
  25. {scar_cli-0.2.0 → scar_cli-0.4.0}/tests/test_store.py +12 -0
  26. {scar_cli-0.2.0 → scar_cli-0.4.0}/uv.lock +1 -1
  27. scar_cli-0.2.0/CHANGELOG.md +0 -20
  28. scar_cli-0.2.0/ROADMAP.md +0 -46
  29. scar_cli-0.2.0/src/scar/match.py +0 -51
  30. {scar_cli-0.2.0 → scar_cli-0.4.0}/.github/workflows/ci.yml +0 -0
  31. {scar_cli-0.2.0 → scar_cli-0.4.0}/.github/workflows/pr-validation.yml +0 -0
  32. {scar_cli-0.2.0 → scar_cli-0.4.0}/.github/workflows/release.yml +0 -0
  33. {scar_cli-0.2.0 → scar_cli-0.4.0}/.gitignore +0 -0
  34. {scar_cli-0.2.0 → scar_cli-0.4.0}/.scars/0001-git-grep-ere-pitfalls.landmine.md +0 -0
  35. {scar_cli-0.2.0 → scar_cli-0.4.0}/.scars/0002-agent-direct-hook-install.deadend.md +0 -0
  36. {scar_cli-0.2.0 → scar_cli-0.4.0}/.scars/README.md +0 -0
  37. {scar_cli-0.2.0 → scar_cli-0.4.0}/.scars/candidates/fp-log.txt +0 -0
  38. {scar_cli-0.2.0 → scar_cli-0.4.0}/.scars/template.md +0 -0
  39. {scar_cli-0.2.0 → scar_cli-0.4.0}/CONTRIBUTING.md +0 -0
  40. {scar_cli-0.2.0 → scar_cli-0.4.0}/IDEA.md +0 -0
  41. {scar_cli-0.2.0 → scar_cli-0.4.0}/LICENSE +0 -0
  42. {scar_cli-0.2.0 → scar_cli-0.4.0}/STRESS-TEST.md +0 -0
  43. {scar_cli-0.2.0 → scar_cli-0.4.0}/experiments/anchor-survival/PROTOCOL.md +0 -0
  44. {scar_cli-0.2.0 → scar_cli-0.4.0}/experiments/anchor-survival/RESULTS.md +0 -0
  45. {scar_cli-0.2.0 → scar_cli-0.4.0}/experiments/anchor-survival/long_replay.py +0 -0
  46. {scar_cli-0.2.0 → scar_cli-0.4.0}/experiments/anchor-survival/replay.py +0 -0
  47. {scar_cli-0.2.0 → scar_cli-0.4.0}/experiments/auto-authorship/FINDINGS.md +0 -0
  48. {scar_cli-0.2.0 → scar_cli-0.4.0}/experiments/auto-authorship/PROTOCOL.md +0 -0
  49. {scar_cli-0.2.0 → scar_cli-0.4.0}/experiments/fence-honor/.gitignore +0 -0
  50. {scar_cli-0.2.0 → scar_cli-0.4.0}/experiments/fence-honor/PROTOCOL.md +0 -0
  51. {scar_cli-0.2.0 → scar_cli-0.4.0}/experiments/fence-honor/RESULTS.md +0 -0
  52. {scar_cli-0.2.0 → scar_cli-0.4.0}/experiments/fence-honor/fixture/.scars/0001-vendor-retry-window.fence.md +0 -0
  53. {scar_cli-0.2.0 → scar_cli-0.4.0}/experiments/fence-honor/fixture/.scars/0002-evicting-session-store.deadend.md +0 -0
  54. {scar_cli-0.2.0 → scar_cli-0.4.0}/experiments/fence-honor/fixture/.scars/0003-export-column-order.landmine.md +0 -0
  55. {scar_cli-0.2.0 → scar_cli-0.4.0}/experiments/fence-honor/fixture/README.md +0 -0
  56. {scar_cli-0.2.0 → scar_cli-0.4.0}/experiments/fence-honor/fixture/payments/retry.py +0 -0
  57. {scar_cli-0.2.0 → scar_cli-0.4.0}/experiments/fence-honor/fixture/reports/export.py +0 -0
  58. {scar_cli-0.2.0 → scar_cli-0.4.0}/experiments/fence-honor/fixture/services/sessions.py +0 -0
  59. {scar_cli-0.2.0 → scar_cli-0.4.0}/experiments/fence-honor/grade.py +0 -0
  60. {scar_cli-0.2.0 → scar_cli-0.4.0}/experiments/harvest/harvest.py +0 -0
  61. {scar_cli-0.2.0 → scar_cli-0.4.0}/src/scar/__init__.py +0 -0
  62. {scar_cli-0.2.0 → scar_cli-0.4.0}/src/scar/harvest.py +0 -0
  63. {scar_cli-0.2.0 → scar_cli-0.4.0}/src/scar/lint.py +0 -0
  64. {scar_cli-0.2.0 → scar_cli-0.4.0}/src/scar/model.py +0 -0
  65. {scar_cli-0.2.0 → scar_cli-0.4.0}/tests/test_harvest.py +0 -0
  66. {scar_cli-0.2.0 → scar_cli-0.4.0}/tests/test_hooks.py +0 -0
  67. {scar_cli-0.2.0 → scar_cli-0.4.0}/tests/test_lifecycle.py +0 -0
  68. {scar_cli-0.2.0 → scar_cli-0.4.0}/tests/test_lint.py +0 -0
  69. {scar_cli-0.2.0 → scar_cli-0.4.0}/tests/test_model.py +0 -0
@@ -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
- status: active
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
@@ -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
- status: active
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: "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
+ - 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: candidate
17
+ status: active
17
18
  ---
18
19
 
19
20
  Scars cite commit SHAs as evidence receipts. Those receipts implicitly assume
@@ -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.2.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
- - MCP serverplanned, so any agent can query the scar graph
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
- - MCP serverplanned, so any agent can query the scar graph
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 --diff <d> # machine mode: top-k scars for a diff as JSON/markdown (hook backend)
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-mcp` exposes: `scar_query(paths|symbols|pattern)`, `scar_why(path)`, `scar_draft(type, title, body, anchors, evidence)` (writes to `candidates/`, never directly to active). Works for any MCP-capable agent — Cursor, Windsurf, custom.
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)())
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "scar-cli"
3
- version = "0.2.0"
3
+ version = "0.4.0"
4
4
  description = "SCAR — version control for negative knowledge (deadends, fences, landmines)"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -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 .model import ParseError, parse_scar_text
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
- label = f"challenged {s.type}" if s.status == "challenged" else s.type
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
- found = 0
136
- for f in store._scar_files():
137
- try:
138
- s = parse_scar_text(f.read_text(encoding="utf-8"))
139
- except ParseError:
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
- store = ScarStore.discover(Path(args.path).resolve())
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
- hits = rank_for_edit(store, Path(args.path).resolve(), args.content or "",
159
- top_k=args.top_k)
160
- broken = store.broken()
161
- parts = []
162
- if hits:
163
- blocks = [
164
- f"[{'challenged ' if s.status == 'challenged' else ''}{s.type} #{s.id} "
165
- f"| severity: {s.severity} | confidence: {s.confidence}] "
166
- f"{s.title}\n{s.body[:MAX_BODY_CHARS]}" for s in hits
167
- ]
168
- parts.append(
169
- "SCAR pre-edit check — negative knowledge anchored to code you are "
170
- f"about to modify ({len(hits)} match(es)). Honor these unless the "
171
- "user explicitly overrides; full records in .scars/.\n\n"
172
- + "\n\n".join(blocks))
173
- if broken:
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": "\n\n".join(parts)}}))
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="Claude Code hook handlers (payload on stdin)")
239
- p.add_argument("kind", choices=["precheck", "session-notice", "stop-drafter"])
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", required=True)
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
- broken = store.broken()
75
- parts = []
76
- if hits:
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