devcouncil 0.1.1 → 0.3.0

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 (159) hide show
  1. package/README.md +201 -6
  2. package/package.json +9 -2
  3. package/pyproject.toml +34 -2
  4. package/src/devcouncil/app/config.py +348 -12
  5. package/src/devcouncil/app/orchestrator.py +10 -6
  6. package/src/devcouncil/app/state_machine.py +4 -0
  7. package/src/devcouncil/artifacts/graph.py +32 -5
  8. package/src/devcouncil/assets/__init__.py +1 -0
  9. package/src/devcouncil/assets/devcouncil-logo.svg +60 -0
  10. package/src/devcouncil/assets/devcouncil_logo_premium.png +0 -0
  11. package/src/devcouncil/cli/commands/agents.py +292 -0
  12. package/src/devcouncil/cli/commands/artifacts.py +6 -3
  13. package/src/devcouncil/cli/commands/check.py +220 -0
  14. package/src/devcouncil/cli/commands/config.py +43 -4
  15. package/src/devcouncil/cli/commands/cost.py +57 -0
  16. package/src/devcouncil/cli/commands/dashboard.py +6 -1
  17. package/src/devcouncil/cli/commands/design.py +186 -0
  18. package/src/devcouncil/cli/commands/doctor.py +379 -22
  19. package/src/devcouncil/cli/commands/evidence.py +48 -0
  20. package/src/devcouncil/cli/commands/go.py +532 -33
  21. package/src/devcouncil/cli/commands/handoff.py +69 -0
  22. package/src/devcouncil/cli/commands/hook.py +296 -15
  23. package/src/devcouncil/cli/commands/init.py +161 -20
  24. package/src/devcouncil/cli/commands/integrate.py +1371 -124
  25. package/src/devcouncil/cli/commands/logs.py +106 -0
  26. package/src/devcouncil/cli/commands/map.py +80 -10
  27. package/src/devcouncil/cli/commands/okf.py +245 -0
  28. package/src/devcouncil/cli/commands/plan.py +256 -55
  29. package/src/devcouncil/cli/commands/prompt.py +18 -7
  30. package/src/devcouncil/cli/commands/repair.py +50 -24
  31. package/src/devcouncil/cli/commands/report.py +8 -0
  32. package/src/devcouncil/cli/commands/reset_demo_state.py +4 -2
  33. package/src/devcouncil/cli/commands/rollback.py +27 -28
  34. package/src/devcouncil/cli/commands/run.py +195 -54
  35. package/src/devcouncil/cli/commands/runs.py +223 -0
  36. package/src/devcouncil/cli/commands/scaffold.py +32 -0
  37. package/src/devcouncil/cli/commands/semantic.py +47 -0
  38. package/src/devcouncil/cli/commands/setup.py +145 -6
  39. package/src/devcouncil/cli/commands/shell.py +73 -0
  40. package/src/devcouncil/cli/commands/skills.py +267 -0
  41. package/src/devcouncil/cli/commands/status.py +30 -15
  42. package/src/devcouncil/cli/commands/trace.py +47 -3
  43. package/src/devcouncil/cli/commands/verify.py +144 -3
  44. package/src/devcouncil/cli/commands/watch.py +32 -12
  45. package/src/devcouncil/cli/commands/watch_fs.py +40 -0
  46. package/src/devcouncil/cli/main.py +91 -7
  47. package/src/devcouncil/domain/evidence.py +29 -2
  48. package/src/devcouncil/domain/gap.py +27 -1
  49. package/src/devcouncil/domain/task.py +31 -2
  50. package/src/devcouncil/execution/checkpoints.py +256 -0
  51. package/src/devcouncil/execution/context_builder.py +1 -1
  52. package/src/devcouncil/execution/fs_watcher.py +205 -0
  53. package/src/devcouncil/execution/handoff.py +102 -0
  54. package/src/devcouncil/execution/hook_policy.py +162 -74
  55. package/src/devcouncil/execution/patch.py +65 -10
  56. package/src/devcouncil/execution/permissions.py +24 -24
  57. package/src/devcouncil/execution/policy_engine.py +350 -0
  58. package/src/devcouncil/execution/prompt_builder.py +751 -23
  59. package/src/devcouncil/execution/shell_session.py +231 -0
  60. package/src/devcouncil/execution/task_runner.py +24 -9
  61. package/src/devcouncil/executors/agent_registry.py +596 -0
  62. package/src/devcouncil/executors/coding_cli.py +791 -39
  63. package/src/devcouncil/executors/mini_swe.py +6 -0
  64. package/src/devcouncil/executors/native/agent.py +135 -19
  65. package/src/devcouncil/executors/openhands.py +6 -0
  66. package/src/devcouncil/gating/checks/clean_git.py +3 -1
  67. package/src/devcouncil/gating/checks/secret_scan_check.py +47 -21
  68. package/src/devcouncil/gating/policy.py +190 -11
  69. package/src/devcouncil/hardware.py +184 -0
  70. package/src/devcouncil/indexing/ast_matcher.py +17 -7
  71. package/src/devcouncil/indexing/lsp.py +45 -4
  72. package/src/devcouncil/indexing/repo_mapper.py +1284 -15
  73. package/src/devcouncil/indexing/semantic_index.py +221 -0
  74. package/src/devcouncil/integrations/actions.py +166 -0
  75. package/src/devcouncil/integrations/check.py +426 -0
  76. package/src/devcouncil/integrations/claude_assets.py +444 -0
  77. package/src/devcouncil/integrations/code_review_graph.py +13 -2
  78. package/src/devcouncil/integrations/github_intent.py +149 -0
  79. package/src/devcouncil/integrations/gitnexus.py +45 -2
  80. package/src/devcouncil/integrations/mcp/server.py +1944 -32
  81. package/src/devcouncil/integrations/opencode_devcouncil_plugin.mjs +24 -0
  82. package/src/devcouncil/integrations/pr_comments.py +9 -0
  83. package/src/devcouncil/knowledge/__init__.py +23 -0
  84. package/src/devcouncil/knowledge/design.py +374 -0
  85. package/src/devcouncil/knowledge/design_conformance.py +317 -0
  86. package/src/devcouncil/knowledge/fetch.py +223 -0
  87. package/src/devcouncil/knowledge/frontmatter.py +51 -0
  88. package/src/devcouncil/knowledge/okf.py +202 -0
  89. package/src/devcouncil/knowledge/skill_bridge.py +96 -0
  90. package/src/devcouncil/knowledge/sources.py +239 -0
  91. package/src/devcouncil/live/cards.py +181 -25
  92. package/src/devcouncil/live/repair_prompt.py +29 -6
  93. package/src/devcouncil/live/reviewer.py +72 -13
  94. package/src/devcouncil/live/signals.py +2 -2
  95. package/src/devcouncil/live/summary.py +18 -8
  96. package/src/devcouncil/live/transcripts.py +47 -11
  97. package/src/devcouncil/llm/cache.py +20 -8
  98. package/src/devcouncil/llm/model_defaults.yaml +44 -0
  99. package/src/devcouncil/llm/provider.py +617 -49
  100. package/src/devcouncil/llm/router.py +337 -53
  101. package/src/devcouncil/optimization/__init__.py +1 -0
  102. package/src/devcouncil/optimization/gepa_agent.py +318 -0
  103. package/src/devcouncil/optimization/skillopt.py +673 -0
  104. package/src/devcouncil/planning/arbiter_service.py +10 -2
  105. package/src/devcouncil/planning/correction_manifest.py +346 -0
  106. package/src/devcouncil/planning/critique_service.py +16 -4
  107. package/src/devcouncil/planning/plan_service.py +86 -6
  108. package/src/devcouncil/planning/prompt_enhancer_service.py +206 -1
  109. package/src/devcouncil/planning/repair_service.py +8 -2
  110. package/src/devcouncil/planning/spec_service.py +37 -3
  111. package/src/devcouncil/repo/ci_scaffold.py +165 -0
  112. package/src/devcouncil/repo/gitignore.py +123 -0
  113. package/src/devcouncil/repo/sca.py +384 -0
  114. package/src/devcouncil/reporting/json_report.py +22 -1
  115. package/src/devcouncil/reporting/markdown_report.py +29 -1
  116. package/src/devcouncil/reporting/okf_bundle_writer.py +364 -0
  117. package/src/devcouncil/reporting/okf_html.py +323 -0
  118. package/src/devcouncil/reporting/report_builder.py +18 -1
  119. package/src/devcouncil/skills/__init__.py +19 -0
  120. package/src/devcouncil/skills/library/README.md +46 -0
  121. package/src/devcouncil/skills/library/ai-training.md +50 -0
  122. package/src/devcouncil/skills/library/android.md +50 -0
  123. package/src/devcouncil/skills/library/backend.md +52 -0
  124. package/src/devcouncil/skills/library/core-engineering.md +95 -0
  125. package/src/devcouncil/skills/library/data-engineering.md +47 -0
  126. package/src/devcouncil/skills/library/desktop.md +46 -0
  127. package/src/devcouncil/skills/library/devops.md +48 -0
  128. package/src/devcouncil/skills/library/game-dev.md +46 -0
  129. package/src/devcouncil/skills/library/ios.md +48 -0
  130. package/src/devcouncil/skills/library/mobile-cross-platform.md +46 -0
  131. package/src/devcouncil/skills/library/security.md +48 -0
  132. package/src/devcouncil/skills/library/systems.md +48 -0
  133. package/src/devcouncil/skills/library/web.md +47 -0
  134. package/src/devcouncil/skills/library/windows.md +47 -0
  135. package/src/devcouncil/skills/registry.py +408 -0
  136. package/src/devcouncil/storage/db.py +140 -3
  137. package/src/devcouncil/storage/models.py +125 -0
  138. package/src/devcouncil/storage/native.py +559 -0
  139. package/src/devcouncil/storage/repositories.py +157 -78
  140. package/src/devcouncil/telemetry/cost.py +123 -17
  141. package/src/devcouncil/telemetry/logging_setup.py +244 -0
  142. package/src/devcouncil/telemetry/model_pricing.yaml +48 -0
  143. package/src/devcouncil/telemetry/pricing.py +28 -0
  144. package/src/devcouncil/telemetry/stages.py +141 -0
  145. package/src/devcouncil/telemetry/traces.py +62 -7
  146. package/src/devcouncil/telemetry/tracker.py +24 -10
  147. package/src/devcouncil/ui/dashboard.py +393 -28
  148. package/src/devcouncil/utils/redaction.py +9 -3
  149. package/src/devcouncil/utils/subprocess_env.py +69 -0
  150. package/src/devcouncil/verification/acceptance_compiler.py +253 -0
  151. package/src/devcouncil/verification/ad_hoc_check.py +135 -0
  152. package/src/devcouncil/verification/diff_coverage.py +353 -0
  153. package/src/devcouncil/verification/implementation_reviewer.py +11 -2
  154. package/src/devcouncil/verification/next_actions.py +189 -0
  155. package/src/devcouncil/verification/sandbox.py +181 -0
  156. package/src/devcouncil/verification/test_resolver.py +91 -0
  157. package/src/devcouncil/verification/verifier.py +1549 -143
  158. package/uv.lock +205 -64
  159. package/src/devcouncil/indexing/symbol_index.py +0 -0
