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 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)