corkern 2.0.0rc2__py3-none-any.whl

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 (52) hide show
  1. corkern/__init__.py +1 -0
  2. corkern/claude_commands/corkern-distill.md +12 -0
  3. corkern/claude_commands/corkern-harvest.md +13 -0
  4. corkern/claude_commands/corkern-memory-prune.md +15 -0
  5. corkern/claude_commands/corkern-status.md +6 -0
  6. corkern/claude_print.py +30 -0
  7. corkern/commands/__init__.py +0 -0
  8. corkern/commands/add_cmd.py +104 -0
  9. corkern/commands/ai_cmd.py +238 -0
  10. corkern/commands/crg_cmd.py +60 -0
  11. corkern/commands/distill_cmd.py +106 -0
  12. corkern/commands/export_cmd.py +305 -0
  13. corkern/commands/graphify_cmd.py +128 -0
  14. corkern/commands/harvest_cmd.py +412 -0
  15. corkern/commands/init_cmd.py +30 -0
  16. corkern/commands/init_device_cmd.py +396 -0
  17. corkern/commands/iwe_cmd.py +122 -0
  18. corkern/commands/list_cmd.py +105 -0
  19. corkern/commands/metrics_cmd.py +81 -0
  20. corkern/commands/migrate_cmd.py +69 -0
  21. corkern/commands/prune_cmd.py +127 -0
  22. corkern/commands/reminders.py +145 -0
  23. corkern/commands/remove_cmd.py +32 -0
  24. corkern/commands/secrets_cmd.py +177 -0
  25. corkern/commands/sessions_cmd.py +52 -0
  26. corkern/commands/setup_cmd.py +92 -0
  27. corkern/commands/signing_check.py +216 -0
  28. corkern/commands/signing_cmd.py +275 -0
  29. corkern/commands/status_cmd.py +421 -0
  30. corkern/commands/trust_cmd.py +342 -0
  31. corkern/config.py +352 -0
  32. corkern/devices.py +74 -0
  33. corkern/export_format.py +93 -0
  34. corkern/guard.py +630 -0
  35. corkern/hooks/corkern-commit-memory.sh +35 -0
  36. corkern/hooks/corkern-pull.sh +12 -0
  37. corkern/hooks/corkern-push.sh +20 -0
  38. corkern/hooks/corkern-session-notify.sh +15 -0
  39. corkern/hooks/corkern.sh +47 -0
  40. corkern/hooks/lib.sh +553 -0
  41. corkern/hooks_install.py +64 -0
  42. corkern/instance.py +134 -0
  43. corkern/knowledge_repo.py +246 -0
  44. corkern/main.py +112 -0
  45. corkern/memory_index.py +36 -0
  46. corkern/migrate.py +211 -0
  47. corkern/secret_scan.py +86 -0
  48. corkern-2.0.0rc2.dist-info/METADATA +110 -0
  49. corkern-2.0.0rc2.dist-info/RECORD +52 -0
  50. corkern-2.0.0rc2.dist-info/WHEEL +4 -0
  51. corkern-2.0.0rc2.dist-info/entry_points.txt +2 -0
  52. corkern-2.0.0rc2.dist-info/licenses/LICENSE +21 -0
