scar-cli 0.3.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 (67) hide show
  1. {scar_cli-0.3.0 → scar_cli-0.4.0}/CHANGELOG.md +17 -0
  2. {scar_cli-0.3.0 → scar_cli-0.4.0}/PKG-INFO +10 -1
  3. {scar_cli-0.3.0 → scar_cli-0.4.0}/README.md +9 -0
  4. scar_cli-0.4.0/ROADMAP.md +48 -0
  5. {scar_cli-0.3.0 → scar_cli-0.4.0}/SCAR-FORMAT.md +11 -0
  6. scar_cli-0.4.0/hook/scar-hooks.py +15 -0
  7. {scar_cli-0.3.0 → scar_cli-0.4.0}/pyproject.toml +1 -1
  8. {scar_cli-0.3.0 → scar_cli-0.4.0}/src/scar/cli.py +16 -2
  9. scar_cli-0.3.0/hook/scar-hooks.py → scar_cli-0.4.0/src/scar/installer.py +33 -46
  10. {scar_cli-0.3.0 → scar_cli-0.4.0}/tests/test_installer.py +38 -0
  11. scar_cli-0.3.0/ROADMAP.md +0 -46
  12. {scar_cli-0.3.0 → scar_cli-0.4.0}/.github/workflows/ci.yml +0 -0
  13. {scar_cli-0.3.0 → scar_cli-0.4.0}/.github/workflows/pr-validation.yml +0 -0
  14. {scar_cli-0.3.0 → scar_cli-0.4.0}/.github/workflows/release.yml +0 -0
  15. {scar_cli-0.3.0 → scar_cli-0.4.0}/.gitignore +0 -0
  16. {scar_cli-0.3.0 → scar_cli-0.4.0}/.scars/0001-git-grep-ere-pitfalls.landmine.md +0 -0
  17. {scar_cli-0.3.0 → scar_cli-0.4.0}/.scars/0002-agent-direct-hook-install.deadend.md +0 -0
  18. {scar_cli-0.3.0 → scar_cli-0.4.0}/.scars/0003-installer-binds-to-active-venv-scar.landmine.md +0 -0
  19. {scar_cli-0.3.0 → scar_cli-0.4.0}/.scars/0004-promote-roundtrip-drops-expires-evidence.landmine.md +0 -0
  20. {scar_cli-0.3.0 → scar_cli-0.4.0}/.scars/0005-history-rewrite-orphans-commit-evidence.landmine.md +0 -0
  21. {scar_cli-0.3.0 → scar_cli-0.4.0}/.scars/README.md +0 -0
  22. {scar_cli-0.3.0 → scar_cli-0.4.0}/.scars/candidates/fp-log.txt +0 -0
  23. {scar_cli-0.3.0 → scar_cli-0.4.0}/.scars/template.md +0 -0
  24. {scar_cli-0.3.0 → scar_cli-0.4.0}/AGENTS.md +0 -0
  25. {scar_cli-0.3.0 → scar_cli-0.4.0}/CONTRIBUTING.md +0 -0
  26. {scar_cli-0.3.0 → scar_cli-0.4.0}/IDEA.md +0 -0
  27. {scar_cli-0.3.0 → scar_cli-0.4.0}/LICENSE +0 -0
  28. {scar_cli-0.3.0 → scar_cli-0.4.0}/SPEC.md +0 -0
  29. {scar_cli-0.3.0 → scar_cli-0.4.0}/STRESS-TEST.md +0 -0
  30. {scar_cli-0.3.0 → scar_cli-0.4.0}/experiments/anchor-survival/PROTOCOL.md +0 -0
  31. {scar_cli-0.3.0 → scar_cli-0.4.0}/experiments/anchor-survival/RESULTS.md +0 -0
  32. {scar_cli-0.3.0 → scar_cli-0.4.0}/experiments/anchor-survival/long_replay.py +0 -0
  33. {scar_cli-0.3.0 → scar_cli-0.4.0}/experiments/anchor-survival/replay.py +0 -0
  34. {scar_cli-0.3.0 → scar_cli-0.4.0}/experiments/auto-authorship/FINDINGS.md +0 -0
  35. {scar_cli-0.3.0 → scar_cli-0.4.0}/experiments/auto-authorship/PROTOCOL.md +0 -0
  36. {scar_cli-0.3.0 → scar_cli-0.4.0}/experiments/fence-honor/.gitignore +0 -0
  37. {scar_cli-0.3.0 → scar_cli-0.4.0}/experiments/fence-honor/PROTOCOL.md +0 -0
  38. {scar_cli-0.3.0 → scar_cli-0.4.0}/experiments/fence-honor/RESULTS.md +0 -0
  39. {scar_cli-0.3.0 → scar_cli-0.4.0}/experiments/fence-honor/fixture/.scars/0001-vendor-retry-window.fence.md +0 -0
  40. {scar_cli-0.3.0 → scar_cli-0.4.0}/experiments/fence-honor/fixture/.scars/0002-evicting-session-store.deadend.md +0 -0
  41. {scar_cli-0.3.0 → scar_cli-0.4.0}/experiments/fence-honor/fixture/.scars/0003-export-column-order.landmine.md +0 -0
  42. {scar_cli-0.3.0 → scar_cli-0.4.0}/experiments/fence-honor/fixture/README.md +0 -0
  43. {scar_cli-0.3.0 → scar_cli-0.4.0}/experiments/fence-honor/fixture/payments/retry.py +0 -0
  44. {scar_cli-0.3.0 → scar_cli-0.4.0}/experiments/fence-honor/fixture/reports/export.py +0 -0
  45. {scar_cli-0.3.0 → scar_cli-0.4.0}/experiments/fence-honor/fixture/services/sessions.py +0 -0
  46. {scar_cli-0.3.0 → scar_cli-0.4.0}/experiments/fence-honor/grade.py +0 -0
  47. {scar_cli-0.3.0 → scar_cli-0.4.0}/experiments/harvest/harvest.py +0 -0
  48. {scar_cli-0.3.0 → scar_cli-0.4.0}/src/scar/__init__.py +0 -0
  49. {scar_cli-0.3.0 → scar_cli-0.4.0}/src/scar/agent.py +0 -0
  50. {scar_cli-0.3.0 → scar_cli-0.4.0}/src/scar/harvest.py +0 -0
  51. {scar_cli-0.3.0 → scar_cli-0.4.0}/src/scar/hooks.py +0 -0
  52. {scar_cli-0.3.0 → scar_cli-0.4.0}/src/scar/lint.py +0 -0
  53. {scar_cli-0.3.0 → scar_cli-0.4.0}/src/scar/match.py +0 -0
  54. {scar_cli-0.3.0 → scar_cli-0.4.0}/src/scar/mcp.py +0 -0
  55. {scar_cli-0.3.0 → scar_cli-0.4.0}/src/scar/model.py +0 -0
  56. {scar_cli-0.3.0 → scar_cli-0.4.0}/src/scar/render.py +0 -0
  57. {scar_cli-0.3.0 → scar_cli-0.4.0}/src/scar/store.py +0 -0
  58. {scar_cli-0.3.0 → scar_cli-0.4.0}/tests/test_cli.py +0 -0
  59. {scar_cli-0.3.0 → scar_cli-0.4.0}/tests/test_harvest.py +0 -0
  60. {scar_cli-0.3.0 → scar_cli-0.4.0}/tests/test_hooks.py +0 -0
  61. {scar_cli-0.3.0 → scar_cli-0.4.0}/tests/test_lifecycle.py +0 -0
  62. {scar_cli-0.3.0 → scar_cli-0.4.0}/tests/test_lint.py +0 -0
  63. {scar_cli-0.3.0 → scar_cli-0.4.0}/tests/test_match.py +0 -0
  64. {scar_cli-0.3.0 → scar_cli-0.4.0}/tests/test_mcp.py +0 -0
  65. {scar_cli-0.3.0 → scar_cli-0.4.0}/tests/test_model.py +0 -0
  66. {scar_cli-0.3.0 → scar_cli-0.4.0}/tests/test_store.py +0 -0
  67. {scar_cli-0.3.0 → scar_cli-0.4.0}/uv.lock +0 -0
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
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
+
3
20
  ## [0.3.0](https://github.com/Daily-Nerd/Scar/compare/v0.2.0...v0.3.0) (2026-06-12)
