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.
- traceforge/__init__.py +72 -0
- traceforge/__main__.py +5 -0
- traceforge/_generated.py +254 -0
- traceforge/adapters/__init__.py +12 -0
- traceforge/adapters/base.py +82 -0
- traceforge/adapters/genai_otel.py +164 -0
- traceforge/adapters/mapped_json.py +297 -0
- traceforge/adapters/otel.py +220 -0
- traceforge/boundary/__init__.py +46 -0
- traceforge/boundary/data/boundary-model.joblib +0 -0
- traceforge/boundary/decode.py +87 -0
- traceforge/boundary/features.py +137 -0
- traceforge/boundary/inference.py +267 -0
- traceforge/boundary/inferencer.py +146 -0
- traceforge/classify/__init__.py +95 -0
- traceforge/classify/cmd.py +109 -0
- traceforge/classify/coding.py +213 -0
- traceforge/classify/config.py +554 -0
- traceforge/classify/core.py +266 -0
- traceforge/classify/data/binary_info.yaml +358 -0
- traceforge/classify/data/canonical_tools.yaml +251 -0
- traceforge/classify/data/effect_overrides.yaml +480 -0
- traceforge/classify/data/mcp_profiles.yaml +711 -0
- traceforge/classify/data/recommendation_rules.yaml +111 -0
- traceforge/classify/data/risk.yaml +534 -0
- traceforge/classify/data/shell_defaults.yaml +95 -0
- traceforge/classify/data/shell_rules.yaml +1192 -0
- traceforge/classify/data/tool_classifications.yaml +215 -0
- traceforge/classify/data/verb_inference.yaml +218 -0
- traceforge/classify/mcp.py +181 -0
- traceforge/classify/phases.py +75 -0
- traceforge/classify/powershell.py +93 -0
- traceforge/classify/registry.py +138 -0
- traceforge/classify/risk.py +553 -0
- traceforge/classify/rules.py +215 -0
- traceforge/classify/schema.yaml +282 -0
- traceforge/classify/shell.py +503 -0
- traceforge/classify/tools.py +91 -0
- traceforge/classify/workflow.py +32 -0
- traceforge/cli/__init__.py +42 -0
- traceforge/cli/config_cmd.py +130 -0
- traceforge/cli/detect.py +46 -0
- traceforge/cli/download_cmd.py +74 -0
- traceforge/cli/factory.py +60 -0
- traceforge/cli/gate_cmd.py +30 -0
- traceforge/cli/init_cmd.py +86 -0
- traceforge/cli/replay.py +130 -0
- traceforge/cli/runner.py +181 -0
- traceforge/cli/score.py +217 -0
- traceforge/cli/status.py +65 -0
- traceforge/cli/watch.py +297 -0
- traceforge/config/__init__.py +80 -0
- traceforge/config/defaults.py +149 -0
- traceforge/config/loader.py +239 -0
- traceforge/config/mappings.py +104 -0
- traceforge/config/models.py +579 -0
- traceforge/enricher.py +576 -0
- traceforge/formatting/__init__.py +12 -0
- traceforge/formatting/budget.py +92 -0
- traceforge/formatting/density.py +154 -0
- traceforge/gate/__init__.py +17 -0
- traceforge/gate/client.py +145 -0
- traceforge/gate/external.py +305 -0
- traceforge/gate/registry.py +108 -0
- traceforge/gate/server.py +174 -0
- traceforge/gates/__init__.py +8 -0
- traceforge/gates/pii.py +352 -0
- traceforge/gates/pii_patterns.yaml +228 -0
- traceforge/governance/__init__.py +121 -0
- traceforge/governance/assessor.py +441 -0
- traceforge/governance/budget.py +59 -0
- traceforge/governance/canonical.py +56 -0
- traceforge/governance/codec.py +414 -0
- traceforge/governance/context.py +249 -0
- traceforge/governance/drift.py +196 -0
- traceforge/governance/emitter.py +234 -0
- traceforge/governance/envelope.py +138 -0
- traceforge/governance/ifc.py +364 -0
- traceforge/governance/integrity.py +162 -0
- traceforge/governance/labeler.py +250 -0
- traceforge/governance/mcp_drift.py +328 -0
- traceforge/governance/monitor.py +539 -0
- traceforge/governance/observer.py +276 -0
- traceforge/governance/persistence.py +401 -0
- traceforge/governance/phase1.py +77 -0
- traceforge/governance/pii.py +276 -0
- traceforge/governance/pipeline.py +840 -0
- traceforge/governance/registry.py +110 -0
- traceforge/governance/results.py +183 -0
- traceforge/governance/risk_wrapper.py +113 -0
- traceforge/governance/rules.py +368 -0
- traceforge/governance/scorer.py +262 -0
- traceforge/governance/shield.py +318 -0
- traceforge/governance/state.py +466 -0
- traceforge/governance/types.py +176 -0
- traceforge/mappings/__init__.py +1 -0
- traceforge/mappings/aider.yaml +216 -0
- traceforge/mappings/aider_markdown.yaml +113 -0
- traceforge/mappings/amazonq.yaml +56 -0
- traceforge/mappings/antigravity.yaml +88 -0
- traceforge/mappings/claude.yaml +93 -0
- traceforge/mappings/cline.yaml +286 -0
- traceforge/mappings/codex.yaml +158 -0
- traceforge/mappings/continue_dev.yaml +57 -0
- traceforge/mappings/copilot.yaml +266 -0
- traceforge/mappings/copilot_markdown.yaml +72 -0
- traceforge/mappings/copilot_vscode.yaml +181 -0
- traceforge/mappings/crewai.yaml +592 -0
- traceforge/mappings/goose.yaml +128 -0
- traceforge/mappings/langgraph.yaml +165 -0
- traceforge/mappings/maf.yaml +90 -0
- traceforge/mappings/maf_transcript.yaml +99 -0
- traceforge/mappings/openai_agents.yaml +144 -0
- traceforge/mappings/opencode.yaml +473 -0
- traceforge/mappings/openhands.yaml +320 -0
- traceforge/mappings/pydantic_ai.yaml +183 -0
- traceforge/mappings/smolagents.yaml +89 -0
- traceforge/mappings/sweagent.yaml +82 -0
- traceforge/migrations/__init__.py +1 -0
- traceforge/migrations/env.py +60 -0
- traceforge/migrations/models.py +145 -0
- traceforge/migrations/runner.py +44 -0
- traceforge/migrations/script.py.mako +25 -0
- traceforge/migrations/versions/0001_initial.py +176 -0
- traceforge/migrations/versions/__init__.py +4 -0
- traceforge/models.py +29 -0
- traceforge/parsers/__init__.py +21 -0
- traceforge/parsers/aider.py +416 -0
- traceforge/parsers/base.py +226 -0
- traceforge/parsers/copilot.py +314 -0
- traceforge/phase/__init__.py +50 -0
- traceforge/phase/data/phase-model.joblib +0 -0
- traceforge/phase/data/potion-base-8M/README.md +99 -0
- traceforge/phase/data/potion-base-8M/config.json +13 -0
- traceforge/phase/data/potion-base-8M/model.safetensors +0 -0
- traceforge/phase/data/potion-base-8M/modules.json +14 -0
- traceforge/phase/data/potion-base-8M/tokenizer.json +1 -0
- traceforge/phase/event_rows.py +107 -0
- traceforge/phase/features.py +468 -0
- traceforge/phase/inference.py +279 -0
- traceforge/phase/inferencer.py +171 -0
- traceforge/phase/segmentation.py +258 -0
- traceforge/pipeline.py +891 -0
- traceforge/preprocessors/__init__.py +34 -0
- traceforge/preprocessors/amazonq.py +224 -0
- traceforge/preprocessors/antigravity.py +116 -0
- traceforge/preprocessors/claude.py +95 -0
- traceforge/preprocessors/cline.py +36 -0
- traceforge/preprocessors/codex.py +311 -0
- traceforge/preprocessors/continue_dev.py +119 -0
- traceforge/preprocessors/copilot_vscode.py +171 -0
- traceforge/preprocessors/goose.py +156 -0
- traceforge/preprocessors/maf_transcript.py +84 -0
- traceforge/preprocessors/openai_agents.py +86 -0
- traceforge/preprocessors/opencode.py +85 -0
- traceforge/preprocessors/openhands.py +36 -0
- traceforge/preprocessors/pydantic_ai.py +62 -0
- traceforge/preprocessors/registry.py +24 -0
- traceforge/preprocessors/smolagents.py +90 -0
- traceforge/py.typed +0 -0
- traceforge/sdk/__init__.py +59 -0
- traceforge/sdk/gate_policy.py +63 -0
- traceforge/sdk/gate_types.py +140 -0
- traceforge/sdk/pipeline.py +265 -0
- traceforge/sdk/verdict.py +81 -0
- traceforge/sinks/__init__.py +23 -0
- traceforge/sinks/base.py +95 -0
- traceforge/sinks/callback.py +132 -0
- traceforge/sinks/console.py +112 -0
- traceforge/sinks/factory.py +94 -0
- traceforge/sinks/jsonl.py +125 -0
- traceforge/sinks/otel_exporter.py +212 -0
- traceforge/sinks/parquet.py +260 -0
- traceforge/sinks/s3.py +206 -0
- traceforge/sinks/sqlite_output.py +234 -0
- traceforge/sinks/webhook.py +136 -0
- traceforge/sources/__init__.py +18 -0
- traceforge/sources/auto_detect.py +173 -0
- traceforge/sources/base.py +45 -0
- traceforge/sources/file_poll.py +136 -0
- traceforge/sources/file_watch.py +221 -0
- traceforge/sources/http_poll.py +141 -0
- traceforge/sources/replay.py +63 -0
- traceforge/sources/sqlite.py +187 -0
- traceforge/sources/sse.py +198 -0
- traceforge/telemetry/__init__.py +192 -0
- traceforge/title/__init__.py +23 -0
- traceforge/title/_resolve.py +79 -0
- traceforge/title/context.py +295 -0
- traceforge/title/data/boilerplate_files.json +14 -0
- traceforge/title/heuristics.py +429 -0
- traceforge/title/hygiene.py +90 -0
- traceforge/title/inference.py +314 -0
- traceforge/title/inferencer.py +477 -0
- traceforge/title/naming.py +398 -0
- traceforge/trace.py +291 -0
- traceforge/tracking/__init__.py +30 -0
- traceforge/tracking/models.py +115 -0
- traceforge/tracking/phase_tracker.py +288 -0
- traceforge/types.py +315 -0
- traceforge_toolkit-0.1.0.dist-info/METADATA +188 -0
- traceforge_toolkit-0.1.0.dist-info/RECORD +205 -0
- traceforge_toolkit-0.1.0.dist-info/WHEEL +4 -0
- traceforge_toolkit-0.1.0.dist-info/entry_points.txt +2 -0
- traceforge_toolkit-0.1.0.dist-info/licenses/LICENSE +21 -0
traceforge/cli/watch.py
ADDED
|
@@ -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
|
+
"""
|