Skip to content

Commit 1cbd900

Browse files
author
codejunkie99
committed
fix: address tldraw review findings
2 parents 7e74c8d + 8ba0293 commit 1cbd900

96 files changed

Lines changed: 8122 additions & 571 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.agent/AGENTS.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,19 @@ are the exact failure mode this layer prevents.
4141
- Load a full `SKILL.md` only when its triggers match the current task
4242
- Every skill has a self-rewrite hook; invoke it after failures
4343

44+
## Design Systems
45+
- If the project root contains `DESIGN.md`, treat it as the source of truth
46+
for visual design decisions and load `skills/design-md/SKILL.md` when a
47+
task mentions `DESIGN.md`, Google Stitch, design tokens, design system,
48+
or visual design. (The skill's `preconditions` field gates loading on
49+
`DESIGN.md` actually existing — keep this rule in lockstep with
50+
`skills/_manifest.jsonl` to avoid same-task / different-harness drift.)
51+
- Prefer exact tokens, component rules, and design rationale from
52+
`DESIGN.md` over invented colors, typography, spacing, shadows, or motion.
53+
- Do not modify `DESIGN.md` unless the user explicitly asks for a design
54+
system change; implementation work consumes the contract, it doesn't
55+
edit it.
56+
4457
## Protocols
4558
- `protocols/permissions.md` — read before any tool call
4659
- `protocols/tool_schemas/` — typed interfaces for external tools
@@ -55,6 +68,12 @@ Daily driver, highest-leverage first:
5568
in one shot (stage + graduate + render). For rules you already know.
5669
- `show.py` — one-screen dashboard of brain state: episodes, candidates,
5770
lessons, failing skills, activity graph.
71+
- `data_layer_export.py` — local cross-harness activity/data-layer export:
72+
agent events, cron timelines, tokens/cost estimates, categories,
73+
harness mix, `dashboard.html`, and `daily-report.md`.
74+
- `data_flywheel_export.py` — local export of approved, redacted runs into
75+
trace records, context cards, eval cases, training-ready JSONL, and
76+
flywheel metrics. It does not train models or call APIs.
5877
- `list_candidates.py` / `graduate.py` / `reject.py` / `reopen.py` — review
5978
protocol for patterns the dream cycle has staged.
6079
- `memory_reflect.py <skill> <action> <outcome>` — log a significant event.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"""Cross-platform locked append for episodic JSONL writes.
2+
3+
POSIX `write(2)` in O_APPEND mode is atomic for payloads up to PIPE_BUF
4+
(4 KB on Linux, 512 B minimum per POSIX). Most episodic entries fit,
5+
but failure entries with reflection + context + detail can exceed that,
6+
and two harness hooks writing from the same process (or from two Pi
7+
sessions on the same repo) can interleave bytes mid-line. Silent
8+
corruption is worse than a visible error because every downstream
9+
reader (`auto_dream.py`, `cluster.py`, `context_budget.py`,
10+
`show.py`) skips `JSONDecodeError` lines without surfacing the loss.
11+
12+
This module serializes appends with `fcntl.flock(LOCK_EX)` on POSIX.
13+
On platforms without `fcntl` (native Windows Python) the lock is a
14+
no-op and behavior matches the pre-lock baseline. WSL, git-bash via
15+
Cygwin, macOS, and Linux all provide `fcntl`.
16+
"""
17+
import json
18+
import os
19+
20+
try:
21+
import fcntl # POSIX
22+
_HAVE_FLOCK = True
23+
except ImportError:
24+
_HAVE_FLOCK = False
25+
26+
27+
def append_jsonl(path: str, entry: dict) -> dict:
28+
"""Serialize `entry` to one JSON line and append to `path`.
29+
30+
Uses `open(..., "ab")` (append-binary) to bypass Python's text-mode
31+
buffering and guarantee a single `write(2)` per call. `fcntl.flock`
32+
provides cross-process mutual exclusion on POSIX.
33+
"""
34+
payload = (json.dumps(entry) + "\n").encode("utf-8")
35+
os.makedirs(os.path.dirname(path), exist_ok=True)
36+
with open(path, "ab") as f:
37+
if _HAVE_FLOCK:
38+
fcntl.flock(f.fileno(), fcntl.LOCK_EX)
39+
try:
40+
f.write(payload)
41+
f.flush()
42+
finally:
43+
if _HAVE_FLOCK:
44+
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
45+
return entry