@@ -0,0 +1,231 @@
1
+ """Guarded shell command session for DevCouncil tasks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import os
7
+ import shlex
8
+ import shutil
9
+ import subprocess
10
+ import uuid
11
+ from pathlib import Path
12
+
13
+ from rich.console import Console
14
+
15
+ from devcouncil.domain.evidence import CommandResult
16
+ from devcouncil.domain.task import Task
17
+ from devcouncil.execution.checkpoints import CheckpointService
18
+ from devcouncil.execution.hook_policy import HookPolicy
19
+ from devcouncil.storage.db import get_db
20
+ from devcouncil.storage.native import ShellCommandRepository, ShellSessionRepository, TaskLeaseRepository
21
+ from devcouncil.storage.repositories import EvidenceRepository, TaskRepository
22
+ from devcouncil.telemetry.traces import TraceLogger
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ class ShellBackend:
28
+ def run_command(self, command: str, cwd: Path, env: dict[str, str] | None = None) -> subprocess.CompletedProcess[str]:
29
+ raise NotImplementedError
30
+
31
+
32
+ class CommandLoopBackend(ShellBackend):
33
+ def run_command(self, command: str, cwd: Path, env: dict[str, str] | None = None) -> subprocess.CompletedProcess[str]:
34
+ args = shlex.split(command, posix=(os.name != "nt"))
35
+ return subprocess.run(
36
+ args,
37
+ cwd=cwd,
38
+ capture_output=True,
39
+ text=True,
40
+ encoding="utf-8",
41
+ errors="replace",
42
+ env=env,
43
+ )
44
+
45
+
46
+ class ShellWrappedBackend(ShellBackend):
47
+ """Runs each command through an explicit shell (pwsh/bash/zsh/...)."""
48
+
49
+ _LAUNCHERS = {
50
+ "pwsh": ["pwsh", "-NoProfile", "-Command"],
51
+ "powershell": ["powershell", "-NoProfile", "-Command"],
52
+ "bash": ["bash", "-lc"],
53
+ "zsh": ["zsh", "-lc"],
54
+ "sh": ["sh", "-lc"],
55
+ }
56
+
57
+ def __init__(self, shell: str):
58
+ launcher = self._LAUNCHERS.get(shell)
59
+ if launcher is None:
60
+ supported = ", ".join(sorted(self._LAUNCHERS))
61
+ raise ValueError(f"Unknown shell backend '{shell}'. Use auto or one of: {supported}.")
62
+ if not shutil.which(launcher[0]):
63
+ raise ValueError(f"Shell '{launcher[0]}' is not installed or not on PATH.")
64
+ self.launcher = launcher
65
+
66
+ def run_command(self, command: str, cwd: Path, env: dict[str, str] | None = None) -> subprocess.CompletedProcess[str]:
67
+ return subprocess.run(
68
+ [*self.launcher, command],
69
+ cwd=cwd,
70
+ capture_output=True,
71
+ text=True,
72
+ encoding="utf-8",
73
+ errors="replace",
74
+ env=env,
75
+ )
76
+
77
+
78
+ _EVIDENCE_COMMAND_HINTS = ("pytest", "ruff", "mypy", "npm test", "npm run lint", "npm run typecheck")
79
+
80
+ console = Console()
81
+
82
+
83
+ class GuardedShellSession:
84
+ def __init__(self, project_root: Path, task: Task, *, shell: str = "auto"):
85
+ self.project_root = project_root.resolve()
86
+ self.task = task
87
+ self.shell = shell
88
+ # HookPolicy (not the bare TaskPolicyEngine) so a chained command is split into
89
+ # its segments and EACH is allowlisted, plus git-safety denies (force-push,
90
+ # --no-verify, protected-branch reset). Critical for the `--shell bash/zsh`
91
+ # backend, which hands the whole string to a real shell that interprets
92
+ # `;`/`&&`/`|` — a single whole-string allowlist match would let an allowed
93
+ # prefix smuggle an arbitrary command past the gate.
94
+ self.policy = HookPolicy(self.project_root)
95
+ self.backend: ShellBackend = (
96
+ CommandLoopBackend() if shell in {"auto", "loop"} else ShellWrappedBackend(shell)
97
+ )
98
+ self.lease_token: str | None = None
99
+ self.session_id: str | None = None
100
+ self.log_dir = self.project_root / ".devcouncil" / "logs"
101
+ self.log_dir.mkdir(parents=True, exist_ok=True)
102
+
103
+ def start(self, *, force: bool = False) -> None:
104
+ db = get_db(self.project_root)
105
+ if not db:
106
+ raise RuntimeError("DevCouncil not initialized.")
107
+ with db.get_session() as session:
108
+ lease = TaskLeaseRepository(session).acquire(
109
+ self.task.id,
110
+ owner="dev shell",
111
+ agent=self.shell,
112
+ force=force,
113
+ )
114
+ self.lease_token = lease.lease_token
115
+ shell_session = ShellSessionRepository(session).start(
116
+ self.task.id,
117
+ self.shell,
118
+ str(self.project_root),
119
+ lease_id=lease.id,
120
+ )
121
+ self.session_id = shell_session.id
122
+ self.task.status = "running"
123
+ TaskRepository(session).save(self.task)
124
+ CheckpointService(self.project_root).create_before(self.task.id)
125
+
126
+ def finish(self) -> None:
127
+ db = get_db(self.project_root)
128
+ if not db or not self.lease_token:
129
+ return
130
+ with db.get_session() as session:
131
+ if self.session_id:
132
+ ShellSessionRepository(session).finish(self.session_id, "finished")
133
+ TaskLeaseRepository(session).release(self.task.id, self.lease_token)
134
+
135
+ def run_one(self, command: str) -> int:
136
+ normalized = " ".join(command.split())
137
+ decision = self.policy.evaluate_command(normalized, self.task)
138
+ log_id = uuid.uuid4().hex[:8]
139
+ stdout_path = self.log_dir / f"{self.task.id}-{log_id}.stdout.log"
140
+ stderr_path = self.log_dir / f"{self.task.id}-{log_id}.stderr.log"
141
+
142
+ if decision.action == "deny":
143
+ logger.warning("Shell command DENIED for %s: %s (%s)", self.task.id, normalized, decision.reason)
144
+ self._record_command(normalized, "denied", reason=decision.reason)
145
+ TraceLogger(self.project_root).log_event(
146
+ "shell_command_denied",
147
+ {"command": normalized, "reason": decision.reason},
148
+ task_id=self.task.id,
149
+ )
150
+ # Tell the user *why* — a silent non-zero exit is unactionable.
151
+ console.print(
152
+ f"[red]Command denied for {self.task.id}:[/red] {decision.reason or 'not permitted by task policy.'}"
153
+ )
154
+ console.print(
155
+ "[dim]Add it to the task's allowed_commands, or run it outside DevCouncil.[/dim]"
156
+ )
157
+ return 1
158
+
159
+ logger.info("Shell command for %s: %s", self.task.id, normalized)
160
+ try:
161
+ result = self.backend.run_command(normalized, self.project_root)
162
+ except (NotImplementedError, FileNotFoundError, OSError) as exc:
163
+ logger.warning("Shell command could not run for %s: %s (%s)", self.task.id, normalized, exc)
164
+ self._record_command(normalized, "denied", reason=str(exc))
165
+ console.print(f"[red]Could not run '{normalized}':[/red] {exc}")
166
+ return 1
167
+
168
+ stdout_path.write_text(result.stdout or "", encoding="utf-8")
169
+ stderr_path.write_text(result.stderr or "", encoding="utf-8")
170
+ # Echo the command output so the guarded shell is actually usable.
171
+ if result.stdout:
172
+ console.print(result.stdout, end="", markup=False, highlight=False)
173
+ if result.stderr:
174
+ console.print(result.stderr, end="", markup=False, highlight=False, style="dim")
175
+ status = "finished" if result.returncode == 0 else "failed"
176
+ self._record_command(
177
+ normalized,
178
+ status,
179
+ exit_code=result.returncode,
180
+ stdout_path=str(stdout_path),
181
+ stderr_path=str(stderr_path),
182
+ )
183
+ if any(hint in normalized for hint in _EVIDENCE_COMMAND_HINTS):
184
+ self._save_command_evidence(normalized, result.returncode, result.stdout or result.stderr or "")
185
+ TraceLogger(self.project_root).log_event(
186
+ "shell_command_finished",
187
+ {"command": normalized, "exit_code": result.returncode},
188
+ task_id=self.task.id,
189
+ )
190
+ return result.returncode
191
+
192
+ def _record_command(
193
+ self,
194
+ command: str,
195
+ status: str,
196
+ *,
197
+ exit_code: int | None = None,
198
+ reason: str = "",
199
+ stdout_path: str = "",
200
+ stderr_path: str = "",
201
+ ) -> None:
202
+ db = get_db(self.project_root)
203
+ if not db:
204
+ return
205
+ with db.get_session() as session:
206
+ ShellCommandRepository(session).record(
207
+ self.task.id,
208
+ command,
209
+ status,
210
+ session_id=self.session_id,
211
+ exit_code=exit_code,
212
+ reason=reason,
213
+ stdout_path=stdout_path,
214
+ stderr_path=stderr_path,
215
+ )
216
+
217
+ def _save_command_evidence(self, command: str, exit_code: int, summary: str) -> None:
218
+ db = get_db(self.project_root)
219
+ if not db:
220
+ return
221
+ with db.get_session() as session:
222
+ EvidenceRepository(session).save_command_result(
223
+ self.task.id,
224
+ CommandResult(
225
+ command=command,
226
+ exit_code=exit_code,
227
+ stdout_path="",
228
+ stderr_path="",
229
+ summary=summary[:500],
230
+ ),
231
+ )
@@ -1,11 +1,15 @@
1
1
  import hashlib
2
+ import os
2
3
  import subprocess
3
4
  import logging
4
5
  import re
5
6
  import shlex
6
7
  from pathlib import Path
7
- from typing import Literal, Optional
8
+ from typing import Literal, Optional, TYPE_CHECKING
8
9
  from devcouncil.domain.task import Task
10
+
11
+ if TYPE_CHECKING:
12
+ from devcouncil.app.config import DevCouncilConfig
9
13
  from devcouncil.execution.permissions import PermissionManager
10
14
  from devcouncil.domain.evidence import CommandResult
11
15
  from devcouncil.app.errors import ExecutionError
@@ -24,6 +28,16 @@ class TaskRunner:
24
28
  self.project_root = project_root
25
29
  self.permissions = permission_manager
26
30
  self.patch_engine = PatchEngine(project_root)
31
+ # Load config and build the trace logger once: run_command previously
32
+ # re-parsed config.yaml on every command, and each operation rebuilt a
33
+ # TraceLogger. Both are reusable for the lifetime of the runner.
34
+ self.config: "Optional[DevCouncilConfig]"
35
+ try:
36
+ from devcouncil.app.config import load_config
37
+ self.config = load_config(project_root)
38
+ except Exception:
39
+ self.config = None
40
+ self.tracer = TraceLogger(project_root)
27
41
 
28
42
  def _validate_path_within_root(self, path: str) -> None:
29
43
  """Ensure a path resolves to a location within the project root."""
@@ -36,7 +50,7 @@ class TaskRunner:
36
50
  self._validate_path_within_root(path)
37
51
  self.permissions.validate_action("file_write", path, task, operation=operation)
38
52
  applied = self.patch_engine.apply_patch(patch)
39
- TraceLogger(self.project_root).log_event(
53
+ self.tracer.log_event(
40
54
  "tool_patch_applied",
41
55
  {"paths": sorted(changes), "success": applied},
42
56
  task_id=task.id,
@@ -106,15 +120,16 @@ class TaskRunner:
106
120
  logger.info(f"Executing authorized command: {command}")
107
121
 
108
122
  try:
109
- from devcouncil.app.config import load_config
110
- config = load_config(self.project_root)
111
- timeout = config.execution.command_timeout
123
+ timeout = self.config.execution.command_timeout if self.config is not None else 300
112
124
  except Exception:
113
125
  timeout = 300
114
126
 
115
127
  try:
116
128
  result = subprocess.run(
117
- shlex.split(command, posix=False),
129
+ # POSIX-correct tokenization on POSIX hosts (so quoted args like
130
+ # `pytest -k "a and b"` split right and match the allowlist), Windows
131
+ # rules on Windows. Matches shell_session.py.
132
+ shlex.split(command, posix=(os.name != "nt")),
118
133
  shell=False,
119
134
  capture_output=True,
120
135
  text=True,
@@ -129,7 +144,7 @@ class TaskRunner:
129
144
  stderr_path = self._save_command_log(task.id, command, "stderr", stderr)
130
145
  stdout_summary = redact_string(stdout[-500:])
131
146
  stderr_summary = redact_string(stderr[-500:])
132
- TraceLogger(self.project_root).log_event(
147
+ self.tracer.log_event(
133
148
  "command_executed",
134
149
  {"command": command, "exit_code": result.returncode},
135
150
  task_id=task.id,
@@ -149,14 +164,14 @@ class TaskRunner:
149
164
  """Write content to a file if allowed by permissions."""
150
165
  self._validate_path_within_root(path)
151
166
  full_path = resolve_project_path(self.project_root, path)
152
- operation = "modify" if full_path.exists() else "create"
167
+ operation: Literal["create", "modify"] = "modify" if full_path.exists() else "create"
153
168
  self.permissions.validate_action("file_write", path, task, operation=operation)
154
169
 
155
170
  logger.info(f"Writing authorized file: {path}")
156
171
  try:
157
172
  full_path.parent.mkdir(parents=True, exist_ok=True)
158
173
  full_path.write_text(content, encoding="utf-8")
159
- TraceLogger(self.project_root).log_event(
174
+ self.tracer.log_event(
160
175
  "file_written",
161
176
  {"path": path},
162
177
  task_id=task.id,