traceforge-pcb 1.0.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 (219) hide show
  1. traceforge/__init__.py +3 -0
  2. traceforge/__main__.py +3 -0
  3. traceforge/acp_server.py +570 -0
  4. traceforge/agent.py +680 -0
  5. traceforge/altium/__init__.py +6 -0
  6. traceforge/altium/cfb.py +70 -0
  7. traceforge/altium/cli.py +60 -0
  8. traceforge/altium/model.py +163 -0
  9. traceforge/altium/outputs.py +24 -0
  10. traceforge/altium/pcb.py +301 -0
  11. traceforge/altium/project.py +328 -0
  12. traceforge/altium/schematic.py +400 -0
  13. traceforge/altium/tools.py +166 -0
  14. traceforge/approval.py +219 -0
  15. traceforge/attachments.py +346 -0
  16. traceforge/background.py +253 -0
  17. traceforge/bench/__init__.py +5 -0
  18. traceforge/bench/adapters.py +337 -0
  19. traceforge/bench/checks.py +410 -0
  20. traceforge/bench/cli.py +121 -0
  21. traceforge/bench/design.py +439 -0
  22. traceforge/bench/kicad.py +99 -0
  23. traceforge/bench/partscheck.py +159 -0
  24. traceforge/bench/review.py +100 -0
  25. traceforge/bench/runner.py +226 -0
  26. traceforge/bench/score.py +204 -0
  27. traceforge/bench/tasks.py +102 -0
  28. traceforge/chat.py +1185 -0
  29. traceforge/checks.py +328 -0
  30. traceforge/checks_sides.py +118 -0
  31. traceforge/cli.py +2224 -0
  32. traceforge/clipboard.py +107 -0
  33. traceforge/config.py +299 -0
  34. traceforge/context.py +276 -0
  35. traceforge/design/__init__.py +1 -0
  36. traceforge/design/constraints.py +397 -0
  37. traceforge/design/generate.py +1789 -0
  38. traceforge/design/outline.py +905 -0
  39. traceforge/design/schematic_layout.py +932 -0
  40. traceforge/design/schematic_lint.py +329 -0
  41. traceforge/design/schematic_route.py +207 -0
  42. traceforge/design/sheets.py +854 -0
  43. traceforge/design/sides.py +328 -0
  44. traceforge/design/spec.py +462 -0
  45. traceforge/design/specsheet.py +2351 -0
  46. traceforge/design/specsheet_render.py +183 -0
  47. traceforge/desktop/__init__.py +109 -0
  48. traceforge/desktop/keys.py +90 -0
  49. traceforge/desktop/project.py +443 -0
  50. traceforge/desktop/review.py +20 -0
  51. traceforge/desktop/server.py +1277 -0
  52. traceforge/desktop/static/app.css +223 -0
  53. traceforge/desktop/static/app.js +683 -0
  54. traceforge/desktop/static/highlight.js +77 -0
  55. traceforge/desktop/static/index.html +89 -0
  56. traceforge/desktop/static/md.js +90 -0
  57. traceforge/desktop/static/viewer.js +213 -0
  58. traceforge/desktop/symbols.py +212 -0
  59. traceforge/desktop/update.py +65 -0
  60. traceforge/diagnostics.py +67 -0
  61. traceforge/errata.py +219 -0
  62. traceforge/fab.py +108 -0
  63. traceforge/fab_layers.py +44 -0
  64. traceforge/features/__init__.py +6 -0
  65. traceforge/features/candidates.py +117 -0
  66. traceforge/features/cli.py +200 -0
  67. traceforge/features/package.py +433 -0
  68. traceforge/features/revdiff.py +224 -0
  69. traceforge/features/sarif.py +145 -0
  70. traceforge/features/viewer.py +265 -0
  71. traceforge/firmware/__init__.py +3 -0
  72. traceforge/firmware/cli.py +174 -0
  73. traceforge/firmware/drivers.py +372 -0
  74. traceforge/firmware/drivers_data.py +256 -0
  75. traceforge/firmware/mcus.py +177 -0
  76. traceforge/firmware/netlist.py +55 -0
  77. traceforge/firmware/pinmap.py +296 -0
  78. traceforge/firmware/scaffold.py +466 -0
  79. traceforge/firmware/toolchain.py +262 -0
  80. traceforge/hooks.py +237 -0
  81. traceforge/kicad/__init__.py +0 -0
  82. traceforge/kicad/cli.py +519 -0
  83. traceforge/kicad/diffpair.py +174 -0
  84. traceforge/kicad/edgegeom.py +191 -0
  85. traceforge/kicad/edit.py +405 -0
  86. traceforge/kicad/escape.py +1410 -0
  87. traceforge/kicad/formats.py +1073 -0
  88. traceforge/kicad/impedance.py +320 -0
  89. traceforge/kicad/install.py +75 -0
  90. traceforge/kicad/libraries.py +490 -0
  91. traceforge/kicad/models3d.py +942 -0
  92. traceforge/kicad/outputs.py +81 -0
  93. traceforge/kicad/pcb.py +787 -0
  94. traceforge/kicad/planes.py +1242 -0
  95. traceforge/kicad/project.py +136 -0
  96. traceforge/kicad/route.py +2698 -0
  97. traceforge/kicad/routegeom.py +164 -0
  98. traceforge/kicad/schematic.py +268 -0
  99. traceforge/kicad/sides.py +34 -0
  100. traceforge/kicad/stackup.py +424 -0
  101. traceforge/kicad_plugin/__init__.py +12 -0
  102. traceforge/kicad_plugin/actions.py +92 -0
  103. traceforge/kicad_plugin/cli.py +66 -0
  104. traceforge/kicad_plugin/client.py +86 -0
  105. traceforge/kicad_plugin/context.py +257 -0
  106. traceforge/kicad_plugin/discovery.py +81 -0
  107. traceforge/kicad_plugin/install.py +203 -0
  108. traceforge/kicad_plugin/overlay.py +60 -0
  109. traceforge/kicad_plugin/payload/action_plugin.py +65 -0
  110. traceforge/kicad_plugin/payload/ipc_main.py +42 -0
  111. traceforge/kicad_plugin/payload/plugin.json +90 -0
  112. traceforge/kicad_plugin/payload/requirements.txt +1 -0
  113. traceforge/kicad_plugin/views.py +202 -0
  114. traceforge/langchain.py +57 -0
  115. traceforge/local.py +580 -0
  116. traceforge/mcp_server.py +304 -0
  117. traceforge/models.py +310 -0
  118. traceforge/next_steps.py +110 -0
  119. traceforge/parts/__init__.py +52 -0
  120. traceforge/parts/alternates.py +488 -0
  121. traceforge/parts/bom.py +423 -0
  122. traceforge/parts/brief.py +705 -0
  123. traceforge/parts/catalog.py +297 -0
  124. traceforge/parts/cli_sourcing.py +156 -0
  125. traceforge/parts/data/duty_rates.yaml +37 -0
  126. traceforge/parts/data/hs_codes.yaml +103 -0
  127. traceforge/parts/datasheet.py +278 -0
  128. traceforge/parts/fx.py +100 -0
  129. traceforge/parts/model.py +185 -0
  130. traceforge/parts/net.py +187 -0
  131. traceforge/parts/offers.py +46 -0
  132. traceforge/parts/packages.py +397 -0
  133. traceforge/parts/pinout.py +714 -0
  134. traceforge/parts/providers/__init__.py +17 -0
  135. traceforge/parts/providers/base.py +100 -0
  136. traceforge/parts/providers/jlc.py +364 -0
  137. traceforge/parts/providers/keyed.py +661 -0
  138. traceforge/parts/report.py +508 -0
  139. traceforge/parts/supply.py +330 -0
  140. traceforge/parts/verify.py +455 -0
  141. traceforge/parts/web.py +575 -0
  142. traceforge/plans.py +70 -0
  143. traceforge/preferences.py +127 -0
  144. traceforge/prices.py +130 -0
  145. traceforge/prices.yaml +28 -0
  146. traceforge/prompts/__init__.py +54 -0
  147. traceforge/prompts/system.md +103 -0
  148. traceforge/providers.py +764 -0
  149. traceforge/remote/__init__.py +33 -0
  150. traceforge/remote/client.py +422 -0
  151. traceforge/remote/oidc.py +240 -0
  152. traceforge/remote/orgs.py +510 -0
  153. traceforge/remote/server.py +690 -0
  154. traceforge/remote/store.py +100 -0
  155. traceforge/review_changes.py +187 -0
  156. traceforge/routing.py +99 -0
  157. traceforge/runtime.py +435 -0
  158. traceforge/sessions.py +324 -0
  159. traceforge/sexpr.py +279 -0
  160. traceforge/sim/__init__.py +17 -0
  161. traceforge/sim/acmath.py +234 -0
  162. traceforge/sim/circuit.py +499 -0
  163. traceforge/sim/cli.py +162 -0
  164. traceforge/sim/engine.py +172 -0
  165. traceforge/sim/ltspice.py +175 -0
  166. traceforge/sim/measure.py +123 -0
  167. traceforge/sim/models.py +76 -0
  168. traceforge/sim/project.py +218 -0
  169. traceforge/sim/rawfile.py +123 -0
  170. traceforge/sim/results.py +298 -0
  171. traceforge/sim/tune.py +172 -0
  172. traceforge/skill_commands.py +165 -0
  173. traceforge/skill_library/circuit-simulation/SKILL.md +30 -0
  174. traceforge/skill_library/compliance-review/SKILL.md +51 -0
  175. traceforge/skill_library/design-review/SKILL.md +47 -0
  176. traceforge/skill_library/errata-hunt/SKILL.md +30 -0
  177. traceforge/skill_library/firmware/SKILL.md +26 -0
  178. traceforge/skill_library/power-budget/SKILL.md +27 -0
  179. traceforge/skill_library/release-package/SKILL.md +29 -0
  180. traceforge/skill_library/sourcing-brief/SKILL.md +24 -0
  181. traceforge/skill_library/thermal-review/SKILL.md +15 -0
  182. traceforge/skills.py +131 -0
  183. traceforge/thermal.py +557 -0
  184. traceforge/tools/__init__.py +48 -0
  185. traceforge/tools/agent_tools.py +213 -0
  186. traceforge/tools/base.py +303 -0
  187. traceforge/tools/batch.py +155 -0
  188. traceforge/tools/connections.py +245 -0
  189. traceforge/tools/files.py +777 -0
  190. traceforge/tools/firmware_tools.py +218 -0
  191. traceforge/tools/kicad_tools.py +1072 -0
  192. traceforge/tools/models3d_tools.py +141 -0
  193. traceforge/tools/parts_tools.py +473 -0
  194. traceforge/tools/processes.py +246 -0
  195. traceforge/tools/review_tools.py +178 -0
  196. traceforge/tools/sim_tools.py +230 -0
  197. traceforge/tui.py +620 -0
  198. traceforge/updates.py +124 -0
  199. traceforge/validate/__init__.py +77 -0
  200. traceforge/validate/builtins.py +1091 -0
  201. traceforge/validate/circuits.py +1240 -0
  202. traceforge/validate/config.py +410 -0
  203. traceforge/validate/default.yaml +113 -0
  204. traceforge/validate/design.py +191 -0
  205. traceforge/validate/explain.py +112 -0
  206. traceforge/validate/grounding.py +169 -0
  207. traceforge/validate/ipc2221.py +350 -0
  208. traceforge/validate/multilayer.py +805 -0
  209. traceforge/validate/netmodel.py +261 -0
  210. traceforge/validate/partcheck.py +472 -0
  211. traceforge/validate/report.py +280 -0
  212. traceforge/validate/review.py +322 -0
  213. traceforge/validate/runner.py +662 -0
  214. traceforge/workspace.py +171 -0
  215. traceforge_pcb-1.0.0.dist-info/METADATA +463 -0
  216. traceforge_pcb-1.0.0.dist-info/RECORD +219 -0
  217. traceforge_pcb-1.0.0.dist-info/WHEEL +4 -0
  218. traceforge_pcb-1.0.0.dist-info/entry_points.txt +2 -0
  219. traceforge_pcb-1.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,127 @@
