backstory-cli 0.1.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 (35) hide show
  1. backstory_cli-0.1.0/PKG-INFO +5 -0
  2. backstory_cli-0.1.0/README.md +268 -0
  3. backstory_cli-0.1.0/pyproject.toml +18 -0
  4. backstory_cli-0.1.0/setup.cfg +4 -0
  5. backstory_cli-0.1.0/src/backstory/__init__.py +2 -0
  6. backstory_cli-0.1.0/src/backstory/__main__.py +6 -0
  7. backstory_cli-0.1.0/src/backstory/attach.py +131 -0
  8. backstory_cli-0.1.0/src/backstory/cli.py +381 -0
  9. backstory_cli-0.1.0/src/backstory/config.py +49 -0
  10. backstory_cli-0.1.0/src/backstory/contradiction.py +75 -0
  11. backstory_cli-0.1.0/src/backstory/dump.py +235 -0
  12. backstory_cli-0.1.0/src/backstory/git.py +32 -0
  13. backstory_cli-0.1.0/src/backstory/hooks.py +94 -0
  14. backstory_cli-0.1.0/src/backstory/init.py +96 -0
  15. backstory_cli-0.1.0/src/backstory/okf.py +346 -0
  16. backstory_cli-0.1.0/src/backstory/retrieval.py +239 -0
  17. backstory_cli-0.1.0/src/backstory/storage.py +58 -0
  18. backstory_cli-0.1.0/src/backstory/summarize.py +184 -0
  19. backstory_cli-0.1.0/src/backstory/transcript.py +177 -0
  20. backstory_cli-0.1.0/src/backstory_cli.egg-info/PKG-INFO +5 -0
  21. backstory_cli-0.1.0/src/backstory_cli.egg-info/SOURCES.txt +33 -0
  22. backstory_cli-0.1.0/src/backstory_cli.egg-info/dependency_links.txt +1 -0
  23. backstory_cli-0.1.0/src/backstory_cli.egg-info/entry_points.txt +2 -0
  24. backstory_cli-0.1.0/src/backstory_cli.egg-info/top_level.txt +1 -0
  25. backstory_cli-0.1.0/tests/test_attach.py +110 -0
  26. backstory_cli-0.1.0/tests/test_cli.py +93 -0
  27. backstory_cli-0.1.0/tests/test_config.py +47 -0
  28. backstory_cli-0.1.0/tests/test_contradiction.py +118 -0
  29. backstory_cli-0.1.0/tests/test_dump.py +184 -0
  30. backstory_cli-0.1.0/tests/test_hooks.py +93 -0
  31. backstory_cli-0.1.0/tests/test_init.py +98 -0
  32. backstory_cli-0.1.0/tests/test_retrieval.py +214 -0
  33. backstory_cli-0.1.0/tests/test_storage.py +64 -0
  34. backstory_cli-0.1.0/tests/test_summarize.py +113 -0
  35. backstory_cli-0.1.0/tests/test_transcript.py +179 -0