corkern/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "2.0.0rc2"
@@ -0,0 +1,12 @@
1
+ Run `corkern distill` to synthesise cross-project insights from all corkern memory files.
2
+
3
+ Steps:
4
+ 1. Ask the user whether to run interactively or auto-save:
5
+ - **Auto** (saves to `memory/global/synthesis.md`): run `corkern distill --auto`
6
+ - **Interactive** (review with Claude): run `corkern distill`
7
+ 2. Run the chosen command via Bash.
8
+ 3. If `--auto` was used, read `memory/global/synthesis.md` in the knowledge repo (`$CORKERN_MEMORY_DIR`) and summarise the findings to the user.
9
+ 4. For any **Stale Candidates** identified: offer to add `status: stale` to their frontmatter — one file at a time, with user confirmation for each.
10
+ 5. For any **Universal Principles** identified (rules appearing in 2+ projects): offer to add them to `memory/global/principles.md` and remove them from the individual project memory files — with user confirmation.
11
+
12
+ Always confirm with the user before modifying any memory file.
@@ -0,0 +1,13 @@
1
+ Run `corkern harvest` to extract memories from past Claude conversations for the current project.
2
+
3
+ Steps:
4
+ 1. Run `corkern prune` first to show which conversations are new, changed or already harvested.
5
+ 2. Run `corkern harvest` (detects the project from the current directory). With several conversations it asks which to process — answer `all` or numbers.
6
+ 3. Review the new memory files it lists in `memory/<slug>/` of the knowledge repo; they are already added to `MEMORY.md`. Existing files are never overwritten (a new version gets a `.harvested-<time>` suffix) — merge those with the user.
7
+ 4. If it saved a `harvested-<date>.md` bulk file instead, run `corkern refine`.
8
+
9
+ Flags:
10
+ - `corkern harvest --interactive` — opens an interactive Claude session with the conversations as context, so the user can add their own recollections.
11
+ - `corkern harvest --all` — reprocesses already harvested conversations.
12
+
13
+ Secrets in transcripts are replaced by a marker before anything is committed. After harvesting, run `/corkern-distill` to check whether any extracted memories should be promoted to global.
@@ -0,0 +1,15 @@
1
+ Run `corkern memory-prune` to archive expired or stale memory files for the current project.
2
+
3
+ Steps:
4
+ 1. Run `corkern memory-prune --dry-run` to preview candidates without touching anything.
5
+ 2. Show the user the list of expired/stale files found.
6
+ 3. Explain what each candidate is and why it was flagged (expired date, status: stale marker).
7
+ 4. Ask the user to confirm before proceeding.
8
+ 5. If confirmed, run `corkern memory-prune` (which asks for confirmation again in the terminal).
9
+
10
+ Notes:
11
+ - Files with `expires: YYYY-MM-DD` in frontmatter are candidates after that date passes.
12
+ - Files with `status: stale` in frontmatter are candidates regardless of date.
13
+ - Archived files go to `memory/<slug>/archive/` — never deleted, always recoverable.
14
+ - To mark a file stale: add `status: stale` inside its frontmatter `metadata:` block.
15
+ - To mark a file with an expiry: add `expires: YYYY-MM-DD` inside `metadata:`.
@@ -0,0 +1,6 @@
1
+ Run `corkern status` to check corkern's health.
2
+
3
+ - `corkern status` — summary: sync, signing and trust, secrets removed, memory index sizes, hooks, projects.
4
+ - `corkern status -v` — full diagnostics: every project's wiring, hook scripts, slash commands, Claude Code memory folders not synced into corkern, knowledge graphs per project, IWE.
5
+
6
+ Run this when something seems off with memory sync, or after setting up on a new machine. Report problems (✗) and warnings (!) to the user with the fix each line suggests.
@@ -0,0 +1,30 @@
1
+ """Text-in/text-out `claude -p` calls (distill --auto, harvest, refine).
2
+
3
+ The prompt carries memory text that teammates wrote, so these calls get nothing beyond the
4
+ text: no tools at all (`--tools ""`), only the user's own settings (`--setting-sources user`:
5
+ no project or local settings, so no hooks from a repo), no MCP servers (`--strict-mcp-config`
6
+ without `--mcp-config`), and they start in a fresh private empty folder instead of the
7
+ knowledge repo (guard.capture_tool_isolated). Flags checked against Claude Code 2.1.285 and
8
+ 2.1.287 (`claude --help`).
9
+ """
10
+ from __future__ import annotations
11
+
12
+ from pathlib import Path
13
+
14
+ from corkern import guard
15
+
16
+ ISOLATION = ("--setting-sources", "user", "--strict-mcp-config", "--tools", "")
17
+
18
+
19
+ def ask(claude: str, prompt: str, model: str | None = None) -> "guard.subprocess.CompletedProcess":
20
+ argv = [claude, "-p", *ISOLATION, *(["--model", model] if model else [])]
21
+ return guard.capture_tool_isolated(argv, prompt)
22
+
23
+
24
+ def interactive_cwd(preferred: Path | None = None) -> Path:
25
+ """Where an interactive claude session starts: `preferred` (default: the current folder)
26
+ unless it is missing or inside the knowledge repo — then $HOME."""
27
+ where = Path(preferred) if preferred is not None else Path.cwd()
28
+ if not where.is_dir() or guard.in_knowledge_repo(where):
29
+ return Path.home()
30
+ return where
File without changes
@@ -0,0 +1,104 @@
1
+ import sys
2
+ from pathlib import Path
3
+ import typer
4
+ from corkern import guard
5
+ from corkern.commands.signing_cmd import _ask
6
+ from corkern.config import (
7
+ UnstorablePath, _parse_conf, check_projects, confirm_projects, conf_entries, load_config,
8
+ resolve_project_path, same_path, store_project_path, valid_slug,
9
+ )
10
+
11
+
12
+ def _fail(message: str) -> None:
13
+ typer.echo(message, err=True)
14
+ raise typer.Exit(1)
15
+
16
+
17
+ def add_project(
18
+ slug: str = typer.Argument(None, help="Project slug (default: the folder name, like v1)"),
19
+ path: Path = typer.Argument(None, help="Project path (default: cwd)"),
20
+ confirm_all: bool = typer.Option(False, "--confirm-all",
21
+ help="List every project not confirmed on this machine; confirm them after one question"),
22
+ confirm_listed: bool = typer.Option(False, "--confirm-listed",
23
+ help="Confirm the projects whose path matches the slug:path lines on stdin "
24
+ "(this machine's own v1 registry; used by migrate/3)"),
25
+ ) -> None:
26
+ """Add a project to tracking — or confirm on this machine a project that is already registered.
27
+
28
+ corkern.conf is shared: a project's path is used to read its Claude Code conversations
29
+ (harvest, prune) or to wire its folder (init-device) only once this machine confirmed it.
30
+ """
31
+ if confirm_all or confirm_listed:
32
+ _confirm_many(confirm_listed)
33
+ return
34
+ if slug is None:
35
+ slug = (path or Path(".")).resolve().name
36
+ if not valid_slug(slug):
37
+ _fail(f"Invalid slug {slug!r}: use letters, digits, '.', '_' or '-', "
38
+ "starting with a letter or digit.")
39
+ resolved = (path or Path(".")).resolve()
40
+ try:
41
+ stored = store_project_path(resolved)
42
+ except UnstorablePath as e:
43
+ _fail(f"Invalid path {str(resolved)!r}: {e}.")
44
+
45
+ repo, projects = load_config()
46
+ # Like v1: any line for the slug counts, including other devices' (e.g. Windows) lines
47
+ if slug in {s for s, _ in conf_entries(guard.read_text(repo.conf_file))}:
48
+ check = check_projects(repo.path).get(slug)
49
+ match = next((p for p in (check.candidates if check else ()) if same_path(p, resolved)), None)
50
+ if match is None:
51
+ _fail(f"Slug '{slug}' already exists. Remove it first.")
52
+ _confirm(repo.path, {slug: match})
53
+ typer.echo(f"Confirmed {slug} → {match} on this machine")
54
+ return
55
+ for other, other_path in projects.items():
56
+ if other_path.resolve() == resolved:
57
+ _fail(f"Path '{resolved}' is already tracked under slug '{other}'.")
58
+
59
+ _confirm(repo.path, {slug: resolve_project_path(stored)})
60
+ guard.append_text(repo.conf_file, f"{slug}:{stored}\n")
61
+ typer.echo(f"Added {slug} → {resolved}")
62
+
63
+
64
+ def _confirm(repo: Path, items: dict[str, Path]) -> None:
65
+ try:
66
+ confirm_projects(repo, items)
67
+ except (OSError, guard.V1ProtectedError) as e:
68
+ _fail(f"Could not record the confirmation in {repo}/.git/corkern: {e}")
69
+
70
+
71
+ def _confirm_many(listed: bool) -> None:
72
+ repo, _ = load_config()
73
+ open_ = [c for c in check_projects(repo.path).values() if not c.confirmed]
74
+ if listed:
75
+ theirs = _parse_conf(sys.stdin.read(), "stdin")
76
+ found: dict[str, Path] = {}
77
+ for c in open_:
78
+ if c.slug not in theirs:
79
+ continue
80
+ match = next((p for p in c.candidates if same_path(p, theirs[c.slug])), None)
81
+ if match is None:
82
+ typer.echo(f" ! {c.slug}: the registry's path differs from the listed one "
83
+ f"({', '.join(map(str, c.candidates))} ≠ {theirs[c.slug]}) — not confirmed")
84
+ else:
85
+ found[c.slug] = match
86
+ _confirm(repo.path, found)
87
+ for slug, p in sorted(found.items()):
88
+ typer.echo(f" ✓ {slug} → {p} confirmed on this machine")
89
+ return
90
+ single = [c for c in open_ if not c.duplicate]
91
+ if not open_:
92
+ typer.echo("✓ Every project is confirmed on this machine.")
93
+ return
94
+ typer.echo("Not confirmed on this machine (corkern.conf is shared — check each path is yours):")
95
+ for c in single:
96
+ typer.echo(f" {c.slug} → {c.candidates[0]} ({c.lines[0]})")
97
+ for c in open_:
98
+ if c.duplicate:
99
+ typer.echo(f" ! {c.explain()}")
100
+ if single and _ask(f"Confirm these {len(single)} project(s) on this machine? [y/N] ", False):
101
+ _confirm(repo.path, {c.slug: c.candidates[0] for c in single})
102
+ typer.echo(f"✓ {len(single)} project(s) confirmed")
103
+ elif single:
104
+ typer.echo("Nothing confirmed.")
@@ -0,0 +1,238 @@
1
+ """get-AIinstructions and session-context — what Claude sees at the start of a session.
2
+
3
+ `session-context` is called by the SessionStart hook; its stdout is injected into Claude's
4
+ context, so it prints only useful text and never fails loudly.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import sys
10
+ from datetime import datetime, timedelta
11
+ from pathlib import Path
12
+
13
+ import typer
14
+
15
+ from corkern import devices, guard
16
+ from corkern.commands import metrics_cmd
17
+ from corkern.commands.reminders import GLOBAL, format_items, open_items, read_todo
18
+ from corkern.commands.secrets_cmd import log_entries
19
+ from corkern.commands.trust_cmd import UNTRUSTED, held_entries
20
+ from corkern.config import NOT_CONFIRMED, check_projects, conf_entries, load_config, valid_slug
21
+ from corkern.knowledge_repo import repo_symlinks, shown, tracked_machine_local
22
+ from corkern.memory_index import usage
23
+
24
+ PROG = "corkern"
25
+
26
+
27
+ def instructions_text() -> str:
28
+ repo, _ = load_config()
29
+ rows = []
30
+ for slug, raw in conf_entries(repo.read_conf()):
31
+ if valid_slug(slug):
32
+ rows.append(f"- `{slug}` → {raw.strip() or '~ (home)'}")
33
+ projects = "\n".join(rows) or "- (none yet)"
34
+ kr = repo.path
35
+ return f"""# Corkern — AI Instructions
36
+
37
+ Corkern is this developer's memory sync system for Claude Code. A private git
38
+ "knowledge repo" keeps Claude's memory files in sync across machines automatically.
39
+
40
+ ## What it does
41
+
42
+ 1. Pulls other devices' memories when a session starts, and between replies
43
+ 2. Commits each memory write immediately, with the file name in the commit message
44
+ 3. Pushes memories (global ones right away, project ones at session end)
45
+ 4. Never blocks Claude and never loses text: conflicting edits are kept side by side
46
+ as `<name>.conflict-<host>-<time>.md` — mention these to the user if you see them
47
+
48
+ ## Knowledge repo
49
+
50
+ `{kr}`
51
+
52
+ Memory files live under `{kr}/memory/<slug>/`. Global (cross-project) memories live
53
+ under `{kr}/memory/global/`.
54
+
55
+ ## Tracked projects
56
+
57
+ {projects}
58
+
59
+ ## CLI ({PROG})
60
+
61
+ {PROG} list | ls Tracked projects and whether their paths exist
62
+ {PROG} add <slug> [path] Track a project (default path: current directory)
63
+ {PROG} remove | rm <slug> Stop tracking a project
64
+ {PROG} status | doctor Knowledge repo and project overview
65
+ {PROG} remind "text" [slug] Add a reminder (default: current project, else global)
66
+ {PROG} todos | todo [slug|--all] List open reminders
67
+ {PROG} done <n> [slug] Mark reminder n as done
68
+ {PROG} sessions [slug] Recent session notes
69
+ {PROG} log [slug] Git history of a project's memory
70
+ {PROG} signing This machine's commit-signing key
71
+ {PROG} trust Which devices' memories this machine accepts
72
+ {PROG} secrets [--scan] Secrets removed before commit / check the repo
73
+ {PROG} init-device Point Claude Code's memory for each project here
74
+ {PROG} iwe [--setup|--install] IWE knowledge graph over the memories (MCP server)
75
+ {PROG} graphify [slug] [--install] [--full] Build/update the project knowledge graph
76
+ {PROG} crg [slug] [--install] code-review-graph (fast, AST-only)
77
+ {PROG} metrics Memory size, context pressure, growth
78
+ {PROG} harvest [slug] [--all] [-i] Memories from past Claude conversations
79
+ {PROG} refine [slug] Split harvested-*.md bulk files
80
+ {PROG} prune [slug] Conversation inventory (harvested / pending)
81
+ {PROG} distill [--auto] Cross-project synthesis with Claude
82
+ (confirm with the user first)
83
+ {PROG} memory-prune [slug] [--dry-run] Archive expired/stale memories (never deletes;
84
+ confirm with the user first)
85
+ {PROG} get-AIinstructions | ai This document
86
+
87
+ ## Secrets
88
+
89
+ Never write passwords, tokens or keys into memory files. If one slips in, corkern replaces it
90
+ with `[secret removed by corkern: <kind>]` before the commit; the original stays on this
91
+ machine only. Tell the user when you see such a marker.
92
+
93
+ ## Reminders
94
+
95
+ Add a reminder when work starts but won't finish this session, or when the user wants
96
+ to come back to something; clear it with `{PROG} done <n> [slug]` when the work is done.
97
+ Open reminders are shown at the start of every session.
98
+
99
+ ## Session notes
100
+
101
+ At the end of significant work, write a short note (5–10 bullets: done, next, decisions):
102
+
103
+ `{kr}/memory/<slug>/sessions/YYYY-MM-DD-HHmm.md`
104
+
105
+ It is committed and synced automatically; the next session on any machine sees it.
106
+ """
107
+
108
+
109
+ def get_ai_instructions() -> None:
110
+ """Print the corkern context document for AI assistants."""
111
+ typer.echo(instructions_text())
112
+
113
+
114
+ def _box(title: str, rows: list[str], footer: str | None = None) -> str:
115
+ lines = [f"╔═ Corkern: {title} " + "═" * max(3, 50 - len(title))]
116
+ lines += [f"║{row}" for row in rows]
117
+ if footer:
118
+ lines.append(f"║ {footer}")
119
+ lines.append("╚" + "═" * 55)
120
+ return "\n".join(lines)
121
+
122
+
123
+ def _todo_box(repo, slug: str) -> str | None:
124
+ lines = read_todo(repo, slug)
125
+ if not lines or not open_items(lines):
126
+ return None
127
+ return _box(f"open reminders [{slug}]", format_items(lines),
128
+ f"→ clear with: {PROG} done <n> {slug}")
129
+
130
+
131
+ def _surfacing_files(repo, slug: str) -> str | None:
132
+ """Memory files marked `surfaces: true` in their frontmatter (v1 'actionable' files)."""
133
+ rows = []
134
+ folder = repo.memory_dir / slug
135
+ for name, _ in sorted(guard.list_files(folder, ".md")):
136
+ if name in ("MEMORY.md", "todo.md"):
137
+ continue
138
+ head = guard.read_text(folder / name).split("\n")[:10]
139
+ if any("".join(line.split()) == "surfaces:true" for line in head):
140
+ desc = next((line.split(":", 1)[1].strip().strip('"') for line in head
141
+ if line.startswith("description:")), "")
142
+ rows.append(f" → {name}" + (f" — {desc}" if desc else ""))
143
+ return _box(f"actionable memory files [{slug}]", rows) if rows else None
144
+
145
+
146
+ def _unconfirmed_box(checks) -> list[str]:
147
+ open_ = sorted(s for s, c in checks.items() if not c.confirmed)
148
+ if not open_:
149
+ return []
150
+ return [_box(f"{len(open_)} project(s) {NOT_CONFIRMED}", [
151
+ f" {' '.join(open_[:10])}" + (" …" if len(open_) > 10 else ""),
152
+ " corkern.conf is shared; until this machine confirms a project's path, corkern does not",
153
+ " read its conversations, wire its folder or show its reminders. Tell the user."],
154
+ f"→ {PROG} add <slug> <path>, or {PROG} init-device to confirm")]
155
+
156
+
157
+ def _safety_boxes(repo, slug: str | None) -> list[str]:
158
+ """Things the user must know at session start: held-back commits, removed secrets,
159
+ an index Claude Code no longer loads completely."""
160
+ boxes = []
161
+ links = repo_symlinks(repo.path)
162
+ if links:
163
+ boxes.append(_box("symlinks in the knowledge repo — held/ignored", [
164
+ " These synced files are symlinks; corkern never follows them and treats them",
165
+ " as absent (a link can point at any file on this machine):",
166
+ *[f" {shown(rel)}" for rel in links[:10]],
167
+ *([f" … and {len(links) - 10} more"] if len(links) > 10 else []),
168
+ " Tell the user. Do not read or write them; replace them with regular files."],
169
+ f"→ {PROG} status lists them"))
170
+ tracked = tracked_machine_local(repo.path)
171
+ if tracked:
172
+ boxes.append(_box("per-machine files tracked in the knowledge repo", [
173
+ *[f" tracked per-machine file: {shown(rel)} — remove it on the remote" for rel in tracked[:10]],
174
+ " corkern never stages them again (this machine's copy stays local). Tell the user."],
175
+ f"→ {PROG} status"))
176
+ entries = held_entries(repo.path)
177
+ if entries:
178
+ untrusted = len({sha for sha, reason in entries if reason == UNTRUSTED})
179
+ reasons = list(dict.fromkeys(reason for sha, reason in entries if reason != UNTRUSTED))
180
+ boxes.append(_box("memories held back", [
181
+ " Nothing new from the remote was loaded this session. Reasons:",
182
+ *([f" - {untrusted} commit(s) not signed by a device this machine trusts"] if untrusted else []),
183
+ *[f" - {shown(reason)}" for reason in reasons[:8]],
184
+ " Tell the user; never recreate held content by hand. Review with:"],
185
+ f"→ {PROG} trust"))
186
+ recent = [e for e in log_entries()
187
+ if e[0] >= f"{datetime.now() - timedelta(days=3):%Y-%m-%d %H:%M:%S}"]
188
+ if recent:
189
+ rows = [f" {when[:16]} {rel} ({kinds})" for when, rel, kinds, _ in recent[-5:]]
190
+ boxes.append(_box("secrets removed before commit", [
191
+ " A secret was written into a memory and replaced by a marker before syncing:",
192
+ *rows, " Tell the user, and do not write secrets into memory files."],
193
+ f"→ originals stay on this machine: {PROG} secrets"))
194
+ index = guard.read_shared(repo.memory_dir / slug / "MEMORY.md") if slug else None
195
+ if index is not None:
196
+ u = usage(index)
197
+ if u.over or u.near:
198
+ state = "OVER" if u.over else "close to"
199
+ boxes.append(_box(f"MEMORY.md {state} Claude Code's read limit [{slug}]", [
200
+ f" {u.describe()}" + (" — entries past it are not loaded." if u.over else "."),
201
+ " Please shorten it now: one line per entry, details in topic files,",
202
+ " merge or drop stale entries (lines also arrive from other devices)."]))
203
+ return boxes
204
+
205
+
206
+ def _cwd_from_hook_stdin() -> Path:
207
+ if not sys.stdin.isatty():
208
+ try:
209
+ event = json.loads(sys.stdin.read() or "{}")
210
+ if isinstance(event, dict) and event.get("cwd"):
211
+ return Path(event["cwd"])
212
+ except (json.JSONDecodeError, OSError):
213
+ pass
214
+ return Path.cwd()
215
+
216
+
217
+ def session_context() -> None:
218
+ """(For the SessionStart hook) instructions, open reminders, actionable files."""
219
+ repo, projects = load_config()
220
+ cwd = _cwd_from_hook_stdin().resolve()
221
+ here = next((s for s, p in projects.items() if p.resolve() == cwd), None)
222
+ checks = check_projects(repo.path)
223
+ # only a path this machine confirmed brings a project's reminders and files into the session
224
+ slug = next((s for s, c in checks.items() if c.confirmed and c.path.resolve() == cwd), None)
225
+ text = instructions_text()
226
+ try:
227
+ devices.update(repo, projects, touch=here) # this device's manifest: last seen here
228
+ except (OSError, guard.SymlinkInRepoError):
229
+ pass # never break session start
230
+ if here:
231
+ try:
232
+ metrics_cmd.record(repo, here, text) # a session count, no project content
233
+ except (OSError, guard.SymlinkInRepoError):
234
+ pass # metrics must never break session start
235
+ parts = [text, *_safety_boxes(repo, slug), *_unconfirmed_box(checks)]
236
+ for s in [GLOBAL] + ([slug] if slug and slug != GLOBAL else []):
237
+ parts += [box for box in (_todo_box(repo, s), _surfacing_files(repo, s)) if box]
238
+ typer.echo("\n".join(parts))
@@ -0,0 +1,60 @@
1
+ """crg — build or install code-review-graph for a project (v1 `corkern crg` parity).
2
+
3
+ Same safety rules as graphify: projects inside v1 / ~/.claude are refused, and --install is
4
+ refused when it would write a protected place (code-review-graph install writes the
5
+ project's .claude/settings.json, .mcp.json and CLAUDE.md).
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import typer
10
+
11
+ from corkern import guard
12
+ from corkern.commands.reminders import fail, slug_for_cwd
13
+ from corkern.config import check_projects, load_config
14
+
15
+ TOOL = "code-review-graph"
16
+
17
+
18
+ def crg(
19
+ slug: str = typer.Argument(None, help="Project slug (default: current project)"),
20
+ install: bool = typer.Option(False, "--install", help="Install MCP server, hooks, CLAUDE.md"),
21
+ ) -> None:
22
+ """Build/update the project's code-review-graph (fast, AST-only, no LLM)."""
23
+ repo, projects = load_config()
24
+ slug = slug or slug_for_cwd(projects)
25
+ if not slug:
26
+ fail("Could not detect project slug. Usage: corkern crg [slug] [--install]\n"
27
+ f" Known slugs: {' '.join(sorted(projects))}")
28
+ if slug not in projects:
29
+ fail(f"Slug '{slug}' not found in corkern.conf")
30
+ check = check_projects(repo.path)[slug] # corkern.conf is shared: only a path confirmed here
31
+ if not check.confirmed:
32
+ fail(f"Refusing to run code-review-graph in the folder of {check.explain()}")
33
+ project = check.path
34
+ if not project.is_dir():
35
+ fail(f"Project directory not found: {project}")
36
+ guard.assert_safe(project)
37
+ exe = guard.which(TOOL)
38
+ if not exe:
39
+ fail(f"{TOOL} is not installed.\n Install it with: pipx install {TOOL}")
40
+
41
+ typer.echo(f"\ncorkern crg: {slug}\n Project: {project}")
42
+ if install:
43
+ try:
44
+ guard.assert_safe(project / ".claude")
45
+ except guard.V1ProtectedError:
46
+ fail(f"Refusing --install for '{slug}': {TOOL} would write {project / '.claude'}, "
47
+ "which is protected (global Claude settings).")
48
+ if guard.run_tool(project, [exe, "install"]) != 0:
49
+ fail(f"{TOOL} install failed")
50
+ typer.echo(f" ✓ {TOOL} installed (MCP server + hooks + CLAUDE.md) — restart Claude Code")
51
+ return
52
+
53
+ db = project / ".code-review-graph"
54
+ typer.echo(f" {'Updating' if db.exists() else 'Building'} the graph (AST only, no LLM)...")
55
+ rc = guard.run_tool(project, [exe, "build", "--repo", str(project)])
56
+ if rc != 0:
57
+ fail(f"{TOOL} build exited with code {rc}")
58
+ typer.echo(f" ✓ Graph: {db}")
59
+ if not (project / ".mcp.json").exists():
60
+ typer.echo(f" Next step — register it with Claude Code: corkern crg {slug} --install")
@@ -0,0 +1,106 @@
1
+ """distill — synthesise cross-project insights from all memories with Claude (v1 parity).
2
+
3
+ corkern distill build the prompt, open an interactive Claude session with it
4
+ corkern distill --auto run Claude non-interactively → memory/global/synthesis.md
5
+
6
+ Fixes vs v1 distill.sh:
7
+ - the previous synthesis.md is only replaced when Claude succeeds (v1 truncated it before
8
+ running Claude, so a failed run destroyed the last synthesis)
9
+ - no crash after saving (v1 referenced $BRAIN — a variable left over from the tool's old name
10
+ "brain" that is never set — under `set -u`)
11
+ - the project list in the prompt comes from corkern.conf (v1 hard-coded it)
12
+ - the prompt file (a copy of every memory) is written to this machine's state dir (0600), never
13
+ into the knowledge repo, where a synced symlink could redirect the write
14
+ """
15
+ from __future__ import annotations
16
+
17
+ from datetime import datetime
18
+
19
+ import typer
20
+
21
+ from corkern import claude_print, guard
22
+ from corkern.commands.reminders import fail
23
+ from corkern.config import conf_entries, load_config, state_dir, valid_slug
24
+
25
+ PROMPT_NAME = ".distill-prompt.md"
26
+
27
+ HEADER = """You are analyzing a personal knowledge base ("corkern") that captures everything
28
+ Claude Code has learned working with this developer across multiple projects.
29
+
30
+ Memory files are organized as: memory/<project>/<topic>.md
31
+ Projects present: {projects}
32
+
33
+ Please produce a structured synthesis with these sections:
34
+
35
+ ## Universal Principles
36
+ Rules or preferences that appear across 2+ projects (verbatim or in spirit).
37
+ For each: quote the source project(s) and note if it should be added to global/CLAUDE.md.
38
+
39
+ ## Cross-Project Opportunities
40
+ Insights from one project that would benefit others where NOT yet recorded.
41
+
42
+ ## Stale Candidates
43
+ Memory files that appear outdated, superseded, or about completed work.
44
+ For each: state why it may be stale.
45
+
46
+ ## Contradictions
47
+ Rules or preferences that conflict between projects.
48
+ For each: surface the tension and suggest resolution.
49
+
50
+ ## Who This Developer Is
51
+ A short paragraph synthesising working style, values, and instincts from all evidence combined.
52
+ Be specific — avoid generic statements.
53
+
54
+ ---
55
+ MEMORY FILES:
56
+ """
57
+
58
+
59
+ def build_prompt(repo) -> tuple[str, int]:
60
+ slugs = list(dict.fromkeys(s for s, _ in conf_entries(repo.read_conf())
61
+ if valid_slug(s)))
62
+ files = guard.walk_files(repo.memory_dir, ".md")
63
+ parts = [HEADER.format(projects=", ".join([*slugs, "global"]))]
64
+ for f in files:
65
+ parts.append(f"\n=== {f.relative_to(repo.path)} ===\n{guard.read_text(f)}\n")
66
+ return "".join(parts), len(files)
67
+
68
+
69
+ def distill(
70
+ auto: bool = typer.Option(False, "--auto", help="Run non-interactively → memory/global/synthesis.md"),
71
+ model: str = typer.Option(None, "--model", help="Claude model (default: Claude Code's default)"),
72
+ ) -> None:
73
+ """Synthesise cross-project insights from all memories with Claude."""
74
+ repo, _ = load_config()
75
+ typer.echo("=== Corkern Distill ===\n\nCollecting memories...")
76
+ prompt, count = build_prompt(repo)
77
+ prompt_file = state_dir() / PROMPT_NAME
78
+ guard.write_private(prompt_file, prompt)
79
+ typer.echo(f"✓ Collected {count} files ({len(prompt.encode())} bytes).\n")
80
+ claude = guard.which("claude")
81
+ model_args = ["--model", model] if model else []
82
+
83
+ if auto:
84
+ if not claude:
85
+ fail("'claude' CLI not found — install Claude Code or run without --auto.")
86
+ typer.echo("Running Claude analysis...")
87
+ result = claude_print.ask(claude, prompt, model)
88
+ if result.returncode != 0 or not result.stdout.strip():
89
+ fail(f"Claude failed (exit {result.returncode}) — previous synthesis.md left unchanged.")
90
+ now = datetime.now()
91
+ front = (f"---\nname: synthesis\ndescription: Cross-project synthesis — {now:%Y-%m-%d}\n"
92
+ f"metadata:\n type: reference\n generated: {now:%Y-%m-%dT%H:%M}\n---\n\n")
93
+ guard.write_text(repo.memory_dir / "global" / "synthesis.md", front + result.stdout)
94
+ typer.echo("✓ Saved: memory/global/synthesis.md")
95
+ typer.echo(" (Committed and synced by the next corkern hook run.)")
96
+ return
97
+
98
+ typer.echo(f"Prompt saved: {prompt_file}\n")
99
+ if not claude:
100
+ typer.echo(f"Claude CLI not found. To analyse manually, paste {prompt_file} into any Claude session.")
101
+ return
102
+ typer.echo("Starting interactive Claude analysis session...\n"
103
+ "(Claude will open with all your memories as context.)\n")
104
+ # from where the user is — unless that is inside the knowledge repo (its .claude/ is shared)
105
+ guard.exec_tool([claude, "--append-system-prompt-file", str(prompt_file), *model_args],
106
+ cwd=claude_print.interactive_cwd())