agent-trace-cli 0.1.0__py3-none-any.whl
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.
- agent_trace/__init__.py +3 -0
- agent_trace/blame.py +471 -0
- agent_trace/blame_git.py +133 -0
- agent_trace/blame_meta.py +167 -0
- agent_trace/cli.py +2680 -0
- agent_trace/commit_link.py +226 -0
- agent_trace/config.py +137 -0
- agent_trace/context.py +385 -0
- agent_trace/conversations.py +358 -0
- agent_trace/git_notes.py +491 -0
- agent_trace/hooks/__init__.py +163 -0
- agent_trace/hooks/base.py +233 -0
- agent_trace/hooks/claude.py +483 -0
- agent_trace/hooks/codex.py +232 -0
- agent_trace/hooks/cursor.py +278 -0
- agent_trace/hooks/git.py +144 -0
- agent_trace/ledger.py +955 -0
- agent_trace/models.py +618 -0
- agent_trace/record.py +417 -0
- agent_trace/registry.py +230 -0
- agent_trace/remote.py +673 -0
- agent_trace/rewrite.py +176 -0
- agent_trace/rules.py +209 -0
- agent_trace/schemas/commit-link.schema.json +34 -0
- agent_trace/schemas/git-note.schema.json +88 -0
- agent_trace/schemas/ledger.schema.json +97 -0
- agent_trace/schemas/remotes.schema.json +30 -0
- agent_trace/schemas/sync-state.schema.json +49 -0
- agent_trace/schemas/trace-record.schema.json +130 -0
- agent_trace/session.py +136 -0
- agent_trace/storage.py +229 -0
- agent_trace/summary.py +465 -0
- agent_trace/summary_presets.py +188 -0
- agent_trace/sync.py +1182 -0
- agent_trace/telemetry.py +142 -0
- agent_trace/trace.py +460 -0
- agent_trace_cli-0.1.0.dist-info/METADATA +535 -0
- agent_trace_cli-0.1.0.dist-info/RECORD +42 -0
- agent_trace_cli-0.1.0.dist-info/WHEEL +5 -0
- agent_trace_cli-0.1.0.dist-info/entry_points.txt +2 -0
- agent_trace_cli-0.1.0.dist-info/licenses/LICENSE +201 -0
- agent_trace_cli-0.1.0.dist-info/top_level.txt +1 -0
agent_trace/rewrite.py
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Post-rewrite ledger remapping — updates ledger commit SHAs after rebase/amend.
|
|
3
|
+
|
|
4
|
+
Git's ``post-rewrite`` hook provides ``old_sha new_sha`` lines on stdin.
|
|
5
|
+
For each pair we:
|
|
6
|
+
|
|
7
|
+
1. Remap the ledger row's ``commit_sha`` (and ``parent_sha`` if it was
|
|
8
|
+
itself rewritten).
|
|
9
|
+
2. Compare ``diff_signature(old)`` against ``diff_signature(new)``. If
|
|
10
|
+
they differ — i.e., the rewrite changed the tree (amend with edits,
|
|
11
|
+
rebase with conflict resolution, ``rebase --exec`` that mutates
|
|
12
|
+
content) — drop the remapped row and rebuild the ledger from the new
|
|
13
|
+
commit. The line numbers and hashes in the old ledger are stale; the
|
|
14
|
+
deterministic answer for the new commit comes from re-running the
|
|
15
|
+
builder against its actual diff.
|
|
16
|
+
3. Move the corresponding ``refs/notes/agent-trace`` note onto the new
|
|
17
|
+
SHA (removing the orphaned old one). Without this step notes silently
|
|
18
|
+
orphan on unreachable commits after rebase / amend.
|
|
19
|
+
|
|
20
|
+
No external dependencies — stdlib only.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import json
|
|
26
|
+
import subprocess
|
|
27
|
+
import sys
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _git_silent(*args: str, cwd: str | None = None) -> int:
|
|
32
|
+
"""Run a git command, return its exit code; suppress output."""
|
|
33
|
+
try:
|
|
34
|
+
r = subprocess.run(
|
|
35
|
+
["git", *args],
|
|
36
|
+
capture_output=True, text=True, cwd=cwd, timeout=30,
|
|
37
|
+
)
|
|
38
|
+
return r.returncode
|
|
39
|
+
except Exception:
|
|
40
|
+
return 1
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _remove_note(commit_sha: str, repo_dir: str) -> None:
|
|
44
|
+
"""Best-effort removal of an agent-trace note on ``commit_sha``.
|
|
45
|
+
Silent if the note doesn't exist."""
|
|
46
|
+
if not commit_sha:
|
|
47
|
+
return
|
|
48
|
+
_git_silent(
|
|
49
|
+
"notes", "--ref", "agent-trace", "remove", "--ignore-missing", commit_sha,
|
|
50
|
+
cwd=repo_dir,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def rewrite_ledgers(project_dir: str | None = None) -> int:
|
|
55
|
+
"""After rebase/amend, remap (or rebuild) ledgers and move notes.
|
|
56
|
+
|
|
57
|
+
Reads old→new SHA mapping from stdin (git provides this in post-rewrite).
|
|
58
|
+
Returns count of ledger rows touched (remapped + rebuilt + dropped).
|
|
59
|
+
"""
|
|
60
|
+
if project_dir is None:
|
|
61
|
+
import os
|
|
62
|
+
project_dir = os.getcwd()
|
|
63
|
+
|
|
64
|
+
sha_map: dict[str, str] = {}
|
|
65
|
+
for line in sys.stdin:
|
|
66
|
+
line = line.strip()
|
|
67
|
+
if not line:
|
|
68
|
+
continue
|
|
69
|
+
parts = line.split()
|
|
70
|
+
if len(parts) >= 2:
|
|
71
|
+
sha_map[parts[0]] = parts[1]
|
|
72
|
+
|
|
73
|
+
if not sha_map:
|
|
74
|
+
return 0
|
|
75
|
+
|
|
76
|
+
from .ledger import build_attribution_ledger, diff_signature
|
|
77
|
+
from .storage import get_ledgers_path, resolve_project_id
|
|
78
|
+
|
|
79
|
+
pid = resolve_project_id(project_dir, create=False)
|
|
80
|
+
if not pid:
|
|
81
|
+
return 0
|
|
82
|
+
ledgers_path = get_ledgers_path(pid)
|
|
83
|
+
if not ledgers_path.exists():
|
|
84
|
+
return 0
|
|
85
|
+
|
|
86
|
+
ledgers: list[dict] = []
|
|
87
|
+
try:
|
|
88
|
+
for line in ledgers_path.read_text().splitlines():
|
|
89
|
+
line = line.strip()
|
|
90
|
+
if line:
|
|
91
|
+
try:
|
|
92
|
+
ledgers.append(json.loads(line))
|
|
93
|
+
except json.JSONDecodeError:
|
|
94
|
+
continue
|
|
95
|
+
except OSError:
|
|
96
|
+
return 0
|
|
97
|
+
|
|
98
|
+
# First pass: SHA remap (commit_sha + parent_sha) and bookkeep which
|
|
99
|
+
# rows moved so we can later decide rebuild-vs-keep.
|
|
100
|
+
touched = 0
|
|
101
|
+
moved: list[tuple[str, str, dict]] = [] # (old_sha, new_sha, ledger)
|
|
102
|
+
for ledger in ledgers:
|
|
103
|
+
old_sha = ledger.get("commit_sha", "")
|
|
104
|
+
if old_sha in sha_map:
|
|
105
|
+
new_sha = sha_map[old_sha]
|
|
106
|
+
ledger["commit_sha"] = new_sha
|
|
107
|
+
touched += 1
|
|
108
|
+
moved.append((old_sha, new_sha, ledger))
|
|
109
|
+
old_parent = ledger.get("parent_sha", "")
|
|
110
|
+
if old_parent and old_parent in sha_map:
|
|
111
|
+
ledger["parent_sha"] = sha_map[old_parent]
|
|
112
|
+
|
|
113
|
+
if touched == 0:
|
|
114
|
+
return 0
|
|
115
|
+
|
|
116
|
+
# Second pass: rebuild rows whose tree-diff changed.
|
|
117
|
+
# `ledgers` may be mutated in-place (rebuild) or have entries removed
|
|
118
|
+
# (rebuild returned None — the rewrite removed all AI lines).
|
|
119
|
+
rebuilt_old_shas: set[str] = set()
|
|
120
|
+
rebuild_failures: set[str] = set()
|
|
121
|
+
rebuilt_replacements: dict[str, dict | None] = {} # new_sha -> rebuilt ledger or None
|
|
122
|
+
for old_sha, new_sha, _ in moved:
|
|
123
|
+
old_sig = diff_signature(old_sha, project_dir)
|
|
124
|
+
new_sig = diff_signature(new_sha, project_dir)
|
|
125
|
+
if old_sig is None or new_sig is None or old_sig == new_sig:
|
|
126
|
+
continue
|
|
127
|
+
# Tree changed — rebuild deterministically against the new commit.
|
|
128
|
+
try:
|
|
129
|
+
new_ledger = build_attribution_ledger(project_dir, commit_ref=new_sha)
|
|
130
|
+
except Exception:
|
|
131
|
+
rebuild_failures.add(old_sha)
|
|
132
|
+
continue
|
|
133
|
+
rebuilt_old_shas.add(old_sha)
|
|
134
|
+
rebuilt_replacements[new_sha] = new_ledger
|
|
135
|
+
|
|
136
|
+
# Rewrite the in-memory list with rebuilt entries swapped in (or dropped
|
|
137
|
+
# when the rebuild produced None).
|
|
138
|
+
if rebuilt_replacements:
|
|
139
|
+
new_list: list[dict] = []
|
|
140
|
+
for ledger in ledgers:
|
|
141
|
+
sha = ledger.get("commit_sha", "")
|
|
142
|
+
if sha in rebuilt_replacements:
|
|
143
|
+
rep = rebuilt_replacements.pop(sha)
|
|
144
|
+
if rep is not None:
|
|
145
|
+
new_list.append(rep)
|
|
146
|
+
# If rep is None, the rewrite removed all AI lines; drop the row.
|
|
147
|
+
else:
|
|
148
|
+
new_list.append(ledger)
|
|
149
|
+
ledgers = new_list
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
with open(ledgers_path, "w") as f:
|
|
153
|
+
for ledger in ledgers:
|
|
154
|
+
f.write(json.dumps(ledger) + "\n")
|
|
155
|
+
except OSError:
|
|
156
|
+
return 0
|
|
157
|
+
|
|
158
|
+
# Third pass: notes. Always drop the old note. Attach a fresh note for
|
|
159
|
+
# rebuilt ledgers; for purely-remapped ones, attach using the (already
|
|
160
|
+
# remapped) ledger. If the rebuild dropped the row, no note is attached.
|
|
161
|
+
try:
|
|
162
|
+
from .git_notes import attach_note_after_ledger
|
|
163
|
+
|
|
164
|
+
# Build a quick lookup of current rows by commit_sha (post-rebuild).
|
|
165
|
+
by_sha = {l.get("commit_sha", ""): l for l in ledgers}
|
|
166
|
+
|
|
167
|
+
for old_sha, new_sha, _ in moved:
|
|
168
|
+
_remove_note(old_sha, project_dir)
|
|
169
|
+
ledger = by_sha.get(new_sha)
|
|
170
|
+
if ledger is not None:
|
|
171
|
+
attach_note_after_ledger(project_dir, ledger)
|
|
172
|
+
except Exception:
|
|
173
|
+
# Don't let note bookkeeping fail the post-rewrite hook.
|
|
174
|
+
pass
|
|
175
|
+
|
|
176
|
+
return touched
|
agent_trace/rules.py
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Rule management for coding agents.
|
|
3
|
+
|
|
4
|
+
Prebuilt rules teach coding agents (Cursor, Claude Code, Codex, ...)
|
|
5
|
+
how to use agent-trace features. Each rule is identified by a short
|
|
6
|
+
name and can be added/removed independently per tool.
|
|
7
|
+
|
|
8
|
+
The list of supported tools is the adapter registry (``hooks``). To
|
|
9
|
+
support a rule for a new agent, add the rule body under the agent's
|
|
10
|
+
``name`` key in ``AVAILABLE_RULES`` — the adapter itself decides where
|
|
11
|
+
the file lives (``rules_dir`` + ``rule_extension``).
|
|
12
|
+
|
|
13
|
+
No external dependencies — stdlib only.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import sys
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
from .hooks import get_adapter, iter_adapters
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _supported_tools() -> tuple[str, ...]:
|
|
26
|
+
"""Tools that declare a ``rules_dir`` are eligible for rules."""
|
|
27
|
+
return tuple(a.name for a in iter_adapters() if a.supports_rules())
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# Exported for argparse ``choices=`` and other callers. Computed lazily
|
|
31
|
+
# so registering a new adapter is reflected without restarting Python.
|
|
32
|
+
class _ToolChoices:
|
|
33
|
+
def __iter__(self):
|
|
34
|
+
return iter(_supported_tools())
|
|
35
|
+
|
|
36
|
+
def __contains__(self, item):
|
|
37
|
+
return item in _supported_tools()
|
|
38
|
+
|
|
39
|
+
def __getitem__(self, idx):
|
|
40
|
+
return _supported_tools()[idx]
|
|
41
|
+
|
|
42
|
+
def __len__(self):
|
|
43
|
+
return len(_supported_tools())
|
|
44
|
+
|
|
45
|
+
def __repr__(self):
|
|
46
|
+
return f"TOOL_CHOICES({_supported_tools()!r})"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
TOOL_CHOICES = _ToolChoices()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# ===================================================================
|
|
53
|
+
# Rule paths (delegated to the adapter)
|
|
54
|
+
# ===================================================================
|
|
55
|
+
|
|
56
|
+
def _rule_path(rule_name: str, tool: str, project_dir: str | None = None) -> Path:
|
|
57
|
+
adapter = get_adapter(tool)
|
|
58
|
+
if adapter is None or not adapter.supports_rules():
|
|
59
|
+
raise ValueError(f"Unknown tool or tool does not support rules: {tool}")
|
|
60
|
+
return adapter.rule_path(rule_name, project_dir)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ===================================================================
|
|
64
|
+
# Rule content: context-for-agents
|
|
65
|
+
# ===================================================================
|
|
66
|
+
|
|
67
|
+
_CONTEXT_FOR_AGENTS_DESCRIPTION = (
|
|
68
|
+
"Teaches the agent to retrieve conversation context behind "
|
|
69
|
+
"AI-attributed code using `agent-trace context`"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
_CONTEXT_FOR_AGENTS_BODY = """\
|
|
73
|
+
# agent-trace: AI conversation context for code
|
|
74
|
+
|
|
75
|
+
This project uses agent-trace to embed conversation context into code. When you read a file that contains AI-generated code, you can retrieve the conversation that produced it.
|
|
76
|
+
|
|
77
|
+
## Step 1: Get attribution metadata (always do this first)
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
agent-trace context <file> --lines START-END --json
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
This returns attribution segments with metadata for each line range: whether it was AI or human authored, which model/tool wrote it, and crucially the `conversation_size` (character count, line count, turn count) and a short `preview` of the conversation.
|
|
84
|
+
|
|
85
|
+
If there are no AI-attributed segments, or the preview gives you enough context, stop here.
|
|
86
|
+
|
|
87
|
+
## Step 2: Decide how to load the full conversation
|
|
88
|
+
|
|
89
|
+
Look at `conversation_size` for each AI-attributed segment:
|
|
90
|
+
|
|
91
|
+
- **Small transcript (< 3000 characters):** Load it directly in your context. Run:
|
|
92
|
+
```
|
|
93
|
+
agent-trace context <file> --lines START-END --full --json
|
|
94
|
+
```
|
|
95
|
+
Read the `conversation_content` field directly.
|
|
96
|
+
|
|
97
|
+
- **Large transcript (>= 3000 characters):** Do NOT load it into your main context window — it will pollute your working context. Instead, delegate to a subagent:
|
|
98
|
+
- Spawn a subagent (e.g. using the Task tool, a background agent, or whatever mechanism your host provides).
|
|
99
|
+
- The subagent should run:
|
|
100
|
+
```
|
|
101
|
+
agent-trace context <file> --lines START-END --full --json
|
|
102
|
+
```
|
|
103
|
+
- Instruct the subagent to read the full `conversation_content` and return a focused summary. Tell the subagent what you need to know — e.g. "summarize the rationale for this implementation", "what constraints were discussed", "why was this approach chosen over alternatives".
|
|
104
|
+
- Use the subagent's returned summary in your reasoning.
|
|
105
|
+
|
|
106
|
+
## When to use this
|
|
107
|
+
|
|
108
|
+
- When you're about to modify AI-generated code and want to understand the original intent
|
|
109
|
+
- When you encounter code whose purpose or design isn't clear from the code itself
|
|
110
|
+
- When the user asks about why code was written a certain way
|
|
111
|
+
- You do NOT need to fetch context for every file you read — use judgment
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
_CONTEXT_FOR_AGENTS_CURSOR = """\
|
|
115
|
+
---
|
|
116
|
+
description: agent-trace context retrieval for AI-attributed code
|
|
117
|
+
alwaysApply: true
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
""" + _CONTEXT_FOR_AGENTS_BODY
|
|
121
|
+
|
|
122
|
+
_CONTEXT_FOR_AGENTS_CLAUDE = _CONTEXT_FOR_AGENTS_BODY
|
|
123
|
+
_CONTEXT_FOR_AGENTS_CODEX = _CONTEXT_FOR_AGENTS_BODY
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# ===================================================================
|
|
127
|
+
# Rule registry
|
|
128
|
+
# ===================================================================
|
|
129
|
+
#
|
|
130
|
+
# Each rule maps a tool ``name`` (matching an adapter) to the rule body
|
|
131
|
+
# for that tool. Tools that aren't keyed simply skip the rule.
|
|
132
|
+
|
|
133
|
+
AVAILABLE_RULES: dict[str, dict[str, Any]] = {
|
|
134
|
+
"context-for-agents": {
|
|
135
|
+
"description": _CONTEXT_FOR_AGENTS_DESCRIPTION,
|
|
136
|
+
"cursor": _CONTEXT_FOR_AGENTS_CURSOR,
|
|
137
|
+
"claude": _CONTEXT_FOR_AGENTS_CLAUDE,
|
|
138
|
+
"codex": _CONTEXT_FOR_AGENTS_CODEX,
|
|
139
|
+
},
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# ===================================================================
|
|
144
|
+
# Rule operations
|
|
145
|
+
# ===================================================================
|
|
146
|
+
|
|
147
|
+
def add_rule(rule_name: str, tool: str, project_dir: str | None = None) -> str:
|
|
148
|
+
"""Write a rule file for the given tool. Returns the path written."""
|
|
149
|
+
if rule_name not in AVAILABLE_RULES:
|
|
150
|
+
print(f"Unknown rule: {rule_name}", file=sys.stderr)
|
|
151
|
+
print(f"Available rules: {', '.join(AVAILABLE_RULES.keys())}", file=sys.stderr)
|
|
152
|
+
sys.exit(1)
|
|
153
|
+
|
|
154
|
+
if tool not in _supported_tools():
|
|
155
|
+
print(f"Unknown tool: {tool}", file=sys.stderr)
|
|
156
|
+
print(f"Available tools: {', '.join(_supported_tools())}", file=sys.stderr)
|
|
157
|
+
sys.exit(1)
|
|
158
|
+
|
|
159
|
+
rule_def = AVAILABLE_RULES[rule_name]
|
|
160
|
+
if tool not in rule_def:
|
|
161
|
+
print(f"Rule '{rule_name}' is not defined for tool '{tool}'.", file=sys.stderr)
|
|
162
|
+
sys.exit(1)
|
|
163
|
+
content = rule_def[tool]
|
|
164
|
+
path = _rule_path(rule_name, tool, project_dir)
|
|
165
|
+
|
|
166
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
167
|
+
path.write_text(content)
|
|
168
|
+
return str(path)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def remove_rule(rule_name: str, tool: str, project_dir: str | None = None) -> str | None:
|
|
172
|
+
"""Remove a rule file. Returns the path removed, or None if not found."""
|
|
173
|
+
if tool not in _supported_tools():
|
|
174
|
+
print(f"Unknown tool: {tool}", file=sys.stderr)
|
|
175
|
+
print(f"Available tools: {', '.join(_supported_tools())}", file=sys.stderr)
|
|
176
|
+
sys.exit(1)
|
|
177
|
+
|
|
178
|
+
path = _rule_path(rule_name, tool, project_dir)
|
|
179
|
+
if path.exists():
|
|
180
|
+
path.unlink()
|
|
181
|
+
return str(path)
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def show_rules(project_dir: str | None = None) -> list[dict[str, str]]:
|
|
186
|
+
"""Scan for active agent-trace rules. Returns list of {name, tool, path}."""
|
|
187
|
+
active: list[dict[str, str]] = []
|
|
188
|
+
|
|
189
|
+
for rule_name, rule_def in AVAILABLE_RULES.items():
|
|
190
|
+
for tool in _supported_tools():
|
|
191
|
+
if tool not in rule_def:
|
|
192
|
+
continue
|
|
193
|
+
path = _rule_path(rule_name, tool, project_dir)
|
|
194
|
+
if path.exists():
|
|
195
|
+
active.append({
|
|
196
|
+
"name": rule_name,
|
|
197
|
+
"tool": tool,
|
|
198
|
+
"path": str(path),
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
return active
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def list_available_rules() -> list[dict[str, str]]:
|
|
205
|
+
"""List all available prebuilt rules with descriptions."""
|
|
206
|
+
return [
|
|
207
|
+
{"name": name, "description": rule_def["description"]}
|
|
208
|
+
for name, rule_def in AVAILABLE_RULES.items()
|
|
209
|
+
]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://raw.githubusercontent.com/ujjalsharma100/agent-trace-cli/main/schemas/commit-link.schema.json",
|
|
4
|
+
"title": "CommitLink",
|
|
5
|
+
"description": "One line in commit-links.jsonl. Optional ledger duplicates the ledger record when stored via remote POST.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": [
|
|
8
|
+
"commit_sha",
|
|
9
|
+
"parent_sha",
|
|
10
|
+
"trace_ids",
|
|
11
|
+
"files_changed",
|
|
12
|
+
"committed_at",
|
|
13
|
+
"created_at"
|
|
14
|
+
],
|
|
15
|
+
"additionalProperties": true,
|
|
16
|
+
"properties": {
|
|
17
|
+
"commit_sha": { "type": "string", "minLength": 1 },
|
|
18
|
+
"parent_sha": { "type": ["string", "null"] },
|
|
19
|
+
"trace_ids": {
|
|
20
|
+
"type": "array",
|
|
21
|
+
"items": { "type": "string" }
|
|
22
|
+
},
|
|
23
|
+
"files_changed": {
|
|
24
|
+
"type": "array",
|
|
25
|
+
"items": { "type": "string" }
|
|
26
|
+
},
|
|
27
|
+
"committed_at": { "type": ["string", "null"] },
|
|
28
|
+
"created_at": { "type": "string" },
|
|
29
|
+
"ledger": {
|
|
30
|
+
"description": "Full ledger object when included (e.g. remote sync)",
|
|
31
|
+
"type": "object"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://raw.githubusercontent.com/ujjalsharma100/agent-trace-cli/main/schemas/git-note.schema.json",
|
|
4
|
+
"title": "GitNotePayload",
|
|
5
|
+
"description": "JSON blob under refs/notes/agent-trace. Schema 2.0: stats only count AI lines (anything else is implicitly NO_ATTRIBUTION).",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["version", "trace_ids", "ledger_hash", "stats"],
|
|
8
|
+
"additionalProperties": false,
|
|
9
|
+
"properties": {
|
|
10
|
+
"version": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"const": "2.0"
|
|
13
|
+
},
|
|
14
|
+
"trace_ids": {
|
|
15
|
+
"type": "array",
|
|
16
|
+
"items": { "type": "string" }
|
|
17
|
+
},
|
|
18
|
+
"ledger_hash": {
|
|
19
|
+
"type": "string",
|
|
20
|
+
"pattern": "^sha256:[0-9a-f]+$"
|
|
21
|
+
},
|
|
22
|
+
"stats": {
|
|
23
|
+
"type": "object",
|
|
24
|
+
"required": ["ai_lines"],
|
|
25
|
+
"additionalProperties": false,
|
|
26
|
+
"properties": {
|
|
27
|
+
"ai_lines": { "type": "integer", "minimum": 0 },
|
|
28
|
+
"total_changed_lines": { "type": "integer", "minimum": 0 }
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"ledger": {
|
|
32
|
+
"type": "object",
|
|
33
|
+
"description": "Inline per-line attribution (optional, AI segments only)",
|
|
34
|
+
"required": ["files"],
|
|
35
|
+
"additionalProperties": false,
|
|
36
|
+
"properties": {
|
|
37
|
+
"files": {
|
|
38
|
+
"type": "object",
|
|
39
|
+
"additionalProperties": {
|
|
40
|
+
"type": "object",
|
|
41
|
+
"required": ["line_attributions"],
|
|
42
|
+
"additionalProperties": false,
|
|
43
|
+
"properties": {
|
|
44
|
+
"line_attributions": {
|
|
45
|
+
"type": "array",
|
|
46
|
+
"items": {
|
|
47
|
+
"type": "object",
|
|
48
|
+
"required": ["start_line", "end_line", "type", "trace_id"],
|
|
49
|
+
"additionalProperties": true,
|
|
50
|
+
"properties": {
|
|
51
|
+
"start_line": { "type": "integer", "minimum": 1 },
|
|
52
|
+
"end_line": { "type": "integer", "minimum": 1 },
|
|
53
|
+
"type": { "type": "string", "const": "ai" },
|
|
54
|
+
"trace_id": { "type": "string" },
|
|
55
|
+
"model_id": { "type": "string" },
|
|
56
|
+
"conversation_id": { "type": "string" }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"summary": {
|
|
66
|
+
"type": "object",
|
|
67
|
+
"description": "Map of conversation_id (sha256 over the original local transcript URL) to summary prose (optional)",
|
|
68
|
+
"additionalProperties": { "type": "string" }
|
|
69
|
+
},
|
|
70
|
+
"prompts": {
|
|
71
|
+
"type": "array",
|
|
72
|
+
"items": { "type": "string" }
|
|
73
|
+
},
|
|
74
|
+
"all_session_conversations": {
|
|
75
|
+
"type": "array",
|
|
76
|
+
"description": "Every conversation_id from traces in the commit staging window (not only those attributed in the commit), with optional stored summary per id",
|
|
77
|
+
"items": {
|
|
78
|
+
"type": "object",
|
|
79
|
+
"required": ["conversation_id", "summary"],
|
|
80
|
+
"additionalProperties": false,
|
|
81
|
+
"properties": {
|
|
82
|
+
"conversation_id": { "type": "string" },
|
|
83
|
+
"summary": { "type": ["string", "null"] }
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://raw.githubusercontent.com/ujjalsharma100/agent-trace-cli/main/schemas/ledger.schema.json",
|
|
4
|
+
"title": "AttributionLedger",
|
|
5
|
+
"description": "One line in ledgers.jsonl. Schema 2.0: only AI segments are recorded. Anything not recorded is implicitly NO_ATTRIBUTION. Each AI segment carries per-line evidence (hash + content) for audit.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": [
|
|
8
|
+
"version",
|
|
9
|
+
"commit_sha",
|
|
10
|
+
"parent_sha",
|
|
11
|
+
"committed_at",
|
|
12
|
+
"created_at",
|
|
13
|
+
"trace_ids",
|
|
14
|
+
"files"
|
|
15
|
+
],
|
|
16
|
+
"additionalProperties": false,
|
|
17
|
+
"properties": {
|
|
18
|
+
"version": {
|
|
19
|
+
"type": "string",
|
|
20
|
+
"enum": ["2.0"]
|
|
21
|
+
},
|
|
22
|
+
"commit_sha": { "type": "string", "minLength": 1 },
|
|
23
|
+
"parent_sha": { "type": ["string", "null"] },
|
|
24
|
+
"parent_committed_at": { "type": ["string", "null"] },
|
|
25
|
+
"committed_at": { "type": ["string", "null"] },
|
|
26
|
+
"created_at": { "type": "string" },
|
|
27
|
+
"trace_ids": {
|
|
28
|
+
"type": "array",
|
|
29
|
+
"items": { "type": "string" }
|
|
30
|
+
},
|
|
31
|
+
"files": {
|
|
32
|
+
"type": "object",
|
|
33
|
+
"additionalProperties": { "$ref": "#/$defs/fileAttribution" }
|
|
34
|
+
},
|
|
35
|
+
"derived_from": {
|
|
36
|
+
"description": "Set when the commit was produced by cherry-pick / revert / merge — used to explain why fallback attribution kicked in.",
|
|
37
|
+
"type": "object",
|
|
38
|
+
"required": ["kind"],
|
|
39
|
+
"additionalProperties": false,
|
|
40
|
+
"properties": {
|
|
41
|
+
"kind": { "type": "string", "enum": ["cherry-pick", "revert", "merge"] },
|
|
42
|
+
"source_sha": { "type": "string" },
|
|
43
|
+
"parents": { "type": "array", "items": { "type": "string" } }
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"used_fallback": {
|
|
47
|
+
"description": "True when one or more lines were attributed via the global hash-index fallback rather than the staging-window pool.",
|
|
48
|
+
"type": "boolean"
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"$defs": {
|
|
52
|
+
"lineEvidence": {
|
|
53
|
+
"type": "object",
|
|
54
|
+
"required": ["line", "hash", "content"],
|
|
55
|
+
"additionalProperties": false,
|
|
56
|
+
"properties": {
|
|
57
|
+
"line": { "type": "integer", "minimum": 1 },
|
|
58
|
+
"hash": { "type": "string" },
|
|
59
|
+
"content": { "type": "string" }
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"lineSegment": {
|
|
63
|
+
"type": "object",
|
|
64
|
+
"required": ["start_line", "end_line", "type", "trace_id"],
|
|
65
|
+
"additionalProperties": false,
|
|
66
|
+
"properties": {
|
|
67
|
+
"start_line": { "type": "integer", "minimum": 1 },
|
|
68
|
+
"end_line": { "type": "integer", "minimum": 1 },
|
|
69
|
+
"type": {
|
|
70
|
+
"type": "string",
|
|
71
|
+
"const": "ai"
|
|
72
|
+
},
|
|
73
|
+
"trace_id": { "type": "string", "minLength": 1 },
|
|
74
|
+
"model_id": { "type": ["string", "null"] },
|
|
75
|
+
"conversation_id": {
|
|
76
|
+
"type": ["string", "null"],
|
|
77
|
+
"description": "Opaque conversation identifier (sha256 over the original local transcript URL); same id used in trace records."
|
|
78
|
+
},
|
|
79
|
+
"evidence": {
|
|
80
|
+
"type": "array",
|
|
81
|
+
"items": { "$ref": "#/$defs/lineEvidence" }
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
"fileAttribution": {
|
|
86
|
+
"type": "object",
|
|
87
|
+
"required": ["line_attributions"],
|
|
88
|
+
"additionalProperties": false,
|
|
89
|
+
"properties": {
|
|
90
|
+
"line_attributions": {
|
|
91
|
+
"type": "array",
|
|
92
|
+
"items": { "$ref": "#/$defs/lineSegment" }
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://raw.githubusercontent.com/ujjalsharma100/agent-trace-cli/main/schemas/remotes.schema.json",
|
|
4
|
+
"title": "RemotesConfig",
|
|
5
|
+
"description": "Named remotes for ~/.agent-trace/projects/<project_id>/remotes.json — git-remote-like URLs and auth references.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": { "$ref": "#/$defs/remote" },
|
|
8
|
+
"$defs": {
|
|
9
|
+
"remote": {
|
|
10
|
+
"type": "object",
|
|
11
|
+
"required": ["url"],
|
|
12
|
+
"additionalProperties": false,
|
|
13
|
+
"properties": {
|
|
14
|
+
"url": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"minLength": 1
|
|
17
|
+
},
|
|
18
|
+
"auth": {
|
|
19
|
+
"type": "object",
|
|
20
|
+
"required": ["type", "token_ref"],
|
|
21
|
+
"additionalProperties": false,
|
|
22
|
+
"properties": {
|
|
23
|
+
"type": { "type": "string" },
|
|
24
|
+
"token_ref": { "type": "string" }
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://raw.githubusercontent.com/ujjalsharma100/agent-trace-cli/main/schemas/sync-state.schema.json",
|
|
4
|
+
"title": "SyncState",
|
|
5
|
+
"description": "Per-remote manifest of synced item IDs (~/.agent-trace/projects/<id>/sync-state.json). The manifest is the source of truth for what's on the server; cursors are paging hints only.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["remotes"],
|
|
8
|
+
"additionalProperties": false,
|
|
9
|
+
"properties": {
|
|
10
|
+
"version": { "type": "integer" },
|
|
11
|
+
"remotes": {
|
|
12
|
+
"type": "object",
|
|
13
|
+
"additionalProperties": { "$ref": "#/$defs/remoteSync" }
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"$defs": {
|
|
17
|
+
"syncedManifest": {
|
|
18
|
+
"type": "object",
|
|
19
|
+
"additionalProperties": false,
|
|
20
|
+
"properties": {
|
|
21
|
+
"trace_ids": { "type": "array", "items": { "type": "string" } },
|
|
22
|
+
"ledger_shas": { "type": "array", "items": { "type": "string" } },
|
|
23
|
+
"commit_link_shas": { "type": "array", "items": { "type": "string" } },
|
|
24
|
+
"blob_shas": { "type": "array", "items": { "type": "string" } },
|
|
25
|
+
"conversation_ids": { "type": "array", "items": { "type": "string" } },
|
|
26
|
+
"summary_keys": { "type": "array", "items": { "type": "string" } }
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"pullCursors": {
|
|
30
|
+
"type": "object",
|
|
31
|
+
"additionalProperties": false,
|
|
32
|
+
"properties": {
|
|
33
|
+
"traces": { "type": ["string", "null"] },
|
|
34
|
+
"ledgers": { "type": ["string", "null"] },
|
|
35
|
+
"commit_links": { "type": ["string", "null"] },
|
|
36
|
+
"conversations": { "type": ["string", "null"] },
|
|
37
|
+
"summaries": { "type": ["string", "null"] }
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"remoteSync": {
|
|
41
|
+
"type": "object",
|
|
42
|
+
"additionalProperties": false,
|
|
43
|
+
"properties": {
|
|
44
|
+
"synced": { "$ref": "#/$defs/syncedManifest" },
|
|
45
|
+
"cursor": { "$ref": "#/$defs/pullCursors" }
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|