4
21
 
5
22
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scar-cli
3
- Version: 0.3.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
@@ -84,6 +84,15 @@ 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
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
87
96
  ```
88
97
 
89
98
  Wiring MCP-capable agents:
@@ -75,6 +75,15 @@ 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
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
78
87
  ```
79
88
 
80
89
  Wiring MCP-capable agents:
@@ -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.
@@ -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.3.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"
@@ -201,6 +201,15 @@ def _cmd_agent(args) -> int:
201
201
  return 0
202
202
 
203
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
+
204
213
  def main(argv: list[str] | None = None) -> int:
205
214
  parser = argparse.ArgumentParser(prog="scar",
206
215
  description="version control for negative knowledge")
@@ -233,8 +242,11 @@ def main(argv: list[str] | None = None) -> int:
233
242
  p = sub.add_parser("harvest", help="mine git history for candidate scars")
234
243
  p.add_argument("repo", nargs="?", default=".")
235
244
 
236
- p = sub.add_parser("hook", help="Claude Code hook handlers (payload on stdin)")
237
- 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")
238
250
 
239
251
  sub.add_parser("mcp", help="run the SCAR MCP stdio server")
240
252
 
@@ -256,6 +268,8 @@ def main(argv: list[str] | None = None) -> int:
256
268
  from .mcp import serve