@@ -0,0 +1,5 @@
1
+ Metadata-Version: 2.4
2
+ Name: backstory-cli
3
+ Version: 0.1.0
4
+ Summary: Every AI-assisted commit has a backstory. Never lose it again.
5
+ Requires-Python: >=3.11
@@ -0,0 +1,268 @@
1
+ # Backstory
2
+
3
+ [![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-blue)](https://www.python.org/)
4
+ [![OKF](https://img.shields.io/badge/knowledge%20format-OKF-orange)](https://cloud.google.com/blog/products/data-analytics)
5
+
6
+ **Git shows what changed. Backstory shows why.**
7
+
8
+ Backstory preserves the decision-making process behind AI-assisted code
9
+ changes. It captures session context from AI coding tools, extracts the
10
+ durable reasoning (decisions, risks, alternatives), stores it as local
11
+ markdown, and links it to Git commits so you can retrieve the *why* later.
12
+
13
+ > **Why not just write good commit messages?**
14
+ > Commit messages describe what changed. They rarely capture the rejected
15
+ > alternatives, the risks you accepted, or the reasoning trail across a
16
+ > multi-step AI session. Backstory fills that gap: it stores the decision
17
+ > trail the AI tool produced, not just the final diff.
18
+
19
+ - Local-first by default — everything stays in your repo
20
+ - Recovers *why* a decision was made, after the codebase has moved on
21
+ - Stores durable session memory as OKF markdown — human-readable,
22
+ Git-friendly, and agent-friendly
23
+ - Links memory to Git commits so the reasoning stays searchable
24
+ - Detects contradictions when later changes reverse earlier decisions
25
+ - Extracts decisions, risks, and follow-ups — not raw chat logs
26
+
27
+ Backstory is not an AI coding agent. It is the memory layer around
28
+ AI-assisted coding.
29
+
30
+ ## Example
31
+
32
+ You ask an AI agent to fix subscription renewal logic.
33
+
34
+ Later, you can ask:
35
+
36
+ ```bash
37
+ backstory why HEAD
38
+ ```
39
+
40
+ And get back something like:
41
+
42
+ ```text
43
+ Commit: 8f21c9a
44
+ Message: Fix subscription renewal handling
45
+ Agent: Claude Code
46
+
47
+ Why this changed:
48
+ The webhook handler was not updating the next billing date after
49
+ successful recurring charges. Failed payments were not separated
50
+ from cancellations.
51
+
52
+ Key decisions:
53
+ - subscription.charged updates next_due_on
54
+ - payment.failed marks subscription as pending, not cancelled
55
+ - webhook handling must be idempotent
56
+
57
+ Files changed:
58
+ - app/api/webhooks/razorpay/route.ts
59
+ - lib/subscription.ts
60
+
61
+ Risks:
62
+ - Idempotency depends on storing Razorpay event IDs
63
+ - Existing subscriptions need a next_due_on backfill
64
+ ```
65
+
66
+ That is the useful part: not just what changed, but why it changed.
67
+
68
+ ## Contradiction Detection
69
+
70
+ Backstory also watches for new changes that appear to reverse earlier recorded
71
+ decisions.
72
+
73
+ ```text
74
+ ⚠ This change may contradict a decision from commit 8f21c9a:
75
+ "payment.failed should mark subscription as pending, not cancelled"
76
+ ```
77
+
78
+ That turns the tool from an archive into a guardrail.
79
+
80
+ ## What Gets Stored
81
+
82
+ Here is what that same session looks like on disk — only the extracted
83
+ reasoning, no raw conversation:
84
+
85
+ ```markdown
86
+ ---
87
+ type: Backstory Session
88
+ title: Fix subscription renewal handling
89
+ description: The webhook handler was not updating the next billing date after successful recurring charges.
90
+ resource: git:8f21c9a
91
+ tags: [backstory, ai-session]
92
+ timestamp: 2026-07-06T12:00:00+00:00
93
+ session_id: sha256:a1b2c3d4e5f6...
94
+ agent: claude-code
95
+ model: claude-sonnet-5
96
+ source: manual
97
+ branch: main
98
+ head: 8f21c9a...
99
+ commit_hash: 8f21c9a...
100
+ commit_message: Fix subscription renewal handling
101
+ files_changed:
102
+ - app/api/webhooks/razorpay/route.ts
103
+ - lib/subscription.ts
104
+ ---
105
+
106
+ # Task
107
+
108
+ Fix subscription renewal handling
109
+
110
+ # Decisions
111
+
112
+ - subscription.charged updates next_due_on
113
+ - payment.failed marks subscription as pending, not cancelled
114
+ - webhook handling must be idempotent
115
+
116
+ # Risks
117
+
118
+ - Idempotency depends on storing Razorpay event IDs
119
+ - Existing subscriptions need a next_due_on backfill
120
+
121
+ # Follow-ups
122
+
123
+ - Add migration script for existing subscriptions
124
+ ```
125
+
126
+ This file lives at `.backstory/knowledge/sessions/sha256-a1b2c3d4....md`.
127
+ It is human-readable, Git-friendly, and stores only the durable
128
+ decisions — not the full chat transcript.
129
+
130
+ ## How It Works
131
+
132
+ ![Backstory flow diagram](docs/assets/how-it-works-flow.gif)
133
+
134
+ ![Backstory terminal walkthrough](docs/assets/how-it-works-terminal.gif)
135
+
136
+ The capture and retrieval pipeline has four steps:
137
+
138
+ 1. **Capture** — A tool-native hook, callback, or transcript exporter
139
+ captures the AI coding session and hands it to Backstory.
140
+ 2. **Ingest** — Backstory extracts the durable decisions, risks, follow-ups,
141
+ and changed files from the session. The raw conversation is discarded —
142
+ only the reasoning is kept.
143
+ 3. **Link** — The extracted session is attached to the relevant Git commit
144
+ via `backstory attach HEAD`, creating a stable record in
145
+ `.backstory/knowledge/sessions/`.
146
+ 4. **Retrieve** — Later, you can query the stored reasoning by commit, file,
147
+ line, range, or diff using commands like `backstory why HEAD`.
148
+
149
+ Git stays the linkage layer. Backstory stores the reasoning.
150
+
151
+ ## Quick Start
152
+
153
+ Install the PyPI package:
154
+
155
+ ```bash
156
+ python -m pip install backstory-cli
157
+ ```
158
+
159
+ The installed command is still:
160
+
161
+ ```bash
162
+ backstory --help
163
+ ```
164
+
165
+ ```bash
166
+ backstory init
167
+ backstory why HEAD
168
+ ```
169
+
170
+ Backstory is designed for tool-native capture. The AI tool should hand session
171
+ context to Backstory through its own hook, callback, or transcript exporter.
172
+ Backstory then ingests that session internally.
173
+
174
+ If you need to import a transcript export directly:
175
+
176
+ ```bash
177
+ backstory dump --agent claude --transcript ./transcript.md
178
+ ```
179
+
180
+ ## Core Commands
181
+
182
+ | Command | What it does |
183
+ |---|---|
184
+ | `backstory init` | Set up Backstory in the current repo |
185
+ | `backstory dump` | Ingest an AI session into OKF markdown |
186
+ | `backstory attach HEAD` | Link a session to a commit |
187
+ | `backstory why HEAD` | Explain why a commit happened |
188
+ | `backstory show <session>` | View a stored session |
189
+ | `backstory search <query>` | Search past sessions and decisions |
190
+ | `backstory file <path>` | Show AI context relevant to a file |
191
+ | `backstory line <path>:<line>` | Show the decision behind a specific line |
192
+ | `backstory range <path>:<start>-<end>` | Show context for a range of lines |
193
+ | `backstory code <path>:<start>-<end>` | Show why a code block exists |
194
+ | `backstory diff` | Explain the reasoning behind the current diff |
195
+ | `backstory status` | Show Backstory state in this repo |
196
+ | `backstory redact` | Re-scan and redact sensitive data from stored sessions |
197
+
198
+ ## Redaction
199
+
200
+ Backstory automatically scans for and redacts sensitive data before it reaches
201
+ disk. Here is what a session looks like when a transcript contains an API key:
202
+
203
+ ```text
204
+ Before redaction:
205
+ decisions:
206
+ - "Store the key in RAZORPAY_API_KEY=sk_live_abcd1234..."
207
+ - "Webhook endpoint is at https://api.example.com/webhooks"
208
+
209
+ After redaction:
210
+ decisions:
211
+ - "Store the key in RAZORPAY_API_KEY=***"
212
+ - "Webhook endpoint is at https://api.example.com/webhooks"
213
+ ```
214
+
215
+ The redaction step runs during `backstory dump` and can be re-run later with
216
+ `backstory redact` if new patterns are added. Raw transcripts are never
217
+ stored as the durable session record — only the redacted, extracted reasoning
218
+ persists.
219
+
220
+ ## Storage
221
+
222
+ ```text
223
+ .backstory/
224
+ config.json
225
+ index.sqlite
226
+ knowledge/
227
+ index.md
228
+ sessions/
229
+ index.md
230
+ latest.md
231
+ sha256-<session>.md
232
+ redactions/
233
+ tombstones.log
234
+ ```
235
+
236
+ Session memory is stored as OKF-style markdown so it stays human-readable,
237
+ Git-friendly, and agent-friendly.
238
+
239
+ ## Privacy
240
+
241
+ Backstory is local-first.
242
+
243
+ - No cloud service is required
244
+ - No telemetry is required
245
+ - Session memory stays inside the repository
246
+ - Raw transcripts are not persisted as the durable session record
247
+ - Backstory stores extracted decisions, risks, follow-ups, changed files, and
248
+ Git context — not raw conversation
249
+
250
+ ## Integration
251
+
252
+ Backstory is designed for tool-native integration with Claude, Codex, Cursor,
253
+ and similar AI tools.
254
+
255
+ The preferred path is a tool-specific hook, callback, or transcript exporter
256
+ that hands the session to Backstory automatically. `dump` exists as the
257
+ ingestion step, not the primary workflow.
258
+
259
+ ## Documentation
260
+
261
+ - [Engineering walkthrough](docs/engineering-walkthrough.md)
262
+ - [Product spec](docs/prd.md)
263
+ - [Retrieval model](docs/retrieval.md)
264
+
265
+ ---
266
+
267
+ If you find this useful, [starring the repo](https://github.com/arpitkath/backstory)
268
+ helps others discover it.
@@ -0,0 +1,18 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "backstory-cli"
7
+ version = "0.1.0"
8
+ description = "Every AI-assisted commit has a backstory. Never lose it again."
9
+ requires-python = ">=3.11"
10
+
11
+ [project.scripts]
12
+ backstory = "backstory.cli:main"
13
+
14
+ [tool.setuptools]
15
+ package-dir = {"" = "src"}
16
+
17
+ [tool.setuptools.packages.find]
18
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,2 @@
1
+ """backstory - AI commit memory layer."""
2
+
@@ -0,0 +1,6 @@
1
+ from .cli import main
2
+
3
+
4
+ if __name__ == "__main__":
5
+ raise SystemExit(main())
6
+
@@ -0,0 +1,131 @@
1
+ from __future__ import annotations
2
+
3
+ import subprocess
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from backstory.dump import clear_pending_session, load_pending_session
8
+ from backstory.okf import parse_session_markdown, render_session_markdown, session_id_to_filename
9
+ from backstory.storage import build_storage_paths, ensure_storage_layout
10
+
11
+
12
+ def attach_pending_to_commit(repo_root: Path, commit_hash: str) -> dict[str, Any] | None:
13
+ """Attach the latest pending AI session to a Git commit.
14
+
15
+ Steps:
16
+ 1. Load the pending session.
17
+ 2. Update it with the commit hash.
18
+ 3. Write a summary file.
19
+ 4. Save commit-to-session mapping via Git notes.
20
+ 5. Clear the pending session.
21
+
22
+ Returns the updated session dict, or ``None`` if no pending session exists.
23
+ """
24
+ session = load_pending_session(repo_root)
25
+ if session is None:
26
+ return None
27
+
28
+ # --- Link session to commit ---
29
+ commit_msg = _get_commit_message(repo_root, commit_hash)
30
+ session["commit"] = {
31
+ "hash": commit_hash,
32
+ "message": commit_msg or "",
33
+ }
34
+
35
+ paths = ensure_storage_layout(repo_root)
36
+ stable_path = paths.sessions / session_id_to_filename(session["session_id"])
37
+ stable_path.write_text(render_session_markdown(session), encoding="utf-8")
38
+
39
+ # --- Write a Git note ---
40
+ _write_git_note(repo_root, commit_hash, session)
41
+
42
+ # --- Clear pending ---
43
+ clear_pending_session(repo_root)
44
+
45
+ return session
46
+
47
+
48
+ def _get_commit_message(repo_root: Path, commit_hash: str) -> str | None:
49
+ """Get the subject line of a commit."""
50
+ try:
51
+ result = subprocess.run(
52
+ ["git", "log", "-1", "--format=%s", commit_hash],
53
+ cwd=repo_root,
54
+ capture_output=True,
55
+ text=True,
56
+ check=False,
57
+ )
58
+ if result.returncode == 0:
59
+ return result.stdout.strip() or None
60
+ return None
61
+ except OSError:
62
+ return None
63
+
64
+
65
+ def _write_git_note(repo_root: Path, commit_hash: str, session: dict) -> None:
66
+ """Attach a Git note with session metadata to the commit."""
67
+ import json
68
+
69
+ note = json.dumps(
70
+ {
71
+ "ai_session": session.get("session_id"),
72
+ "agent": session.get("agent", {}).get("name"),
73
+ "created_at": session.get("created_at"),
74
+ },
75
+ indent=2,
76
+ )
77
+ try:
78
+ subprocess.run(
79
+ ["git", "notes", "add", "-f", "-m", note, commit_hash],
80
+ cwd=repo_root,
81
+ capture_output=True,
82
+ text=True,
83
+ check=False,
84
+ )
85
+ except OSError:
86
+ pass # Git notes are best-effort
87
+
88
+
89
+ def _render_summary(session: dict, commit_hash: str, commit_msg: str | None) -> str:
90
+ """Render a human-readable summary markdown file."""
91
+ rendered = render_session_markdown(session)
92
+ parsed = parse_session_markdown(rendered)
93
+ lines: list[str] = []
94
+ lines.append(f"# backstory for Commit {commit_hash}")
95
+ lines.append("")
96
+ if commit_msg:
97
+ lines.append(f"**{commit_msg}**")
98
+ lines.append("")
99
+ lines.append("## Task")
100
+ lines.append("")
101
+ lines.append(parsed.task_title)
102
+ lines.append("")
103
+ if parsed.why:
104
+ lines.append("## Why")
105
+ lines.append("")
106
+ lines.append(parsed.why)
107
+ lines.append("")
108
+ if parsed.files_changed:
109
+ lines.append("## Files Changed")
110
+ lines.append("")
111
+ for f in parsed.files_changed:
112
+ lines.append(f"- `{f}`")
113
+ lines.append("")
114
+ if parsed.decisions:
115
+ lines.append("## Key Decisions")
116
+ lines.append("")
117
+ for d in parsed.decisions:
118
+ lines.append(f"- {d}")
119
+ lines.append("")
120
+ if parsed.risks:
121
+ lines.append("## Risks")
122
+ lines.append("")
123
+ for r in parsed.risks:
124
+ lines.append(f"- {r}")
125
+ lines.append("")
126
+ lines.append(f"Agent: {parsed.agent_name}")
127
+ if parsed.agent_model:
128
+ lines.append(f"Model: {parsed.agent_model}")
129
+ lines.append("")
130
+ lines.append(f"Session ID: {parsed.session_id or session.get('session_id', 'unknown')}")
131
+ return "\n".join(lines)