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.
Files changed (42) hide show
  1. agent_trace/__init__.py +3 -0
  2. agent_trace/blame.py +471 -0
  3. agent_trace/blame_git.py +133 -0
  4. agent_trace/blame_meta.py +167 -0
  5. agent_trace/cli.py +2680 -0
  6. agent_trace/commit_link.py +226 -0
  7. agent_trace/config.py +137 -0
  8. agent_trace/context.py +385 -0
  9. agent_trace/conversations.py +358 -0
  10. agent_trace/git_notes.py +491 -0
  11. agent_trace/hooks/__init__.py +163 -0
  12. agent_trace/hooks/base.py +233 -0
  13. agent_trace/hooks/claude.py +483 -0
  14. agent_trace/hooks/codex.py +232 -0
  15. agent_trace/hooks/cursor.py +278 -0
  16. agent_trace/hooks/git.py +144 -0
  17. agent_trace/ledger.py +955 -0
  18. agent_trace/models.py +618 -0
  19. agent_trace/record.py +417 -0
  20. agent_trace/registry.py +230 -0
  21. agent_trace/remote.py +673 -0
  22. agent_trace/rewrite.py +176 -0
  23. agent_trace/rules.py +209 -0
  24. agent_trace/schemas/commit-link.schema.json +34 -0
  25. agent_trace/schemas/git-note.schema.json +88 -0
  26. agent_trace/schemas/ledger.schema.json +97 -0
  27. agent_trace/schemas/remotes.schema.json +30 -0
  28. agent_trace/schemas/sync-state.schema.json +49 -0
  29. agent_trace/schemas/trace-record.schema.json +130 -0
  30. agent_trace/session.py +136 -0
  31. agent_trace/storage.py +229 -0
  32. agent_trace/summary.py +465 -0
  33. agent_trace/summary_presets.py +188 -0
  34. agent_trace/sync.py +1182 -0
  35. agent_trace/telemetry.py +142 -0
  36. agent_trace/trace.py +460 -0
  37. agent_trace_cli-0.1.0.dist-info/METADATA +535 -0
  38. agent_trace_cli-0.1.0.dist-info/RECORD +42 -0
  39. agent_trace_cli-0.1.0.dist-info/WHEEL +5 -0
  40. agent_trace_cli-0.1.0.dist-info/entry_points.txt +2 -0
  41. agent_trace_cli-0.1.0.dist-info/licenses/LICENSE +201 -0
  42. 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
+ }