mirror of
https://github.com/alirezarezvani/claude-skills.git
synced 2026-09-05 08:05:57 +00:00
Five of six findings were real. Verified each against the code rather than taking them at face value; one was wrong and is noted below. 1. validate_examples.py carried a stale header from the spec-only phase -- "PARKED AS AN ASSET (deliberately not .py yet)... ON IMPLEMENTATION: rename to skills/agent-memory/scripts/validate_examples.py". The file is already at exactly that path. Rewritten to describe what it now is, keeping the substantive part: why it exists, the seven check families, why it compares the doc's algorithm by source text rather than exec()-ing a code fence, and that nothing runs it automatically. 2. Missing shebang -- added. The same finding also claimed mode 100644; that half is wrong, `git ls-files -s` shows 100755 for all five scripts. 3. hooks.json's description still opened "CONTRACT ONLY -- the referenced scripts are not yet implemented", true of none of them now. Trimmed to keep only the UserPromptSubmit provisionality (9.5 is genuinely still open) and to record the measured latency alongside it. 4. The session-id fallback was the finding worth the most. `session_id` is the right key -- engineering/security-guidance's shipped hook reads the same one -- but the fallback was the CONSTANT "unknown-session", and sessions dedupe by value. Had the key ever been absent, every session would collapse onto one id, len(set(sessions)) would plateau at 1, and every claim would cap at L1 forever with no error anywhere. Now falls back to the transcript's own basename, which IS the session id. Verified end-to-end with session_id omitted from the payload: the atom records the real session UUID. 5. Dead `now` parameter on _eligible_l1 -- removed. 6. marketplace.json metadata still said 104 agents / 120 slash commands, pre-existing drift on a line this branch already edits. Trued up to 110/130. Re-verified after: 69 checks 0 failures, SKILL.md 6/6 PASS, both blocking gates still fire by name, check_paths 620 files clean, counters and plugin-json pass, all 5 scripts --help, all 3 hooks parse. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EM5xmJ7AmTMg31rq68BCym
91 lines
3.5 KiB
Python
91 lines
3.5 KiB
Python
#!/usr/bin/env python3
|
|
"""SessionEnd -- capture L0 -> L1, detect contradictions, stage promotions.
|
|
|
|
DESIGN.md 5.3. Runs `async: true` so it can never delay session teardown.
|
|
|
|
The pipeline, in order, because the order is the safety property:
|
|
|
|
1. extract rule-based, high-precision markers only (9.2 option (a))
|
|
2. REDACT every atom, before anything is written (6 rule 1)
|
|
3. merge increment observations, extend sessions, raise confidence to
|
|
the max -- never lower (4.1.3)
|
|
4. detect 4.2.1's two rules; mark the OLDER atom contested
|
|
5. write atomic os.replace under a 5s-bounded lock (5.4); on
|
|
contention the atoms are DROPPED and the loss is logged to
|
|
.memory/errors.log -- not stderr, which for an async hook
|
|
goes nowhere a human reads
|
|
6. promote L1->L2->L3 on recurrence, staged to .memory/staged/
|
|
|
|
Step 6 NEVER writes CLAUDE.md. Adoption is a separate, explicit, human step
|
|
(`/cs:memory adopt`), which backs both CLAUDE.md files up first.
|
|
|
|
A missing .memory/atoms.jsonl is the normal first-run state, not an error.
|
|
Disable with AGENT_MEMORY_SESSIONEND=0.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
sys.path.insert(0, os.path.join(
|
|
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
|
"skills", "agent-memory", "scripts"))
|
|
|
|
|
|
def main():
|
|
if os.environ.get("AGENT_MEMORY_SESSIONEND") == "0":
|
|
return 0
|
|
try:
|
|
raw = "" if sys.stdin.isatty() else sys.stdin.read()
|
|
payload = json.loads(raw) if raw.strip() else {}
|
|
except Exception:
|
|
return 0
|
|
|
|
transcript = payload.get("transcript_path")
|
|
if not transcript or not os.path.exists(transcript):
|
|
return 0
|
|
|
|
try:
|
|
import memory_core as core
|
|
import memory_extract as extract
|
|
import memory_promote as promote
|
|
|
|
cwd = payload.get("cwd") or os.getcwd()
|
|
project = os.path.basename(os.path.abspath(cwd))
|
|
# A CONSTANT fallback here would be a silent, permanent bug. Sessions
|
|
# are deduped by value (5.3), so if `session_id` were ever absent,
|
|
# every session would collapse onto one id, `len(set(sessions))` would
|
|
# plateau at 1, and EVERY claim would be capped at L1 forever with no
|
|
# error anywhere -- the gates need 2-3 distinct sessions. So fall back
|
|
# to the transcript's own basename, which IS the session: Claude Code
|
|
# names each transcript for its session id.
|
|
session = payload.get("session_id") or os.path.splitext(
|
|
os.path.basename(transcript))[0] or "unknown-session"
|
|
store = core.AtomStore(os.path.join(cwd, ".memory"))
|
|
|
|
new_atoms = extract.extract(transcript, project, session)
|
|
if not new_atoms:
|
|
return 0
|
|
|
|
atoms, _n_new, _n_merged = extract.merge_into_store(store, new_atoms)
|
|
core.mark_contradictions(atoms)
|
|
|
|
if not store.write(atoms):
|
|
# 5.4 -- the one place data disappears. It is logged inside write().
|
|
return 0
|
|
|
|
l2, _blocked = promote.promote_l1_to_l2(atoms)
|
|
l3, notes = promote.promote_l2_to_l3(atoms + l2)
|
|
warnings = promote.apply_caps(atoms + l2 + l3)
|
|
if l2 or l3:
|
|
promote.stage(store, l2, l3, notes, warnings)
|
|
except Exception:
|
|
# Never blocks teardown. A lost capture costs one re-observation; L1 is
|
|
# the recoverable tier by construction (5.4).
|
|
return 0
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|