traceforge-toolkit 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 (205) hide show
  1. traceforge/__init__.py +72 -0
  2. traceforge/__main__.py +5 -0
  3. traceforge/_generated.py +254 -0
  4. traceforge/adapters/__init__.py +12 -0
  5. traceforge/adapters/base.py +82 -0
  6. traceforge/adapters/genai_otel.py +164 -0
  7. traceforge/adapters/mapped_json.py +297 -0
  8. traceforge/adapters/otel.py +220 -0
  9. traceforge/boundary/__init__.py +46 -0
  10. traceforge/boundary/data/boundary-model.joblib +0 -0
  11. traceforge/boundary/decode.py +87 -0
  12. traceforge/boundary/features.py +137 -0
  13. traceforge/boundary/inference.py +267 -0
  14. traceforge/boundary/inferencer.py +146 -0
  15. traceforge/classify/__init__.py +95 -0
  16. traceforge/classify/cmd.py +109 -0
  17. traceforge/classify/coding.py +213 -0
  18. traceforge/classify/config.py +554 -0
  19. traceforge/classify/core.py +266 -0
  20. traceforge/classify/data/binary_info.yaml +358 -0
  21. traceforge/classify/data/canonical_tools.yaml +251 -0
  22. traceforge/classify/data/effect_overrides.yaml +480 -0
  23. traceforge/classify/data/mcp_profiles.yaml +711 -0
  24. traceforge/classify/data/recommendation_rules.yaml +111 -0
  25. traceforge/classify/data/risk.yaml +534 -0
  26. traceforge/classify/data/shell_defaults.yaml +95 -0
  27. traceforge/classify/data/shell_rules.yaml +1192 -0
  28. traceforge/classify/data/tool_classifications.yaml +215 -0
  29. traceforge/classify/data/verb_inference.yaml +218 -0
  30. traceforge/classify/mcp.py +181 -0
  31. traceforge/classify/phases.py +75 -0
  32. traceforge/classify/powershell.py +93 -0
  33. traceforge/classify/registry.py +138 -0
  34. traceforge/classify/risk.py +553 -0
  35. traceforge/classify/rules.py +215 -0
  36. traceforge/classify/schema.yaml +282 -0
  37. traceforge/classify/shell.py +503 -0
  38. traceforge/classify/tools.py +91 -0
  39. traceforge/classify/workflow.py +32 -0
  40. traceforge/cli/__init__.py +42 -0
  41. traceforge/cli/config_cmd.py +130 -0
  42. traceforge/cli/detect.py +46 -0
  43. traceforge/cli/download_cmd.py +74 -0
  44. traceforge/cli/factory.py +60 -0
  45. traceforge/cli/gate_cmd.py +30 -0
  46. traceforge/cli/init_cmd.py +86 -0
  47. traceforge/cli/replay.py +130 -0
  48. traceforge/cli/runner.py +181 -0
  49. traceforge/cli/score.py +217 -0
  50. traceforge/cli/status.py +65 -0
  51. traceforge/cli/watch.py +297 -0
  52. traceforge/config/__init__.py +80 -0
  53. traceforge/config/defaults.py +149 -0
  54. traceforge/config/loader.py +239 -0
  55. traceforge/config/mappings.py +104 -0
  56. traceforge/config/models.py +579 -0
  57. traceforge/enricher.py +576 -0
  58. traceforge/formatting/__init__.py +12 -0
  59. traceforge/formatting/budget.py +92 -0
  60. traceforge/formatting/density.py +154 -0
  61. traceforge/gate/__init__.py +17 -0
  62. traceforge/gate/client.py +145 -0
  63. traceforge/gate/external.py +305 -0
  64. traceforge/gate/registry.py +108 -0
  65. traceforge/gate/server.py +174 -0
  66. traceforge/gates/__init__.py +8 -0
  67. traceforge/gates/pii.py +352 -0
  68. traceforge/gates/pii_patterns.yaml +228 -0
  69. traceforge/governance/__init__.py +121 -0
  70. traceforge/governance/assessor.py +441 -0
  71. traceforge/governance/budget.py +59 -0
  72. traceforge/governance/canonical.py +56 -0
  73. traceforge/governance/codec.py +414 -0
  74. traceforge/governance/context.py +249 -0
  75. traceforge/governance/drift.py +196 -0
  76. traceforge/governance/emitter.py +234 -0
  77. traceforge/governance/envelope.py +138 -0
  78. traceforge/governance/ifc.py +364 -0
  79. traceforge/governance/integrity.py +162 -0
  80. traceforge/governance/labeler.py +250 -0
  81. traceforge/governance/mcp_drift.py +328 -0
  82. traceforge/governance/monitor.py +539 -0
  83. traceforge/governance/observer.py +276 -0
  84. traceforge/governance/persistence.py +401 -0
  85. traceforge/governance/phase1.py +77 -0
  86. traceforge/governance/pii.py +276 -0
  87. traceforge/governance/pipeline.py +840 -0
  88. traceforge/governance/registry.py +110 -0
  89. traceforge/governance/results.py +183 -0
  90. traceforge/governance/risk_wrapper.py +113 -0
  91. traceforge/governance/rules.py +368 -0
  92. traceforge/governance/scorer.py +262 -0
  93. traceforge/governance/shield.py +318 -0
  94. traceforge/governance/state.py +466 -0
  95. traceforge/governance/types.py +176 -0
  96. traceforge/mappings/__init__.py +1 -0
  97. traceforge/mappings/aider.yaml +216 -0
  98. traceforge/mappings/aider_markdown.yaml +113 -0
  99. traceforge/mappings/amazonq.yaml +56 -0
  100. traceforge/mappings/antigravity.yaml +88 -0
  101. traceforge/mappings/claude.yaml +93 -0
  102. traceforge/mappings/cline.yaml +286 -0
  103. traceforge/mappings/codex.yaml +158 -0
  104. traceforge/mappings/continue_dev.yaml +57 -0
  105. traceforge/mappings/copilot.yaml +266 -0
  106. traceforge/mappings/copilot_markdown.yaml +72 -0
  107. traceforge/mappings/copilot_vscode.yaml +181 -0
  108. traceforge/mappings/crewai.yaml +592 -0
  109. traceforge/mappings/goose.yaml +128 -0
  110. traceforge/mappings/langgraph.yaml +165 -0
  111. traceforge/mappings/maf.yaml +90 -0
  112. traceforge/mappings/maf_transcript.yaml +99 -0
  113. traceforge/mappings/openai_agents.yaml +144 -0
  114. traceforge/mappings/opencode.yaml +473 -0
  115. traceforge/mappings/openhands.yaml +320 -0
  116. traceforge/mappings/pydantic_ai.yaml +183 -0
  117. traceforge/mappings/smolagents.yaml +89 -0
  118. traceforge/mappings/sweagent.yaml +82 -0
  119. traceforge/migrations/__init__.py +1 -0
  120. traceforge/migrations/env.py +60 -0
  121. traceforge/migrations/models.py +145 -0
  122. traceforge/migrations/runner.py +44 -0
  123. traceforge/migrations/script.py.mako +25 -0
  124. traceforge/migrations/versions/0001_initial.py +176 -0
  125. traceforge/migrations/versions/__init__.py +4 -0
  126. traceforge/models.py +29 -0
  127. traceforge/parsers/__init__.py +21 -0
  128. traceforge/parsers/aider.py +416 -0
  129. traceforge/parsers/base.py +226 -0
  130. traceforge/parsers/copilot.py +314 -0
  131. traceforge/phase/__init__.py +50 -0
  132. traceforge/phase/data/phase-model.joblib +0 -0
  133. traceforge/phase/data/potion-base-8M/README.md +99 -0
  134. traceforge/phase/data/potion-base-8M/config.json +13 -0
  135. traceforge/phase/data/potion-base-8M/model.safetensors +0 -0
  136. traceforge/phase/data/potion-base-8M/modules.json +14 -0
  137. traceforge/phase/data/potion-base-8M/tokenizer.json +1 -0
  138. traceforge/phase/event_rows.py +107 -0
  139. traceforge/phase/features.py +468 -0
  140. traceforge/phase/inference.py +279 -0
  141. traceforge/phase/inferencer.py +171 -0
  142. traceforge/phase/segmentation.py +258 -0
  143. traceforge/pipeline.py +891 -0
  144. traceforge/preprocessors/__init__.py +34 -0
  145. traceforge/preprocessors/amazonq.py +224 -0
  146. traceforge/preprocessors/antigravity.py +116 -0
  147. traceforge/preprocessors/claude.py +95 -0
  148. traceforge/preprocessors/cline.py +36 -0
  149. traceforge/preprocessors/codex.py +311 -0
  150. traceforge/preprocessors/continue_dev.py +119 -0
  151. traceforge/preprocessors/copilot_vscode.py +171 -0
  152. traceforge/preprocessors/goose.py +156 -0
  153. traceforge/preprocessors/maf_transcript.py +84 -0
  154. traceforge/preprocessors/openai_agents.py +86 -0
  155. traceforge/preprocessors/opencode.py +85 -0
  156. traceforge/preprocessors/openhands.py +36 -0
  157. traceforge/preprocessors/pydantic_ai.py +62 -0
  158. traceforge/preprocessors/registry.py +24 -0
  159. traceforge/preprocessors/smolagents.py +90 -0
  160. traceforge/py.typed +0 -0
  161. traceforge/sdk/__init__.py +59 -0
  162. traceforge/sdk/gate_policy.py +63 -0
  163. traceforge/sdk/gate_types.py +140 -0
  164. traceforge/sdk/pipeline.py +265 -0
  165. traceforge/sdk/verdict.py +81 -0
  166. traceforge/sinks/__init__.py +23 -0
  167. traceforge/sinks/base.py +95 -0
  168. traceforge/sinks/callback.py +132 -0
  169. traceforge/sinks/console.py +112 -0
  170. traceforge/sinks/factory.py +94 -0
  171. traceforge/sinks/jsonl.py +125 -0
  172. traceforge/sinks/otel_exporter.py +212 -0
  173. traceforge/sinks/parquet.py +260 -0
  174. traceforge/sinks/s3.py +206 -0
  175. traceforge/sinks/sqlite_output.py +234 -0
  176. traceforge/sinks/webhook.py +136 -0
  177. traceforge/sources/__init__.py +18 -0
  178. traceforge/sources/auto_detect.py +173 -0
  179. traceforge/sources/base.py +45 -0
  180. traceforge/sources/file_poll.py +136 -0
  181. traceforge/sources/file_watch.py +221 -0
  182. traceforge/sources/http_poll.py +141 -0
  183. traceforge/sources/replay.py +63 -0
  184. traceforge/sources/sqlite.py +187 -0
  185. traceforge/sources/sse.py +198 -0
  186. traceforge/telemetry/__init__.py +192 -0
  187. traceforge/title/__init__.py +23 -0
  188. traceforge/title/_resolve.py +79 -0
  189. traceforge/title/context.py +295 -0
  190. traceforge/title/data/boilerplate_files.json +14 -0
  191. traceforge/title/heuristics.py +429 -0
  192. traceforge/title/hygiene.py +90 -0
  193. traceforge/title/inference.py +314 -0
  194. traceforge/title/inferencer.py +477 -0
  195. traceforge/title/naming.py +398 -0
  196. traceforge/trace.py +291 -0
  197. traceforge/tracking/__init__.py +30 -0
  198. traceforge/tracking/models.py +115 -0
  199. traceforge/tracking/phase_tracker.py +288 -0
  200. traceforge/types.py +315 -0
  201. traceforge_toolkit-0.1.0.dist-info/METADATA +188 -0
  202. traceforge_toolkit-0.1.0.dist-info/RECORD +205 -0
  203. traceforge_toolkit-0.1.0.dist-info/WHEEL +4 -0
  204. traceforge_toolkit-0.1.0.dist-info/entry_points.txt +2 -0
  205. traceforge_toolkit-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,297 @@
