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,387 @@
1
+ """Task decomposition with dependency resolution, ported from /build Phase 3.
2
+
3
+ Breaks a spec into ordered, atomic tasks with dependency tracking.
4
+ Uses topological sort to determine execution order.
5
+ """
6
+
7
+ from collections import deque
8
+ from pathlib import Path
9
+
10
+ from misterdev.core.models import Task
11
+ from misterdev.llm.responses import extract_json_array
12
+ from misterdev.utils.file_utils import safe_ref_slug
13
+ from misterdev.core.planning.assessment import ProjectAssessment
14
+ from misterdev.core.modes import BuildMode
15
+ from misterdev.llm.client import BaseLLMClient
16
+ from misterdev.logging_setup import setup_logger
17
+
18
+ logger = setup_logger(__name__)
19
+
20
+ # /build ordering: infrastructure > core types > core logic > features >
21
+ # integration > tests > fixes > cleanup
22
+ CATEGORY_ORDER = {
23
+ "infrastructure": 0,
24
+ "core": 1,
25
+ "feature": 2,
26
+ "integration": 3,
27
+ "test": 4,
28
+ "fix": 5,
29
+ "docs": 6,
30
+ "cleanup": 7,
31
+ }
32
+
33
+ MAX_TASKS = 30
34
+
35
+ DECOMPOSE_PROMPT = """You are a task decomposer for a software project.
36
+
37
+ Given the project assessment and spec below, break the work into an ordered
38
+ list of atomic, verifiable tasks. Each task must be completable in one editing
39
+ session (1-20 files) and have a concrete "done" condition.
40
+
41
+ ## Project Assessment
42
+
43
+ ### Structure
44
+ - Type: {project_type}
45
+ - Languages: {languages}
46
+ - Frameworks: {frameworks}
47
+ - Build: {build_command}
48
+ - Test: {test_command}
49
+ - Package manager: {package_manager}
50
+
51
+ ### Health
52
+ - Builds: {builds}
53
+ - Tests pass: {tests_pass} ({test_count} tests, {test_failures} failures)
54
+ - Lint clean: {lint_clean}
55
+
56
+ ### Features
57
+ - Existing: {existing_features}
58
+ - Incomplete: {incomplete_features}
59
+ - Missing: {missing_features}
60
+ - Broken: {broken_items}
61
+ - Stubs: {stub_items}
62
+
63
+ ### Context
64
+ - Purpose: {purpose}
65
+ - Conventions: {conventions}
66
+
67
+ ### Risk
68
+ - Tech debt score: {debt_score}/100
69
+ - Risk level: {risk_level}
70
+
71
+ ### Project Files (REAL paths and their symbols — the actual code that exists)
72
+ {file_map}
73
+ {targets_section}
74
+ ## Spec
75
+ {spec}
76
+
77
+ ## Mode
78
+ {mode}
79
+
80
+ ## Rules
81
+ - files_to_modify and context_files MUST be REAL paths taken from the Project
82
+ Files map above. NEVER invent a path or guess one from a feature/test name: to
83
+ change a function, find the file in the map that already defines it and put
84
+ THAT path in files_to_modify. Use files_to_create only for a file that is
85
+ genuinely absent from the map.
86
+ {targets_rule}{existing_guidance}- Max {max_tasks} tasks. Prioritize: must-fix > must-complete > should-add.
87
+ - Order: infrastructure > core types > core logic > features > integration > tests > fixes > cleanup.
88
+ - Each task's files_to_modify must not overlap with another task's files_to_create unless a dependency is declared.
89
+ - For DEBUG mode: order by build-blocking > test-blocking > runtime errors > warnings.
90
+ - For COMPLETE mode: order by infrastructure > core > features > tests > docs > cleanup.
91
+ - acceptance_criteria must be specific and testable (e.g., "pytest tests/test_auth.py passes" not "auth works").
92
+ - context_files should include imports, interfaces, or types that the task needs to read but not modify.
93
+
94
+ Return a JSON array of task objects. Each object must have exactly these fields:
95
+ id (string, like "T-001"),
96
+ title (string),
97
+ description (string),
98
+ acceptance_criteria (string),
99
+ files_to_create (array of strings),
100
+ files_to_modify (array of strings),
101
+ context_files (array of strings, files needed for context but not modified),
102
+ dependencies (array of task id strings),
103
+ complexity ("trivial", "small", "medium", "large", or "architectural"),
104
+ category ("infrastructure", "core", "feature", "integration", "test", "fix", "docs", or "cleanup")
105
+
106
+ Return ONLY the JSON array, no markdown fences or other text."""
107
+
108
+
109
+ def _targets_prompt(targets: list[dict] | None) -> tuple[str, str]:
110
+ """Render the optional sub-project (targets) section and grounding rule.
111
+
112
+ Returns ("", "") when there are no targets, so the single-target prompt is
113
+ unchanged. When present, the planner is told the sub-project boundaries and
114
+ instructed to keep each task within ONE target so gate routing stays clean.
115
+ """
116
+ if not targets:
117
+ return "", ""
118
+ lines = ["\n### Sub-projects (targets — each has its OWN build/test toolchain)"]
119
+ for t in targets:
120
+ path = (t.get("path") or "").strip("/")
121
+ if not path:
122
+ continue
123
+ name = t.get("name") or path
124
+ gate = t.get("build_command") or t.get("test_command") or "?"
125
+ lines.append(f"- {name}: files under `{path}/` (gate: {gate})")
126
+ section = "\n".join(lines) + "\n"
127
+ rule = (
128
+ "- This repo has multiple sub-projects (targets above). Each task's files "
129
+ "MUST stay within ONE target's directory — never mix files from different "
130
+ "targets in a single task (they are built/tested with different "
131
+ "toolchains). Split cross-target work into separate, dependency-ordered "
132
+ "tasks (e.g. a core task, then the frontend task that consumes it).\n"
133
+ )
134
+ return section, rule
135
+
136
+
137
+ def decompose_spec(
138
+ spec: str,
139
+ assessment: ProjectAssessment,
140
+ mode: BuildMode,
141
+ llm_client: BaseLLMClient,
142
+ project_ref: str,
143
+ max_tasks: int = MAX_TASKS,
144
+ file_map: str = "",
145
+ targets: list[dict] | None = None,
146
+ ) -> list[Task]:
147
+ """Use LLM to decompose a spec into ordered tasks with dependencies."""
148
+ s = assessment.structure
149
+ h = assessment.health
150
+ f = assessment.features
151
+ c = assessment.context
152
+
153
+ # CREATE is greenfield (files_to_create is expected); every other mode acts
154
+ # on an existing tree, where recreating existing files clobbers real code.
155
+ existing_guidance = (
156
+ ""
157
+ if mode == BuildMode.CREATE
158
+ else (
159
+ "- This is an EXISTING project, not a greenfield build. Use "
160
+ "files_to_create ONLY for files that genuinely do not exist yet; for "
161
+ "any file that already exists use files_to_modify. NEVER emit tasks "
162
+ "that recreate existing files, modules, build config, or scaffolding.\n"
163
+ )
164
+ )
165
+
166
+ targets_section, targets_rule = _targets_prompt(targets)
167
+
168
+ prompt = DECOMPOSE_PROMPT.format(
169
+ existing_guidance=existing_guidance,
170
+ targets_section=targets_section,
171
+ targets_rule=targets_rule,
172
+ file_map=file_map.strip() or "(file map unavailable — infer paths cautiously)",
173
+ project_type=s.project_type,
174
+ languages=", ".join(s.languages) if s.languages else "unknown",
175
+ frameworks=", ".join(s.frameworks) if s.frameworks else "none",
176
+ build_command=s.build_command or "none",
177
+ test_command=s.test_command or "none",
178
+ package_manager=s.package_manager or "unknown",
179
+ builds="yes" if h.builds else "no",
180
+ tests_pass="yes" if h.tests_pass else "no",
181
+ test_count=h.test_count,
182
+ test_failures=h.test_failures,
183
+ lint_clean="yes" if h.lint_clean else "no",
184
+ existing_features=", ".join(fi.name for fi in f.existing)
185
+ if f.existing
186
+ else "none",
187
+ incomplete_features=", ".join(
188
+ f"{fi.name} ({fi.complexity})" for fi in f.incomplete
189
+ )
190
+ if f.incomplete
191
+ else "none",
192
+ missing_features=", ".join(f"{fi.name} ({fi.complexity})" for fi in f.missing)
193
+ if f.missing
194
+ else "none",
195
+ broken_items=", ".join(f.broken) if f.broken else "none",
196
+ stub_items=", ".join(f.stubs) if f.stubs else "none",
197
+ purpose=c.purpose or "unknown",
198
+ conventions=c.conventions or "none specified",
199
+ debt_score=assessment.tech_debt.score,
200
+ risk_level=assessment.risk.level,
201
+ spec=spec,
202
+ mode=mode.value,
203
+ max_tasks=max_tasks,
204
+ )
205
+
206
+ logger.info("Decomposing spec into tasks via LLM...")
207
+ response = llm_client.generate_code(
208
+ prompt, "You are a precise task decomposer. Return only valid JSON."
209
+ )
210
+
211
+ tasks = _parse_tasks(response, project_ref)
212
+
213
+ # Ground the plan in the real tree before anything else: a file that already
214
+ # exists is a modification, not a creation. Without this, COMPLETE mode on an
215
+ # existing project emits "create" tasks for files that exist and the executor
216
+ # clobbers real code (observed: a 30-task from-scratch rebuild of a mature
217
+ # repo). Run before dependency detection so reclassified files participate.
218
+ _ground_task_paths(tasks, project_ref)
219
+
220
+ # Detect implicit dependencies from file overlaps
221
+ _add_implicit_dependencies(tasks)
222
+
223
+ # Validate and cap
224
+ if len(tasks) > max_tasks:
225
+ logger.warning(f"LLM returned {len(tasks)} tasks, capping at {max_tasks}")
226
+ tasks = tasks[:max_tasks]
227
+
228
+ return tasks
229
+
230
+
231
+ def _parse_tasks(response: str, project_ref: str) -> list[Task]:
232
+ """Parse LLM JSON response into Task objects."""
233
+ # Strip markdown fences if present
234
+ text = response.strip()
235
+ # Strip markdown fences
236
+ if "```" in text:
237
+ lines = text.split("\n")
238
+ lines = [ln for ln in lines if not ln.strip().startswith("```")]
239
+ text = "\n".join(lines).strip()
240
+
241
+ raw_tasks = extract_json_array(text, default=None)
242
+ if not isinstance(raw_tasks, list):
243
+ logger.error("No JSON task array found in LLM response")
244
+ return []
245
+
246
+ tasks = []
247
+ seen_ids: set[str] = set()
248
+ for i, raw in enumerate(raw_tasks):
249
+ try:
250
+ # Sanitize the LLM-supplied id (it becomes a git branch name, commit
251
+ # grep key, progress/contract dict key) and dependency refs with the
252
+ # SAME function so cross-references stay consistent. Drop duplicates
253
+ # so they don't collapse in dependency maps.
254
+ tid = safe_ref_slug(raw.get("id") or "", fallback=f"T-{i + 1:03d}")
255
+ if tid in seen_ids:
256
+ logger.warning(f"Skipping duplicate task id {tid!r}")
257
+ continue
258
+ seen_ids.add(tid)
259
+ deps = [
260
+ slug
261
+ for d in raw.get("dependencies", [])
262
+ if (slug := safe_ref_slug(d, fallback=""))
263
+ ]
264
+ task = Task(
265
+ id=tid,
266
+ title=raw.get("title", ""),
267
+ description=raw.get("description", ""),
268
+ acceptance_criteria=raw.get("acceptance_criteria", ""),
269
+ files_to_create=raw.get("files_to_create", []),
270
+ files_to_modify=raw.get("files_to_modify", []),
271
+ context_files=raw.get("context_files", []),
272
+ dependencies=deps,
273
+ complexity=raw.get("complexity", "medium"),
274
+ category=raw.get("category", "feature"),
275
+ type="markdown_planner",
276
+ status="pending",
277
+ project_ref=project_ref,
278
+ processor_data=raw,
279
+ )
280
+ tasks.append(task)
281
+ except Exception as e:
282
+ logger.warning(f"Skipping malformed task: {e}")
283
+
284
+ return tasks
285
+
286
+
287
+ def _ground_task_paths(tasks: list[Task], project_ref: str) -> None:
288
+ """Reclassify create->modify for files that already exist on disk.
289
+
290
+ The decomposing LLM, especially in COMPLETE mode on an existing project,
291
+ routinely lists existing files under files_to_create. Acting on that would
292
+ recreate (and clobber) real code. A path that already exists is a
293
+ modification; only genuinely absent paths stay creations.
294
+ """
295
+ root = Path(project_ref)
296
+ for t in tasks:
297
+ still_create: list[str] = []
298
+ for f in t.files_to_create:
299
+ if f and (root / f).exists():
300
+ if f not in t.files_to_modify:
301
+ t.files_to_modify.append(f)
302
+ logger.warning(
303
+ f"Task {t.id}: '{f}' already exists; treating as modify, not create."
304
+ )
305
+ else:
306
+ still_create.append(f)
307
+ t.files_to_create = still_create
308
+
309
+
310
+ def _add_implicit_dependencies(tasks: list[Task]) -> None:
311
+ """If task B modifies a file that task A creates, B depends on A."""
312
+ creates_map: dict[str, str] = {}
313
+ for t in tasks:
314
+ for f in t.files_to_create:
315
+ creates_map[f] = t.id
316
+
317
+ for t in tasks:
318
+ for f in t.files_to_modify:
319
+ creator_id = creates_map.get(f)
320
+ if creator_id and creator_id != t.id and creator_id not in t.dependencies:
321
+ t.dependencies.append(creator_id)
322
+ logger.info(
323
+ f"Implicit dependency: {t.id} depends on {creator_id} (file {f})"
324
+ )
325
+
326
+
327
+ def topological_sort(tasks: list[Task]) -> list[Task]:
328
+ """Sort tasks respecting dependencies. Falls back to category order on ties.
329
+
330
+ Returns tasks in execution order. Raises ValueError on cycles.
331
+ """
332
+ task_map = {t.id: t for t in tasks}
333
+ in_degree: dict[str, int] = {t.id: 0 for t in tasks}
334
+ dependents: dict[str, list[str]] = {t.id: [] for t in tasks}
335
+
336
+ for t in tasks:
337
+ for dep_id in t.dependencies:
338
+ if dep_id in task_map:
339
+ in_degree[t.id] += 1
340
+ dependents[dep_id].append(t.id)
341
+
342
+ # Seed queue with zero-dependency tasks, sorted by category order
343
+ queue: deque[str] = deque()
344
+ ready = [tid for tid, deg in in_degree.items() if deg == 0]
345
+ ready.sort(key=lambda tid: CATEGORY_ORDER.get(task_map[tid].category, 99))
346
+ queue.extend(ready)
347
+
348
+ result = []
349
+ while queue:
350
+ tid = queue.popleft()
351
+ result.append(task_map[tid])
352
+ for dep_tid in dependents[tid]:
353
+ in_degree[dep_tid] -= 1
354
+ if in_degree[dep_tid] == 0:
355
+ queue.append(dep_tid)
356
+
357
+ if len(result) != len(tasks):
358
+ # Cycle detected; append remaining tasks anyway with a warning
359
+ remaining = [t for t in tasks if t.id not in {r.id for r in result}]
360
+ logger.warning(
361
+ f"Dependency cycle detected involving tasks: "
362
+ f"{[t.id for t in remaining]}. Appending in category order."
363
+ )
364
+ remaining.sort(key=lambda t: CATEGORY_ORDER.get(t.category, 99))
365
+ result.extend(remaining)
366
+
367
+ return result
368
+
369
+
370
+ def format_plan(tasks: list[Task], mode: BuildMode) -> str:
371
+ """Format task list as a markdown plan table (for --dry-run)."""
372
+ lines = [
373
+ "## Build Plan\n",
374
+ f"**Mode**: {mode.value} | **Tasks**: {len(tasks)}\n",
375
+ "| # | ID | Title | Category | Complexity | Depends On | Files |",
376
+ "|---|------|-------|----------|------------|------------|-------|",
377
+ ]
378
+ for i, t in enumerate(tasks, 1):
379
+ deps = ", ".join(t.dependencies) if t.dependencies else "-"
380
+ n_create = len(t.files_to_create)
381
+ n_modify = len(t.files_to_modify)
382
+ files = f"{n_create} create, {n_modify} modify"
383
+ lines.append(
384
+ f"| {i} | {t.id} | {t.title} | {t.category} | "
385
+ f"{t.complexity} | {deps} | {files} |"
386
+ )
387
+ return "\n".join(lines)
@@ -0,0 +1,103 @@
1
+ """Metacognitive Trace Auditing and Project-Specific Learning.
2
+
3
+ Reviews build session history to generate permanent project-specific rules.
4
+ """
5
+
6
+ import json
7
+ from pathlib import Path
8
+ from typing import List, Any
9
+
10
+ from misterdev.logging_setup import setup_logger
11
+ from misterdev.llm.client import BaseLLMClient
12
+ from misterdev.llm.responses import extract_json_array
13
+
14
+ logger = setup_logger(__name__)
15
+
16
+
17
+ class SessionAuditor:
18
+ """Audits task execution traces to extract 'Lessons Learned'."""
19
+
20
+ def __init__(self, project_path: Path, llm_client: BaseLLMClient):
21
+ self.project_path = project_path
22
+ self.llm = llm_client
23
+ self.lessons_file = project_path / ".orchestrator" / "lessons.json"
24
+
25
+ def audit_session(
26
+ self,
27
+ completed_tasks: List[Any],
28
+ failed_tasks: List[Any],
29
+ scratchpad_content: str,
30
+ ) -> str:
31
+ """Runs the audit and returns a 'Project Logic Patch' string."""
32
+ logger.info("Auditing build session for metacognitive learning...")
33
+
34
+ prompt = f"""Review the following session traces for this project.
35
+ Identify any project-specific 'pitfalls', 'conventions', or 'requirements' that were discovered.
36
+
37
+ Successful Patterns:
38
+ {completed_tasks}
39
+
40
+ Failed Attempts / Stalls:
41
+ {failed_tasks}
42
+
43
+ Scratchpad Learnings:
44
+ {scratchpad_content}
45
+
46
+ Generate a list of 1-5 'Project Specific Rules' for future agents.
47
+ Example: 'Always run black before committing', 'The database connection must be closed manually in tests'.
48
+
49
+ Return a JSON array of strings. Return ONLY the JSON array.
50
+ """
51
+ try:
52
+ response = self.llm.generate_code(
53
+ prompt, "You are a senior project auditor."
54
+ )
55
+ new_rules = _extract_json_array(response)
56
+ if new_rules:
57
+ self._save_lessons(new_rules)
58
+ return "\n".join(f"- {r}" for r in new_rules)
59
+ except Exception as e:
60
+ logger.error(f"Metacognitive audit failed: {e}")
61
+
62
+ return "No new lessons learned."
63
+
64
+ def _save_lessons(self, new_rules: List[str]):
65
+ """Permanently appends rules to the project's lessons file."""
66
+ self.lessons_file.parent.mkdir(parents=True, exist_ok=True)
67
+ existing = []
68
+ if self.lessons_file.exists():
69
+ try:
70
+ existing = json.loads(self.lessons_file.read_text(encoding="utf-8"))
71
+ except (json.JSONDecodeError, OSError):
72
+ existing = []
73
+
74
+ # Coerce to strings before dedup: the LLM sometimes returns JSON objects
75
+ # instead of plain rule strings, and set()-based dedup crashes on the
76
+ # unhashable dicts (silently killing every audit). dict.fromkeys keeps
77
+ # insertion order while removing duplicates.
78
+ def _as_text(r):
79
+ return r if isinstance(r, str) else json.dumps(r, ensure_ascii=False)
80
+
81
+ merged = [_as_text(r) for r in existing] + [_as_text(r) for r in new_rules]
82
+ updated = list(dict.fromkeys(merged))
83
+ self.lessons_file.write_text(json.dumps(updated, indent=2), encoding="utf-8")
84
+ logger.info(f"Project logic patched with {len(new_rules)} new rules.")
85
+
86
+ def get_lessons_context(self) -> str:
87
+ """Retrieves existing lessons for prompt injection."""
88
+ if not self.lessons_file.exists():
89
+ return ""
90
+ try:
91
+ rules = json.loads(self.lessons_file.read_text(encoding="utf-8"))
92
+ if not rules:
93
+ return ""
94
+ return "## Project-Specific Lessons (Historical)\n" + "\n".join(
95
+ f"- {r}" for r in rules
96
+ )
97
+ except (json.JSONDecodeError, OSError):
98
+ return ""
99
+
100
+
101
+ def _extract_json_array(response: str) -> List[str]:
102
+ """Extract a JSON array from an LLM response (shared helper)."""
103
+ return extract_json_array(response)