ponens 1.0.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.
- ponens-1.0.0/PKG-INFO +9 -0
- ponens-1.0.0/ponens/__init__.py +0 -0
- ponens-1.0.0/ponens/adapters/__init__.py +26 -0
- ponens-1.0.0/ponens/adapters/claude_code.py +176 -0
- ponens-1.0.0/ponens/adapters/codex.py +22 -0
- ponens-1.0.0/ponens/adapters/gemini.py +22 -0
- ponens-1.0.0/ponens/agent.py +84 -0
- ponens-1.0.0/ponens/cli.py +675 -0
- ponens-1.0.0/ponens/client.py +49 -0
- ponens-1.0.0/ponens/emit.py +414 -0
- ponens-1.0.0/ponens/formatting.py +162 -0
- ponens-1.0.0/ponens/policy_compiler.py +1296 -0
- ponens-1.0.0/ponens/reasoners.py +153 -0
- ponens-1.0.0/ponens/registry.py +498 -0
- ponens-1.0.0/ponens/sync.py +311 -0
- ponens-1.0.0/ponens/trace.py +1752 -0
- ponens-1.0.0/ponens/visualizer.html +4652 -0
- ponens-1.0.0/ponens.egg-info/PKG-INFO +9 -0
- ponens-1.0.0/ponens.egg-info/SOURCES.txt +24 -0
- ponens-1.0.0/ponens.egg-info/dependency_links.txt +1 -0
- ponens-1.0.0/ponens.egg-info/entry_points.txt +2 -0
- ponens-1.0.0/ponens.egg-info/requires.txt +3 -0
- ponens-1.0.0/ponens.egg-info/top_level.txt +1 -0
- ponens-1.0.0/pyproject.toml +25 -0
- ponens-1.0.0/setup.cfg +4 -0
- ponens-1.0.0/tests/test_e2e.py +517 -0
ponens-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ponens
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Author, govern, and validate reasoning traces — the open CLI for the reasoning-policies ecosystem
|
|
5
|
+
Author-email: Denis Ignatovich <denis@aestheticintegration.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Provides-Extra: yaml
|
|
9
|
+
Requires-Dist: pyyaml>=6; extra == "yaml"
|
|
File without changes
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Session-transcript adapters.
|
|
2
|
+
|
|
3
|
+
Each adapter maps one agent's native transcript format to the normalized event stream
|
|
4
|
+
that `emit.build_trace` consumes — so the trace-building core stays agent-agnostic and a
|
|
5
|
+
new agent is just a new module here. An adapter module exposes:
|
|
6
|
+
|
|
7
|
+
NAME : str
|
|
8
|
+
IMPLEMENTED : bool
|
|
9
|
+
default_transcript() -> str | None
|
|
10
|
+
read_entries(path) -> list
|
|
11
|
+
parse(entries) -> {"events", "model", "assistant", "last_reasoning"}
|
|
12
|
+
"""
|
|
13
|
+
from . import claude_code, codex, gemini
|
|
14
|
+
|
|
15
|
+
ADAPTERS = {a.NAME: a for a in (claude_code, gemini, codex)}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_adapter(name):
|
|
19
|
+
try:
|
|
20
|
+
return ADAPTERS[name]
|
|
21
|
+
except KeyError:
|
|
22
|
+
raise KeyError(name)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def adapter_names():
|
|
26
|
+
return list(ADAPTERS)
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""Claude Code transcript adapter.
|
|
2
|
+
|
|
3
|
+
Maps a Claude Code session transcript (a JSONL of messages, each with content blocks
|
|
4
|
+
— text / thinking / tool_use / tool_result) into the **normalized event stream** that
|
|
5
|
+
`emit.build_trace` consumes. Everything Claude-specific lives here: the tool-name
|
|
6
|
+
vocabulary, the message/content-block shape, and the todo (TaskCreate/TaskUpdate) signal.
|
|
7
|
+
|
|
8
|
+
To support another agent, add a sibling module exposing the same surface:
|
|
9
|
+
NAME, IMPLEMENTED, default_transcript(), read_entries(path), parse(entries)
|
|
10
|
+
`parse` returns {"events": [...], "model": str, "assistant": str, "last_reasoning": str|None},
|
|
11
|
+
where each event is one of:
|
|
12
|
+
{"t": "directive", "text": str} # a user instruction
|
|
13
|
+
{"t": "todo_create", "subject": str|None} # the agent's plan
|
|
14
|
+
{"t": "todo_update", "task_id": str, "status": str}
|
|
15
|
+
{"t": "action", "type": ..., "category": ..., "rationale": ..., "file": ...,
|
|
16
|
+
"is_edit": bool, "command": ..., "query": ..., "description": ..., "result": ...,
|
|
17
|
+
"error": bool, "decision": {...}|None, "fallback_label": ...}
|
|
18
|
+
"""
|
|
19
|
+
import glob
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
|
|
23
|
+
NAME = "claude-code"
|
|
24
|
+
IMPLEMENTED = True
|
|
25
|
+
|
|
26
|
+
# Claude Code tool name -> canonical trace action type
|
|
27
|
+
TOOL_MAP = {
|
|
28
|
+
"Read": "ReadFile",
|
|
29
|
+
"Edit": "EditFile",
|
|
30
|
+
"MultiEdit": "EditFile",
|
|
31
|
+
"Write": "CreateFile",
|
|
32
|
+
"NotebookEdit": "EditFile",
|
|
33
|
+
"Grep": "SearchCode",
|
|
34
|
+
"Glob": "ExploreDirectory",
|
|
35
|
+
"WebSearch": "SearchWeb",
|
|
36
|
+
"WebFetch": "ReadDocumentation",
|
|
37
|
+
"AskUserQuestion": "AskUser",
|
|
38
|
+
}
|
|
39
|
+
# Meta/bookkeeping tools that aren't part of the engineering record.
|
|
40
|
+
# (TaskCreate/TaskUpdate are NOT skipped — they're the agent's plan, surfaced as todo
|
|
41
|
+
# events so the core can derive PlanDeclared meta-actions; never emitted as actions.)
|
|
42
|
+
SKIP_TOOLS = {"TaskList", "TaskGet", "TaskOutput", "TaskStop",
|
|
43
|
+
"ToolSearch", "ExitPlanMode", "EnterPlanMode", "Skill"}
|
|
44
|
+
FILE_TOOLS = {"Read", "Edit", "MultiEdit", "Write", "NotebookEdit"}
|
|
45
|
+
EDIT_TOOLS = {"Edit", "MultiEdit", "Write", "NotebookEdit"}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _bash_action(cmd):
|
|
49
|
+
c = (cmd or "").lower()
|
|
50
|
+
if "git commit" in c:
|
|
51
|
+
return "GitCommit"
|
|
52
|
+
if "git diff" in c:
|
|
53
|
+
return "GitDiff"
|
|
54
|
+
if "git status" in c:
|
|
55
|
+
return "GitStatus"
|
|
56
|
+
if any(t in c for t in ("pytest", "npm test", "go test", "cargo test", "unittest")):
|
|
57
|
+
return "RunTests"
|
|
58
|
+
return "RunCommand"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _action_type(tool, inp):
|
|
62
|
+
if tool == "Bash":
|
|
63
|
+
return _bash_action(inp.get("command", ""))
|
|
64
|
+
return TOOL_MAP.get(tool, "RunCommand")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _reasoning(content):
|
|
68
|
+
"""The agent's reasoning before its tool calls — the motivation. Prefers the
|
|
69
|
+
human-facing text; falls back to the model's thinking blocks."""
|
|
70
|
+
texts, thinks = [], []
|
|
71
|
+
for b in content:
|
|
72
|
+
if not isinstance(b, dict):
|
|
73
|
+
continue
|
|
74
|
+
if b.get("type") == "tool_use":
|
|
75
|
+
break
|
|
76
|
+
if b.get("type") == "text" and b.get("text"):
|
|
77
|
+
texts.append(b["text"].strip())
|
|
78
|
+
elif b.get("type") == "thinking" and (b.get("thinking") or b.get("text")):
|
|
79
|
+
thinks.append((b.get("thinking") or b.get("text")).strip())
|
|
80
|
+
return " ".join(texts).strip() or " ".join(thinks).strip()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def default_transcript():
|
|
84
|
+
"""The newest Claude Code session transcript for the current project, if any."""
|
|
85
|
+
proj = "-" + os.path.abspath(os.getcwd()).strip("/").replace("/", "-")
|
|
86
|
+
d = os.path.join(os.path.expanduser("~"), ".claude", "projects", proj)
|
|
87
|
+
js = sorted(glob.glob(os.path.join(d, "*.jsonl")), key=os.path.getmtime)
|
|
88
|
+
return js[-1] if js else None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def read_entries(path):
|
|
92
|
+
entries = []
|
|
93
|
+
with open(path) as f:
|
|
94
|
+
for line in f:
|
|
95
|
+
line = line.strip()
|
|
96
|
+
if line:
|
|
97
|
+
try:
|
|
98
|
+
entries.append(json.loads(line))
|
|
99
|
+
except json.JSONDecodeError:
|
|
100
|
+
continue
|
|
101
|
+
return entries
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def parse(entries):
|
|
105
|
+
# 1. collect tool results by tool_use_id
|
|
106
|
+
results = {}
|
|
107
|
+
for e in entries:
|
|
108
|
+
if e.get("type") != "user":
|
|
109
|
+
continue
|
|
110
|
+
content = (e.get("message") or {}).get("content")
|
|
111
|
+
if isinstance(content, list):
|
|
112
|
+
for b in content:
|
|
113
|
+
if isinstance(b, dict) and b.get("type") == "tool_result":
|
|
114
|
+
body = b.get("content")
|
|
115
|
+
if isinstance(body, list):
|
|
116
|
+
body = " ".join(x.get("text", "") for x in body if isinstance(x, dict))
|
|
117
|
+
results[b.get("tool_use_id")] = {"text": body or "", "error": bool(b.get("is_error"))}
|
|
118
|
+
|
|
119
|
+
# 2. walk in order -> normalized events
|
|
120
|
+
events, model, last_reasoning = [], "unknown", None
|
|
121
|
+
for e in entries:
|
|
122
|
+
et = e.get("type")
|
|
123
|
+
if et == "user":
|
|
124
|
+
c = (e.get("message") or {}).get("content")
|
|
125
|
+
if isinstance(c, str) and c.strip():
|
|
126
|
+
events.append({"t": "directive", "text": c.strip()})
|
|
127
|
+
continue
|
|
128
|
+
if et != "assistant":
|
|
129
|
+
continue
|
|
130
|
+
msg = e.get("message") or {}
|
|
131
|
+
if model == "unknown":
|
|
132
|
+
model = msg.get("model", "unknown")
|
|
133
|
+
content = msg.get("content") or []
|
|
134
|
+
reasoning = _reasoning(content)
|
|
135
|
+
if reasoning:
|
|
136
|
+
last_reasoning = reasoning
|
|
137
|
+
for b in content:
|
|
138
|
+
if not isinstance(b, dict) or b.get("type") != "tool_use":
|
|
139
|
+
continue
|
|
140
|
+
tool, inp = b.get("name"), (b.get("input") or {})
|
|
141
|
+
if tool == "TaskCreate":
|
|
142
|
+
events.append({"t": "todo_create", "subject": inp.get("subject") or inp.get("description")})
|
|
143
|
+
continue
|
|
144
|
+
if tool == "TaskUpdate":
|
|
145
|
+
events.append({"t": "todo_update", "task_id": str(inp.get("taskId") or ""),
|
|
146
|
+
"status": inp.get("status")})
|
|
147
|
+
continue
|
|
148
|
+
if tool in SKIP_TOOLS:
|
|
149
|
+
continue
|
|
150
|
+
res = results.get(b.get("id"))
|
|
151
|
+
rationale = reasoning or last_reasoning # carryover; the core truncates
|
|
152
|
+
if tool == "AskUserQuestion":
|
|
153
|
+
q = (inp.get("questions") or [{}])[0]
|
|
154
|
+
events.append({
|
|
155
|
+
"t": "action", "type": "ExclusiveDecision", "category": "gateway",
|
|
156
|
+
"rationale": rationale,
|
|
157
|
+
"decision": {"question": q.get("question") or q.get("header") or "decision"},
|
|
158
|
+
"result": res["text"] if res else None, "error": bool(res and res["error"])})
|
|
159
|
+
continue
|
|
160
|
+
events.append({
|
|
161
|
+
"t": "action",
|
|
162
|
+
"type": _action_type(tool, inp),
|
|
163
|
+
"category": "reasoning" if tool in ("Grep", "Glob") else "activity",
|
|
164
|
+
"rationale": rationale,
|
|
165
|
+
"description": inp.get("description"),
|
|
166
|
+
"file": inp.get("file_path") if tool in FILE_TOOLS else None,
|
|
167
|
+
"is_edit": tool in EDIT_TOOLS,
|
|
168
|
+
"command": inp.get("command") if tool == "Bash" else None,
|
|
169
|
+
"query": inp.get("pattern") or inp.get("query"),
|
|
170
|
+
"result": res["text"] if res else None,
|
|
171
|
+
"error": bool(res and res["error"]),
|
|
172
|
+
"fallback_label": tool,
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
return {"events": events, "model": model, "assistant": "ponens",
|
|
176
|
+
"last_reasoning": last_reasoning}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Codex CLI transcript adapter — stub.
|
|
2
|
+
|
|
3
|
+
Not yet implemented. To add support, mirror `claude_code.py`: read Codex's session
|
|
4
|
+
transcript and emit the normalized event stream (directives, todos, and `action` events
|
|
5
|
+
with canonical types). Set IMPLEMENTED = True when `parse` is real.
|
|
6
|
+
"""
|
|
7
|
+
NAME = "codex"
|
|
8
|
+
IMPLEMENTED = False
|
|
9
|
+
|
|
10
|
+
_MSG = "the Codex adapter is not implemented yet — only 'claude-code' is supported today"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def default_transcript():
|
|
14
|
+
return None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def read_entries(path):
|
|
18
|
+
raise NotImplementedError(_MSG)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def parse(entries):
|
|
22
|
+
raise NotImplementedError(_MSG)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Gemini CLI transcript adapter — stub.
|
|
2
|
+
|
|
3
|
+
Not yet implemented. To add support, mirror `claude_code.py`: read Gemini's session
|
|
4
|
+
transcript and emit the normalized event stream (directives, todos, and `action` events
|
|
5
|
+
with canonical types). Set IMPLEMENTED = True when `parse` is real.
|
|
6
|
+
"""
|
|
7
|
+
NAME = "gemini"
|
|
8
|
+
IMPLEMENTED = False
|
|
9
|
+
|
|
10
|
+
_MSG = "the Gemini adapter is not implemented yet — only 'claude-code' is supported today"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def default_transcript():
|
|
14
|
+
return None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def read_entries(path):
|
|
18
|
+
raise NotImplementedError(_MSG)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def parse(entries):
|
|
22
|
+
raise NotImplementedError(_MSG)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""`ponens agent` — print the agent workflow guide.
|
|
2
|
+
|
|
3
|
+
The guide is embedded here (not a repo file) so an agent with only the CLI installed can
|
|
4
|
+
self-onboard: `ponens agent` prints how to produce a good trace, `ponens agent --review`
|
|
5
|
+
prints the reviewing-agent protocol. The fuller canonical versions live in AGENT_PROMPT.md
|
|
6
|
+
and REVIEW_AGENT_PROMPT.md at the repo root.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
GUIDE = """\
|
|
10
|
+
ponens — agent guide
|
|
11
|
+
|
|
12
|
+
You produce a reasoning trace: a curated, verifiable record of WHAT you built and WHY —
|
|
13
|
+
not a transcript replay. It has two layers, treated differently:
|
|
14
|
+
- Atomic actions ground-truth evidence (files, commands, results). NEVER rewrite them.
|
|
15
|
+
- Meta-action narrative the curated story. Seeded from raw directives ("yes", "ok fix it") —
|
|
16
|
+
YOU rewrite it into clean intent.
|
|
17
|
+
|
|
18
|
+
Make the reasoning RIGOROUS: separate what you ESTABLISHED (proved / tested / verified, backed by
|
|
19
|
+
an artifact) from what you merely ASSERTED (prose). Declare what you did not check, and flag where
|
|
20
|
+
formal methods or other verification belong — an unverified, load-bearing claim on a high-stakes
|
|
21
|
+
path is exactly such a place.
|
|
22
|
+
|
|
23
|
+
Workflow — after you finish the work:
|
|
24
|
+
|
|
25
|
+
1. EMIT ponens emit -o trace.json
|
|
26
|
+
2. CURATE ponens trace meta ls trace.json
|
|
27
|
+
ponens trace meta set trace.json <id> --title "<clean intent>" [--status completed]
|
|
28
|
+
ponens trace meta merge trace.json <into> <id...> # fold dead-ends / false starts
|
|
29
|
+
ponens trace retitle trace.json --title "..." --outcome "..."
|
|
30
|
+
3. ENRICH ponens trace artifact trace.json --type <SourceCode|VerificationResult|...> \\
|
|
31
|
+
--name "..." --producer-action-id <n> # declare artifacts -> lineage
|
|
32
|
+
ponens trace residual add trace.json \\
|
|
33
|
+
--kind <assumption|unverified|out_of_scope|limitation|open_question> \\
|
|
34
|
+
--severity <info|low|medium|high|critical> --statement "..." \\
|
|
35
|
+
[--suggested-check "how a reviewer could close it"] # declare your gaps
|
|
36
|
+
4. GRADE ponens trace grade trace.json # a hygiene floor to CLEAR, not a score to game
|
|
37
|
+
5. GOVERN ponens registry update
|
|
38
|
+
ponens policies add tests_before_commit --into trace.json # best-practice policies
|
|
39
|
+
ponens trace check trace.json # a real gate (exit code)
|
|
40
|
+
6. SHARE ponens trace view trace.json # read the reasoning (zoomable)
|
|
41
|
+
ponens bind && ponens push # bind 1:1 to the commit, publish for review
|
|
42
|
+
|
|
43
|
+
A trace with NO declared residuals is suspicious, not clean. The value to a reviewer is that you
|
|
44
|
+
disclosed what you did NOT establish.
|
|
45
|
+
|
|
46
|
+
Reviewing a trace instead of producing one? ponens agent --review
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
REVIEW_GUIDE = """\
|
|
50
|
+
ponens — reviewing-agent guide
|
|
51
|
+
|
|
52
|
+
Review a change by reading its reasoning TRACE, not just its diff. Targeted verification, not trust.
|
|
53
|
+
|
|
54
|
+
ponens trace status <file> # orient: intent, outcome, grade
|
|
55
|
+
ponens trace grade <file> # where the trace is thin (incl. lineage)
|
|
56
|
+
ponens trace residuals <file> # the declared gaps, by severity — your work-list
|
|
57
|
+
ponens trace reproduce <file> --run # re-run the recorded commands; report divergence
|
|
58
|
+
ponens trace check <file> # run the attached policies
|
|
59
|
+
|
|
60
|
+
Procedure:
|
|
61
|
+
1. Orient — intent, outcome, changed files, lineage. No artifacts/lineage is itself a
|
|
62
|
+
reviewability gap.
|
|
63
|
+
2. Verify the positive space proportionally — re-check the consequential proofs/tests; downgrade
|
|
64
|
+
any unbacked "verified" claim to an undeclared `unverified` residual.
|
|
65
|
+
3. Work the residual surface, highest severity first — run each suggested_check if cheap.
|
|
66
|
+
4. Hunt the UNDECLARED gaps — anything the change touches that is neither verified nor declared.
|
|
67
|
+
5. Verdict — approve only if no open blocking residual remains and every consequential claim was
|
|
68
|
+
re-verified; else request-changes (list the residual_ids to close) or escalate-to-human.
|
|
69
|
+
|
|
70
|
+
Never treat prose as evidence. Never auto-resolve an open_question. Traces are immutable — gaps
|
|
71
|
+
close in a SUCCESSOR trace, not by editing this one.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def cmd_agent(args):
|
|
76
|
+
print(REVIEW_GUIDE if getattr(args, "review", False) else GUIDE)
|
|
77
|
+
return 0
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def register(subparsers):
|
|
81
|
+
p = subparsers.add_parser("agent",
|
|
82
|
+
help="Print the agent workflow guide (how to produce or review a trace)")
|
|
83
|
+
p.add_argument("--review", action="store_true", help="Print the reviewing-agent guide instead")
|
|
84
|
+
p.set_defaults(func=cmd_agent)
|