.agent/harness/hooks/on_failure.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Failures are learning. High pain score + rewrite flag after repeat offenses."""
22
import json, datetime, os
33
from ._provenance import build_source
4+
from ._episodic_io import append_jsonl
45

56
ROOT = os.path.join(os.path.dirname(__file__), "..", "..")
67
EPISODIC = os.path.join(ROOT, "memory/episodic/AGENT_LEARNINGS.jsonl")
@@ -11,7 +12,7 @@
1112
def _count_recent_failures(skill_name):
1213
if not os.path.exists(EPISODIC):
1314
return 0
14-
cutoff = datetime.datetime.now() - datetime.timedelta(days=WINDOW_DAYS)
15+
cutoff = datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(days=WINDOW_DAYS)
1516
count = 0
1617
for line in open(EPISODIC):
1718
line = line.strip()
@@ -24,7 +25,10 @@ def _count_recent_failures(skill_name):
2425
if e.get("skill") != skill_name or e.get("result") != "failure":
2526
continue
2627
try:
27-
if datetime.datetime.fromisoformat(e["timestamp"]) > cutoff:
28+
ts = datetime.datetime.fromisoformat(e["timestamp"])
29+
if ts.tzinfo is None:
30+
ts = ts.replace(tzinfo=datetime.timezone.utc)
31+
if ts > cutoff:
2832
count += 1
2933
except (KeyError, ValueError):
3034
continue
@@ -47,7 +51,7 @@ def on_failure(skill_name, action, error, context="", confidence=0.9,
4751
# schema migration is recorded with its true importance and pain score;
4852
# the dream-cycle salience can't distinguish failure severity otherwise.
4953
entry = {
50-
"timestamp": datetime.datetime.now().isoformat(),
54+
"timestamp": datetime.datetime.now(datetime.timezone.utc).isoformat(),
5155
"skill": skill_name,
5256
"action": action[:200],
5357
"result": "failure",
@@ -69,7 +73,4 @@ def on_failure(skill_name, action, error, context="", confidence=0.9,
6973
f"Flag for rewrite."
7074
)
7175
entry["pain_score"] = 10
72-
os.makedirs(os.path.dirname(EPISODIC), exist_ok=True)
73-
with open(EPISODIC, "a") as f:
74-
f.write(json.dumps(entry) + "\n")
75-
return entry
76+
return append_jsonl(EPISODIC, entry)
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
#!/usr/bin/env python3
2+
"""Process Pi Coding Agent tool_result events into episodic memory entries.
3+
4+
Pi exposes a project-local extension system. The adapter installs
5+
`.pi/extensions/memory-hook.ts`, which forwards every `tool_result` event to
6+
this script via stdin. We normalize Pi's event shape into the same
7+
tool_input/tool_response structure used by the Claude Code hook so the
8+
importance scoring, reflection generation, and success classification stay
9+
consistent across harnesses.
10+
"""
11+
import json
12+
import os
13+
import sys
14+
15+
HERE = os.path.dirname(os.path.abspath(__file__))
16+
AGENT_ROOT = os.path.abspath(os.path.join(HERE, "..", ".."))
17+
18+
sys.path.insert(0, os.path.join(AGENT_ROOT, "harness"))
19+
sys.path.insert(0, os.path.join(AGENT_ROOT, "tools"))
20+
21+
from hooks.post_execution import log_execution # noqa: E402
22+
from hooks.on_failure import on_failure # noqa: E402
23+
import hooks.claude_code_post_tool as cc # noqa: E402
24+
25+
26+
_PI_TO_CANONICAL = {
27+
"bash": "Bash",
28+
"edit": "Edit",
29+
"write": "Write",
30+
"read": "Read",
31+
"grep": "Grep",
32+
"find": "Find",
33+
"ls": "LS",
34+
"task": "Task",
35+
"todowrite": "TodoWrite",
36+
"todo_write": "TodoWrite",
37+
"webfetch": "WebFetch",
38+
"web_fetch": "WebFetch",
39+
}
40+
41+
# Pi tends to use camelCase for tool_input keys; the shared
42+
# claude_code_post_tool helpers (cc._action_label, cc._reflection,
43+
# cc._importance) expect the snake_case keys Claude Code sends.
44+
# Normalize a known set so action labels and reflections come out
45+
# meaningful instead of degrading to "edit: ?" / "Edited ?".
46+
_PI_INPUT_KEY_MAP = {
47+
"filePath": "file_path",
48+
"filepath": "file_path",
49+
"path": "file_path",
50+
"oldString": "old_string",
51+
"oldstring": "old_string",
52+
"newString": "new_string",
53+
"newstring": "new_string",
54+
"content": "content",
55+
"command": "command",
56+
"todos": "todos",
57+
"todo": "todos",
58+
"url": "url",
59+
"pattern": "pattern",
60+
"query": "query",
61+
}
62+
63+
64+
def _tool_name(name: str) -> str:
65+
if not isinstance(name, str):
66+
return "Unknown"
67+
lowered = name.strip().lower()
68+
if lowered in _PI_TO_CANONICAL:
69+
return _PI_TO_CANONICAL[lowered]
70+
if "_" in name:
71+
return "".join(part[:1].upper() + part[1:] for part in name.split("_") if part)
72+
return name[:1].upper() + name[1:]
73+
74+
75+
def _normalize_input(raw) -> dict:
76+
"""Map Pi tool_input keys to the snake_case shape cc.* helpers expect."""
77+
if not isinstance(raw, dict):
78+
if raw is None:
79+
return {}
80+
return {"raw": str(raw)}
81+
out: dict = {}
82+
for k, v in raw.items():
83+
out[_PI_INPUT_KEY_MAP.get(k, k)] = v
84+
return out
85+
86+
87+
def _extract_text(content) -> str:
88+
if isinstance(content, str):
89+
return content[:500]
90+
if not isinstance(content, list):
91+
return ""
92+
texts = []
93+
for item in content:
94+
if isinstance(item, dict) and item.get("type") == "text":
95+
text = item.get("text")
96+
if isinstance(text, str) and text:
97+
texts.append(text)
98+
return " ".join(texts)[:500]
99+
100+
101+
def _normalize_response(event: dict) -> dict:
102+
"""Map Pi tool_result shape to the Claude hook's response schema."""
103+
resp: dict = {"is_error": bool(event.get("isError", False))}
104+
details = event.get("details")
105+
content = event.get("content")
106+
107+
if isinstance(details, dict):
108+
if isinstance(details.get("output"), str):
109+
resp["output"] = details["output"][:500]
110+
if isinstance(details.get("stdout"), str):
111+
resp["stdout"] = details["stdout"][:500]
112+
if isinstance(details.get("stderr"), str):
113+
resp["stderr"] = details["stderr"][:300]
114+
if isinstance(details.get("error"), str):
115+
resp["error"] = details["error"][:300]
116+
if isinstance(details.get("text"), str):
117+
resp["text"] = details["text"][:500]
118+
if "exitCode" in details:
119+
resp["exit_code"] = details.get("exitCode")
120+
if "cancelled" in details:
121+
resp["interrupted"] = bool(details.get("cancelled"))
122+
if "truncated" in details:
123+
resp["truncated"] = bool(details.get("truncated"))
124+
resp["details"] = details
125+
126+
text = _extract_text(content)
127+
if text:
128+
resp.setdefault("output", text)
129+
resp["content"] = [{"type": "text", "text": text}]
130+
131+
if not any(k in resp for k in ("output", "stdout", "text", "content")):
132+
if isinstance(details, str):
133+
resp["output"] = details[:500]
134+
elif details is not None:
135+
resp["output"] = json.dumps(details, default=str)[:500]
136+
137+
return resp
138+
139+
140+
def _emit_malformed(reason: str, raw_excerpt: str) -> None:
141+
"""Record an explicit failure entry instead of silently logging a bogus
142+
'Unknown success'. If Pi changes payload shape or sends malformed
143+
JSON, this surfaces in AGENT_LEARNINGS.jsonl as a real signal.
144+
"""
145+
excerpt = raw_excerpt[:200] if isinstance(raw_excerpt, str) else ""
146+
# importance MUST be numeric — downstream salience_score() does
147+
# `importance / 10.0` and a string would crash context_budget,
148+
# show.py, and auto_dream readers. 5 ≈ "medium" on the 1-10 scale.
149+
on_failure(
150+
skill_name="pi",
151+
action="hook:malformed_payload",
152+
error=f"pi tool_result payload malformed: {reason}",
153+
context=excerpt,
154+
confidence=0.95,
155+
importance=5,
156+
pain_score=2,
157+
)
158+
159+
160+
def main() -> None:
161+
raw = ""
162+
try:
163+
raw = sys.stdin.read()
164+
except OSError as e:
165+
_emit_malformed(f"stdin read failed: {e}", "")
166+
return
167+
168+
if not raw or not raw.strip():
169+
_emit_malformed("empty payload", "")
170+
return
171+
172+
try:
173+
payload = json.loads(raw)
174+
except json.JSONDecodeError as e:
175+
_emit_malformed(f"json decode error: {e.msg}", raw)
176+
return
177+
178+
if not isinstance(payload, dict):
179+
_emit_malformed(f"payload is {type(payload).__name__}, expected object", raw)
180+
return
181+
182+
if "tool_name" not in payload:
183+
_emit_malformed("missing tool_name", raw)
184+
return
185+
186+
tool_name = _tool_name(payload.get("tool_name") or "Unknown")
187+
tool_input = _normalize_input(payload.get("tool_input"))
188+
tool_response = _normalize_response(payload)
189+
190+
success = cc._is_success(tool_name, tool_input, tool_response)
191+
importance = cc._importance(tool_name, json.dumps(tool_input))
192+
action = cc._action_label(tool_name, tool_input)
193+
reflection = cc._reflection(tool_name, tool_input, tool_response, success)
194+
detail = cc._detail(tool_name, tool_input, tool_response, success)
195+
196+
pscore = cc._pain_score(importance, success)
197+
if success:
198+
log_execution(
199+
skill_name="pi",
200+
action=action,
201+
result=detail,
202+
success=True,
203+
reflection=reflection,
204+
importance=importance,
205+
confidence=0.7,
206+
pain_score=pscore,
207+
)
208+
else:
209+
on_failure(
210+
skill_name="pi",
211+
action=action,
212+
error=reflection,
213+
context=detail,
214+
confidence=0.7,
215+
importance=importance,
216+
pain_score=pscore,
217+
)
218+
219+
220+
if __name__ == "__main__":
221+
main()