1
+ """Watch command — primary daemon that auto-detects, watches sources, governs, and sinks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ import logging
8
+ import signal
9
+ import sys
10
+ from pathlib import Path
11
+
12
+ import click
13
+ import yaml
14
+
15
+ from traceforge.cli.runner import (
16
+ ResolvedPipeline,
17
+ load_mapping_path,
18
+ resolve_pipelines,
19
+ watch_directory,
20
+ watch_jsonl_file,
21
+ )
22
+ from traceforge.cli.score import ScoreServer
23
+ from traceforge.sources.auto_detect import detect_frameworks
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ @click.command()
29
+ @click.option("--config", "config_path", type=click.Path(exists=True), default=None)
30
+ @click.option(
31
+ "--frameworks",
32
+ default=None,
33
+ help="Comma-separated frameworks to watch (default: all detected).",
34
+ )
35
+ @click.option("--once", is_flag=True, help="Process existing files then exit (no watching).")
36
+ @click.option("--no-score", is_flag=True, help="Don't start the Score API server.")
37
+ @click.option(
38
+ "--log-level", default="INFO", type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR"])
39
+ )
40
+ def watch(
41
+ config_path: str | None, frameworks: str | None, once: bool, no_score: bool, log_level: str
42
+ ) -> None:
43
+ """Watch detected frameworks, run governance pipeline, emit to sinks."""
44
+ logging.basicConfig(
45
+ level=getattr(logging, log_level),
46
+ format="%(asctime)s %(levelname)-5s %(name)s: %(message)s",
47
+ datefmt="%H:%M:%S",
48
+ )
49
+
50
+ config = _load_config(config_path)
51
+ fw_list = [f.strip() for f in frameworks.split(",")] if frameworks else None
52
+
53
+ # Auto-detect
54
+ detected = detect_frameworks(fw_list)
55
+ if not detected:
56
+ click.echo("No frameworks detected. Nothing to watch.")
57
+ sys.exit(0)
58
+
59
+ click.echo(f"Detected {len(detected)} framework(s): {', '.join(d.name for d in detected)}")
60
+
61
+ # Resolve pipelines
62
+ pipelines = resolve_pipelines(detected)
63
+ if not pipelines:
64
+ click.echo("No pipelines could be resolved.")
65
+ sys.exit(0)
66
+
67
+ # Initialize governance
68
+ from traceforge.cli.factory import create_default_pipeline
69
+ from traceforge.governance.persistence import SystemStore
70
+
71
+ db_path = Path.home() / ".traceforge" / "system.db"
72
+ db_path.parent.mkdir(parents=True, exist_ok=True)
73
+ store = SystemStore(db_path)
74
+
75
+ # Load policy from config if available
76
+ policy = None
77
+ if config and config.get("governance", {}).get("tool_preflight_gate"):
78
+ from traceforge.sdk.gate_policy import GatePolicy
79
+
80
+ dotted = config["governance"]["tool_preflight_gate"]
81
+ from traceforge.governance.pipeline import _import_dotted
82
+
83
+ gate_fn = _import_dotted(dotted)
84
+ policy = GatePolicy().preflight(gate_fn)
85
+
86
+ pipeline = create_default_pipeline(store, policy=policy)
87
+
88
+ # Start Gate IPC server (for CLI-based frameworks: Claude Code, Copilot CLI, etc.)
89
+ from traceforge.gate.server import GateServer
90
+
91
+ gate_server = GateServer(pipeline)
92
+ gate_server.start()
93
+ click.echo(f"Gate IPC server listening on {gate_server.sock_path}")
94
+
95
+ # Register a default session so CLI clients can find us without knowing session_id upfront.
96
+ # Detected framework sessions are also registered individually.
97
+ gate_server.register_session("_default")
98
+ for p in pipelines:
99
+ gate_server.register_session(p.name)
100
+
101
+ # Start Score API
102
+ score_server: ScoreServer | None = None
103
+ if not no_score:
104
+ listen = (
105
+ config.get("score", {}).get("listen", "localhost:7331") if config else "localhost:7331"
106
+ )
107
+ score_server = ScoreServer(pipeline, listen=listen)
108
+ score_server.start_background()
109
+
110
+ # Run async event loop
111
+ try:
112
+ if once:
113
+ asyncio.run(_run_once(pipelines, pipeline, store))
114
+ else:
115
+ asyncio.run(_run_watch(pipelines, pipeline, store))
116
+ except KeyboardInterrupt:
117
+ click.echo("\nShutting down...")
118
+ finally:
119
+ from traceforge.gate.registry import unregister_pid
120
+
121
+ gate_server.stop()
122
+ unregister_pid()
123
+ if score_server:
124
+ score_server.stop()
125
+ store.close()
126
+
127
+
128
+ async def _run_watch(
129
+ pipelines: list[ResolvedPipeline],
130
+ governance: "GovernancePipeline",
131
+ store: "SystemStore",
132
+ ) -> None:
133
+ """Run all pipeline watchers concurrently."""
134
+ tasks = []
135
+ for p in pipelines:
136
+ tasks.append(asyncio.create_task(_watch_pipeline(p, governance), name=f"watch-{p.name}"))
137
+
138
+ # Handle graceful shutdown
139
+ loop = asyncio.get_event_loop()
140
+ stop_event = asyncio.Event()
141
+
142
+ def _signal_handler():
143
+ stop_event.set()
144
+
145
+ for sig in (signal.SIGINT, signal.SIGTERM):
146
+ try:
147
+ loop.add_signal_handler(sig, _signal_handler)
148
+ except NotImplementedError:
149
+ # Windows doesn't support add_signal_handler
150
+ pass
151
+
152
+ click.echo(f"Watching {len(pipelines)} pipeline(s). Press Ctrl+C to stop.")
153
+
154
+ # Wait until stop signal or all tasks complete
155
+ done, pending = await asyncio.wait(
156
+ [*tasks, asyncio.create_task(stop_event.wait())],
157
+ return_when=asyncio.FIRST_COMPLETED,
158
+ )
159
+
160
+ # Cancel remaining tasks
161
+ for t in pending:
162
+ t.cancel()
163
+ await asyncio.gather(*pending, return_exceptions=True)
164
+
165
+
166
+ async def _run_once(
167
+ pipelines: list[ResolvedPipeline],
168
+ governance: "GovernancePipeline",
169
+ store: "SystemStore",
170
+ ) -> None:
171
+ """Process existing files once and exit."""
172
+ for p in pipelines:
173
+ await _process_pipeline_once(p, governance)
174
+
175
+
176
+ async def _watch_pipeline(pipeline: ResolvedPipeline, governance: "GovernancePipeline") -> None:
177
+ """Watch a single pipeline's source and feed events through the unified pipeline."""
178
+ logger.info("Starting watcher for %s at %s", pipeline.name, pipeline.source_path)
179
+
180
+ if not (pipeline.source_path.is_dir() or pipeline.source_path.is_file()):
181
+ logger.warning("Source path does not exist: %s", pipeline.source_path)
182
+ return
183
+
184
+ adapter, event_pipeline = _build_pipeline_runtime(pipeline, governance)
185
+ try:
186
+ if pipeline.source_path.is_dir():
187
+ # Watch directory for JSONL files
188
+ pattern = "*.jsonl" if pipeline.name != "continue" else "*.json"
189
+ async for _file_path, line in watch_directory(pipeline.source_path, pattern=pattern):
190
+ await _feed_line(line, adapter, event_pipeline)
191
+ else:
192
+ # Watch single file
193
+ async for line in watch_jsonl_file(pipeline.source_path, start_at="end"):
194
+ await _feed_line(line, adapter, event_pipeline)
195
+ finally:
196
+ # Emit any buffered (unpaired tool-start) events and release sink resources.
197
+ await event_pipeline.close()
198
+
199
+
200
+ async def _process_pipeline_once(
201
+ pipeline: ResolvedPipeline, governance: "GovernancePipeline"
202
+ ) -> None:
203
+ """Process existing content in a pipeline's source once (no watching)."""
204
+ logger.info("Processing %s at %s", pipeline.name, pipeline.source_path)
205
+
206
+ if not (pipeline.source_path.is_dir() or pipeline.source_path.is_file()):
207
+ return
208
+
209
+ adapter, event_pipeline = _build_pipeline_runtime(pipeline, governance)
210
+ try:
211
+ if pipeline.source_path.is_dir():
212
+ pattern = "*.jsonl" if pipeline.name != "continue" else "*.json"
213
+ for f in pipeline.source_path.rglob(pattern):
214
+ if f.is_file():
215
+ for line in f.read_text(encoding="utf-8").splitlines():
216
+ stripped = line.strip()
217
+ if stripped:
218
+ await _feed_line(stripped, adapter, event_pipeline)
219
+ else:
220
+ for line in pipeline.source_path.read_text(encoding="utf-8").splitlines():
221
+ stripped = line.strip()
222
+ if stripped:
223
+ await _feed_line(stripped, adapter, event_pipeline)
224
+ finally:
225
+ await event_pipeline.close()
226
+
227
+
228
+ def _build_sinks(pipeline: ResolvedPipeline) -> list:
229
+ """Instantiate the configured sink objects for a resolved pipeline (once).
230
+
231
+ Delegates to the shared :func:`traceforge.sinks.factory.build_sinks` so the
232
+ daemon and the SDK build sinks from one mapping of the full ``SinkConfig``
233
+ union.
234
+ """
235
+ from traceforge.sinks.factory import build_sinks
236
+
237
+ return build_sinks(pipeline.sinks)
238
+
239
+
240
+ def _build_pipeline_runtime(pipeline: ResolvedPipeline, governance: "GovernancePipeline"):
241
+ """Build the ``(adapter, EventPipeline)`` runtime for a resolved pipeline.
242
+
243
+ The daemon runs the same unified pipeline as the SDK: adapt -> enrich ->
244
+ classify -> govern -> sinks, with ``governance`` wired in as a *stage* so every
245
+ emitted event carries ``metadata.governance``. Governance is one stage here, not
246
+ the pipeline itself. The shared governance engine also backs the Gate/Score IPC
247
+ servers, so session budget/drift state stays unified across observation and
248
+ preflight. Live ML structuring (phase + boundary labeling) is enabled by
249
+ default, matching the SDK ``Pipeline``.
250
+ """
251
+ from traceforge.adapters.mapped_json import MappedJsonAdapter
252
+ from traceforge.enricher import Enricher
253
+ from traceforge.pipeline import EventPipeline
254
+
255
+ mapping_path = load_mapping_path(pipeline.adapter.mapping)
256
+ adapter = MappedJsonAdapter.from_yaml(str(mapping_path), session_id=pipeline.name)
257
+ event_pipeline = EventPipeline(
258
+ sinks=_build_sinks(pipeline),
259
+ enricher=Enricher(),
260
+ governance=governance,
261
+ )
262
+ return adapter, event_pipeline
263
+
264
+
265
+ async def _feed_line(line: str, adapter, event_pipeline) -> None:
266
+ """Parse one raw line and push each resulting event through the pipeline."""
267
+ try:
268
+ data = json.loads(line)
269
+ except json.JSONDecodeError:
270
+ logger.debug("Skipping non-JSON line")
271
+ return
272
+
273
+ for event in adapter.parse_dict(data):
274
+ await event_pipeline.push(event)
275
+
276
+
277
+ def _load_config(config_path: str | None) -> dict | None:
278
+ """Load config from file path or default locations."""
279
+ import os
280
+
281
+ if config_path:
282
+ path = Path(config_path)
283
+ else:
284
+ env = os.environ.get("TRACEFORGE_CONFIG")
285
+ if env:
286
+ path = Path(env)
287
+ elif Path("traceforge.yaml").exists():
288
+ path = Path("traceforge.yaml")
289
+ elif (Path.home() / ".traceforge" / "config.yaml").exists():
290
+ path = Path.home() / ".traceforge" / "config.yaml"
291
+ else:
292
+ return None
293
+
294
+ if not path.exists():
295
+ return None
296
+
297
+ return yaml.safe_load(path.read_text(encoding="utf-8"))
@@ -0,0 +1,80 @@
1
+ """Traceforge configuration — Pydantic Settings with hierarchical YAML config."""
2
+
3
+ from traceforge.config.models import (
4
+ AdapterConfig,
5
+ BudgetConfig,
6
+ FileWatchSourceConfig,
7
+ FilePollSourceConfig,
8
+ GovernanceConfig,
9
+ HttpGateConfig,
10
+ HttpPollSourceConfig,
11
+ MappedJsonAdapterConfig,
12
+ OtelSpanAdapterConfig,
13
+ PipelineConfig,
14
+ ReplaySourceConfig,
15
+ SDKConfig,
16
+ SessionNamingApiConfig,
17
+ SessionNamingConfig,
18
+ SessionNamingHeuristicConfig,
19
+ SinkConfig,
20
+ SourceConfig,
21
+ SSESourceConfig,
22
+ SqliteSinkConfig,
23
+ SubprocessGateConfig,
24
+ JsonlSinkConfig,
25
+ S3SinkConfig,
26
+ ExternalGateConfig,
27
+ TitleApiConfig,
28
+ TitleConfig,
29
+ ActivityTitlingConfig,
30
+ ActivityTitlingApiConfig,
31
+ TraceforgeConfig,
32
+ )
33
+ from traceforge.config.loader import load_config, get_config, reset_config
34
+ from traceforge.config.mappings import resolve_mapping_path, list_available_mappings
35
+
36
+ __all__ = [
37
+ # Root config
38
+ "TraceforgeConfig",
39
+ # Source configs
40
+ "SourceConfig",
41
+ "FileWatchSourceConfig",
42
+ "FilePollSourceConfig",
43
+ "HttpPollSourceConfig",
44
+ "SSESourceConfig",
45
+ "ReplaySourceConfig",
46
+ # Adapter configs
47
+ "AdapterConfig",
48
+ "MappedJsonAdapterConfig",
49
+ "OtelSpanAdapterConfig",
50
+ # Sink configs
51
+ "SinkConfig",
52
+ "SqliteSinkConfig",
53
+ "JsonlSinkConfig",
54
+ "S3SinkConfig",
55
+ # Pipeline
56
+ "PipelineConfig",
57
+ # SDK
58
+ "SDKConfig",
59
+ # Governance
60
+ "GovernanceConfig",
61
+ "BudgetConfig",
62
+ "ExternalGateConfig",
63
+ "SubprocessGateConfig",
64
+ "HttpGateConfig",
65
+ # Title
66
+ "TitleConfig",
67
+ "TitleApiConfig",
68
+ "SessionNamingConfig",
69
+ "SessionNamingHeuristicConfig",
70
+ "SessionNamingApiConfig",
71
+ "ActivityTitlingConfig",
72
+ "ActivityTitlingApiConfig",
73
+ # Loading
74
+ "load_config",
75
+ "get_config",
76
+ "reset_config",
77
+ # Mappings
78
+ "resolve_mapping_path",
79
+ "list_available_mappings",
80
+ ]
@@ -0,0 +1,149 @@
1
+ """Default config template written to ~/.traceforge/config.yaml on first access."""
2
+
3
+ DEFAULT_CONFIG_YAML = """\
4
+ # Traceforge configuration
5
+ # Docs: https://github.com/dfinson/traceforge
6
+ #
7
+ # This file was auto-created on first use. Edit to customize.
8
+ # Precedence: constructor args > env vars > ./traceforge.yaml > this file > defaults
9
+ #
10
+ # Environment variable override: set TRACEFORGE_CONFIG=/path/to/config.yaml
11
+
12
+ log_level: INFO
13
+
14
+ # ─── Auto-detection ─────────────────────────────────────────────────────────
15
+ # On startup, traceforge scans well-known paths for installed AI coding agents.
16
+ # Detected frameworks are watched automatically — no explicit pipeline config needed.
17
+ auto_detect:
18
+ enabled: true
19
+ # Restrict to specific frameworks (empty = detect all):
20
+ # frameworks: [claude, codex, continue, cline, goose, amazonq, aider]
21
+
22
+ # ─── Score API (preflight scoring endpoint) ─────────────────────────────────
23
+ # Always available for gate integrations to ask "should this tool call proceed?"
24
+ score:
25
+ enabled: true
26
+ listen: localhost:7331
27
+
28
+ # ─── Sinks (where governance results go) ────────────────────────────────────
29
+ # Default: SQLite (queryable history) + Console (real-time alerts)
30
+ # Uncomment additional sinks as needed.
31
+ #
32
+ # sinks:
33
+ # - type: sqlite
34
+ # path: ~/.traceforge/traceforge.db
35
+ # - type: console
36
+ # filter: [warn, deny, escalate]
37
+ # - type: jsonl
38
+ # path: ~/.traceforge/output/{session_id}.jsonl
39
+ # - type: webhook
40
+ # url: https://hooks.slack.com/services/...
41
+ # filter: [deny, escalate]
42
+ # - type: otel
43
+ # endpoint: http://localhost:4318/v1/traces
44
+ # service_name: traceforge
45
+
46
+ # ─── Governance ─────────────────────────────────────────────────────────────
47
+ governance:
48
+ pii_scanning: true
49
+ integrity_verification: true
50
+ # budget:
51
+ # max_tool_calls: 200
52
+ # max_by_effect:
53
+ # destructive: 10
54
+ #
55
+ # ── External preflight gate (out-of-process tool-call decider) ──────────────
56
+ # Delegate the ALLOW/DENY decision to an external Policy Decision Point instead
57
+ # of an in-process Python callback. Fail-CLOSED by default (any error/timeout/
58
+ # non-2xx/bad output => DENY). Mutually exclusive with `tool_preflight_gate`.
59
+ # Choose ONE of the two forms below.
60
+ #
61
+ # HTTP PDP (recommended — e.g. an OPA REST server):
62
+ # preflight_gate:
63
+ # type: http
64
+ # endpoint: http://localhost:8181/v1/data/traceforge/verdict
65
+ # timeout: 2.0
66
+ # fail_open: false # false = fail-closed (DENY on error). Keep false.
67
+ # headers: # optional, e.g. auth tokens
68
+ # Authorization: "Bearer ${PDP_TOKEN}"
69
+ # max_input_bytes: 65536 # per-string cap on tool input sent to the decider
70
+ #
71
+ # Subprocess decider (portable / air-gapped — e.g. `opa eval`):
72
+ # preflight_gate:
73
+ # type: subprocess
74
+ # command: "opa eval -I -f raw data.traceforge.verdict"
75
+ # timeout: 10.0
76
+ # fail_open: false
77
+ # max_input_bytes: 65536
78
+
79
+ # ─── Phase tracker (session-level phase segmentation) ───────────────────────
80
+ # Smooths per-event activity labels into stable workflow phases via a debounced
81
+ # majority vote. Defaults are literature-seeded; recalibrate with the
82
+ # phase-tracker-window-sweep experiment.
83
+ # phase_tracker:
84
+ # enabled: true
85
+ # window_size: 3 # sliding window whose mode is the current phase
86
+ # debounce: 2 # consecutive events before a boundary commits
87
+ # phase_root_depth: 1 # dot-path depth grouped into the boundary root
88
+
89
+ # ─── SDK configuration (in-process push mode) ───────────────────────────────
90
+ sdk:
91
+ batch_size: 64
92
+ flush_interval: 5.0
93
+ max_queue_size: 10000
94
+
95
+ # ─── Titling ────────────────────────────────────────────────────────────────
96
+ # Two title surfaces, each with a free/offline floor and an opt-in LLM API tier
97
+ # that engages only when the provider's API key is present in the environment
98
+ # (e.g. export OPENAI_API_KEY=...). No key => it silently stays on the offline
99
+ # floor. The floor is emitted immediately; the API upgrade (when configured and
100
+ # keyed) is applied later, off the hot path, so live emission is never blocked.
101
+ #
102
+ # * session_naming — the title from the first substantive user message; floor
103
+ # is a free heuristic over the user's own words.
104
+ # * activity_titling — the activity/step (span) titles; floor is the packaged,
105
+ # offline ONNX model shipped with every install.
106
+ #
107
+ # title:
108
+ # session_naming:
109
+ # strategy: heuristic # heuristic | api
110
+ # heuristic:
111
+ # method: hybrid # clip | imperative | keyphrase | hybrid
112
+ # max_words: 8
113
+ # max_chars: 60
114
+ # api:
115
+ # model: gpt-4o-mini # any LiteLLM model, e.g. anthropic/claude-3-5-haiku,
116
+ # # azure/<deployment>, ollama/llama3 (+ api_base)
117
+ # api_base: null # for azure / ollama / vllm / openai-compatible
118
+ # api_key_env: null # override which env var holds the key
119
+ # timeout: 10
120
+ # max_tokens: 24
121
+ # activity_titling:
122
+ # strategy: model # model | api (model = packaged offline ONNX titler)
123
+ # api:
124
+ # model: gpt-4o-mini # any LiteLLM model string (as above)
125
+ # api_base: null # for azure / ollama / vllm / openai-compatible
126
+ # api_key_env: null # override which env var holds the key
127
+ # timeout: 10
128
+ # max_tokens: 256 # one call returns the activity + all step titles
129
+
130
+ # ─── Additional mapping directories ────────────────────────────────────────
131
+ mappings_dirs:
132
+ - ~/.traceforge/mappings
133
+
134
+ # ─── Explicit pipelines (advanced — overrides auto-detect) ──────────────────
135
+ # Define explicit source → adapter → sink pipelines when auto-detect isn't enough.
136
+ #
137
+ # pipelines:
138
+ # - name: claude-local
139
+ # source:
140
+ # type: file_watch
141
+ # path: ~/.claude/projects/-Users-me-myproject/latest.jsonl
142
+ # start_at: end
143
+ # adapter:
144
+ # type: mapped_json
145
+ # mapping: claude
146
+ # sinks:
147
+ # - type: sqlite
148
+ # path: ~/.traceforge/traces.db
149
+ """