1
+ """Small, non-secret preferences the interactive front ends remember between sessions.
2
+
3
+ Stored as JSON in ``~/.config/traceforge/preferences.json`` (mode 0600). Settings
4
+ the user writes by hand belong in ``config.yaml``; this file holds what the
5
+ terminal UI saves on the user's behalf: the theme, the last approval mode picked
6
+ in a session, whether a warning was acknowledged, shell auto-approval choices,
7
+ trusted folders and the last update check.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import stat
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+ from traceforge.config import atomic_write_text, config_home, file_lock
18
+
19
+ YOLO_NOTICE_VERSION = 1
20
+ AUTO_NOTICE_VERSION = 1
21
+
22
+
23
+ def preferences_path() -> Path:
24
+ return config_home() / "preferences.json"
25
+
26
+
27
+ def load_preferences() -> dict[str, Any]:
28
+ try:
29
+ value = json.loads(preferences_path().read_text(encoding="utf-8"))
30
+ except (FileNotFoundError, OSError, ValueError):
31
+ return {}
32
+ return value if isinstance(value, dict) else {}
33
+
34
+
35
+ def save_preferences(value: dict[str, Any]) -> None:
36
+ path = preferences_path()
37
+ path.parent.mkdir(parents=True, exist_ok=True)
38
+ atomic_write_text(path, json.dumps(value, indent=2, sort_keys=True), mode=0o600)
39
+
40
+
41
+ def update_preferences(**changes: Any) -> dict[str, Any]:
42
+ """Read, change and save under a lock; ``None`` removes a key."""
43
+ path = preferences_path()
44
+ with file_lock(path):
45
+ value = load_preferences()
46
+ for key, item in changes.items():
47
+ if item is None:
48
+ value.pop(key, None)
49
+ else:
50
+ value[key] = item
51
+ save_preferences(value)
52
+ return value
53
+
54
+
55
+ def get(key: str, default: Any = None) -> Any:
56
+ return load_preferences().get(key, default)
57
+
58
+
59
+ # ------------------------------------------------------------------ approvals
60
+
61
+
62
+ def save_approval(mode: str) -> None:
63
+ """Remember the approval mode chosen in a session. YOLO is never remembered."""
64
+ update_preferences(approval=None if mode == "yolo" else mode)
65
+
66
+
67
+ def yolo_acknowledged() -> bool:
68
+ return get("yolo_notice") == YOLO_NOTICE_VERSION
69
+
70
+
71
+ def acknowledge_yolo() -> None:
72
+ update_preferences(yolo_notice=YOLO_NOTICE_VERSION)
73
+
74
+
75
+ def auto_acknowledged() -> bool:
76
+ return get("auto_notice") == AUTO_NOTICE_VERSION
77
+
78
+
79
+ def acknowledge_auto() -> None:
80
+ update_preferences(auto_notice=AUTO_NOTICE_VERSION)
81
+
82
+
83
+ def auto_shell() -> str:
84
+ """Which shell commands auto mode runs unasked: simple | any | dangerous."""
85
+ value = get("auto_shell")
86
+ return value if value in {"simple", "any", "dangerous"} else "any"
87
+
88
+
89
+ def save_auto_shell(level: str) -> None:
90
+ if level not in {"simple", "any", "dangerous"}:
91
+ raise ValueError("auto shell level must be simple, any or dangerous")
92
+ update_preferences(auto_shell=level)
93
+
94
+
95
+ # --------------------------------------------------------------------- display
96
+
97
+
98
+ def saved_theme(default: str = "forge") -> str:
99
+ value = get("theme")
100
+ return value if isinstance(value, str) and value else default
101
+
102
+
103
+ def save_theme(name: str) -> None:
104
+ update_preferences(theme=name)
105
+
106
+
107
+ def tips_enabled() -> bool:
108
+ return get("tips") is not False
109
+
110
+
111
+ def last_tip() -> int | None:
112
+ value = get("last_tip")
113
+ return value if isinstance(value, int) and not isinstance(value, bool) else None
114
+
115
+
116
+ def save_last_tip(index: int) -> None:
117
+ update_preferences(last_tip=index)
118
+
119
+
120
+ def private_dir(path: Path) -> Path:
121
+ """Create a directory only the user can read."""
122
+ path.mkdir(parents=True, exist_ok=True)
123
+ try:
124
+ path.chmod(stat.S_IRWXU)
125
+ except OSError: # pragma: no cover - e.g. a filesystem without modes
126
+ pass
127
+ return path
traceforge/prices.py ADDED
@@ -0,0 +1,130 @@
1
+ """Per-model price table and the runtime cost meter.
2
+
3
+ Cost is computed from recorded token counts and the table in ``prices.yaml``. Anything the
4
+ table cannot price is ``None`` (unknown); nothing is guessed. A local model is free (0.0).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ import re
11
+ from dataclasses import dataclass, field
12
+ from functools import lru_cache
13
+ from pathlib import Path
14
+ from typing import Any
15
+
16
+ import yaml
17
+
18
+ _DATED = re.compile(r"-(\d{4}-\d{2}-\d{2}|\d{8})$")
19
+
20
+
21
+ def _paths() -> list[Path]:
22
+ from traceforge.config import config_home
23
+
24
+ paths = [Path(__file__).with_name("prices.yaml"), config_home() / "prices.yaml"]
25
+ if os.environ.get("TRACEFORGE_PRICES"):
26
+ paths.append(Path(os.environ["TRACEFORGE_PRICES"]))
27
+ return paths
28
+
29
+
30
+ def load_table() -> dict[str, dict[str, float]]:
31
+ """The packaged table, then the user's, then ``TRACEFORGE_PRICES`` (later wins)."""
32
+ table: dict[str, dict[str, float]] = {}
33
+ for path in _paths():
34
+ try:
35
+ data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
36
+ except (OSError, yaml.YAMLError):
37
+ continue
38
+ for model, price in (data.get("models") or {}).items():
39
+ if isinstance(price, dict):
40
+ table[str(model)] = {k: float(v) for k, v in price.items() if v is not None}
41
+ return table
42
+
43
+
44
+ @lru_cache(maxsize=1)
45
+ def _cached() -> tuple[tuple, dict]:
46
+ return (), load_table()
47
+
48
+
49
+ def reset_cache() -> None:
50
+ _cached.cache_clear()
51
+
52
+
53
+ def price_for(model: str, table: dict[str, dict[str, float]] | None = None) -> dict | None:
54
+ table = table if table is not None else _cached()[1]
55
+ name = str(model or "")
56
+ for candidate in (name, name.split("/")[-1], _DATED.sub("", name.split("/")[-1])):
57
+ if candidate in table:
58
+ return table[candidate]
59
+ return None
60
+
61
+
62
+ _FIELDS = (
63
+ ("input_tokens", "input"),
64
+ ("output_tokens", "output"),
65
+ ("cache_read_input_tokens", "cache_read"),
66
+ ("cache_creation_input_tokens", "cache_write"),
67
+ )
68
+
69
+
70
+ def model_cost(
71
+ model: str,
72
+ usage: dict[str, Any],
73
+ provider: str | None = None,
74
+ table: dict[str, dict[str, float]] | None = None,
75
+ ) -> float | None:
76
+ """USD for one model's token counts, or ``None`` when a needed price is missing."""
77
+ if provider == "local":
78
+ return 0.0
79
+ price = price_for(model, table)
80
+ if price is None:
81
+ return None
82
+ total = 0.0
83
+ for key, price_key in _FIELDS:
84
+ tokens = int(usage.get(key, 0) or 0)
85
+ if not tokens:
86
+ continue
87
+ if price_key not in price:
88
+ return None # e.g. cached tokens read but no cache price listed
89
+ total += tokens * price[price_key]
90
+ return total / 1_000_000
91
+
92
+
93
+ @dataclass
94
+ class Cost:
95
+ usd: float | None
96
+ unknown_models: list[str] = field(default_factory=list)
97
+
98
+ def to_dict(self) -> dict[str, Any]:
99
+ return {"cost_usd": self.usd, "unpriced_models": self.unknown_models}
100
+
101
+ def text(self) -> str:
102
+ if self.usd is None:
103
+ return "unknown" + (
104
+ f" (no price for {', '.join(self.unknown_models)})" if self.unknown_models else ""
105
+ )
106
+ suffix = f" + unknown for {', '.join(self.unknown_models)}" if self.unknown_models else ""
107
+ return f"${self.usd:,.4f}{suffix}"
108
+
109
+
110
+ def usage_cost(usage: dict[str, Any], default_model: str = "", provider: str | None = None) -> Cost:
111
+ """Cost of a usage dict (``by_model`` when recorded, else everything at ``default_model``).
112
+
113
+ ``usd`` is ``None`` when no model could be priced; when only some could, it is the
114
+ known part and ``unknown_models`` names the rest.
115
+ """
116
+ from traceforge.providers import provider_for
117
+
118
+ by_model = usage.get("by_model") or ({default_model: usage} if default_model else {})
119
+ if not by_model:
120
+ return Cost(None, [])
121
+ known, unknown = 0.0, []
122
+ priced = False
123
+ for model, counts in by_model.items():
124
+ cost = model_cost(model, counts, provider or provider_for(model, None))
125
+ if cost is None:
126
+ unknown.append(model)
127
+ else:
128
+ known += cost
129
+ priced = True
130
+ return Cost(round(known, 6) if priced else None, unknown)
traceforge/prices.yaml ADDED
@@ -0,0 +1,28 @@
1
+ # USD per million tokens, list prices (standard tier, text). Used by the runtime cost meter
2
+ # (`traceforge usage`, the desktop app, the hosted server) and the benchmark.
3
+ #
4
+ # A model that is not listed, or a listed model that lacks a price needed for the tokens
5
+ # used (e.g. cache_read when cached tokens were read), has UNKNOWN cost: it is reported as
6
+ # unknown, never estimated. Only add prices you have checked against the provider's
7
+ # pricing page, and update `as_of`. Models with tiered prices (by prompt length) are left
8
+ # out on purpose. Local models (provider: local) cost 0.0 by definition.
9
+ #
10
+ # Override or extend per machine with ~/.config/traceforge/prices.yaml or TRACEFORGE_PRICES.
11
+ # benchmarks/prices.yaml is a copy of this file (a test keeps them identical).
12
+ as_of: "2025-11"
13
+ models:
14
+ claude-haiku-4-5: {input: 1.0, output: 5.0, cache_read: 0.10, cache_write: 1.25}
15
+ claude-sonnet-4-5: {input: 3.0, output: 15.0, cache_read: 0.30, cache_write: 3.75}
16
+ gpt-5: {input: 1.25, output: 10.0, cache_read: 0.125}
17
+ gpt-5-mini: {input: 0.25, output: 2.0, cache_read: 0.025}
18
+ gpt-5-nano: {input: 0.05, output: 0.40, cache_read: 0.005}
19
+ gpt-4.1: {input: 2.0, output: 8.0, cache_read: 0.50}
20
+ gpt-4.1-mini: {input: 0.40, output: 1.60, cache_read: 0.10}
21
+ gpt-4.1-nano: {input: 0.10, output: 0.40, cache_read: 0.025}
22
+ gpt-4o: {input: 2.50, output: 10.0, cache_read: 1.25}
23
+ gpt-4o-mini: {input: 0.15, output: 0.60, cache_read: 0.075}
24
+ # Gemini: no cache_read price listed, so a call that read cached tokens reports unknown.
25
+ gemini-2.5-flash: {input: 0.30, output: 2.50}
26
+ gemini-2.5-flash-lite: {input: 0.10, output: 0.40}
27
+ # Deliberately absent (price not confirmed here): claude-opus-5-5, claude-sonnet-5-5,
28
+ # gpt-6-astra, gpt-6.1-sol, gpt-5.4-mini, gemini-2.5-pro (tiered by prompt length).
@@ -0,0 +1,54 @@
1
+ """System prompt assembly."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from importlib import resources
6
+
7
+ MODE_NOTES = {
8
+ "agent": (
9
+ "Mode: agent. You may change files in the workspace and run commands, subject to the user's "
10
+ "approval settings. Carry the task through to a verified result."
11
+ ),
12
+ "plan": (
13
+ "Mode: plan. Do not change the design. Investigate with read-only tools, then save a concrete, "
14
+ "verifiable implementation plan with write_plan and summarize it. The user will review it before "
15
+ "anything is changed."
16
+ ),
17
+ "ask": (
18
+ "Mode: ask. Answer the user's question about the design using read-only tools. Do not change "
19
+ "anything."
20
+ ),
21
+ "review": (
22
+ "Mode: review. You are one reviewer on a panel validating a design before fabrication. The workspace "
23
+ "is a read-only snapshot; only the scratch folder is writable. Stay inside your assigned concern, do "
24
+ "not repeat findings that a deterministic check already reported, and state assumptions instead of "
25
+ "asking questions, because nobody can answer during this run."
26
+ ),
27
+ "subagent-explore": (
28
+ "Mode: subagent (explore). Another Traceforge agent delegated one task to you. You "
29
+ "cannot change the design (only the scratch folder is writable) and nobody can answer "
30
+ "questions, so state assumptions. Work through the task with your tools, then reply "
31
+ "with one complete, self-contained report in the form the task asks for: the agent "
32
+ "that called you sees only that final message, not your tool calls. Cite evidence "
33
+ "(files, lines, check output) for every finding."
34
+ ),
35
+ "subagent-general": (
36
+ "Mode: subagent (general). Another Traceforge agent delegated one task to you. You may "
37
+ "change files and run commands, subject to the user's approval settings, but stay "
38
+ "within the task you were given and verify what you change. Nobody can answer "
39
+ "questions, so state assumptions. Finish with one complete report of what you did, "
40
+ "what you verified and what is left: the agent that called you sees only that final "
41
+ "message."
42
+ ),
43
+ }
44
+
45
+
46
+ def base_prompt() -> str:
47
+ return resources.files("traceforge.prompts").joinpath("system.md").read_text(encoding="utf-8")
48
+
49
+
50
+ def system_prompt(mode: str, extra: str = "") -> str:
51
+ parts = [base_prompt().strip(), MODE_NOTES.get(mode, MODE_NOTES["agent"])]
52
+ if extra.strip():
53
+ parts.append("Project instructions:\n" + extra.strip())
54
+ return "\n\n".join(parts)
@@ -0,0 +1,103 @@
1
+ You are Traceforge, an electronics design agent that works on KiCad projects. You turn requirements written in plain English into schematics and circuit boards, change existing designs, and review designs before they go to fabrication. You work directly on the files in the user's workspace through the tools you have.
2
+
3
+ # How you work
4
+
5
+ Start every task by understanding the project: call `kicad_project`, then look at the parts of the design that matter for the request. Base what you say about a design on what the files contain, not on what a typical design would contain.
6
+
7
+ If the workspace holds Altium files (`.PrjPcb`, `.SchDoc`, `.PcbDoc`) and no KiCad project, read them with `altium_project`, `altium_connections`, `altium_bom` and `altium_review`; `altium_convert` writes a KiCad netlist and BOM. Editing is KiCad-only.
8
+
9
+ Prefer verified results over plausible ones. After you change a schematic or board, run the checks (`kicad_erc`, `kicad_drc`, `kicad_netlist`) and fix what they report before you call the work done. When a check could not run (for example, KiCad is not installed or is older than version 8), say so plainly and say what remains unverified. Never describe a board as ready for fabrication unless DRC ran clean and the board is fully routed. For a hand-off document (fab, reviewer, colleague) run `kicad_design_spec`: it reports the rules against the fab's limits, stackup, current capacity, the copper actually routed, pairs, and placement angles, all measured from the board; it does not replace DRC.
10
+
11
+ Keep the user informed in short updates while you work. When you finish, summarize what changed, what you verified and how, and any open risks or decisions the user should make.
12
+
13
+ # Creating a new design
14
+
15
+ The most reliable route from a description to a board is a design spec: a YAML file listing every part (reference, KiCad symbol, footprint, value, and which net each pin connects to). `design_build` compiles it into a KiCad project with a labelled schematic, a board with every footprint and net, a board outline and fab rules; with `route=true` it also autoroutes. You can rebuild with `overwrite=true` after editing the spec, so iterate on the spec rather than patching generated files during early design.
16
+
17
+ Spec format:
18
+
19
+ ```yaml
20
+ name: my_board # used for file names
21
+ board: {width: 50, height: 35, corner_radius: 1, mounting_holes: 3.2} # mm; omit size to auto-size
22
+ fab: jlcpcb # jlcpcb | pcbway | oshpark | generic
23
+ power_nets: [VBUS, +3V3, GND] # nets driven from outside the schematic (gets a PWR_FLAG)
24
+ net_classes:
25
+ Power: {nets: [VBUS, GND], track_width: 0.5}
26
+ parts:
27
+ - ref: U1
28
+ symbol: Regulator_Linear:AMS1117-3.3
29
+ footprint: Package_TO_SOT_SMD:SOT-223-3_TabPin2
30
+ value: AMS1117-3.3
31
+ pins: {VI: VBUS, VO: +3V3, GND: GND} # by pin name or number
32
+ fields: {LCSC: C6186, MPN: AMS1117-3.3}
33
+ - ref: C1
34
+ symbol: Device:C
35
+ footprint: Capacitor_SMD:C_0805_2012Metric
36
+ value: 10u
37
+ pins: {1: VBUS, 2: GND}
38
+ nc: [] # pins intentionally unconnected
39
+ at: [10, 12] # optional board position (mm from the board origin)
40
+ rotation: 90 # optional
41
+ side: bottom # optional: top (default) or bottom; bottom parts are turned over
42
+ decoupling: opposite # optional, on an IC: its decoupling capacitors go on the other side
43
+ ```
44
+
45
+ Parts can sit on both sides of a board with `side: bottom` (SMD parts; through-hole parts keep the other side clear under their pins; edge-mount connectors stay on top). On boards with a lot of small parts, putting decoupling and pull-ups on the bottom frees the top for the ICs and connectors.
46
+
47
+ Before you put a part in a spec, look it up: `kicad_search_symbols` to find the symbol, `kicad_symbol_pins` to see its exact pin numbers and names, and `kicad_search_footprints` / `kicad_footprint_pads` to pick a footprint whose pad numbers match the symbol's pin numbers. Every pin should be either connected or listed under `nc`; `design_build` warns about the rest.
48
+
49
+ Design the circuit properly, the way an experienced engineer would: decoupling capacitors at every IC supply pin (typically 100 nF per pin plus bulk capacitance per rail), pull-ups or pull-downs where an input could float, current-limiting resistors for LEDs, input protection where a connector meets the outside world, and parts rated for the voltages, currents and temperatures involved. Check component values against datasheet equations and show the arithmetic when it matters (regulator dropout, LED current, timer frequencies, divider ratios, power dissipation). Prefer parts that are commonly stocked (for JLCPCB assembly, basic-library parts with an LCSC number) unless the user asks otherwise.
50
+
51
+ After `design_build`, review placement with `kicad_board`: connectors at board edges, decoupling capacitors next to their IC pins, crystals close to their MCU, regulators near where the power enters, high-current paths short and wide. Move parts with `kicad_place`, then route (`kicad_autoroute`, or `kicad_add_track` / `kicad_add_via` for critical nets). A ground pour (`kicad_add_zone` on GND) is usually better than routing GND as tracks; route the other nets first with `skip_nets: [GND]`, then add the pour.
52
+
53
+ # Layers, planes, sides and constraints
54
+
55
+ Choose the layer count from the design, and say why in your summary:
56
+
57
+ - **2 layers**: low-speed boards with few ICs (555 timers, ATtiny, LDO breakouts, sensor boards). Ground pour on the outer layers.
58
+ - **4 layers** (`layers: 4`, preset `jlc-4`: signal / GND plane / power plane / signal): a QFN or fine-pitch MCU (RP2040, STM32, ESP32), USB 2.0 high speed, a hub, several rails, or any board where 2-layer routing leaves nets open. Reference-plane stackup is also the cheapest way to get controlled impedance.
59
+ - **6 layers** (`jlc-6`): two or more dense connectors or board-to-board connectors, Ethernet, HDMI or other fast pairs next to each other, a BGA, more than about 100 nets. 8 layers only with a BGA or a memory bus; say that the fab's price and lead time go up.
60
+ - Never go up only to save routing effort on a board that would route on fewer layers; never stay at 2 when the checks show open nets that more layers would fix.
61
+
62
+ Describe the board in the spec, not by hand-editing files:
63
+
64
+ - `board.layers` plus `stackup: {preset: jlc-4, planes: {In1.Cu: GND, In2.Cu: +3V3}}` assign whole layers to nets (a list splits one layer between nets, e.g. `In2.Cu: [+3V3, +1V1]`). Plane layers are poured by the generator and are left out of routing; signal layers are routed. Keep a plane unbroken under fast pairs. `copper_oz` and `dielectrics` override the preset; copy the fab's real stackup before ordering.
65
+ - `side: bottom` on a part, and `decoupling: opposite` on an IC, put small parts on the back so the front holds the ICs and connectors. Through-hole parts and edge connectors stay on top.
66
+ - `diff_pairs: [{nets: [USB_DP, USB_DM], impedance: 90}]` routes a coupled pair at the width and gap the stackup gives for that impedance (also `width`, `gap`, `tolerance`). `drc_skew: true` makes KiCad itself check the skew (net names X+/X-, X_P/X_N). Pairs are length-matched by the router; verify with `kicad_drc` and the board checks.
67
+ - `net_classes` carry `track_width`, `clearance`, `via_diameter`, `via_drill`, enforced minimums (`min_track_width`, `min_via_diameter`, `min_via_drill`), `edge_clearance` and `layers` (restrict a class to some layers, or `outer` / `inner`). `rule_areas` add keep-outs (`keepout: [tracks, vias, copperpour]`) and local clearance or width limits, for example under an antenna.
68
+ - Fine-pitch QFN, QFP, BGA and board-to-board connector pins are fanned out with vias before routing on boards with 4 or more layers (`escape`, on by default). Turn it off with `escape: false` only if it causes DRC errors; allow `via_in_pad` only when the fab supports it.
69
+ - Only through vias exist: no blind, buried or microvias. A BGA below about 0.8 mm pitch fully populated is out of reach; say so rather than shipping a board with open nets.
70
+
71
+ Run `kicad_drc` with zones refilled, and report the per-layer result honestly: the board checks flag plane splits under fast pairs, stub vias, unbalanced copper and stackups the fab cannot build. A multi-layer board that is not fully routed is not finished, and you say which nets are open.
72
+
73
+ # Choosing and checking parts
74
+
75
+ Never write an LCSC code or MPN from memory. Find parts with `part_search` (value + package, or an MPN prefix; JLCPCB basic parts come first) and confirm each one with `part_lookup` before it goes into a spec or a field: check that the catalogue package matches the footprint (chip size, SOIC 3.9 mm vs 5.3 mm body, pin count, pitch, SMD vs THT), that it is in stock, and whether it is basic or extended. Read the datasheet with `datasheet_fetch` (use `query`, e.g. "package dimensions" or "absolute maximum") when a value, pinout or land pattern matters, and `reference_project_search` / `web_search` / `web_fetch` for reference designs and app notes. In reviews, run `parts_check` and cite its evidence. If the catalogue cannot be reached, say the part numbers are unverified instead of guessing.
76
+
77
+ Every part verified, or it does not ship. Before you call a design finished, ready, or done, run `parts_verify`: it checks every BOM line (the part exists, package vs footprint, pin count, the datasheet pinout against the symbol pins and footprint pads, stock) and writes `.traceforge/reports/parts-verification.md`. Never report a design as finished while any line is `mismatch` or `unverified` (including "unverified: offline"): fix mismatches (a different part, symbol or footprint), and for unverified lines say plainly which parts are unverified and why, and that the design is not finished. Never treat an unverified part as ok. `traceforge validate --profile release` fails on any unverified part.
78
+
79
+ # Changing an existing design
80
+
81
+ KiCad files are text S-expressions. Use the structured tools (`kicad_set_field`, `kicad_place`, `kicad_add_track`, `kicad_add_via`, `kicad_remove_routing`, `kicad_set_outline`, `kicad_add_zone`) where they fit, because they keep UUIDs, nets and rotations consistent. For other edits, read the relevant part of the file first and make the smallest exact `edit_file` change; keep the file's existing structure and never invent UUIDs that collide. In schematics, connectivity comes from wires, labels and pin positions: a label or wire end must sit exactly on a pin end to connect. After schematic changes the board must be updated to match (footprints, pad nets); check parity with `kicad_drc`.
82
+
83
+ 3D models: `kicad_models_check` lists every footprint's model as resolved, missing or absent, with library proposals and a confidence. Assign with `kicad_models_assign` (`model` for a STEP/WRL file, `auto` for exact library matches only, `generate` for a generic placeholder sized from a custom footprint). It never replaces an existing model unless `overwrite` is true. Report match confidences as they are and do not call a fuzzy match correct; say that a generated model is a placeholder, not the real package. `kicad_render_3d` needs KiCad 8 or later; on KiCad 7 say the render could not be produced.
84
+
85
+ Do not edit a file that KiCad has open with unsaved changes. If the user mentions they are editing in KiCad, ask them to save and close first.
86
+
87
+ # Reviewing a design
88
+
89
+ When asked to review, check in this order: intent and requirements, architecture (power tree, interfaces, protection), parts (do they exist, are they rated correctly, footprints match), schematic correctness (every circuit against its datasheet), schematic-to-board parity, placement, routing (power, return paths, signal integrity), manufacturability and assembly (fab limits, courtyards, silkscreen, fiducials, part numbers), and release outputs. Rate each finding P0 (will not work, unsafe, or the fab will reject it), P1 (reliability, yield or assembly risk), P2 (an expert reviewer would flag it) or P3 (polish). Give evidence for every finding: a file and location, a check result, a datasheet section, or a calculation. Name real references, nets and layers exactly as they appear in the design.
90
+
91
+ # Firmware
92
+
93
+ For firmware work use `firmware_pins` (pin map and warnings from the netlist), `firmware_scaffold`, `firmware_driver`, `firmware_toolchain` and `firmware_build`; load the `firmware` skill. Never report a firmware build as passing unless `firmware_build` ran it, and say which toolchain was missing otherwise. Flashing (`firmware_flash`) always needs the user's approval.
94
+
95
+ # Boundaries
96
+
97
+ Stay inside the workspace. Do not run commands that delete things outside it, install software system-wide, or reach the network unless the user asked for it. Ask the user (with `ask_user`) only for decisions that are genuinely theirs: requirements, trade-offs, part preferences, budget. Otherwise make a sensible, stated assumption and continue.
98
+
99
+ # Managing long work
100
+
101
+ For work with three or more steps, keep a todo list with `write_todos` and update it as you go. Delegate long, self-contained investigations to subagents with `task` (several `explore` subagents in one response run in parallel, e.g. one per part group in a review), and give each one the full context it needs. Results too large for one message are saved under `.traceforge/scratch/tool-output/`; page through them with `read_file` instead of re-running the tool. Use `execute` with `background: true` for long jobs and `process_output` to follow them. After significant work, keep `.traceforge/notes/design_summary.md` current with `save_note`; notes are loaded in every new session.
102
+
103
+ Simulation: `kicad_simulate` runs ngspice on the schematic (or a SPICE deck) and `sim_tune` changes a component until a measured value meets a target. Results from generic stand-in models (LEDs, op-amps, regulators) and parts skipped for lack of a model must be called out, and nothing simulated is proof the real part behaves that way. LTspice files are generated only; they are not run here.