.agent/harness/hooks/post_execution.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Runs after every action. Appends a structured entry to episodic memory."""
2-
import json, datetime, os
2+
import datetime, os
33
from ._provenance import build_source
4+
from ._episodic_io import append_jsonl
45

56
ROOT = os.path.join(os.path.dirname(__file__), "..", "..")
67
EPISODIC = os.path.join(ROOT, "memory/episodic/AGENT_LEARNINGS.jsonl")
@@ -15,11 +16,10 @@ def log_execution(skill_name, action, result, success, reflection="",
1516
a higher value (e.g. 5) for high-importance successful operations so
1617
recurring patterns cross the dream-cycle promotion threshold (7.0).
1718
"""
18-
os.makedirs(os.path.dirname(EPISODIC), exist_ok=True)
1919
if pain_score is None:
2020
pain_score = 2 if success else 7
2121
entry = {
22-
"timestamp": datetime.datetime.now().isoformat(),
22+
"timestamp": datetime.datetime.now(datetime.timezone.utc).isoformat(),
2323
"skill": skill_name,
2424
"action": action[:200],
2525
"result": "success" if success else "failure",
@@ -31,6 +31,4 @@ def log_execution(skill_name, action, result, success, reflection="",
3131
"source": build_source(skill_name),
3232
"evidence_ids": list(evidence_ids) if evidence_ids else [],
3333
}
34-
with open(EPISODIC, "a") as f:
35-
f.write(json.dumps(entry) + "\n")
36-
return entry
34+
return append_jsonl(EPISODIC, entry)

.agent/harness/salience.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,17 @@ def salience_score(entry: dict) -> float:
88
if not ts:
99
return 0.0
1010
try:
11-
age_days = (datetime.datetime.now()
12-
- datetime.datetime.fromisoformat(ts)).days
11+
parsed = datetime.datetime.fromisoformat(ts)
12+
if parsed.tzinfo is None:
13+
parsed = parsed.replace(tzinfo=datetime.timezone.utc)
14+
# Negative age can happen during the naive-→-UTC migration window
15+
# if a legacy naive-local timestamp now reads as a few hours in the
16+
# future. Floor at 0 so recency stays in [0, 10] instead of inflating.
17+
age_days = max(0, (datetime.datetime.now(datetime.timezone.utc) - parsed).days)
1318
except ValueError:
1419
age_days = 999
1520
pain = entry.get("pain_score", 5)
1621
importance = entry.get("importance", 5)
1722
recurrence = entry.get("recurrence_count", 1)
18-
recency = max(0.0, 10.0 - age_days * 0.3)
23+
recency = max(0.0, min(10.0, 10.0 - age_days * 0.3))
1924
return recency * (pain / 10.0) * (importance / 10.0) * min(recurrence, 3)

0 commit comments

Comments
 (0)