misterdev 0.2.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 (136) hide show
  1. misterdev/__init__.py +3 -0
  2. misterdev/agent.py +2166 -0
  3. misterdev/agent_helpers.py +194 -0
  4. misterdev/analyzers/__init__.py +0 -0
  5. misterdev/analyzers/project_analyzer/__init__.py +246 -0
  6. misterdev/analyzers/project_analyzer/detection.py +146 -0
  7. misterdev/analyzers/project_analyzer/merge.py +137 -0
  8. misterdev/analyzers/project_analyzer/overview.py +312 -0
  9. misterdev/analyzers/project_analyzer/prompts.py +83 -0
  10. misterdev/cli.py +370 -0
  11. misterdev/config.py +521 -0
  12. misterdev/core/__init__.py +0 -0
  13. misterdev/core/audit.py +88 -0
  14. misterdev/core/config.py +10 -0
  15. misterdev/core/context/__init__.py +0 -0
  16. misterdev/core/context/change_tracker.py +218 -0
  17. misterdev/core/context/contracts/__init__.py +63 -0
  18. misterdev/core/context/contracts/_log.py +9 -0
  19. misterdev/core/context/contracts/_text.py +12 -0
  20. misterdev/core/context/contracts/extraction.py +30 -0
  21. misterdev/core/context/contracts/python_generic.py +42 -0
  22. misterdev/core/context/contracts/registry.py +127 -0
  23. misterdev/core/context/contracts/rust_line.py +225 -0
  24. misterdev/core/context/contracts/rust_tree_sitter.py +141 -0
  25. misterdev/core/context/lsp.py +174 -0
  26. misterdev/core/context/scratchpad.py +111 -0
  27. misterdev/core/context/topography/__init__.py +43 -0
  28. misterdev/core/context/topography/_log.py +10 -0
  29. misterdev/core/context/topography/cache.py +91 -0
  30. misterdev/core/context/topography/engine.py +172 -0
  31. misterdev/core/context/topography/graph.py +675 -0
  32. misterdev/core/context/topography/nodes.py +55 -0
  33. misterdev/core/context/topography/parsers.py +95 -0
  34. misterdev/core/context/topography/syntax.py +54 -0
  35. misterdev/core/economics/__init__.py +0 -0
  36. misterdev/core/economics/context_budget.py +175 -0
  37. misterdev/core/economics/embeddings.py +232 -0
  38. misterdev/core/economics/free_models.py +108 -0
  39. misterdev/core/economics/llm_cache.py +105 -0
  40. misterdev/core/economics/model_catalog.py +79 -0
  41. misterdev/core/economics/model_ledger.py +331 -0
  42. misterdev/core/economics/model_selector.py +281 -0
  43. misterdev/core/execution/__init__.py +0 -0
  44. misterdev/core/execution/bounded.py +50 -0
  45. misterdev/core/execution/container.py +221 -0
  46. misterdev/core/execution/error_classifier.py +366 -0
  47. misterdev/core/execution/error_resolver.py +201 -0
  48. misterdev/core/execution/governance.py +283 -0
  49. misterdev/core/execution/outcomes.py +50 -0
  50. misterdev/core/execution/progress.py +120 -0
  51. misterdev/core/execution/project.py +231 -0
  52. misterdev/core/execution/registry.py +97 -0
  53. misterdev/core/execution/runtime.py +279 -0
  54. misterdev/core/gitcmd.py +39 -0
  55. misterdev/core/integration/__init__.py +0 -0
  56. misterdev/core/integration/mcp.py +368 -0
  57. misterdev/core/integration/mcp_gather.py +186 -0
  58. misterdev/core/models.py +35 -0
  59. misterdev/core/modes.py +184 -0
  60. misterdev/core/planning/__init__.py +0 -0
  61. misterdev/core/planning/advisor.py +89 -0
  62. misterdev/core/planning/assessment.py +135 -0
  63. misterdev/core/planning/decomposer.py +387 -0
  64. misterdev/core/planning/metacognition.py +103 -0
  65. misterdev/core/planning/sovereign.py +308 -0
  66. misterdev/core/planning/targets.py +201 -0
  67. misterdev/core/reporting/__init__.py +0 -0
  68. misterdev/core/reporting/report.py +377 -0
  69. misterdev/core/reporting/report_view.py +151 -0
  70. misterdev/core/task.py +163 -0
  71. misterdev/core/verification/__init__.py +0 -0
  72. misterdev/core/verification/claim_verifier.py +210 -0
  73. misterdev/core/verification/critic.py +324 -0
  74. misterdev/core/verification/gatekeeper/__init__.py +631 -0
  75. misterdev/core/verification/gatekeeper/constants.py +138 -0
  76. misterdev/core/verification/gatekeeper/helpers.py +28 -0
  77. misterdev/core/verification/goal_check.py +219 -0
  78. misterdev/core/verification/independent.py +68 -0
  79. misterdev/core/verification/mutation_gate.py +221 -0
  80. misterdev/core/verification/preflight.py +95 -0
  81. misterdev/core/verification/spec_tests.py +175 -0
  82. misterdev/core/verification/validator.py +495 -0
  83. misterdev/core/verification/vision_verify.py +185 -0
  84. misterdev/core/verification/web_verify.py +408 -0
  85. misterdev/environments/__init__.py +0 -0
  86. misterdev/environments/base_env.py +18 -0
  87. misterdev/environments/container_env.py +87 -0
  88. misterdev/environments/venv_env.py +42 -0
  89. misterdev/llm/__init__.py +0 -0
  90. misterdev/llm/client/__init__.py +152 -0
  91. misterdev/llm/client/base.py +382 -0
  92. misterdev/llm/client/edits.py +70 -0
  93. misterdev/llm/client/embeddings.py +121 -0
  94. misterdev/llm/client/errors.py +134 -0
  95. misterdev/llm/client/providers.py +535 -0
  96. misterdev/llm/client/response.py +24 -0
  97. misterdev/llm/prompt_manager.py +82 -0
  98. misterdev/llm/responses/__init__.py +34 -0
  99. misterdev/llm/responses/apply.py +131 -0
  100. misterdev/llm/responses/json_extract.py +80 -0
  101. misterdev/llm/responses/models.py +43 -0
  102. misterdev/llm/responses/parsing.py +494 -0
  103. misterdev/logging_setup.py +20 -0
  104. misterdev/mcp_server.py +208 -0
  105. misterdev/nl_cli.py +149 -0
  106. misterdev/plugins.py +115 -0
  107. misterdev/py.typed +0 -0
  108. misterdev/task_executors/__init__.py +0 -0
  109. misterdev/task_executors/base_executor.py +10 -0
  110. misterdev/task_executors/markdown_plan_executor/__init__.py +90 -0
  111. misterdev/task_executors/markdown_plan_executor/commands_mixin.py +82 -0
  112. misterdev/task_executors/markdown_plan_executor/context_mixin.py +221 -0
  113. misterdev/task_executors/markdown_plan_executor/critic_spec_mixin.py +174 -0
  114. misterdev/task_executors/markdown_plan_executor/edits_mixin.py +251 -0
  115. misterdev/task_executors/markdown_plan_executor/execute_mixin.py +727 -0
  116. misterdev/task_executors/markdown_plan_executor/gates_mixin.py +203 -0
  117. misterdev/task_executors/markdown_plan_executor/git_mixin.py +219 -0
  118. misterdev/task_executors/markdown_plan_executor/helpers.py +521 -0
  119. misterdev/task_executors/markdown_plan_executor/llm_mixin.py +238 -0
  120. misterdev/task_executors/markdown_plan_executor/results_mixin.py +23 -0
  121. misterdev/tools/__init__.py +19 -0
  122. misterdev/tools/base_tool.py +14 -0
  123. misterdev/tools/command.py +75 -0
  124. misterdev/tools/file_io.py +69 -0
  125. misterdev/tools/formatter.py +26 -0
  126. misterdev/tools/git_tool.py +90 -0
  127. misterdev/utils/__init__.py +0 -0
  128. misterdev/utils/file_utils.py +169 -0
  129. misterdev/utils/process.py +23 -0
  130. misterdev-0.2.0.dist-info/METADATA +326 -0
  131. misterdev-0.2.0.dist-info/RECORD +136 -0
  132. misterdev-0.2.0.dist-info/WHEEL +5 -0
  133. misterdev-0.2.0.dist-info/entry_points.txt +3 -0
  134. misterdev-0.2.0.dist-info/licenses/COMMERCIAL_LICENSE.md +34 -0
  135. misterdev-0.2.0.dist-info/licenses/LICENSE +661 -0
  136. misterdev-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,218 @@
