mneme-code 3.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.
@@ -0,0 +1,88 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ develop-eggs/
9
+ dist/
10
+ downloads/
11
+ eggs/
12
+ .eggs/
13
+ lib/
14
+ lib64/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ share/python-wheels/
20
+ *.egg-info/
21
+ .installed.cfg
22
+ *.egg
23
+ MANIFEST
24
+ .pytest_cache/
25
+ .coverage
26
+ .coverage.*
27
+ .cache
28
+ nosetests.xml
29
+ coverage.xml
30
+ *.cover
31
+ .hypothesis/
32
+ htmlcov/
33
+ .mypy_cache/
34
+ .ruff_cache/
35
+
36
+ # Virtual envs
37
+ .env
38
+ .venv
39
+ env/
40
+ venv/
41
+ ENV/
42
+ env.bak/
43
+ venv.bak/
44
+
45
+ # Node
46
+ node_modules/
47
+ npm-debug.log*
48
+ yarn-debug.log*
49
+ yarn-error.log*
50
+ pnpm-debug.log*
51
+ .pnpm-store/
52
+ *.tsbuildinfo
53
+
54
+ # TypeScript
55
+ *.js.map
56
+ *.d.ts.map
57
+
58
+ # IDE
59
+ .vscode/
60
+ .idea/
61
+ *.swp
62
+ *.swo
63
+ .DS_Store
64
+
65
+ # OS
66
+ Thumbs.db
67
+ desktop.ini
68
+
69
+ # mneme local
70
+ .mneme/
71
+ *.local.json
72
+ vault-test/
73
+ benchmarks/results/
74
+ benchmarks/_runs/
75
+ benchmarks/*/output/
76
+ benchmarks/*/result.json
77
+ benchmarks/*/hardware.json
78
+
79
+ # Secrets
80
+ .env.local
81
+ .env.*.local
82
+ secrets/
83
+ *.key
84
+ *.pem
85
+
86
+ # Build artifacts
87
+ *.tgz
88
+ *.whl
@@ -0,0 +1,59 @@
1
+ Metadata-Version: 2.4
2
+ Name: mneme-code
3
+ Version: 3.1.0
4
+ Summary: Deterministic Python code-failure memory layer for mneme: redact-before-store, provenance-labelled FailureMemory, vault-ready markdown output.
5
+ Author: Onour Impram
6
+ License: Apache-2.0
7
+ Keywords: code-failure,memory,mneme,redaction,traceback
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: Apache Software License
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
16
+ Requires-Python: >=3.11
17
+ Requires-Dist: mneme-core<4,>=3.0.0
18
+ Requires-Dist: mneme-graph>=0.1.0
19
+ Provides-Extra: dev
20
+ Requires-Dist: mypy>=1.10; extra == 'dev'
21
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
22
+ Requires-Dist: pytest>=8.2; extra == 'dev'
23
+ Requires-Dist: ruff>=0.4.7; extra == 'dev'
24
+ Description-Content-Type: text/markdown
25
+
26
+ # mneme-code
27
+
28
+ Deterministic Python code-failure memory for mneme. Parses CPython tracebacks
29
+ into structured failures, renders them as vault-ground-truth markdown memories,
30
+ and resolves stack frames to [mneme-graph](../mneme-graph) function nodes.
31
+
32
+ Part of the [mneme](https://github.com/TheGoatPsy/mneme) memory engine.
33
+
34
+ ## What it does (v1)
35
+
36
+ - **Traceback parsing.** `parse_traceback` turns a standard CPython traceback
37
+ into a `ParsedTraceback` (exception type, message, frames). It never raises;
38
+ unrecognised input returns `None`.
39
+ - **Failure memories.** `failure_from_traceback` + `failure_to_markdown` produce
40
+ a markdown note the user owns, with provenance (`content_hash`, `observed_at`,
41
+ `trust`) and a confidence label.
42
+ - **Frame resolution.** `resolve_frames` pairs each frame with a mneme-graph
43
+ `function` node when one matches, with a clean fallback to `None`.
44
+
45
+ Redaction (`mneme_core.privacy.redact`) is applied to every user-derived string
46
+ (exception message, code context, file path) before it is stored or rendered.
47
+ No LLM, no network; library functions are pure (the caller injects timestamps).
48
+
49
+ ## Scope and deferrals (v1)
50
+
51
+ The following are **not** implemented yet and are documented here for honesty:
52
+
53
+ - Live test-runner integration.
54
+ - Branch-aware failure tracking.
55
+ - `AGENTS.md` procedural-memory parsing and repo runbook generation.
56
+ - Fix-trajectory wiring: a *fix* is modelled as a `mneme-temporal` claim that
57
+ **supersedes** the failure memory. That lifecycle lives in `mneme-temporal`
58
+ and is reused, not reinvented here.
59
+ - Non-Python tracebacks.
@@ -0,0 +1,34 @@
1
+ # mneme-code
2
+
3
+ Deterministic Python code-failure memory for mneme. Parses CPython tracebacks
4
+ into structured failures, renders them as vault-ground-truth markdown memories,
5
+ and resolves stack frames to [mneme-graph](../mneme-graph) function nodes.
6
+
7
+ Part of the [mneme](https://github.com/TheGoatPsy/mneme) memory engine.
8
+
9
+ ## What it does (v1)
10
+
11
+ - **Traceback parsing.** `parse_traceback` turns a standard CPython traceback
12
+ into a `ParsedTraceback` (exception type, message, frames). It never raises;
13
+ unrecognised input returns `None`.
14
+ - **Failure memories.** `failure_from_traceback` + `failure_to_markdown` produce
15
+ a markdown note the user owns, with provenance (`content_hash`, `observed_at`,
16
+ `trust`) and a confidence label.
17
+ - **Frame resolution.** `resolve_frames` pairs each frame with a mneme-graph
18
+ `function` node when one matches, with a clean fallback to `None`.
19
+
20
+ Redaction (`mneme_core.privacy.redact`) is applied to every user-derived string
21
+ (exception message, code context, file path) before it is stored or rendered.
22
+ No LLM, no network; library functions are pure (the caller injects timestamps).
23
+
24
+ ## Scope and deferrals (v1)
25
+
26
+ The following are **not** implemented yet and are documented here for honesty:
27
+
28
+ - Live test-runner integration.
29
+ - Branch-aware failure tracking.
30
+ - `AGENTS.md` procedural-memory parsing and repo runbook generation.
31
+ - Fix-trajectory wiring: a *fix* is modelled as a `mneme-temporal` claim that
32
+ **supersedes** the failure memory. That lifecycle lives in `mneme-temporal`
33
+ and is reused, not reinvented here.
34
+ - Non-Python tracebacks.
@@ -0,0 +1,71 @@
1
+ """mneme-code: deterministic Python code-failure memory layer.
2
+
3
+ Design invariants
4
+ -----------------
5
+ * No LLM, no network — pure stdlib + mneme-core + mneme-graph.
6
+ * Redact-before-store — ``mneme_core.privacy.redact`` is applied to
7
+ ``exc_message``, every ``code_context``, and every ``file_path``
8
+ *before* any dataclass is constructed or any text is rendered.
9
+ * Provenance on every record — ``content_hash`` (sha256 of canonical
10
+ redacted rendering), ``observed_at`` (injected by caller; never
11
+ produced inside library functions), ``trust`` (default ``"user"``),
12
+ and ``confidence`` (default ``"EXTRACTED"``).
13
+ * Deterministic — same inputs always produce the same ``failure_id``
14
+ and ``content_hash``; no ``datetime.now()`` or ``random`` inside
15
+ library functions.
16
+ * Markdown is the ground-truth artifact — ``failure_to_markdown``
17
+ produces a vault-ready note; the vault is the store.
18
+
19
+ DEFER (not built in this version)
20
+ ----------------------------------
21
+ * Live in-process test-runner plugin (a pytest hook). Console-output parsing
22
+ of pytest/unittest IS built — see ``testrun``.
23
+ * Branch-aware failure tracking.
24
+ * Repository runbook generation.
25
+ * Non-Python tracebacks (JavaScript, Rust, Go, etc.).
26
+
27
+ Public API
28
+ ----------
29
+ from mneme_code.stacktrace import Frame, ParsedTraceback, parse_traceback
30
+ from mneme_code.failure import FailureMemory, failure_from_traceback, failure_to_markdown
31
+ from mneme_code.resolve import resolve_frames
32
+ from mneme_code.agents import ProceduralMemory, parse_agents_md, procedural_to_markdown
33
+ from mneme_code.testrun import (
34
+ TestFailure, parse_pytest_output, parse_unittest_output, failures_from_test_output,
35
+ )
36
+ from mneme_code.trajectory import FixTrajectory, fix_from_failure, fix_to_markdown
37
+ """
38
+
39
+ from mneme_code.agents import ProceduralMemory, parse_agents_md, procedural_to_markdown
40
+ from mneme_code.failure import FailureMemory, failure_from_traceback, failure_to_markdown
41
+ from mneme_code.resolve import resolve_frames
42
+ from mneme_code.stacktrace import Frame, ParsedTraceback, parse_traceback
43
+ from mneme_code.testrun import (
44
+ TestFailure,
45
+ failures_from_test_output,
46
+ parse_pytest_output,
47
+ parse_unittest_output,
48
+ )
49
+ from mneme_code.trajectory import FixTrajectory, fix_from_failure, fix_to_markdown
50
+
51
+ __version__ = "0.2.0"
52
+ __all__ = [
53
+ "__version__",
54
+ "Frame",
55
+ "ParsedTraceback",
56
+ "parse_traceback",
57
+ "FailureMemory",
58
+ "failure_from_traceback",
59
+ "failure_to_markdown",
60
+ "resolve_frames",
61
+ "ProceduralMemory",
62
+ "parse_agents_md",
63
+ "procedural_to_markdown",
64
+ "TestFailure",
65
+ "parse_pytest_output",
66
+ "parse_unittest_output",
67
+ "failures_from_test_output",
68
+ "FixTrajectory",
69
+ "fix_from_failure",
70
+ "fix_to_markdown",
71
+ ]
@@ -0,0 +1,240 @@
1
+ """ProceduralMemory dataclass and vault-ready markdown serialization for AGENTS.md.
2
+
3
+ Parses an AGENTS.md (or any conventions markdown) into structured procedural
4
+ memories, one per level-2 (``##``) heading section.
5
+
6
+ Design invariants (mirror stacktrace.py / failure.py):
7
+ * Pure, deterministic — no clock, no random, no I/O inside parse functions.
8
+ * Redact-before-store — ``mneme_core.privacy.redact`` is applied to heading
9
+ text, body text, and fenced-command text before any dataclass field is set.
10
+ * Never raises — ``parse_agents_md`` catches all exceptions and returns ``[]``.
11
+ * Frozen dataclasses for all public types.
12
+ * ``procedural_to_markdown`` accepts ``created: datetime`` injected by caller.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import hashlib
18
+ import json
19
+ import re
20
+ import uuid
21
+ from dataclasses import dataclass, field
22
+ from datetime import UTC, datetime
23
+
24
+ from mneme_core.privacy import redact
25
+
26
+ # ---------------------------------------------------------------------------
27
+ # Regex helpers
28
+ # ---------------------------------------------------------------------------
29
+
30
+ # Level-2 heading: "## Some Title" (with optional trailing whitespace)
31
+ _H2 = re.compile(r"^##\s+(.+?)\s*$", re.MULTILINE)
32
+
33
+ # Fenced code block: ```[lang]\n...\n``` (DOTALL so body can span lines)
34
+ _FENCE = re.compile(r"```[^\n]*\n(.*?)```", re.DOTALL)
35
+
36
+
37
+ # ---------------------------------------------------------------------------
38
+ # Dataclass
39
+ # ---------------------------------------------------------------------------
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class ProceduralMemory:
44
+ """A single procedural-memory record extracted from one ## section.
45
+
46
+ All string fields have already been passed through ``redact()`` before
47
+ construction; callers must not bypass this by constructing
48
+ ``ProceduralMemory`` directly with raw user content.
49
+
50
+ Attributes:
51
+ memory_id Deterministic UUID5(NAMESPACE_URL, source_path + NUL + heading).
52
+ title Redacted section heading text (without the "## " prefix).
53
+ content Redacted section body (prose text, fenced blocks stripped out).
54
+ commands Tuple of fenced code-block contents found in the section,
55
+ redacted, in document order.
56
+ source_path Vault-relative posix path supplied by the caller.
57
+ content_hash SHA-256 hex of ``redact(full_file_text)``.
58
+ trust Provenance tier; default ``"user"``.
59
+ confidence Confidence label; default ``"EXTRACTED"``.
60
+ """
61
+
62
+ memory_id: str
63
+ title: str
64
+ content: str
65
+ commands: tuple[str, ...]
66
+ source_path: str
67
+ content_hash: str
68
+ trust: str = field(default="user")
69
+ confidence: str = field(default="EXTRACTED")
70
+
71
+
72
+ # ---------------------------------------------------------------------------
73
+ # Parser
74
+ # ---------------------------------------------------------------------------
75
+
76
+
77
+ def parse_agents_md(text: str, *, source_path: str) -> list[ProceduralMemory]:
78
+ """Parse *text* into a list of :class:`ProceduralMemory` records.
79
+
80
+ Each level-2 (``## ``) heading produces one record. Content that appears
81
+ before the first ``## `` heading (including any level-1 ``# `` title line)
82
+ is discarded — it is typically a document title with no actionable procedure.
83
+
84
+ Edge cases:
85
+ * Empty string or whitespace-only → ``[]``.
86
+ * No ``##`` headings → ``[]``.
87
+ * A section with no body text and no commands → ``content=""`` / ``commands=()``.
88
+ * Never raises; all exceptions are swallowed and return ``[]``.
89
+
90
+ Args:
91
+ text: Raw AGENTS.md file content (may be empty).
92
+ source_path: Vault-relative posix path to embed in each record
93
+ (used for ``memory_id`` derivation and the frontmatter).
94
+
95
+ Returns:
96
+ Ordered list of :class:`ProceduralMemory`, one per ``##`` section.
97
+ """
98
+ try:
99
+ return _parse_inner(text, source_path=source_path)
100
+ except Exception: # noqa: BLE001
101
+ return []
102
+
103
+
104
+ def _parse_inner(text: str, *, source_path: str) -> list[ProceduralMemory]:
105
+ """Inner parser — may raise; wrapped by ``parse_agents_md``."""
106
+ if not text or not text.strip():
107
+ return []
108
+
109
+ # content_hash is sha256 of the redacted *full* file text.
110
+ content_hash = hashlib.sha256(redact(text).encode("utf-8")).hexdigest()
111
+
112
+ # Walk lines tracking fenced-code state so a "## " line INSIDE a ``` fence is
113
+ # treated as code, NOT as a heading (otherwise a bash comment like
114
+ # "## install deps" would split a phantom section). Collect (heading, body)
115
+ # pairs; any text before the first heading is preamble and discarded.
116
+ sections: list[tuple[str, str]] = []
117
+ current_heading: str | None = None
118
+ current_body: list[str] = []
119
+ in_fence = False
120
+ for line in text.splitlines():
121
+ if line.lstrip().startswith("```"):
122
+ in_fence = not in_fence
123
+ if current_heading is not None:
124
+ current_body.append(line)
125
+ continue
126
+ if not in_fence:
127
+ heading_match = _H2.match(line)
128
+ if heading_match is not None:
129
+ if current_heading is not None:
130
+ sections.append((current_heading, "\n".join(current_body)))
131
+ current_heading = heading_match.group(1).strip()
132
+ current_body = []
133
+ continue
134
+ if current_heading is not None:
135
+ current_body.append(line)
136
+ if current_heading is not None:
137
+ sections.append((current_heading, "\n".join(current_body)))
138
+
139
+ if not sections:
140
+ # No ## headings found.
141
+ return []
142
+
143
+ memories: list[ProceduralMemory] = []
144
+ for raw_heading, raw_body in sections:
145
+ # Extract fenced commands before removing them from the prose body.
146
+ raw_commands: list[str] = [m.group(1) for m in _FENCE.finditer(raw_body)]
147
+
148
+ # Prose content: remove fenced blocks, strip leading/trailing whitespace.
149
+ prose = _FENCE.sub("", raw_body).strip()
150
+
151
+ # Redact everything before storing.
152
+ title = redact(raw_heading)
153
+ content = redact(prose)
154
+ commands = tuple(redact(cmd) for cmd in raw_commands)
155
+
156
+ # Deterministic UUID5: namespace URL + "source_path\x00heading" (pre-redaction
157
+ # heading used here so identity is stable across redaction-token changes).
158
+ key = f"{source_path}\x00{raw_heading}"
159
+ memory_id = str(uuid.uuid5(uuid.NAMESPACE_URL, key))
160
+
161
+ memories.append(
162
+ ProceduralMemory(
163
+ memory_id=memory_id,
164
+ title=title,
165
+ content=content,
166
+ commands=commands,
167
+ source_path=source_path,
168
+ content_hash=content_hash,
169
+ )
170
+ )
171
+
172
+ return memories
173
+
174
+
175
+ # ---------------------------------------------------------------------------
176
+ # Markdown serializer
177
+ # ---------------------------------------------------------------------------
178
+
179
+
180
+ def procedural_to_markdown(mem: ProceduralMemory, *, created: datetime) -> str:
181
+ """Render a :class:`ProceduralMemory` as a vault-ready markdown note.
182
+
183
+ Frontmatter fields (manual construction, no yaml import — mirrors
184
+ ``failure_to_markdown`` style):
185
+ id mem.memory_id
186
+ type ``"reference"``
187
+ created *created* normalised to tz-aware UTC ISO 8601 w/ microseconds
188
+ tags ``["procedure", "agents-md"]``
189
+ source_path JSON-encoded (safe for arbitrary vault paths)
190
+ content_hash mem.content_hash
191
+ trust mem.trust
192
+
193
+ Body: ``# {title}``, blank line, prose content, then (if commands are
194
+ present) a ``## Commands`` section with each command as a fenced block.
195
+
196
+ The *created* datetime is injected by the caller; this function never
197
+ calls ``datetime.now()``. All content is already redacted upstream.
198
+
199
+ Args:
200
+ mem: :class:`ProceduralMemory` to render.
201
+ created: Tz-aware (or naive-UTC) datetime for the ``created`` field.
202
+
203
+ Returns:
204
+ Vault-ready markdown string (trailing newline included).
205
+ """
206
+ # Normalise to tz-aware UTC.
207
+ ts = created.replace(tzinfo=UTC) if created.tzinfo is None else created.astimezone(UTC)
208
+ created_str = ts.isoformat(timespec="microseconds")
209
+
210
+ fm_lines = [
211
+ "---",
212
+ f"id: {mem.memory_id}",
213
+ "type: reference",
214
+ f"created: {created_str}",
215
+ "tags:",
216
+ " - procedure",
217
+ " - agents-md",
218
+ f"source_path: {json.dumps(mem.source_path)}",
219
+ f"content_hash: {mem.content_hash}",
220
+ f"trust: {mem.trust}",
221
+ "---",
222
+ ]
223
+
224
+ body_lines: list[str] = [
225
+ f"# {mem.title}",
226
+ "",
227
+ mem.content,
228
+ ]
229
+
230
+ if mem.commands:
231
+ body_lines.append("")
232
+ body_lines.append("## Commands")
233
+ body_lines.append("")
234
+ for cmd in mem.commands:
235
+ body_lines.append("```")
236
+ body_lines.append(cmd)
237
+ body_lines.append("```")
238
+ body_lines.append("")
239
+
240
+ return "\n".join(fm_lines) + "\n" + "\n".join(body_lines) + "\n"
@@ -0,0 +1,174 @@
1
+ """Command-line interface for mneme-code.
2
+
3
+ Subcommands
4
+ -----------
5
+ parse-trace [--input FILE] [--vault VAULT_ROOT] [--write] [--source LABEL]
6
+ [--branch NAME | --no-branch]
7
+ Read a CPython traceback from --input (or stdin), parse it, print a
8
+ redacted structured summary to stdout. With --write, write the
9
+ failure_to_markdown output into
10
+ ``<vault>/code-failures/<failure_id>.md`` via atomic_write_text.
11
+ Branch-aware: the note records the vault's current git branch
12
+ (explicit --branch wins; --no-branch disables auto-detection).
13
+
14
+ No new dependencies: argparse only (stdlib).
15
+
16
+ Redaction invariant: all content is redacted by parse_traceback before
17
+ any output or write. The CLI may call ``datetime.now(UTC)`` — the library
18
+ functions never do.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import argparse
24
+ import sys
25
+ from datetime import UTC, datetime
26
+ from pathlib import Path
27
+
28
+ from mneme_code.failure import failure_from_traceback, failure_to_markdown
29
+ from mneme_code.stacktrace import parse_traceback
30
+
31
+
32
+ def _cmd_parse_trace(args: argparse.Namespace) -> int:
33
+ """Implementation of the ``parse-trace`` subcommand.
34
+
35
+ Returns an exit code (0 = success, 1 = not a traceback / error).
36
+ """
37
+ # Read input.
38
+ input_path: str | None = getattr(args, "input", None)
39
+ if input_path is not None:
40
+ try:
41
+ text = Path(input_path).read_text(encoding="utf-8")
42
+ except OSError as exc:
43
+ print(f"mneme-code: cannot read input file: {exc}", file=sys.stderr)
44
+ return 1
45
+ else:
46
+ text = sys.stdin.read()
47
+
48
+ # Parse (redaction happens inside parse_traceback).
49
+ parsed = parse_traceback(text)
50
+ if parsed is None:
51
+ print("not a recognizable traceback", file=sys.stderr)
52
+ return 1
53
+
54
+ # Inject observed_at at the CLI boundary — the library stays pure.
55
+ observed_at = datetime.now(UTC)
56
+ source_label: str | None = getattr(args, "source", None)
57
+
58
+ failure = failure_from_traceback(parsed, observed_at=observed_at, source_label=source_label)
59
+
60
+ # Print redacted summary.
61
+ print(f"failure_id: {failure.failure_id}")
62
+ print(f"exc_type: {failure.exc_type}")
63
+ print(f"exc_message: {failure.exc_message}")
64
+ print(f"frames: {len(failure.frames)}")
65
+ print(f"content_hash: {failure.content_hash}")
66
+ if failure.source_label is not None:
67
+ print(f"source_label: {failure.source_label}")
68
+ for i, frame in enumerate(failure.frames):
69
+ print(f" [{i}] {frame.file_path}:{frame.line} in {frame.function}")
70
+
71
+ # Optional vault write.
72
+ write: bool = getattr(args, "write", False)
73
+ vault_raw: str | None = getattr(args, "vault", None)
74
+ if write:
75
+ if vault_raw is None:
76
+ print("mneme-code: --write requires --vault", file=sys.stderr)
77
+ return 1
78
+ vault_root = Path(vault_raw).resolve()
79
+ out_dir = vault_root / "code-failures"
80
+ out_path = out_dir / f"{failure.failure_id}.md"
81
+ # Branch-aware failure tracking: explicit --branch wins, otherwise
82
+ # auto-detect from the vault's git checkout. Metadata only — the
83
+ # failure_id and content_hash stay branch-independent.
84
+ branch: str | None = getattr(args, "branch", None)
85
+ if branch is None and not getattr(args, "no_branch", False):
86
+ branch = _detect_git_branch(vault_root)
87
+ note = failure_to_markdown(failure, branch=branch)
88
+ try:
89
+ from mneme_core.vault.atomic_write import atomic_write_text
90
+
91
+ out_dir.mkdir(parents=True, exist_ok=True)
92
+ atomic_write_text(out_path, note)
93
+ print(f"written: {out_path}")
94
+ except Exception as exc: # noqa: BLE001
95
+ print(f"mneme-code: write failed: {exc}", file=sys.stderr)
96
+ return 1
97
+
98
+ return 0
99
+
100
+
101
+ def _detect_git_branch(vault_root: Path) -> str | None:
102
+ """Current git branch of *vault_root*, or ``None`` outside a checkout."""
103
+ import subprocess
104
+
105
+ try:
106
+ proc = subprocess.run(
107
+ ["git", "rev-parse", "--abbrev-ref", "HEAD"],
108
+ cwd=str(vault_root),
109
+ capture_output=True,
110
+ text=True,
111
+ encoding="utf-8",
112
+ timeout=5,
113
+ check=False,
114
+ )
115
+ except (OSError, subprocess.TimeoutExpired):
116
+ return None
117
+ name = proc.stdout.strip()
118
+ return name if proc.returncode == 0 and name else None
119
+
120
+
121
+ def main() -> None:
122
+ """Entry point for the mneme-code console script."""
123
+ parser = argparse.ArgumentParser(
124
+ prog="mneme-code",
125
+ description="mneme-code: deterministic Python code-failure memory layer.",
126
+ )
127
+ subparsers = parser.add_subparsers(dest="command", required=True)
128
+
129
+ # parse-trace subcommand
130
+ pt = subparsers.add_parser(
131
+ "parse-trace",
132
+ help="Parse a CPython traceback and print a redacted summary.",
133
+ )
134
+ pt.add_argument(
135
+ "--input",
136
+ metavar="FILE",
137
+ default=None,
138
+ help="Path to a file containing the traceback text (default: stdin).",
139
+ )
140
+ pt.add_argument(
141
+ "--vault",
142
+ metavar="VAULT_ROOT",
143
+ default=None,
144
+ help="Vault root directory (required with --write).",
145
+ )
146
+ pt.add_argument(
147
+ "--write",
148
+ action="store_true",
149
+ default=False,
150
+ help="Write the failure note into <vault>/code-failures/<failure_id>.md.",
151
+ )
152
+ pt.add_argument(
153
+ "--source",
154
+ metavar="LABEL",
155
+ default=None,
156
+ help="Optional source label (e.g. 'ci-run-42').",
157
+ )
158
+ pt.add_argument(
159
+ "--branch",
160
+ metavar="NAME",
161
+ default=None,
162
+ help="Git branch recorded in the note (default: auto-detect).",
163
+ )
164
+ pt.add_argument(
165
+ "--no-branch",
166
+ action="store_true",
167
+ default=False,
168
+ help="Disable branch auto-detection.",
169
+ )
170
+
171
+ args = parser.parse_args()
172
+
173
+ if args.command == "parse-trace":
174
+ sys.exit(_cmd_parse_trace(args))