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,295 @@
1
+ """Distil a span's events into the compact context string the titler consumes.
2
+
3
+ This is the **serve-side** half of the title train/serve contract. The titler
4
+ was fine-tuned on ``distilled_context(window)`` strings built from the labelling
5
+ corpus, where each corpus row is a :class:`~traceforge.types.SessionEvent`
6
+ projected through :func:`traceforge.phase.event_rows.event_to_feature_row`. Live
7
+ inference projects the events of a freshly-closed activity/step span through the
8
+ **same** projection and the **same** distiller here, so there is no train/serve
9
+ skew by construction.
10
+
11
+ The distilled package is a handful of highest-signal, source-agnostic slots --
12
+ ``intent`` (a stated ``report_intent`` gerund, gold-quality title text when
13
+ present), ``actions`` (tool sequence), ``files`` (touched source files, minus
14
+ learned boilerplate), ``symbols`` (salient code identifiers acted upon) and a
15
+ short free-text ``notes`` tail. Everything is mined from the raw payload /
16
+ classification columns, never from a source-specific schema, so the same code
17
+ runs across agent frameworks.
18
+
19
+ The boilerplate file set is **learned from the full training corpus** (files
20
+ present in the overwhelming majority of a corpus's sessions carry ~zero IDF and
21
+ leak from tool-doc / system-prompt example snippets) and frozen into a packaged
22
+ artifact, so it is applied source-agnostically at inference with no corpus
23
+ dependency. See :data:`_BOILER_FILE`.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import json
29
+ import os
30
+ import re
31
+ from pathlib import Path
32
+
33
+ #: tool-call ids ("toolu_bdrk_01XW9..."), hex blobs and long high-entropy ids are
34
+ #: pure noise tokens that leak into narration; stripped from notes/symbols.
35
+ _IDJUNK_RE = re.compile(r"\btoolu_\w+|\b[a-fA-F0-9]{16,}\b|\b[A-Za-z0-9]{24,}\b")
36
+ #: web-fetched bundler assets ("monaco-cvufusc8.js", "index-bcttgcnd.css"): a
37
+ #: stem carrying a hash-like segment -> drop as noise, not a real source file.
38
+ _ASSET_RE = re.compile(r"[-_][a-z0-9]*\d[a-z0-9]*\.|[-_][bcdfghjklmnpqrstvwxz]{6,}\.")
39
+
40
+ #: A real filename = a >=2-char stem + a short alphabetic extension drawn from
41
+ #: the concrete code/doc/config extensions seen in agent traces.
42
+ _FILE_EXT = (
43
+ "py|pyi|md|rst|txt|js|jsx|ts|tsx|mjs|cjs|json|jsonl|yaml|yml|toml|ini|cfg|"
44
+ "conf|sh|bash|ps1|bat|sql|html|htm|css|scss|go|rs|java|kt|c|h|cpp|hpp|cc|"
45
+ "rb|php|cs|swift|lock|xml|csv|tsv|env|gitignore|dockerfile|makefile"
46
+ )
47
+ _FILE_RE = re.compile(rf"^[a-z][\w\-]{{1,}}\.({_FILE_EXT})$")
48
+
49
+ #: Absolute/relative path -> trailing basename, so notes read cleanly.
50
+ _ABS_PATH = re.compile(r"(?:[A-Za-z]:\\|/)[\w\\/.\- ]*?([\w.\-]+\.\w{1,5})")
51
+
52
+ #: A code SYMBOL: a concrete identifier the agent acted on (function/class/const/
53
+ #: dotted path), distinct from a bare filename -- carries the segment's subject.
54
+ _SYM_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]+)*")
55
+ _BTICK_RE = re.compile(r"`([^`\n]{2,40})`")
56
+ #: "structured" = snake_case / camelCase / dotted.path -> almost always a real
57
+ #: domain entity; ranked ahead of bare ALL-CAPS keywords (NOT, NULL, WHERE).
58
+ _STRUCT_RE = re.compile(r"_|[a-z][A-Z]|\.[A-Za-z]")
59
+ _ACRONYM_RE = re.compile(r"^[A-Z][A-Z0-9]+$")
60
+
61
+ #: code-shaped entity test for payload-mined objects (dot-ext, snake/camel,
62
+ #: ALLCAPS>=2, digit, slash, class/function keyword).
63
+ _CODESHAPE = re.compile(
64
+ r"(\.\w{1,5}\b)|(_)|([a-z][A-Z])|(\b[A-Z]{2,}\b)|(\d)|(/)|(\bclass\b|\bfunction\b)"
65
+ )
66
+ _IDENT = re.compile(r"[A-Za-z_][\w./-]{2,}")
67
+
68
+ #: Stopwords for object-side filtering (shared with the title composer).
69
+ STOP = set(
70
+ "the a an of to and in for with on is are be this that it we our us you your i "
71
+ "let lets now first then next also will should can use via into from as at".split()
72
+ )
73
+
74
+ #: Packaged set of boilerplate files learned from the full training corpus.
75
+ _BOILER_FILE = Path(__file__).resolve().parent / "data" / "boilerplate_files.json"
76
+
77
+
78
+ def _load_boilerplate() -> frozenset[str]:
79
+ try:
80
+ with open(_BOILER_FILE, encoding="utf-8") as fh:
81
+ return frozenset(str(x).lower() for x in json.load(fh))
82
+ except (OSError, ValueError):
83
+ return frozenset()
84
+
85
+
86
+ _BOILER = _load_boilerplate()
87
+
88
+
89
+ # ----------------------------------------------------------------- payload text
90
+ def _payload_obj(row: dict):
91
+ pj = row.get("payload_json")
92
+ if not isinstance(pj, str):
93
+ return None
94
+ try:
95
+ return json.loads(pj)
96
+ except ValueError:
97
+ return pj
98
+
99
+
100
+ def _iter_strings(o):
101
+ if isinstance(o, str):
102
+ yield o
103
+ elif isinstance(o, dict):
104
+ for v in o.values():
105
+ yield from _iter_strings(v)
106
+ elif isinstance(o, (list, tuple)):
107
+ for v in o:
108
+ yield from _iter_strings(v)
109
+
110
+
111
+ def payload_text(row: dict) -> str:
112
+ """All string leaves of the serialized payload, flattened."""
113
+ o = _payload_obj(row)
114
+ if o is None:
115
+ return ""
116
+ if isinstance(o, str):
117
+ return o
118
+ return " ".join(_iter_strings(o))
119
+
120
+
121
+ def denoise(t: str) -> str:
122
+ if not t:
123
+ return ""
124
+ t = _ABS_PATH.sub(r"\1", t)
125
+ t = re.split(r"\b(?:function|end_of_edit)\b", t, flags=re.I)[0]
126
+ t = re.sub(r"\breport_intent\b", "", t, flags=re.I)
127
+ return re.sub(r"\s+", " ", t).strip()
128
+
129
+
130
+ def sentences(text: str) -> list[str]:
131
+ return [s.strip() for s in re.split(r"(?<=[.!?\n])\s+", (text or "").strip()) if s.strip()]
132
+
133
+
134
+ # ---------------------------------------------------------------------- intent
135
+ def extract_intent(row: dict) -> str | None:
136
+ """The gerund-form ``arguments.intent`` of a ``report_intent`` tool call --
137
+ gold-quality title text when present, else ``None``."""
138
+ if "report_intent" not in str(row.get("tool_name", "")).lower():
139
+ pj = row.get("payload_json")
140
+ if not isinstance(pj, str) or "report_intent" not in pj:
141
+ return None
142
+ o = _payload_obj(row)
143
+ if isinstance(o, dict) and str(o.get("tool_name", "")).lower() == "report_intent":
144
+ intent = (o.get("arguments") or {}).get("intent")
145
+ if isinstance(intent, str) and len(intent.split()) >= 2:
146
+ return intent.strip()
147
+ return None
148
+
149
+
150
+ # ----------------------------------------------------------------------- slots
151
+ def tool_seq(rows: list[dict]) -> list[str]:
152
+ seen: set[str] = set()
153
+ out: list[str] = []
154
+ for r in rows:
155
+ tn = r.get("tool_name")
156
+ if tn is None:
157
+ continue
158
+ t = str(tn).lower()
159
+ if t in ("none", "nan", ""):
160
+ continue
161
+ if t not in seen:
162
+ seen.add(t)
163
+ out.append(t)
164
+ return out
165
+
166
+
167
+ def _payload_entities(row: dict) -> set[str]:
168
+ """Concrete entities acted UPON: file/binary names, structures, code
169
+ identifiers (not the tool/action verb side)."""
170
+ ents: set[str] = set()
171
+ for col in ("binaries", "structure"):
172
+ v = row.get(col)
173
+ if isinstance(v, (list, tuple)):
174
+ ents |= {str(x).lower() for x in v if x is not None}
175
+ elif v is not None and str(v) != "None":
176
+ ents.add(str(v).lower())
177
+ pj = row.get("payload_json")
178
+ if isinstance(pj, str):
179
+ for m in re.findall(r"[\w.\-]+\.\w{1,5}", pj):
180
+ ents.add(os.path.basename(m).lower())
181
+ for m in _IDENT.findall(pj):
182
+ if _CODESHAPE.search(m):
183
+ ents.add(m.lower())
184
+ return {e for e in ents if len(e) > 1}
185
+
186
+
187
+ def files_touched(rows: list[dict]) -> list[str]:
188
+ seen: set[str] = set()
189
+ out: list[str] = []
190
+ for r in rows:
191
+ for e in _payload_entities(r):
192
+ if _FILE_RE.match(e) and not _ASSET_RE.search(e) and e not in seen:
193
+ seen.add(e)
194
+ out.append(e)
195
+ return out
196
+
197
+
198
+ def clean_notes(narr: list[str]) -> list[str]:
199
+ """Drop tool-call ids / hex blobs that leak into narration."""
200
+ out: list[str] = []
201
+ for s in narr:
202
+ s = _IDJUNK_RE.sub("", s)
203
+ s = re.sub(r"\s{2,}", " ", s).strip()
204
+ if len(s.split()) >= 3:
205
+ out.append(s)
206
+ return out
207
+
208
+
209
+ def narration(rows: list[dict]) -> list[str]:
210
+ out: list[str] = []
211
+ for r in rows:
212
+ k = str(r.get("kind"))
213
+ if k.startswith("message.assistant") or k.startswith("message.user"):
214
+ for s in sentences(denoise(payload_text(r))):
215
+ if 3 <= len(s.split()) <= 40:
216
+ out.append(s)
217
+ return out[:25]
218
+
219
+
220
+ def _sym_ok(m: str, drop: set[str]) -> bool:
221
+ if len(m) < 3 or m.lower() in STOP or m.lower() in drop:
222
+ return False
223
+ if _IDJUNK_RE.search(m) or _ASSET_RE.search(m):
224
+ return False
225
+ return bool(_STRUCT_RE.search(m) or _ACRONYM_RE.match(m))
226
+
227
+
228
+ def salient_symbols(
229
+ rows: list[dict], drop: frozenset[str] = frozenset(), cap: int = 5
230
+ ) -> list[str]:
231
+ """Highest-signal code identifiers acted on in the segment, ranked by
232
+ salience: backtick-quoted > structured (snake/camel/dotted) by frequency >
233
+ bare ALL-CAPS keywords. Source-agnostic (mines raw payload text)."""
234
+ import collections
235
+
236
+ dropl = {d.lower() for d in drop} | {os.path.splitext(d)[0].lower() for d in drop}
237
+ bt: collections.Counter = collections.Counter()
238
+ struct: collections.Counter = collections.Counter()
239
+ acro: collections.Counter = collections.Counter()
240
+ for r in rows:
241
+ txt = payload_text(r)
242
+ if not txt:
243
+ continue
244
+ quoted = {q for span in _BTICK_RE.findall(txt) for q in _SYM_RE.findall(span)}
245
+ for m in _SYM_RE.findall(txt):
246
+ if not _sym_ok(m, dropl):
247
+ continue
248
+ if m in quoted:
249
+ bt[m] += 1
250
+ elif _STRUCT_RE.search(m):
251
+ struct[m] += 1
252
+ else:
253
+ acro[m] += 1
254
+ seen: set[str] = set()
255
+ out: list[str] = []
256
+ for ctr in (bt, struct, acro):
257
+ for w, _ in ctr.most_common():
258
+ k = w.lower()
259
+ if k in seen:
260
+ continue
261
+ seen.add(k)
262
+ out.append(w)
263
+ if len(out) >= cap:
264
+ return out
265
+ return out
266
+
267
+
268
+ # ------------------------------------------------------------------- assemble
269
+ def distilled_context(rows: list[dict]) -> str:
270
+ """The golden platter: the few highest-signal facts about a span.
271
+
272
+ ``rows`` are :func:`event_to_feature_row` projections of the span's events,
273
+ in sequence order. Returns the slot string the titler was trained on, or
274
+ ``"(no signal)"`` when nothing useful is extractable.
275
+ """
276
+ parts: list[str] = []
277
+ intent = next((extract_intent(r) for r in rows if extract_intent(r)), None)
278
+ if intent:
279
+ parts.append(f"intent: {intent}")
280
+ tools = tool_seq(rows)
281
+ if tools:
282
+ parts.append("actions: " + ", ".join(tools[:6]))
283
+ files = [f for f in files_touched(rows) if f not in _BOILER]
284
+ if files:
285
+ parts.append("files: " + ", ".join(files[:5]))
286
+ syms = salient_symbols(rows, drop=frozenset(files) | _BOILER)
287
+ if syms:
288
+ parts.append("symbols: " + ", ".join(syms))
289
+ narr = clean_notes(narration(rows))
290
+ if narr:
291
+ parts.append("notes: " + " ".join(narr[:2])[:240])
292
+ return " | ".join(parts) if parts else "(no signal)"
293
+
294
+
295
+ __all__ = ["STOP", "distilled_context"]
@@ -0,0 +1,14 @@
1
+ [
2
+ "api.js",
3
+ "app.py",
4
+ "auth.ts",
5
+ "environment.md",
6
+ "main.py",
7
+ "package.json",
8
+ "plan.md",
9
+ "requirements.txt",
10
+ "test.js",
11
+ "users.js",
12
+ "usersearch.ts",
13
+ "utils.js"
14
+ ]