1
+ """Diff-based change tracking across tasks.
2
+
3
+ Records what each completed task changed (files, additions, deletions).
4
+ Before a downstream task executes, injects relevant recent changes so
5
+ the LLM knows the current state of files it depends on.
6
+
7
+ Addresses ~20% of later-task failures where the LLM operates on stale
8
+ assumptions about code that was modified by earlier tasks.
9
+ """
10
+
11
+ import json
12
+ import subprocess
13
+ from pathlib import Path
14
+ from typing import Dict, List
15
+
16
+ from misterdev.logging_setup import setup_logger
17
+ from misterdev.utils.file_utils import (
18
+ atomic_write,
19
+ orchestrator_state_file,
20
+ )
21
+
22
+ logger = setup_logger(__name__)
23
+
24
+
25
+ class TaskChange:
26
+ def __init__(
27
+ self,
28
+ task_id: str,
29
+ files: List[str],
30
+ diff_summary: str,
31
+ additions: int,
32
+ deletions: int,
33
+ ):
34
+ self.task_id = task_id
35
+ self.files = files
36
+ self.diff_summary = diff_summary
37
+ self.additions = additions
38
+ self.deletions = deletions
39
+
40
+ def to_dict(self) -> dict:
41
+ return {
42
+ "task_id": self.task_id,
43
+ "files": self.files,
44
+ "diff_summary": self.diff_summary,
45
+ "additions": self.additions,
46
+ "deletions": self.deletions,
47
+ }
48
+
49
+ @staticmethod
50
+ def from_dict(data: dict) -> "TaskChange":
51
+ return TaskChange(
52
+ task_id=data["task_id"],
53
+ files=data.get("files", []),
54
+ diff_summary=data.get("diff_summary", ""),
55
+ additions=data.get("additions", 0),
56
+ deletions=data.get("deletions", 0),
57
+ )
58
+
59
+
60
+ class ChangeTracker:
61
+ """Tracks what each task changed for downstream context injection."""
62
+
63
+ def __init__(self, project_path: Path):
64
+ self.project_path = project_path
65
+ self._file = orchestrator_state_file(project_path, "changes.json")
66
+ self.changes: Dict[str, TaskChange] = {}
67
+ self._load()
68
+
69
+ def _load(self):
70
+ if self._file.exists():
71
+ try:
72
+ data = json.loads(self._file.read_text(encoding="utf-8"))
73
+ for task_id, entry in data.items():
74
+ self.changes[task_id] = TaskChange.from_dict(entry)
75
+ except (json.JSONDecodeError, OSError, KeyError):
76
+ self.changes = {}
77
+
78
+ def _save(self):
79
+ data = {tid: tc.to_dict() for tid, tc in self.changes.items()}
80
+ atomic_write(self._file, json.dumps(data, indent=2))
81
+
82
+ def record_task_changes(
83
+ self, task_id: str, modified_files: List[str]
84
+ ) -> TaskChange:
85
+ """Record what a completed task changed. Call after task commit."""
86
+ diff_summary = self._get_task_diff(task_id, modified_files)
87
+ additions, deletions = self._count_diff_stats(diff_summary)
88
+
89
+ change = TaskChange(
90
+ task_id=task_id,
91
+ files=modified_files,
92
+ diff_summary=diff_summary[:2000], # cap stored diff
93
+ additions=additions,
94
+ deletions=deletions,
95
+ )
96
+ self.changes[task_id] = change
97
+ self._save()
98
+ logger.info(
99
+ f"Tracked changes for {task_id}: {len(modified_files)} files, +{additions}/-{deletions}"
100
+ )
101
+ return change
102
+
103
+ def get_recent_changes_for_files(
104
+ self, target_files: List[str], max_entries: int = 5
105
+ ) -> str:
106
+ """Get recent changes that touched any of the target files."""
107
+ relevant = []
108
+ for change in reversed(list(self.changes.values())):
109
+ overlap = set(change.files) & set(target_files)
110
+ if overlap:
111
+ relevant.append((change, overlap))
112
+ if len(relevant) >= max_entries:
113
+ break
114
+
115
+ if not relevant:
116
+ return ""
117
+
118
+ lines = ["## Recent Changes to Related Files"]
119
+ for change, overlap_files in relevant:
120
+ lines.append(f"\n### {change.task_id} (touched {', '.join(overlap_files)})")
121
+ lines.append(f"+{change.additions}/-{change.deletions} lines")
122
+ if change.diff_summary:
123
+ # Show only the parts relevant to overlapping files
124
+ filtered = self._filter_diff_for_files(
125
+ change.diff_summary, overlap_files
126
+ )
127
+ if filtered:
128
+ lines.append(f"```diff\n{filtered}\n```")
129
+
130
+ return "\n".join(lines)
131
+
132
+ def _get_task_diff(self, task_id: str, files: List[str]) -> str:
133
+ """Return the diff produced by a single task.
134
+
135
+ Each task commits with a message ``task(<id>): ...``. Looking the
136
+ commit up by ID means every task is attributed its own diff regardless
137
+ of how many other tasks committed before this is recorded (a plain
138
+ ``HEAD~1 HEAD`` would give every task in a batch the same final diff).
139
+ Falls back to a file-scoped working-tree diff when the commit can't be
140
+ found (e.g. file-snapshot mode in a non-git repo).
141
+ """
142
+ sha = self._find_task_commit(task_id)
143
+ if sha:
144
+ diff = self._run_git(["diff", f"{sha}~1", sha, "--unified=3", "--", *files])
145
+ if diff:
146
+ return diff
147
+ # Fallback for tasks not committed under the task(<id>): convention:
148
+ # the most recent commit, scoped to the task's files when known.
149
+ scope = ["--", *files] if files else []
150
+ diff = self._run_git(["diff", "HEAD~1", "HEAD", "--unified=3", *scope])
151
+ if diff:
152
+ return diff
153
+ # Last resort: uncommitted working-tree changes for the task's files.
154
+ if files:
155
+ return self._run_git(["diff", "HEAD", "--unified=3", "--", *files])
156
+ return ""
157
+
158
+ def _find_task_commit(self, task_id: str) -> str:
159
+ """Find the SHA of the commit recording this task, if any."""
160
+ out = self._run_git(
161
+ [
162
+ "log",
163
+ "--all",
164
+ "-n",
165
+ "1",
166
+ "--format=%H",
167
+ "--fixed-strings",
168
+ f"--grep=task({task_id}):",
169
+ ]
170
+ )
171
+ lines = out.splitlines()
172
+ return lines[0].strip() if lines else ""
173
+
174
+ def _run_git(self, args: List[str]) -> str:
175
+ """Run a git command (list form, no shell) and return stdout, or ''."""
176
+ try:
177
+ proc = subprocess.run(
178
+ ["git", *args],
179
+ cwd=self.project_path,
180
+ capture_output=True,
181
+ text=True,
182
+ timeout=30,
183
+ )
184
+ return proc.stdout.strip() if proc.returncode == 0 else ""
185
+ except Exception as e:
186
+ logger.debug(f"Could not run git {args[0] if args else ''}: {e}")
187
+ return ""
188
+
189
+ def _count_diff_stats(self, diff: str) -> tuple:
190
+ additions = 0
191
+ deletions = 0
192
+ for line in diff.splitlines():
193
+ if line.startswith("+") and not line.startswith("+++"):
194
+ additions += 1
195
+ elif line.startswith("-") and not line.startswith("---"):
196
+ deletions += 1
197
+ return additions, deletions
198
+
199
+ def _filter_diff_for_files(
200
+ self, diff: str, target_files: set, max_lines: int = 30
201
+ ) -> str:
202
+ """Extract diff hunks for specific files only."""
203
+ lines = diff.splitlines()
204
+ output = []
205
+ in_relevant_file = False
206
+ line_count = 0
207
+
208
+ for line in lines:
209
+ if line.startswith("diff --git"):
210
+ in_relevant_file = any(f in line for f in target_files)
211
+ if in_relevant_file:
212
+ output.append(line)
213
+ line_count += 1
214
+ if line_count >= max_lines:
215
+ output.append("... (truncated)")
216
+ break
217
+
218
+ return "\n".join(output)
@@ -0,0 +1,63 @@
1
+ """Cross-task interface contract registry.
2
+
3
+ After each task completes, extracts the public API it created or modified.
4
+ Before executing downstream tasks, injects those contracts into prompts
5
+ so the LLM knows the exact signatures it must honor.
6
+
7
+ This addresses ~30% of multi-task build failures where one task assumes
8
+ a different interface than what the previous task actually created.
9
+ """
10
+
11
+ from ._log import logger
12
+ from ._text import _extract_name
13
+ from .extraction import _extract_public_symbols
14
+ from .python_generic import _extract_generic_symbols, _extract_python_symbols
15
+ from .registry import Contract, ContractRegistry
16
+ from .rust_line import (
17
+ _collect_enum_variants,
18
+ _collect_signature,
19
+ _collect_struct_fields,
20
+ _collect_trait_methods,
21
+ _extract_generics,
22
+ _extract_impl_methods,
23
+ _extract_impl_name,
24
+ _extract_rust_symbols,
25
+ _strip_visibility,
26
+ )
27
+ from .rust_tree_sitter import (
28
+ _extract_rust_symbols_ts,
29
+ _ts_decl,
30
+ _ts_field_text,
31
+ _ts_is_pub,
32
+ _ts_pub_members,
33
+ _ts_trait_methods,
34
+ _ts_variant_names,
35
+ _walk_rust_ts,
36
+ )
37
+
38
+ __all__ = [
39
+ "logger",
40
+ "Contract",
41
+ "ContractRegistry",
42
+ "_extract_public_symbols",
43
+ "_extract_name",
44
+ "_extract_generic_symbols",
45
+ "_extract_python_symbols",
46
+ "_extract_rust_symbols",
47
+ "_extract_rust_symbols_ts",
48
+ "_strip_visibility",
49
+ "_extract_generics",
50
+ "_extract_impl_name",
51
+ "_extract_impl_methods",
52
+ "_collect_enum_variants",
53
+ "_collect_trait_methods",
54
+ "_collect_signature",
55
+ "_collect_struct_fields",
56
+ "_ts_is_pub",
57
+ "_ts_field_text",
58
+ "_ts_decl",
59
+ "_walk_rust_ts",
60
+ "_ts_pub_members",
61
+ "_ts_variant_names",
62
+ "_ts_trait_methods",
63
+ ]
@@ -0,0 +1,9 @@
1
+ """Shared logger for the contracts package.
2
+
3
+ Uses the original module path as the logger name so log output is
4
+ identical to the pre-split single-file module.
5
+ """
6
+
7
+ from misterdev.logging_setup import setup_logger
8
+
9
+ logger = setup_logger("misterdev.core.context.contracts")
@@ -0,0 +1,12 @@
1
+ """Shared text helpers used by the language-specific extractors."""
2
+
3
+
4
+ def _extract_name(text: str) -> str:
5
+ """Extract identifier name from text (stops at non-alphanumeric)."""
6
+ name = []
7
+ for ch in text.strip():
8
+ if ch.isalnum() or ch == "_":
9
+ name.append(ch)
10
+ else:
11
+ break
12
+ return "".join(name)
@@ -0,0 +1,30 @@
1
+ """Language dispatch for public-symbol extraction."""
2
+
3
+ from typing import Dict, List
4
+
5
+ from .python_generic import _extract_generic_symbols, _extract_python_symbols
6
+ from .rust_line import _extract_rust_symbols
7
+ from .rust_tree_sitter import _extract_rust_symbols_ts
8
+
9
+
10
+ def _extract_public_symbols(content: str, language: str) -> List[Dict[str, str]]:
11
+ """Extract public API symbols from source code.
12
+
13
+ Uses line-by-line heuristic parsing (no regex). Works for Rust, Python,
14
+ TypeScript, Go. Not perfect, but catches the signatures that matter for
15
+ cross-task contracts.
16
+ """
17
+ lines = content.splitlines()
18
+
19
+ if language in ("rust", "rs"):
20
+ # Prefer tree-sitter (handles multi-line signatures, generics, where
21
+ # clauses); fall back to the line parser when the grammar is absent.
22
+ symbols = _extract_rust_symbols_ts(content)
23
+ if not symbols:
24
+ symbols = _extract_rust_symbols(lines)
25
+ elif language in ("python", "py"):
26
+ symbols = _extract_python_symbols(lines)
27
+ else:
28
+ symbols = _extract_generic_symbols(lines)
29
+
30
+ return symbols
@@ -0,0 +1,42 @@
1
+ """Public-symbol extraction for Python and generic C-like languages."""
2
+
3
+ from typing import Dict, List
4
+
5
+ from ._text import _extract_name
6
+
7
+
8
+ def _extract_python_symbols(lines: List[str]) -> List[Dict[str, str]]:
9
+ """Extract top-level def and class from Python (non-underscore)."""
10
+ symbols = []
11
+ for line in lines:
12
+ stripped = line.strip()
13
+ if stripped.startswith("def ") and not stripped.startswith("def _"):
14
+ name = _extract_name(stripped[4:])
15
+ sig = stripped.rstrip(":")
16
+ symbols.append({"kind": "def", "name": name, "signature": sig})
17
+ elif stripped.startswith("class ") and not stripped.startswith("class _"):
18
+ name = _extract_name(stripped[6:])
19
+ symbols.append(
20
+ {"kind": "class", "name": name, "signature": stripped.rstrip(":")}
21
+ )
22
+ return symbols
23
+
24
+
25
+ def _extract_generic_symbols(lines: List[str]) -> List[Dict[str, str]]:
26
+ """Fallback: extract function/type declarations from any C-like language."""
27
+ symbols = []
28
+ for line in lines:
29
+ stripped = line.strip()
30
+ if stripped.startswith("export ") or stripped.startswith("public "):
31
+ symbols.append(
32
+ {"kind": "export", "name": stripped[:60], "signature": stripped[:80]}
33
+ )
34
+ elif stripped.startswith("func "):
35
+ symbols.append(
36
+ {
37
+ "kind": "func",
38
+ "name": _extract_name(stripped[5:]),
39
+ "signature": stripped[:80],
40
+ }
41
+ )
42
+ return symbols
@@ -0,0 +1,127 @@
1
+ """Contract data type and the cross-task contract registry."""
2
+
3
+ import json
4
+ import threading
5
+ from pathlib import Path
6
+ from typing import Dict, List
7
+
8
+ from misterdev.utils.file_utils import (
9
+ atomic_write,
10
+ orchestrator_state_file,
11
+ )
12
+
13
+ from ._log import logger
14
+ from .extraction import _extract_public_symbols
15
+
16
+
17
+ class Contract:
18
+ """A public API contract extracted from a completed task."""
19
+
20
+ def __init__(self, task_id: str, file_path: str, symbols: List[Dict[str, str]]):
21
+ self.task_id = task_id
22
+ self.file_path = file_path
23
+ self.symbols = symbols # [{name, kind, signature}]
24
+
25
+ def format_for_prompt(self) -> str:
26
+ lines = [f"### {self.file_path} (from {self.task_id})"]
27
+ for sym in self.symbols:
28
+ kind = sym.get("kind", "symbol")
29
+ name = sym.get("name", "?")
30
+ sig = sym.get("signature", "")
31
+ lines.append(f"- {kind}: `{sig or name}`")
32
+ return "\n".join(lines)
33
+
34
+
35
+ class ContractRegistry:
36
+ """Manages interface contracts across tasks.
37
+
38
+ After a task completes, call `extract_contracts()` to record what it exported.
39
+ Before a task executes, call `get_contracts_for_task()` to get the interfaces
40
+ it depends on.
41
+ """
42
+
43
+ def __init__(self, project_path: Path):
44
+ self.contracts: Dict[str, List[Contract]] = {} # task_id -> contracts
45
+ self._file = orchestrator_state_file(project_path, "contracts.json")
46
+ self._lock = threading.Lock()
47
+ self._load()
48
+
49
+ def _load(self):
50
+ if self._file.exists():
51
+ try:
52
+ data = json.loads(self._file.read_text(encoding="utf-8"))
53
+ for task_id, entries in data.items():
54
+ self.contracts[task_id] = [
55
+ Contract(task_id, e["file_path"], e["symbols"]) for e in entries
56
+ ]
57
+ except (json.JSONDecodeError, OSError, KeyError):
58
+ self.contracts = {}
59
+
60
+ def _save(self):
61
+ data = {}
62
+ for task_id, contracts in self.contracts.items():
63
+ data[task_id] = [
64
+ {"file_path": c.file_path, "symbols": c.symbols} for c in contracts
65
+ ]
66
+ atomic_write(self._file, json.dumps(data, indent=2))
67
+
68
+ def extract_contracts(
69
+ self,
70
+ task_id: str,
71
+ modified_files: List[str],
72
+ project_path: Path,
73
+ llm_client,
74
+ language: str = "rust",
75
+ ) -> List[Contract]:
76
+ """Extract public API from files modified by a completed task."""
77
+ contracts = []
78
+ for file_path in modified_files:
79
+ full_path = project_path / file_path
80
+ if not full_path.exists():
81
+ continue
82
+ try:
83
+ content = full_path.read_text(encoding="utf-8")
84
+ except (UnicodeDecodeError, OSError):
85
+ continue
86
+
87
+ if len(content.strip()) == 0:
88
+ continue
89
+
90
+ symbols = _extract_public_symbols(content, language)
91
+ if symbols:
92
+ contracts.append(Contract(task_id, file_path, symbols))
93
+
94
+ with self._lock:
95
+ self.contracts[task_id] = contracts
96
+ self._save()
97
+ logger.info(
98
+ f"Extracted {sum(len(c.symbols) for c in contracts)} contracts from {task_id}"
99
+ )
100
+ return contracts
101
+
102
+ def get_contracts_for_task(self, dependency_ids: List[str]) -> str:
103
+ """Format contracts from dependency tasks as prompt context."""
104
+ if not dependency_ids:
105
+ return ""
106
+
107
+ relevant = []
108
+ with self._lock:
109
+ for dep_id in dependency_ids:
110
+ if dep_id in self.contracts:
111
+ relevant.extend(self.contracts[dep_id])
112
+
113
+ if not relevant:
114
+ return ""
115
+
116
+ lines = [
117
+ "## Interface Contracts (from completed dependency tasks)",
118
+ "Your code MUST use these exact signatures. Do not guess or assume different names.\n",
119
+ ]
120
+ for contract in relevant:
121
+ lines.append(contract.format_for_prompt())
122
+ return "\n".join(lines)
123
+
124
+ def get_all_contracts_summary(self) -> str:
125
+ """Summary for reporting."""
126
+ total = sum(len(cs) for cs in self.contracts.values())
127
+ return f"{len(self.contracts)} tasks, {total} total contracts"