polymath-nexus 0.2.0__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.
- nexus/__init__.py +104 -0
- nexus/cli.py +223 -0
- nexus/config.py +45 -0
- nexus/discovery.py +77 -0
- nexus/index.py +689 -0
- nexus/memory/__init__.py +3 -0
- nexus/memory/chunker.py +151 -0
- nexus/memory/embedder.py +212 -0
- nexus/memory/migrate.py +128 -0
- nexus/memory/retriever.py +139 -0
- nexus/memory/store.py +779 -0
- nexus/memory/sync.py +79 -0
- nexus/memory/writer.py +295 -0
- nexus/mirrors.py +258 -0
- nexus/types.py +82 -0
- polymath_nexus-0.2.0.dist-info/METADATA +166 -0
- polymath_nexus-0.2.0.dist-info/RECORD +21 -0
- polymath_nexus-0.2.0.dist-info/WHEEL +5 -0
- polymath_nexus-0.2.0.dist-info/entry_points.txt +2 -0
- polymath_nexus-0.2.0.dist-info/licenses/LICENSE +21 -0
- polymath_nexus-0.2.0.dist-info/top_level.txt +1 -0
nexus/__init__.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Brain — long-term project memory.
|
|
3
|
+
|
|
4
|
+
This is the public API surface of the brain. Anything not exported here is
|
|
5
|
+
internal to brain and may change without notice. The surface is intentionally
|
|
6
|
+
small — the brain's value is in the *file format* (markdown + JSONL + date
|
|
7
|
+
stamps + owner attribution), not in any specific Python API.
|
|
8
|
+
|
|
9
|
+
Long-term contract: nothing inside `nexus.*` imports anything from
|
|
10
|
+
`polymath.*` outside the brain. Enforced by tests/test_brain_decoupling.py.
|
|
11
|
+
|
|
12
|
+
This boundary is what makes the brain extractable into its own package
|
|
13
|
+
later — when that happens, this exact `__init__.py` becomes the top-level
|
|
14
|
+
of the new `brain` distribution.
|
|
15
|
+
|
|
16
|
+
────────────────────────────────────────────────────────────────────────
|
|
17
|
+
Quick reference
|
|
18
|
+
|
|
19
|
+
# Discovery
|
|
20
|
+
from nexus import find_brain_root, find_repo_root, current_owner_slug
|
|
21
|
+
|
|
22
|
+
# Init
|
|
23
|
+
from nexus import init_brain_root
|
|
24
|
+
|
|
25
|
+
# Reading
|
|
26
|
+
from nexus import read_context, build_context_injection
|
|
27
|
+
from nexus import brain_grep, brain_usage
|
|
28
|
+
|
|
29
|
+
# Writing
|
|
30
|
+
from nexus import append_context, write_context
|
|
31
|
+
|
|
32
|
+
# Types
|
|
33
|
+
from nexus import TaskType, CONTEXT_FILES, AuthExpiredError
|
|
34
|
+
|
|
35
|
+
# Memory (chunked, embedded — optional, opt-in)
|
|
36
|
+
from nexus import Chunk, MemoryStore, harvest_session
|
|
37
|
+
"""
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
# ── Discovery ────────────────────────────────────────────────────────────────
|
|
41
|
+
from nexus.discovery import (
|
|
42
|
+
current_owner_slug,
|
|
43
|
+
find_brain_root,
|
|
44
|
+
find_repo_root,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# ── Types (canonical home) ───────────────────────────────────────────────────
|
|
48
|
+
from nexus.types import (
|
|
49
|
+
ALWAYS_INJECT,
|
|
50
|
+
CONTEXT_DESCRIPTIONS,
|
|
51
|
+
CONTEXT_FILES,
|
|
52
|
+
TASK_CONTEXT_MAP,
|
|
53
|
+
AuthExpiredError,
|
|
54
|
+
TaskType,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# ── Config ───────────────────────────────────────────────────────────────────
|
|
58
|
+
from nexus.config import (
|
|
59
|
+
BRAIN_CACHE_DIR,
|
|
60
|
+
BRAIN_DIR_NAME,
|
|
61
|
+
BRAIN_HOME,
|
|
62
|
+
BRAIN_PROJECTS_DIR,
|
|
63
|
+
MIRROR_FILES,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# ── Index (read/write) ───────────────────────────────────────────────────────
|
|
67
|
+
from nexus.index import (
|
|
68
|
+
append_context,
|
|
69
|
+
brain_grep,
|
|
70
|
+
brain_usage,
|
|
71
|
+
build_context_injection,
|
|
72
|
+
context_dir,
|
|
73
|
+
create_project,
|
|
74
|
+
init_brain_root,
|
|
75
|
+
list_context_files,
|
|
76
|
+
list_projects,
|
|
77
|
+
read_context,
|
|
78
|
+
write_context,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# ── Memory (chunked store, optional) ─────────────────────────────────────────
|
|
82
|
+
from nexus.memory.store import Chunk, MemoryStore
|
|
83
|
+
from nexus.memory.writer import (
|
|
84
|
+
HarvestResult,
|
|
85
|
+
harvest_session,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
__all__ = [
|
|
89
|
+
# Discovery
|
|
90
|
+
"current_owner_slug", "find_brain_root", "find_repo_root",
|
|
91
|
+
# Types
|
|
92
|
+
"TaskType", "AuthExpiredError",
|
|
93
|
+
"CONTEXT_FILES", "CONTEXT_DESCRIPTIONS", "TASK_CONTEXT_MAP", "ALWAYS_INJECT",
|
|
94
|
+
# Config
|
|
95
|
+
"BRAIN_HOME", "BRAIN_PROJECTS_DIR", "BRAIN_CACHE_DIR", "BRAIN_DIR_NAME",
|
|
96
|
+
"MIRROR_FILES",
|
|
97
|
+
# Index
|
|
98
|
+
"init_brain_root", "create_project", "list_projects",
|
|
99
|
+
"context_dir", "list_context_files",
|
|
100
|
+
"read_context", "write_context", "append_context",
|
|
101
|
+
"build_context_injection", "brain_grep", "brain_usage",
|
|
102
|
+
# Memory
|
|
103
|
+
"Chunk", "MemoryStore", "HarvestResult", "harvest_session",
|
|
104
|
+
]
|
nexus/cli.py
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""
|
|
2
|
+
`nexus` — standalone CLI for the long-term brain.
|
|
3
|
+
|
|
4
|
+
No LLM dependencies. Pure stdlib + nexus. Works with `pip install nexus`
|
|
5
|
+
(brain-only) or as a dependency of polymath.
|
|
6
|
+
|
|
7
|
+
Subcommands:
|
|
8
|
+
nexus init Create brain/ in cwd
|
|
9
|
+
nexus show [bucket] [--since DATE] [--until DATE] [--by OWNER] [-q PATTERN]
|
|
10
|
+
Search the brain. Grep-first.
|
|
11
|
+
nexus usage Counts per bucket + per owner
|
|
12
|
+
nexus own Print which slug you'll write as
|
|
13
|
+
nexus learn-from "<correction>" Append to rules/<you>.md (deliberate)
|
|
14
|
+
nexus sync Regenerate the AI-tool mirror files
|
|
15
|
+
nexus unmirror Remove the AI-tool mirror files
|
|
16
|
+
nexus spec Print the SPEC.md format contract
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import argparse
|
|
21
|
+
import json
|
|
22
|
+
import sys
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
from nexus.discovery import current_owner_slug, find_brain_root, find_repo_root
|
|
26
|
+
from nexus.index import (
|
|
27
|
+
append_context, brain_grep, brain_usage, context_dir, create_project,
|
|
28
|
+
init_brain_root,
|
|
29
|
+
)
|
|
30
|
+
from nexus.mirrors import regenerate_mirrors, remove_mirrors
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _cmd_init(args) -> int:
|
|
34
|
+
base = init_brain_root(Path(args.cwd) if args.cwd else None)
|
|
35
|
+
repo = find_repo_root() or base.parent
|
|
36
|
+
report = regenerate_mirrors(repo_root=repo, force=getattr(args, "force", False))
|
|
37
|
+
print(f"✓ Brain initialised at {base}")
|
|
38
|
+
if report.written:
|
|
39
|
+
for tool, path in report.written.items():
|
|
40
|
+
print(f" ↳ mirrored to {path.relative_to(repo)} (for {tool})")
|
|
41
|
+
elif not report.skipped:
|
|
42
|
+
print(" (brain is empty; mirrors will be generated after first write)")
|
|
43
|
+
_print_skipped(report, repo)
|
|
44
|
+
print(f"\nYour owner slug: {current_owner_slug()}")
|
|
45
|
+
print("\nNext: `nexus learn-from \"your first rule\"`")
|
|
46
|
+
return 0
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _cmd_show(args) -> int:
|
|
50
|
+
project = args.project or ""
|
|
51
|
+
hits = brain_grep(
|
|
52
|
+
project,
|
|
53
|
+
pattern=args.pattern or "",
|
|
54
|
+
since=args.since or "",
|
|
55
|
+
until=args.until or "",
|
|
56
|
+
by=args.by or "",
|
|
57
|
+
ctx_name=args.bucket or "",
|
|
58
|
+
)
|
|
59
|
+
if not hits:
|
|
60
|
+
print("(no matches)")
|
|
61
|
+
return 0
|
|
62
|
+
if args.json:
|
|
63
|
+
print(json.dumps(hits, indent=2))
|
|
64
|
+
return 0
|
|
65
|
+
for h in hits:
|
|
66
|
+
print(f"{h['file']:40s} {h['text']}")
|
|
67
|
+
return 0
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _cmd_usage(args) -> int:
|
|
71
|
+
project = args.project or ""
|
|
72
|
+
stats = brain_usage(project)
|
|
73
|
+
if args.json:
|
|
74
|
+
print(json.dumps(stats, indent=2))
|
|
75
|
+
return 0
|
|
76
|
+
print(f"Total entries: {stats['total_entries']}\n")
|
|
77
|
+
if stats["buckets"]:
|
|
78
|
+
print("By bucket:")
|
|
79
|
+
for k, v in sorted(stats["buckets"].items(), key=lambda kv: -kv[1]):
|
|
80
|
+
print(f" {k:12s} {v}")
|
|
81
|
+
if stats["owners"]:
|
|
82
|
+
print("\nBy owner:")
|
|
83
|
+
for k, v in sorted(stats["owners"].items(), key=lambda kv: -kv[1]):
|
|
84
|
+
print(f" {k:12s} {v}")
|
|
85
|
+
return 0
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _cmd_own(args) -> int:
|
|
89
|
+
"""Print owner slug and the timezone we'll record on writes."""
|
|
90
|
+
from nexus.memory.store import _local_tz_name
|
|
91
|
+
print(f"owner: {current_owner_slug()}")
|
|
92
|
+
print(f"tz: {_local_tz_name()}")
|
|
93
|
+
return 0
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _cmd_learn_from(args) -> int:
|
|
97
|
+
text = args.text or ""
|
|
98
|
+
if not text:
|
|
99
|
+
print("usage: nexus learn-from \"<rule>\"", file=sys.stderr)
|
|
100
|
+
return 2
|
|
101
|
+
project = args.project or ""
|
|
102
|
+
brain = find_brain_root()
|
|
103
|
+
if brain is None and not project:
|
|
104
|
+
# Need either a brain/ in cwd or a project name for personal scope.
|
|
105
|
+
print("error: no brain/ found in this repo. Run `nexus init` first,\n"
|
|
106
|
+
"or use --project <name> for personal-scope brains.", file=sys.stderr)
|
|
107
|
+
return 1
|
|
108
|
+
if not brain and project:
|
|
109
|
+
create_project(project)
|
|
110
|
+
owner = current_owner_slug()
|
|
111
|
+
written = append_context(project, "rules", text, by_owner=True, author=owner)
|
|
112
|
+
print(f"✓ Logged to {written.relative_to(written.parents[2]) if len(written.parents) >= 3 else written}")
|
|
113
|
+
_dt = __import__("datetime").datetime
|
|
114
|
+
today = _dt.now().astimezone().strftime("%Y-%m-%d")
|
|
115
|
+
print(f" - [{today} {owner}] {text}")
|
|
116
|
+
return 0
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _print_skipped(report, repo: Path) -> None:
|
|
120
|
+
"""Tell the user about mirror files we refused to overwrite. Silence
|
|
121
|
+
here would look like the mirror simply worked."""
|
|
122
|
+
for tool, path in report.backed_up.items():
|
|
123
|
+
print(f" ↳ saved your old {tool} file to {path.relative_to(repo)}")
|
|
124
|
+
if not report.skipped:
|
|
125
|
+
return
|
|
126
|
+
for tool, path in report.skipped.items():
|
|
127
|
+
print(f" skipped {path.relative_to(repo)} — you wrote this, not nexus")
|
|
128
|
+
print(" Move its content into brain/, or run with --force to hand it "
|
|
129
|
+
"over (your version is saved as a .bak first).")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _cmd_sync(args) -> int:
|
|
133
|
+
repo = find_repo_root() or (find_brain_root() or Path.cwd()).parent
|
|
134
|
+
report = regenerate_mirrors(args.project or "", repo_root=repo, force=args.force)
|
|
135
|
+
if not report.written and not report.skipped:
|
|
136
|
+
print("(brain is empty; nothing to mirror)")
|
|
137
|
+
return 0
|
|
138
|
+
for tool, path in report.written.items():
|
|
139
|
+
print(f"✓ {tool}: {path.relative_to(repo)}")
|
|
140
|
+
_print_skipped(report, repo)
|
|
141
|
+
return 0
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _cmd_unmirror(args) -> int:
|
|
145
|
+
report = remove_mirrors(force=args.force)
|
|
146
|
+
for p in report.removed:
|
|
147
|
+
print(f"removed {p}")
|
|
148
|
+
for p in report.skipped:
|
|
149
|
+
print(f"kept {p} — you wrote this, not nexus")
|
|
150
|
+
if report.skipped:
|
|
151
|
+
print("Pass --force to delete it anyway.")
|
|
152
|
+
if not report.removed and not report.skipped:
|
|
153
|
+
print("(no mirror files present)")
|
|
154
|
+
return 0
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _cmd_spec(args) -> int:
|
|
158
|
+
spec = Path(__file__).resolve().parent / "SPEC.md"
|
|
159
|
+
if spec.exists():
|
|
160
|
+
print(spec.read_text())
|
|
161
|
+
return 0
|
|
162
|
+
print(f"SPEC.md not found at {spec}", file=sys.stderr)
|
|
163
|
+
return 1
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
167
|
+
p = argparse.ArgumentParser(
|
|
168
|
+
prog="nexus",
|
|
169
|
+
description="Long-term, team-shareable project brain.",
|
|
170
|
+
)
|
|
171
|
+
p.add_argument("--project", default="", help="project name (personal scope)")
|
|
172
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
173
|
+
|
|
174
|
+
p_init = sub.add_parser("init", help="create brain/ in cwd")
|
|
175
|
+
p_init.add_argument("--cwd", default="", help="target directory (default: cwd)")
|
|
176
|
+
p_init.add_argument("--force", action="store_true",
|
|
177
|
+
help="take over mirror files you wrote by hand (saves a .bak)")
|
|
178
|
+
p_init.set_defaults(func=_cmd_init)
|
|
179
|
+
|
|
180
|
+
p_show = sub.add_parser("show", help="search the brain (grep-first)")
|
|
181
|
+
p_show.add_argument("bucket", nargs="?", default="", help="one of: rules logic code stack data goals decisions glossary personas")
|
|
182
|
+
p_show.add_argument("--since", default="", help="YYYY-MM-DD lower bound")
|
|
183
|
+
p_show.add_argument("--until", default="", help="YYYY-MM-DD upper bound")
|
|
184
|
+
p_show.add_argument("--by", default="", help="owner slug")
|
|
185
|
+
p_show.add_argument("-q", "--pattern", default="", help="substring filter (case-insensitive)")
|
|
186
|
+
p_show.add_argument("--json", action="store_true", help="emit JSON")
|
|
187
|
+
p_show.set_defaults(func=_cmd_show)
|
|
188
|
+
|
|
189
|
+
p_usage = sub.add_parser("usage", help="counts per bucket and per owner (read-only)")
|
|
190
|
+
p_usage.add_argument("--json", action="store_true")
|
|
191
|
+
p_usage.set_defaults(func=_cmd_usage)
|
|
192
|
+
|
|
193
|
+
p_own = sub.add_parser("own", help="print which owner slug you'll write as")
|
|
194
|
+
p_own.set_defaults(func=_cmd_own)
|
|
195
|
+
|
|
196
|
+
p_learn = sub.add_parser("learn-from", help="log a correction to rules/<you>.md")
|
|
197
|
+
p_learn.add_argument("text", nargs="?", default="", help="the rule to log")
|
|
198
|
+
p_learn.set_defaults(func=_cmd_learn_from)
|
|
199
|
+
|
|
200
|
+
p_sync = sub.add_parser("sync", help="regenerate AI-tool mirror files")
|
|
201
|
+
p_sync.add_argument("--force", action="store_true",
|
|
202
|
+
help="take over mirror files you wrote by hand (saves a .bak)")
|
|
203
|
+
p_sync.set_defaults(func=_cmd_sync)
|
|
204
|
+
|
|
205
|
+
p_unmirror = sub.add_parser("unmirror", help="remove AI-tool mirror files")
|
|
206
|
+
p_unmirror.add_argument("--force", action="store_true",
|
|
207
|
+
help="also delete mirror files nexus did not write")
|
|
208
|
+
p_unmirror.set_defaults(func=_cmd_unmirror)
|
|
209
|
+
|
|
210
|
+
p_spec = sub.add_parser("spec", help="print the brain SPEC.md")
|
|
211
|
+
p_spec.set_defaults(func=_cmd_spec)
|
|
212
|
+
|
|
213
|
+
return p
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def main(argv: list[str] | None = None) -> int:
|
|
217
|
+
parser = build_parser()
|
|
218
|
+
args = parser.parse_args(argv)
|
|
219
|
+
return args.func(args)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
if __name__ == "__main__":
|
|
223
|
+
sys.exit(main())
|
nexus/config.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Brain-owned configuration. Independent of polymath.config — that's the
|
|
3
|
+
whole point of the decoupling.
|
|
4
|
+
|
|
5
|
+
Brain data has two homes:
|
|
6
|
+
1. In-repo: <repo>/brain/ ← the canonical, shared brain
|
|
7
|
+
2. Personal: $POLYMATH_BRAIN_HOME or ~/.brain/projects/<name>/
|
|
8
|
+
← personal scratch / no-repo
|
|
9
|
+
projects
|
|
10
|
+
|
|
11
|
+
Long-term contract: BRAIN_HOME lives at ~/.brain/, NOT ~/.polymath/. The
|
|
12
|
+
brain owns its own home directory — it must not depend on Polymath being
|
|
13
|
+
installed to find its own data.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import os
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _brain_home() -> Path:
|
|
22
|
+
"""Personal-scope brain root. Override via $POLYMATH_BRAIN_HOME."""
|
|
23
|
+
override = os.environ.get("POLYMATH_BRAIN_HOME")
|
|
24
|
+
if override:
|
|
25
|
+
return Path(override).expanduser().resolve()
|
|
26
|
+
return Path.home() / ".brain"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
BRAIN_HOME = _brain_home()
|
|
30
|
+
BRAIN_PROJECTS_DIR = BRAIN_HOME / "projects"
|
|
31
|
+
BRAIN_CACHE_DIR = BRAIN_HOME / "cache"
|
|
32
|
+
|
|
33
|
+
# In-repo brain folder name. Always exactly "brain" — this is part of the
|
|
34
|
+
# format SPEC and tools downstream rely on the name. Do not parameterise.
|
|
35
|
+
BRAIN_DIR_NAME = "brain"
|
|
36
|
+
|
|
37
|
+
# Mirror file names. These are what tools downstream auto-discover. The
|
|
38
|
+
# brain writes them; we never read them back (the brain is the source of
|
|
39
|
+
# truth, the mirrors are derived artifacts).
|
|
40
|
+
MIRROR_FILES = {
|
|
41
|
+
"claude": "CLAUDE.md",
|
|
42
|
+
"agents": "AGENTS.md",
|
|
43
|
+
"cursor": ".cursor/rules/brain.mdc",
|
|
44
|
+
"copilot": ".github/copilot-instructions.md",
|
|
45
|
+
}
|
nexus/discovery.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Brain discovery — find a brain in the cwd hierarchy, or fall back to
|
|
3
|
+
personal/no-repo scope.
|
|
4
|
+
|
|
5
|
+
This is intentionally a small, dependency-free module. Anything that needs
|
|
6
|
+
to locate a brain (brain CLI, Polymath orchestrator, third-party tool)
|
|
7
|
+
calls into here. Long-term-stable: the discovery rules are part of the
|
|
8
|
+
SPEC.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import re
|
|
14
|
+
import subprocess
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from nexus.config import BRAIN_DIR_NAME, BRAIN_PROJECTS_DIR
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def find_brain_root(start: Path | None = None) -> Path | None:
|
|
21
|
+
"""
|
|
22
|
+
Walk upward from `start` looking for a `brain/` directory. Mirrors
|
|
23
|
+
git's discovery semantics. Returns the absolute path or None.
|
|
24
|
+
|
|
25
|
+
Resolution: every parent up to the filesystem root. The first match
|
|
26
|
+
wins. This means nested repos containing a `brain/` folder are
|
|
27
|
+
discovered correctly from a sub-directory.
|
|
28
|
+
"""
|
|
29
|
+
cwd = Path(start or Path.cwd()).resolve()
|
|
30
|
+
for directory in [cwd, *cwd.parents]:
|
|
31
|
+
candidate = directory / BRAIN_DIR_NAME
|
|
32
|
+
if candidate.is_dir():
|
|
33
|
+
return candidate
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def current_owner_slug() -> str:
|
|
38
|
+
"""
|
|
39
|
+
Derive a stable owner slug for the current user. Used to namespace
|
|
40
|
+
per-owner files (rules/ayushi.md, decisions/akhil.md) so two devs
|
|
41
|
+
writing simultaneously never collide.
|
|
42
|
+
|
|
43
|
+
Resolution order:
|
|
44
|
+
1. $POLYMATH_BRAIN_OWNER (explicit override)
|
|
45
|
+
2. git config user.email → local part → sanitised slug
|
|
46
|
+
3. $USER
|
|
47
|
+
4. "anon" (never empty)
|
|
48
|
+
"""
|
|
49
|
+
override = os.environ.get("POLYMATH_BRAIN_OWNER", "").strip()
|
|
50
|
+
if override:
|
|
51
|
+
return _slugify(override)
|
|
52
|
+
|
|
53
|
+
email = ""
|
|
54
|
+
try:
|
|
55
|
+
email = subprocess.run(
|
|
56
|
+
["git", "config", "--get", "user.email"],
|
|
57
|
+
capture_output=True, text=True, timeout=2,
|
|
58
|
+
).stdout.strip()
|
|
59
|
+
except Exception:
|
|
60
|
+
email = ""
|
|
61
|
+
raw = email.split("@")[0] if email else os.environ.get("USER", "")
|
|
62
|
+
return _slugify(raw or "anon")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _slugify(raw: str) -> str:
|
|
66
|
+
slug = re.sub(r"[^a-z0-9]+", "-", (raw or "anon").lower()).strip("-")
|
|
67
|
+
return slug or "anon"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def find_repo_root(start: Path | None = None) -> Path | None:
|
|
71
|
+
"""Walk upward looking for `.git/`. Used to anchor mirror files at the
|
|
72
|
+
repo root (CLAUDE.md, AGENTS.md, etc.)."""
|
|
73
|
+
cwd = Path(start or Path.cwd()).resolve()
|
|
74
|
+
for directory in [cwd, *cwd.parents]:
|
|
75
|
+
if (directory / ".git").exists():
|
|
76
|
+
return directory
|
|
77
|
+
return None
|