257
269
  return serve()
258
270
  if args.command == "hook":
271
+ if args.kind in ("install", "uninstall", "status"):
272
+ return _cmd_hook_lifecycle(args)
259
273
  from .hooks import HANDLERS # hot path: imports nothing beyond library
260
274
  return HANDLERS[args.kind]()
261
275
  if args.command in ("challenge", "archive"):
@@ -1,24 +1,15 @@
1
- #!/usr/bin/env python3
2
- """SCAR hook lifecycle manager: install, uninstall, status.
1
+ """Claude Code hook lifecycle management.
3
2
 
4
- Registers the three SCAR hooks in ~/.claude/settings.json as `scar hook
5
- <kind>` commands (absolute path to the scar binary hook environments do not
6
- guarantee PATH). Migrates installs from the legacy standalone-script era:
7
- their settings entries and ~/.claude/hooks/ copies are removed on install.
8
-
9
- Run BY THE USER, never by an agent (see scar 0002): consent is the execution.
10
-
11
- Usage:
12
- python3 scar-hooks.py install [--dry-run]
13
- python3 scar-hooks.py uninstall [--dry-run]
14
- python3 scar-hooks.py status
3
+ The user invokes these commands explicitly. SCAR never installs global hooks
4
+ as a side effect of package installation, ``scar init``, or an agent action.
15
5
  """
16
6
 
7
+ from __future__ import annotations
8
+
17
9
  import json
18
10
  import os
19
11
  import re
20
12
  import shutil
21
- import sys
22
13
  import time
23
14
  from pathlib import Path
24
15
 
@@ -42,36 +33,40 @@ HOOKS = [
42
33
 
43
34
 
44
35
  def find_scar() -> str | None:
45
- # scar 0003: never bind hooks to a venv shim it dies with the venv.
46
- # With $VIRTUAL_ENV active its bin/ shadows PATH, so search without it.
36
+ # scar 0003: never bind hooks to a venv shim that may disappear.
47
37
  venv = os.environ.get("VIRTUAL_ENV")
48
38
  if not venv:
49
39
  return shutil.which("scar")
50
- venv = Path(venv).resolve()
40
+ venv_path = Path(venv).resolve()
51
41
  dirs = [d for d in os.environ.get("PATH", "").split(os.pathsep)
52
- if d and not Path(d).resolve().is_relative_to(venv)]
42
+ if d and not Path(d).resolve().is_relative_to(venv_path)]
53
43
  return shutil.which("scar", path=os.pathsep.join(dirs))
54
44
 
55
45
 
56
- def load_settings():
46
+ def load_settings() -> dict:
57
47
  return json.loads(SETTINGS.read_text(encoding="utf-8")) if SETTINGS.exists() else {}
58
48
 
59
49
 
60
- def save_settings(settings, dry):
50
+ def save_settings(settings: dict, dry: bool) -> None:
61
51
  if dry:
62
52
  return
63
- backup = SETTINGS.with_name(f"settings.json.scar-backup-{int(time.time())}")
64
- shutil.copy2(SETTINGS, backup)
53
+ CLAUDE_DIR.mkdir(parents=True, exist_ok=True)
54
+ if SETTINGS.exists():
55
+ backup = SETTINGS.with_name(f"settings.json.scar-backup-{int(time.time())}")
56
+ shutil.copy2(SETTINGS, backup)
57
+ backup_note = f" (backup: {backup.name})"
58
+ else:
59
+ backup_note = ""
65
60
  SETTINGS.write_text(json.dumps(settings, indent=2) + "\n", encoding="utf-8")
66
- print(f" settings.json written (backup: {backup.name})")
61
+ print(f" settings.json written{backup_note}")
67
62
 
68
63
 
69
- def is_ours(group) -> bool:
64
+ def is_ours(group: dict) -> bool:
70
65
  return any(OURS_RE.search(h.get("command", ""))
71
66
  for h in group.get("hooks", []) if isinstance(h, dict))
72
67
 
73
68
 
74
- def _entry(spec, scar_path):
69
+ def _entry(spec: dict, scar_path: str) -> dict:
75
70
  hook = {"type": "command", "command": f"{scar_path} hook {spec['kind']}",
76
71
  "timeout": spec["timeout"], "statusMessage": spec["status"]}
77
72
  group = {"hooks": [hook]}
@@ -80,23 +75,23 @@ def _entry(spec, scar_path):
80
75
  return group
81
76
 
82
77
 
83
- def _remove_legacy_scripts(dry):
78
+ def _remove_legacy_scripts(dry: bool) -> None:
84
79
  for name in LEGACY_SCRIPTS:
85
- f = HOOKS_DIR / name
86
- if f.exists():
87
- print(f"[migrate] remove legacy script {f}")
80
+ path = HOOKS_DIR / name
81
+ if path.exists():
82
+ print(f"[migrate] remove legacy script {path}")
88
83
  if not dry:
89
- f.unlink()
84
+ path.unlink()
90
85
 
91
86
 
92
- def install(dry):
87
+ def install(dry: bool = False) -> int:
93
88
  scar_path = find_scar()
94
89
  if not scar_path:
95
90
  print("scar binary not found on PATH.")
96
91
  if os.environ.get("VIRTUAL_ENV"):
97
92
  print("Note: an active venv is ignored on purpose — hooks must "
98
93
  "bind to a stable install, not a venv shim (scar 0003).")
99
- print("Install it first: cd <scar-repo> && uv tool install -e .")
94
+ print("Install it first: uv tool install scar-cli")
100
95
  return 1
101
96
  settings = load_settings()
102
97
  hooks_cfg = settings.setdefault("hooks", {})
@@ -123,7 +118,7 @@ def install(dry):
123
118
  return 0
124
119
 
125
120
 
126
- def uninstall(dry):
121
+ def uninstall(dry: bool = False) -> int:
127
122
  settings = load_settings()
128
123
  hooks_cfg = settings.get("hooks", {})
129
124
  changed = False
@@ -144,24 +139,16 @@ def uninstall(dry):
144
139
  return 0
145
140
 
146
141
 
147
- def status():
142
+ def status() -> int:
148
143
  scar_path = find_scar()
149
- print(f"scar binary: {scar_path or 'NOT FOUND (uv tool install -e .)'}")
144
+ print(f"scar binary: {scar_path or 'NOT FOUND (uv tool install scar-cli)'}")
150
145
  hooks_cfg = load_settings().get("hooks", {})
151
146
  for spec in HOOKS:
152
147
  ours = [g for g in hooks_cfg.get(spec["event"], []) if is_ours(g)]
153
- cmds = [h.get("command", "") for g in ours for h in g.get("hooks", [])]
154
- legacy = any(any(s in c for s in LEGACY_SCRIPTS) for c in cmds)
148
+ commands = [h.get("command", "") for g in ours for h in g.get("hooks", [])]
149
+ legacy = any(any(script in command for script in LEGACY_SCRIPTS)
150
+ for command in commands)
155
151
  state = ("legacy (run install to migrate)" if legacy
156
152
  else "installed" if ours else "not installed")
157
153
  print(f"{spec['kind']:16} {spec['event']:13} {state}")
158
154
  return 0
159
-
160
-
161
- if __name__ == "__main__":
162
- args = sys.argv[1:]
163
- dry = "--dry-run" in args
164
- cmd = next((a for a in args if not a.startswith("-")), "status")
165
- sys.exit({"install": lambda: install(dry),
166
- "uninstall": lambda: uninstall(dry),
167
- "status": status}.get(cmd, status)())
@@ -12,6 +12,9 @@ from pathlib import Path
12
12
 
13
13
  import pytest
14
14
 
15
+ from scar import installer
16
+ from scar.cli import main
17
+
15
18
  _SPEC = importlib.util.spec_from_file_location(
16
19
  "scar_hooks", Path(__file__).parent.parent / "hook" / "scar-hooks.py")
17
20
  scar_hooks = importlib.util.module_from_spec(_SPEC)
@@ -64,3 +67,38 @@ def test_install_explains_venv_shadowing_when_no_global_scar(
64
67
  assert scar_hooks.install(dry=True) == 1
65
68
  out = capsys.readouterr().out
66
69
  assert "VIRTUAL_ENV" in out or "venv" in out
70
+
71
+
72
+ @pytest.fixture
73
+ def isolated_settings(tmp_path, monkeypatch):
74
+ claude = tmp_path / ".claude"
75
+ monkeypatch.setattr(installer, "CLAUDE_DIR", claude)
76
+ monkeypatch.setattr(installer, "HOOKS_DIR", claude / "hooks")
77
+ monkeypatch.setattr(installer, "SETTINGS", claude / "settings.json")
78
+ monkeypatch.setattr(installer, "find_scar", lambda: "/stable/bin/scar")
79
+ return claude / "settings.json"
80
+
81
+
82
+ def test_cli_hook_install_then_uninstall(isolated_settings, capsys):
83
+ assert main(["hook", "install"]) == 0
84
+ settings = isolated_settings.read_text(encoding="utf-8")
85
+ assert settings.count("/stable/bin/scar hook") == 3
86
+
87
+ assert main(["hook", "uninstall"]) == 0
88
+ settings = isolated_settings.read_text(encoding="utf-8")
89
+ assert "/stable/bin/scar hook" not in settings
90
+ assert "Scars themselves (.scars/ in repos) are untouched" in capsys.readouterr().out
91
+
92
+
93
+ def test_cli_hook_dry_run_does_not_create_settings(isolated_settings):
94
+ assert main(["hook", "install", "--dry-run"]) == 0
95
+ assert not isolated_settings.exists()
96
+
97
+
98
+ def test_cli_hook_status_reports_each_hook(isolated_settings, capsys):
99
+ assert main(["hook", "status"]) == 0
100
+ out = capsys.readouterr().out
101
+ assert "precheck" in out
102
+ assert "session-notice" in out
103
+ assert "stop-drafter" in out
104
+ assert out.count("not installed") == 3
scar_cli-0.3.0/ROADMAP.md DELETED
@@ -1,46 +0,0 @@
1
- # SCAR — Roadmap
2
-
3
- Restructured after the adversarial review (see [STRESS-TEST.md](STRESS-TEST.md)): **validation before construction**. Every phase has a kill gate decided in advance. No product-company work happens before Phase 2 gates pass.
4
-
5
- ## Phase 0 — Kill-gate experiments (≈4-6 weeks, no product code beyond throwaway prototypes)
6
-
7
- | # | Experiment | Gate (pass/kill) | Cost |
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 2-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 is a v0 requirement. 4 more repos pending for full gate. |
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) | 2 weeks, overlaps |
13
- | 0.5 | Survey 50 Claude Code / Cursor users: "Has Copilot Memory solved re-tried dead ends / bulldozed fences for you?" | Meaningful segment says no + wants repo-resident, reviewable knowledge | 3 days |
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 | 1 week |
15
-
16
- **Any of 0.2, 0.3, 0.4 failing = stop and rethink the architecture. 0.5 failing = the wedge is gone; archive the project with its own deadend scar.** (The repo eating its own dog food on the way out.)
17
-
18
- ## Phase 1 — Format + CLI (OSS), only after Phase 0 passes
19
-
20
- - `SCAR-FORMAT.md` v0.1 published as a spec independent of the tool — the format is the long-term bet.
21
- - `scar` CLI: `init`, `add`, `check`, `why`, `status`, `harvest`, `inject` (Go or Rust; single static binary; <150ms `inject` budget with incremental index under `.git/scar-index`).
22
- - Claude Code plugin: PreToolUse injection + stop-hook candidate drafting.
23
- - Lore trailer ingestion in `harvest` (interop, per stress-test response C).
24
- - Dogfood on this repo and 2-3 friendly real projects.
25
-
26
- ## Phase 2 — Ecosystem
27
-
28
- - MCP server (`scar_query`, `scar_why`, `scar_draft`).
29
- - CI surface: orphan/expiry warnings; opt-in `--strict` for `critical` scars.
30
- - Re-anchoring agent workflow: orphaned scar + orphaning diff → proposed new anchors as a PR.
31
- - Editor surfaces (VS Code gutter marks, LSP code lens) — fences visible to humans, not only agents.
32
- - **Gate to Phase 3:** organic adoption signal (external repos with active scars, inbound interest) + 10 ICP interviews validating org-level pain at a price point. Skeptic risk #5 stands until then.
33
-
34
- ## Phase 3 — The org graph (commercial hypothesis, explicitly unproven)
35
-
36
- - Cross-repo scar aggregation: "this dead end has been hit by 4 teams."
37
- - Recurrence analytics; policy ("payment-path changes must acknowledge fences"); managed harvest.
38
- - Only if Phase 2's gate passed. Otherwise SCAR remains a free standard, and that is a declared-in-advance acceptable ending.
39
-
40
- ## Non-negotiable principles carried from the stress test
41
-
42
- 1. Advisory by default, forever. Blocking is opt-in, per-scar-severity, in CI only.
43
- 2. Max 3 scars / ~120 words each injected per edit. The fatigue budget is a format-level guarantee, not a tuning knob.
44
- 3. Rot must be loud. No scar ever disappears silently; orphaning is a visible state.
45
- 4. Assume zero ongoing human maintenance; design for graceful visible decay.
46
- 5. The format stays open and vendor-neutral even if a company forms. Platform absorption of the format = success, not failure.
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes