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.
- misterdev/__init__.py +3 -0
- misterdev/agent.py +2166 -0
- misterdev/agent_helpers.py +194 -0
- misterdev/analyzers/__init__.py +0 -0
- misterdev/analyzers/project_analyzer/__init__.py +246 -0
- misterdev/analyzers/project_analyzer/detection.py +146 -0
- misterdev/analyzers/project_analyzer/merge.py +137 -0
- misterdev/analyzers/project_analyzer/overview.py +312 -0
- misterdev/analyzers/project_analyzer/prompts.py +83 -0
- misterdev/cli.py +370 -0
- misterdev/config.py +521 -0
- misterdev/core/__init__.py +0 -0
- misterdev/core/audit.py +88 -0
- misterdev/core/config.py +10 -0
- misterdev/core/context/__init__.py +0 -0
- misterdev/core/context/change_tracker.py +218 -0
- misterdev/core/context/contracts/__init__.py +63 -0
- misterdev/core/context/contracts/_log.py +9 -0
- misterdev/core/context/contracts/_text.py +12 -0
- misterdev/core/context/contracts/extraction.py +30 -0
- misterdev/core/context/contracts/python_generic.py +42 -0
- misterdev/core/context/contracts/registry.py +127 -0
- misterdev/core/context/contracts/rust_line.py +225 -0
- misterdev/core/context/contracts/rust_tree_sitter.py +141 -0
- misterdev/core/context/lsp.py +174 -0
- misterdev/core/context/scratchpad.py +111 -0
- misterdev/core/context/topography/__init__.py +43 -0
- misterdev/core/context/topography/_log.py +10 -0
- misterdev/core/context/topography/cache.py +91 -0
- misterdev/core/context/topography/engine.py +172 -0
- misterdev/core/context/topography/graph.py +675 -0
- misterdev/core/context/topography/nodes.py +55 -0
- misterdev/core/context/topography/parsers.py +95 -0
- misterdev/core/context/topography/syntax.py +54 -0
- misterdev/core/economics/__init__.py +0 -0
- misterdev/core/economics/context_budget.py +175 -0
- misterdev/core/economics/embeddings.py +232 -0
- misterdev/core/economics/free_models.py +108 -0
- misterdev/core/economics/llm_cache.py +105 -0
- misterdev/core/economics/model_catalog.py +79 -0
- misterdev/core/economics/model_ledger.py +331 -0
- misterdev/core/economics/model_selector.py +281 -0
- misterdev/core/execution/__init__.py +0 -0
- misterdev/core/execution/bounded.py +50 -0
- misterdev/core/execution/container.py +221 -0
- misterdev/core/execution/error_classifier.py +366 -0
- misterdev/core/execution/error_resolver.py +201 -0
- misterdev/core/execution/governance.py +283 -0
- misterdev/core/execution/outcomes.py +50 -0
- misterdev/core/execution/progress.py +120 -0
- misterdev/core/execution/project.py +231 -0
- misterdev/core/execution/registry.py +97 -0
- misterdev/core/execution/runtime.py +279 -0
- misterdev/core/gitcmd.py +39 -0
- misterdev/core/integration/__init__.py +0 -0
- misterdev/core/integration/mcp.py +368 -0
- misterdev/core/integration/mcp_gather.py +186 -0
- misterdev/core/models.py +35 -0
- misterdev/core/modes.py +184 -0
- misterdev/core/planning/__init__.py +0 -0
- misterdev/core/planning/advisor.py +89 -0
- misterdev/core/planning/assessment.py +135 -0
- misterdev/core/planning/decomposer.py +387 -0
- misterdev/core/planning/metacognition.py +103 -0
- misterdev/core/planning/sovereign.py +308 -0
- misterdev/core/planning/targets.py +201 -0
- misterdev/core/reporting/__init__.py +0 -0
- misterdev/core/reporting/report.py +377 -0
- misterdev/core/reporting/report_view.py +151 -0
- misterdev/core/task.py +163 -0
- misterdev/core/verification/__init__.py +0 -0
- misterdev/core/verification/claim_verifier.py +210 -0
- misterdev/core/verification/critic.py +324 -0
- misterdev/core/verification/gatekeeper/__init__.py +631 -0
- misterdev/core/verification/gatekeeper/constants.py +138 -0
- misterdev/core/verification/gatekeeper/helpers.py +28 -0
- misterdev/core/verification/goal_check.py +219 -0
- misterdev/core/verification/independent.py +68 -0
- misterdev/core/verification/mutation_gate.py +221 -0
- misterdev/core/verification/preflight.py +95 -0
- misterdev/core/verification/spec_tests.py +175 -0
- misterdev/core/verification/validator.py +495 -0
- misterdev/core/verification/vision_verify.py +185 -0
- misterdev/core/verification/web_verify.py +408 -0
- misterdev/environments/__init__.py +0 -0
- misterdev/environments/base_env.py +18 -0
- misterdev/environments/container_env.py +87 -0
- misterdev/environments/venv_env.py +42 -0
- misterdev/llm/__init__.py +0 -0
- misterdev/llm/client/__init__.py +152 -0
- misterdev/llm/client/base.py +382 -0
- misterdev/llm/client/edits.py +70 -0
- misterdev/llm/client/embeddings.py +121 -0
- misterdev/llm/client/errors.py +134 -0
- misterdev/llm/client/providers.py +535 -0
- misterdev/llm/client/response.py +24 -0
- misterdev/llm/prompt_manager.py +82 -0
- misterdev/llm/responses/__init__.py +34 -0
- misterdev/llm/responses/apply.py +131 -0
- misterdev/llm/responses/json_extract.py +80 -0
- misterdev/llm/responses/models.py +43 -0
- misterdev/llm/responses/parsing.py +494 -0
- misterdev/logging_setup.py +20 -0
- misterdev/mcp_server.py +208 -0
- misterdev/nl_cli.py +149 -0
- misterdev/plugins.py +115 -0
- misterdev/py.typed +0 -0
- misterdev/task_executors/__init__.py +0 -0
- misterdev/task_executors/base_executor.py +10 -0
- misterdev/task_executors/markdown_plan_executor/__init__.py +90 -0
- misterdev/task_executors/markdown_plan_executor/commands_mixin.py +82 -0
- misterdev/task_executors/markdown_plan_executor/context_mixin.py +221 -0
- misterdev/task_executors/markdown_plan_executor/critic_spec_mixin.py +174 -0
- misterdev/task_executors/markdown_plan_executor/edits_mixin.py +251 -0
- misterdev/task_executors/markdown_plan_executor/execute_mixin.py +727 -0
- misterdev/task_executors/markdown_plan_executor/gates_mixin.py +203 -0
- misterdev/task_executors/markdown_plan_executor/git_mixin.py +219 -0
- misterdev/task_executors/markdown_plan_executor/helpers.py +521 -0
- misterdev/task_executors/markdown_plan_executor/llm_mixin.py +238 -0
- misterdev/task_executors/markdown_plan_executor/results_mixin.py +23 -0
- misterdev/tools/__init__.py +19 -0
- misterdev/tools/base_tool.py +14 -0
- misterdev/tools/command.py +75 -0
- misterdev/tools/file_io.py +69 -0
- misterdev/tools/formatter.py +26 -0
- misterdev/tools/git_tool.py +90 -0
- misterdev/utils/__init__.py +0 -0
- misterdev/utils/file_utils.py +169 -0
- misterdev/utils/process.py +23 -0
- misterdev-0.2.0.dist-info/METADATA +326 -0
- misterdev-0.2.0.dist-info/RECORD +136 -0
- misterdev-0.2.0.dist-info/WHEEL +5 -0
- misterdev-0.2.0.dist-info/entry_points.txt +3 -0
- misterdev-0.2.0.dist-info/licenses/COMMERCIAL_LICENSE.md +34 -0
- misterdev-0.2.0.dist-info/licenses/LICENSE +661 -0
- misterdev-0.2.0.dist-info/top_level.txt +1 -0
misterdev/config.py
ADDED
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
import yaml
|
|
2
|
+
from dataclasses import asdict, dataclass, field, fields
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
from misterdev.logging_setup import setup_logger
|
|
7
|
+
|
|
8
|
+
logger = setup_logger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# ---------------------------------------------------------------------------
|
|
12
|
+
# Typed config schema (single source of truth).
|
|
13
|
+
#
|
|
14
|
+
# Each knob's name, type, and default live in exactly one place: a dataclass
|
|
15
|
+
# field. DEFAULT_CONFIG is GENERATED from these below, so the dict that the rest
|
|
16
|
+
# of the codebase consumes can never drift from the schema, and a default can no
|
|
17
|
+
# longer be copy-pasted (and diverge) across call sites. Stdlib dataclasses, no
|
|
18
|
+
# pydantic: config is loaded from trusted local YAML, so attribute typing +
|
|
19
|
+
# unknown-key validation is enough; we don't need runtime coercion.
|
|
20
|
+
# ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class LLMSettings:
|
|
25
|
+
provider: str = "openrouter"
|
|
26
|
+
model: str = "anthropic/claude-sonnet-4.6"
|
|
27
|
+
temperature: float = 0.1
|
|
28
|
+
# Extra sampling parameters (top_p, top_k, min_p, repetition_penalty,
|
|
29
|
+
# frequency_penalty, presence_penalty, seed, ...). Each is sent only to
|
|
30
|
+
# models whose OpenRouter supported_parameters include it, so unsupported
|
|
31
|
+
# knobs never cause a 400. temperature is filtered the same way.
|
|
32
|
+
sampling: Dict[str, Any] = field(default_factory=dict)
|
|
33
|
+
api_key_env_var: str = "OPENROUTER_API_KEY"
|
|
34
|
+
streaming: bool = False
|
|
35
|
+
# Extract edits via a structured function-call (apply_edits) when the model
|
|
36
|
+
# supports `tools`, instead of regex-parsing markdown fences. On by default;
|
|
37
|
+
# falls back to markdown parsing for models without tool support.
|
|
38
|
+
use_tools: bool = True
|
|
39
|
+
failover: List[Dict[str, Any]] = field(default_factory=list)
|
|
40
|
+
# Per-task model routing: routing maps complexity/strategy -> tier name,
|
|
41
|
+
# models maps tier name -> model id (or a list of candidate ids). Empty =
|
|
42
|
+
# use the default model.
|
|
43
|
+
routing: Dict[str, Any] = field(default_factory=dict)
|
|
44
|
+
models: Dict[str, Any] = field(default_factory=dict)
|
|
45
|
+
# Ledger-driven dynamic model selection. False = off (default), True = on
|
|
46
|
+
# using selection_posture, "auto" = self-activating: explore cheap/free
|
|
47
|
+
# models on easy tasks while a (category, complexity) cell is immature, then
|
|
48
|
+
# settle into conservative cheap-first per cell once it has matured. Typed
|
|
49
|
+
# Any because it accepts a bool or the string "auto".
|
|
50
|
+
# escalation is the capability ladder, cheapest tier first; each tier name
|
|
51
|
+
# resolves through `models`. The policy uses a cheaper model on early
|
|
52
|
+
# attempts and climbs to the strongest tier by the final attempt.
|
|
53
|
+
dynamic_selection: Any = "auto"
|
|
54
|
+
escalation: List[str] = field(default_factory=list)
|
|
55
|
+
# A cheaper model is trusted for a first attempt only once it has at least
|
|
56
|
+
# min_observations recorded first-try attempts and a first-try success rate
|
|
57
|
+
# at or above first_try_floor.
|
|
58
|
+
min_observations: int = 5
|
|
59
|
+
first_try_floor: float = 0.5
|
|
60
|
+
# Hard-avoid a model on a (category, complexity) cell once the ledger has
|
|
61
|
+
# enough attempts to judge it (>= min_observations) AND its success rate on
|
|
62
|
+
# that cell is below this floor: it is proven unable to do this kind of task,
|
|
63
|
+
# so trying it only burns a failed attempt and forces escalation. Distinct
|
|
64
|
+
# from "unproven" (too few attempts to know). 0 disables the guard.
|
|
65
|
+
incompetence_floor: float = 0.2
|
|
66
|
+
# Skip a model on a NON-final attempt when its ledger avg latency exceeds this
|
|
67
|
+
# many seconds — it is proven too slow to fit the per-task wall-clock budget
|
|
68
|
+
# (e.g. the 5-minute cache window). Only bites once a model has recorded slow
|
|
69
|
+
# calls; unseen models are unaffected. 0 disables the guard (default off,
|
|
70
|
+
# since a sensible ceiling is project/hardware specific).
|
|
71
|
+
max_attempt_latency_seconds: float = 0.0
|
|
72
|
+
# "auto" mode treats a (category, complexity) cell as matured once it has at
|
|
73
|
+
# least this many recorded attempts, after which it stops exploring and
|
|
74
|
+
# behaves conservatively.
|
|
75
|
+
maturity_threshold: int = 12
|
|
76
|
+
# Exploration aggressiveness: "conservative" (cheap only once proven),
|
|
77
|
+
# "balanced" (explore cheap on low/medium-complexity first attempts), or
|
|
78
|
+
# "aggressive" (always try cheapest first). The final attempt is always the
|
|
79
|
+
# strongest tier regardless of posture.
|
|
80
|
+
selection_posture: str = "conservative"
|
|
81
|
+
# Per-complexity reasoning effort, sent only to models that support a
|
|
82
|
+
# reasoning budget. Default leverages reasoning where it pays off (hard
|
|
83
|
+
# tasks) without adding token cost to easy ones; a complexity absent from
|
|
84
|
+
# the map gets no reasoning. Effort values: minimal|low|medium|high|xhigh.
|
|
85
|
+
# medium uses "low" (not "medium"): high reasoning was the dominant per-task
|
|
86
|
+
# latency term (~2m47s of a 4.5m task on a medium reasoning call), and medium
|
|
87
|
+
# tasks rarely need deep reasoning — "low" keeps a single attempt inside the
|
|
88
|
+
# 5-minute cache window while preserving reasoning for genuinely large tasks.
|
|
89
|
+
reasoning_effort: Dict[str, str] = field(
|
|
90
|
+
default_factory=lambda: {"large": "high", "medium": "low"}
|
|
91
|
+
)
|
|
92
|
+
# Harvest OpenRouter's rotating free models into the cheapest tier. On by
|
|
93
|
+
# default for out-of-box cost savings; the quality floor (gates) plus the
|
|
94
|
+
# always-strong final attempt keep output safe. Set false to keep code off
|
|
95
|
+
# third-party free endpoints entirely.
|
|
96
|
+
use_free_models: bool = True
|
|
97
|
+
# Semantic context retrieval: rank candidate code symbols by embedding
|
|
98
|
+
# similarity to the task and keep the most relevant when they exceed the
|
|
99
|
+
# context cap, instead of truncating in arbitrary order. On by default and
|
|
100
|
+
# self-regulating (only embeds when selection is actually needed); degrades
|
|
101
|
+
# to arbitrary order if no embedding model is reachable.
|
|
102
|
+
semantic_retrieval: bool = True
|
|
103
|
+
# Which embedder backs semantic retrieval. "auto" = OpenRouter for an
|
|
104
|
+
# OpenRouter provider, else a local fastembed model (free, offline, no key);
|
|
105
|
+
# "local" forces fastembed; "openrouter" forces the API; "none" disables the
|
|
106
|
+
# dense signal (lexical-only ranking).
|
|
107
|
+
embedding_backend: str = "auto"
|
|
108
|
+
# fastembed model used by the local backend (downloaded once, then cached).
|
|
109
|
+
local_embedding_model: str = "BAAI/bge-small-en-v1.5"
|
|
110
|
+
# Empty = auto-pick the cheapest (free-preferred) embedding model from
|
|
111
|
+
# OpenRouter's embeddings catalog, like free-model harvesting; set an id to
|
|
112
|
+
# pin one. Ranking is forgiving, so cheapest is a fine default.
|
|
113
|
+
embedding_model: str = ""
|
|
114
|
+
# Id substrings preferred when auto-selecting among equally-priced embedding
|
|
115
|
+
# models; defaults to code-aware models since we rank code.
|
|
116
|
+
embedding_prefer: List[str] = field(default_factory=lambda: ["code"])
|
|
117
|
+
# Output dimensionality; 0 leaves the model default (param omitted).
|
|
118
|
+
embedding_dimensions: int = 0
|
|
119
|
+
# Weight of the lexical identifier-overlap signal vs dense cosine when
|
|
120
|
+
# ranking context (0 = pure dense, 1 = pure lexical). Lexical also drives
|
|
121
|
+
# ranking on its own when no embedding model is reachable.
|
|
122
|
+
lexical_weight: float = 0.3
|
|
123
|
+
# Whether to use models/providers that train on your inputs. Off by default:
|
|
124
|
+
# OpenRouter routing is constrained to providers that do not store or train
|
|
125
|
+
# on inputs (provider data_collection="deny"), which is what makes harvesting
|
|
126
|
+
# free models safe. Set true to permit training providers (more/cheaper free
|
|
127
|
+
# models, but your code may be used for training).
|
|
128
|
+
allow_training_models: bool = False
|
|
129
|
+
# Memoize gate-passing LLM outputs keyed by the full prompt, so an identical
|
|
130
|
+
# request reuses the prior result instead of calling a model. On by default:
|
|
131
|
+
# it is content-hashed (auto-invalidates when inputs change) and every hit is
|
|
132
|
+
# re-validated through the gates, so it can only save cost, never ship stale
|
|
133
|
+
# code.
|
|
134
|
+
cache: bool = True
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@dataclass
|
|
138
|
+
class BuildSettings:
|
|
139
|
+
max_tasks: int = 30
|
|
140
|
+
max_consecutive_failures: int = 3
|
|
141
|
+
build_timeout: int = 120
|
|
142
|
+
test_timeout: int = 180
|
|
143
|
+
lint_timeout: int = 120
|
|
144
|
+
parallel_analysis: bool = True
|
|
145
|
+
# Global spend ceiling for a run; the master budget constraint.
|
|
146
|
+
budget: float = 100.0
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@dataclass
|
|
150
|
+
class OrchestratorSettings:
|
|
151
|
+
max_consecutive_failures: int = 3
|
|
152
|
+
max_workers: int = 4
|
|
153
|
+
context_budget_tokens: int = 100000
|
|
154
|
+
# Target files at or below this many lines are sent in full; larger files
|
|
155
|
+
# are sent as a symbol outline plus verbatim windows of the task-relevant
|
|
156
|
+
# symbols, so context (and cost) scales with the edit, not the file size.
|
|
157
|
+
large_file_line_threshold: int = 800
|
|
158
|
+
max_task_attempts: int = 3
|
|
159
|
+
integration_gate: bool = True
|
|
160
|
+
# "auto" keys are budget-driven (BaseLLMClient / convergence loop) so the
|
|
161
|
+
# global build.budget is the single master constraint. Typed Any because
|
|
162
|
+
# they accept "auto", a number, or None.
|
|
163
|
+
max_build_iterations: Any = "auto"
|
|
164
|
+
certainty_threshold: float = 0.5
|
|
165
|
+
max_cost_per_task: Any = "auto"
|
|
166
|
+
allow_test_edits: bool = False
|
|
167
|
+
# Optional LSP semantic gate: when on, a language server checks edited files
|
|
168
|
+
# for errors a syntax check misses (undefined names, type errors). Off by
|
|
169
|
+
# default and timeout-bounded so it can never block a build; lsp_timeout
|
|
170
|
+
# caps how long the whole check may take before it is skipped.
|
|
171
|
+
lsp_diagnostics: bool = False
|
|
172
|
+
lsp_timeout: int = 30
|
|
173
|
+
# Optional runtime smoke gate: when on, the built artifact is launched,
|
|
174
|
+
# probed, and asserted to respond before the build is accepted. Off by
|
|
175
|
+
# default and timeout-bounded (runs in a daemon thread) so it can never
|
|
176
|
+
# block a build; missing/incomplete runtime.smoke config makes it a SKIP.
|
|
177
|
+
# The smoke spec itself lives under the top-level ``runtime.smoke`` key.
|
|
178
|
+
runtime_smoke: bool = False
|
|
179
|
+
# Optional web verification gate: when on, a headless browser (Playwright)
|
|
180
|
+
# drives the running web artifact and runs declarative checks (DOM/text
|
|
181
|
+
# presence, no console errors, axe accessibility, screenshot diff). Off by
|
|
182
|
+
# default and timeout-bounded (daemon thread) so it can never block a build;
|
|
183
|
+
# missing config or a missing Playwright/browser makes it a SKIP. The spec
|
|
184
|
+
# lives under the top-level ``runtime.web`` key.
|
|
185
|
+
web_verify: bool = False
|
|
186
|
+
# Optional vision verification gate: when on, a vision model judges whether a
|
|
187
|
+
# captured screenshot satisfies a stated visual requirement. Off by default
|
|
188
|
+
# and timeout-bounded; no config / no model / no network makes it a SKIP. The
|
|
189
|
+
# spec lives under the top-level ``runtime.vision`` key.
|
|
190
|
+
vision_verify: bool = False
|
|
191
|
+
# Optional mutation-score gate: when on, the project's configured mutation
|
|
192
|
+
# command (top-level ``mutation.command``) is run and its parsed score must
|
|
193
|
+
# meet ``mutation.min_score`` — proving the suite kills injected faults, not
|
|
194
|
+
# just passes. Off by default and timeout-bounded; no config / an unparseable
|
|
195
|
+
# score / a timeout is a SKIP, only a score below the floor is a RED.
|
|
196
|
+
mutation_gate: bool = False
|
|
197
|
+
verify_acceptance: bool = True
|
|
198
|
+
llm_acceptance_judge: bool = True
|
|
199
|
+
# Optional goal-completion check: when on, an LLM judge reads the goal,
|
|
200
|
+
# acceptance criteria, and the build's cumulative diff and reports whether the
|
|
201
|
+
# work actually satisfies the goal (gates green != goal met). Off by default.
|
|
202
|
+
# ADVISORY: it records gaps into the report and logs them but does NOT fail
|
|
203
|
+
# the build, unless block_on_goal_gap is also true. Timeout-bounded; no
|
|
204
|
+
# goal/criteria/client, an unparseable verdict, or a judge error is a SKIP.
|
|
205
|
+
goal_check: bool = False
|
|
206
|
+
block_on_goal_gap: bool = False
|
|
207
|
+
goal_check_timeout: int = 60
|
|
208
|
+
# Spec-as-tests (CONSERVATIVE, opt-in, currently DEFERRED): generate a failing
|
|
209
|
+
# test from a task's acceptance criteria before it is implemented. Off by
|
|
210
|
+
# default. The generation primitive lives in core/spec_tests.py and is tested,
|
|
211
|
+
# but it is NOT wired into the execute loop yet: writing a failing test inside
|
|
212
|
+
# the wave loop would flip the integration-gate baseline red and silently
|
|
213
|
+
# disable that gate, which is not control-flow-neutral. When set true today it
|
|
214
|
+
# only logs that the feature is staged-but-not-wired (see the seam in
|
|
215
|
+
# core/spec_tests.py); it never alters the build loop.
|
|
216
|
+
spec_as_tests: bool = False
|
|
217
|
+
# When spec_as_tests is on, a per-task generated spec test is run (scoped,
|
|
218
|
+
# from .orchestrator/spec_tests/) after the task's gates pass. ADVISORY by
|
|
219
|
+
# default: a still-failing spec test is logged/recorded but does not fail the
|
|
220
|
+
# task (the generated test may itself be imperfect). Set this true to make a
|
|
221
|
+
# red spec test fail acceptance and force a retry — strict TDD.
|
|
222
|
+
spec_as_tests_block: bool = False
|
|
223
|
+
# Golden suite: files the model never sees and may never edit, plus a
|
|
224
|
+
# blocking-gate command. Empty/None = feature off.
|
|
225
|
+
golden_paths: List[str] = field(default_factory=list)
|
|
226
|
+
golden_command: Optional[str] = None
|
|
227
|
+
# AB-MCTS spec refinement fires several serial LLM calls before any work;
|
|
228
|
+
# off by default (marginal value, large latency/cost).
|
|
229
|
+
enable_ab_mcts: bool = False
|
|
230
|
+
# Empirical probe discovery (SMART/CREATE Phase 1.5) runs an LLM call plus
|
|
231
|
+
# ephemeral scripts to verify facts before planning. On by default (it grounds
|
|
232
|
+
# the spec), but set false for a cheaper/faster run that skips the pre-work.
|
|
233
|
+
enable_probes: bool = True
|
|
234
|
+
# Auto-detect polyglot sub-projects (targets) when none are declared, so a
|
|
235
|
+
# monorepo gets per-toolchain gate routing with zero config. Off by default
|
|
236
|
+
# (explicit ``targets`` is more precise); detected commands are best-effort.
|
|
237
|
+
auto_targets: bool = False
|
|
238
|
+
# "auto" (worktree isolation on a git repo, else shared), "shared" (one
|
|
239
|
+
# working tree), or "worktree" (always isolate each parallel task).
|
|
240
|
+
parallel_mode: str = "auto"
|
|
241
|
+
auto_detect_dependencies: bool = False
|
|
242
|
+
# Optional MCP (Model Context Protocol) tool awareness: when on, the tools
|
|
243
|
+
# discovered from the servers in the top-level ``mcp.servers`` list are
|
|
244
|
+
# described to the model in the task context so it knows they exist. Off by
|
|
245
|
+
# default and additive only — it never changes the single-shot build loop.
|
|
246
|
+
# The substrate (connect/discover/call) is always available via project.mcp;
|
|
247
|
+
# this flag gates only the awareness injection. Timeout-bounded throughout.
|
|
248
|
+
mcp_enabled: bool = False
|
|
249
|
+
# Optional agentic MCP tool use: when on (and an MCP manager with discovered
|
|
250
|
+
# tools exists), a BOUNDED pre-edit loop lets the model request MCP tool
|
|
251
|
+
# calls to gather information; results are prepended to the task context and
|
|
252
|
+
# the existing edit-generation path runs unchanged. Off by default and purely
|
|
253
|
+
# additive — when off the executor path is byte-identical to today. Each round
|
|
254
|
+
# is timeout-bounded (the tool call goes through MCPManager.call_tool); the
|
|
255
|
+
# loop is hard-capped by ``mcp_max_tool_rounds``. Implies ``mcp_enabled`` for
|
|
256
|
+
# the awareness/registry, but the gathering loop is gated by this flag alone.
|
|
257
|
+
mcp_tool_use: bool = False
|
|
258
|
+
# Hard ceiling on the agentic gathering loop's rounds (see ``mcp_tool_use``).
|
|
259
|
+
# Each round is at most one model turn plus one tool call; the loop always
|
|
260
|
+
# stops at this count even if the model keeps requesting tools.
|
|
261
|
+
mcp_max_tool_rounds: int = 3
|
|
262
|
+
# Optional governance layer: when on, a risk classifier gates risky commands
|
|
263
|
+
# (destructive/irreversible/paid) at the command seam and an append-only
|
|
264
|
+
# audit trail is written. Off by default and additive — when off the command
|
|
265
|
+
# seam is byte-identical to today. In autonomous (non-interactive) mode a
|
|
266
|
+
# risky command is BLOCKED with an escalation record unless
|
|
267
|
+
# ``governance.auto_approve`` is set; in interactive mode it prompts. Ordinary
|
|
268
|
+
# build/test/lint commands classify as SAFE and always run. The policy spec
|
|
269
|
+
# lives under the top-level ``governance`` key.
|
|
270
|
+
governance: bool = False
|
|
271
|
+
# Bounded continuation-on-truncation for the plain text-generation path.
|
|
272
|
+
# When the model cuts a response off at its output-token limit (finish_reason
|
|
273
|
+
# "length"/"max_tokens") — which truncates a large NEW file's full content and
|
|
274
|
+
# fails the edit gate — issue up to this many follow-up calls asking the model
|
|
275
|
+
# to continue exactly where it stopped, concatenating the text before parsing.
|
|
276
|
+
# Activates ONLY on a truncated response, so when a response finishes normally
|
|
277
|
+
# the path is byte-identical to before. 0 disables it (never continues).
|
|
278
|
+
# Default 2: a pure-win correctness fix that costs nothing on untruncated
|
|
279
|
+
# responses and recovers files up to ~3x the single-shot output cap.
|
|
280
|
+
max_continuations: int = 2
|
|
281
|
+
# Optional adversarial edit critic (independent second component). When on, a
|
|
282
|
+
# SECOND component reviews each CANDIDATE edit before it is applied — ideally
|
|
283
|
+
# a DIFFERENT model (``critic.model``) so it does not inherit the generator's
|
|
284
|
+
# blind spots — and either approves it or returns concrete objections that are
|
|
285
|
+
# fed back to the generator for the next attempt. Off by default and purely
|
|
286
|
+
# additive: when off the executor path is byte-identical to today. Best-effort
|
|
287
|
+
# and timeout-bounded (daemon thread); no client, an unparseable verdict, or a
|
|
288
|
+
# timeout is a SKIP that lets the edit proceed to the real gates. The critic is
|
|
289
|
+
# advisory, never authoritative — the build/test gates remain the ground
|
|
290
|
+
# truth; ``critic_max_rejections`` caps how many regenerations it may force per
|
|
291
|
+
# task before deferring to those gates. The independent model id lives under
|
|
292
|
+
# the top-level ``critic.model`` key.
|
|
293
|
+
# True/False force it on/off; "auto" (default) enables it only for the
|
|
294
|
+
# cross-cutting categories where symptom-fixes, incomplete changes, and
|
|
295
|
+
# duplication cluster (refactor/fix/integration), so the quality review runs
|
|
296
|
+
# out of the box on the risky tasks without paying an extra call on every one.
|
|
297
|
+
adversarial_critic: Any = "auto"
|
|
298
|
+
critic_timeout: int = 60
|
|
299
|
+
critic_max_rejections: int = 2
|
|
300
|
+
# Task categories the critic auto-enables for when adversarial_critic="auto".
|
|
301
|
+
critic_auto_categories: List[str] = field(
|
|
302
|
+
default_factory=lambda: ["refactor", "fix", "integration"]
|
|
303
|
+
)
|
|
304
|
+
# Independent completeness-claim verifier. Before a build composes its spec
|
|
305
|
+
# and tasks, each feature the analyzer flagged "incomplete" and each "stub"
|
|
306
|
+
# file is rechecked against the REAL file + tests by a second component
|
|
307
|
+
# (ideally the independent ``judge.model``). A claim it refutes WITH evidence
|
|
308
|
+
# — documented graceful-degradation, a platform no-op, a parity shim, or
|
|
309
|
+
# already-tested code — is dropped, so no budget is spent "fixing" intentional
|
|
310
|
+
# design. Conservative: only a positive refutation drops a claim; unsure, a
|
|
311
|
+
# skip, an error, or a timeout KEEPS it, so genuine work is never lost. On by
|
|
312
|
+
# default — a cheap once-per-build correctness win that degrades to a no-op
|
|
313
|
+
# when no LLM client is available. ``verify_claims_timeout`` bounds each
|
|
314
|
+
# claim's judgment.
|
|
315
|
+
verify_claims: bool = True
|
|
316
|
+
verify_claims_timeout: int = 45
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
PROMPT_TEMPLATES = {
|
|
320
|
+
"system": (
|
|
321
|
+
"You are an expert developer. Follow the strategy, honor interface contracts exactly, "
|
|
322
|
+
"and ensure your output is syntactically valid.\n"
|
|
323
|
+
"{invariants}\n{consensus_context}"
|
|
324
|
+
),
|
|
325
|
+
# Stable context (task, contracts, code) comes first and ends with
|
|
326
|
+
# {cache_breakpoint}; the volatile tail follows. The client caches everything
|
|
327
|
+
# before the breakpoint, so retries re-read it at ~10% input price. Reordering
|
|
328
|
+
# keeps the cached prefix identical across attempts.
|
|
329
|
+
"task_completion_instruction": (
|
|
330
|
+
"## Task\n{task.description}\n\n"
|
|
331
|
+
"## Acceptance Criteria\n{acceptance_criteria}\n\n"
|
|
332
|
+
"## Files to Edit\n{task.target_files}\n\n"
|
|
333
|
+
"## Interface Contracts (MUST honor exact signatures)\n{interface_contracts}\n\n"
|
|
334
|
+
"## Recent Changes to Related Files\n{recent_changes}\n\n"
|
|
335
|
+
"## Scratchpad (learnings from previous tasks)\n{scratchpad}\n\n"
|
|
336
|
+
"## Code Context\n{code_context}\n"
|
|
337
|
+
"{cache_breakpoint}\n"
|
|
338
|
+
"Modify the EXISTING files in scope. Do NOT create a new file that "
|
|
339
|
+
"re-implements functionality that already exists in the Code Context — "
|
|
340
|
+
"locate and edit the real file instead. Output your changes as markdown "
|
|
341
|
+
"code blocks with file paths."
|
|
342
|
+
),
|
|
343
|
+
# error_logs is LAST (after the breakpoint) so the cached prefix — task,
|
|
344
|
+
# contracts, code — stays identical across attempts and only the changing
|
|
345
|
+
# error text is re-sent uncached.
|
|
346
|
+
"error_correction_instruction": (
|
|
347
|
+
"## Task\n{task.description}\n\n"
|
|
348
|
+
"## Interface Contracts\n{interface_contracts}\n\n"
|
|
349
|
+
"## Code Context\n{code_context}\n"
|
|
350
|
+
"{cache_breakpoint}\n"
|
|
351
|
+
"## Previous Attempt Failed\n{error_logs}\n\n"
|
|
352
|
+
"Fix the error. Output corrected code as markdown code blocks with file paths."
|
|
353
|
+
),
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
# Sections whose keys are schema-validated (used for both DEFAULT_CONFIG
|
|
357
|
+
# generation and project.yaml typo detection).
|
|
358
|
+
_SECTION_SCHEMAS = {
|
|
359
|
+
"llm": LLMSettings,
|
|
360
|
+
"build": BuildSettings,
|
|
361
|
+
"orchestrator": OrchestratorSettings,
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
# Generated from the schemas above — do not hand-edit section contents; change
|
|
365
|
+
# the dataclass fields instead.
|
|
366
|
+
DEFAULT_CONFIG = {
|
|
367
|
+
"orchestrator_version": "1.0",
|
|
368
|
+
"llm": asdict(LLMSettings()),
|
|
369
|
+
"tools": [],
|
|
370
|
+
"environment": {"type": "none"},
|
|
371
|
+
# Runtime gate specs (each off unless its orchestrator flag is true and this
|
|
372
|
+
# carries the matching mapping): `smoke` (runtime_smoke), `web` (web_verify),
|
|
373
|
+
# `vision` (vision_verify). Free-form so projects can describe any
|
|
374
|
+
# launch/url/checks/capture/assert/timeout; not schema-validated like the
|
|
375
|
+
# dataclass sections, mirroring how `environment` is an open dict.
|
|
376
|
+
"runtime": {},
|
|
377
|
+
# MCP (Model Context Protocol) servers to connect to for tool discovery.
|
|
378
|
+
# ``servers`` is a list of {name, command, args, transport} mappings (stdio
|
|
379
|
+
# transport). Empty by default; awareness injection is gated separately by
|
|
380
|
+
# orchestrator.mcp_enabled. Open dict like ``runtime``, not schema-validated.
|
|
381
|
+
"mcp": {"servers": []},
|
|
382
|
+
# Mutation-score gate spec (off unless orchestrator.mutation_gate is true and
|
|
383
|
+
# this carries a ``command``): ``command`` (the mutation-testing command),
|
|
384
|
+
# ``min_score`` (floor, a fraction or percentage), ``timeout`` (seconds).
|
|
385
|
+
# Open dict like ``runtime``, not schema-validated, so any tool/command fits.
|
|
386
|
+
"mutation": {},
|
|
387
|
+
# Governance policy spec (off unless orchestrator.governance is true):
|
|
388
|
+
# ``approval_required`` (extra risky regex patterns), ``auto_approve`` (bool,
|
|
389
|
+
# let risky commands run unattended in autonomous mode), ``network``
|
|
390
|
+
# ("none"|"default", container egress control). Open dict like ``runtime``,
|
|
391
|
+
# not schema-validated, so the pattern list and knobs stay free-form.
|
|
392
|
+
"governance": {"network": "default"},
|
|
393
|
+
# Adversarial-critic spec (off unless orchestrator.adversarial_critic is
|
|
394
|
+
# true): ``model`` (an INDEPENDENT critic model id — different from the
|
|
395
|
+
# generator so it doesn't share its blind spots). Empty leaves the critic on
|
|
396
|
+
# the generator's own model with adversarial framing (weaker independence,
|
|
397
|
+
# logged). Open dict like ``runtime``, not schema-validated.
|
|
398
|
+
"critic": {},
|
|
399
|
+
# Multi-target (polyglot) gate routing. Each entry declares a sub-project
|
|
400
|
+
# with its own toolchain: {name, path, build_command, test_command?,
|
|
401
|
+
# lint_command?, typecheck_command?}. A task's gate is routed to the target
|
|
402
|
+
# that owns its files; with no targets (the default) the top-level commands
|
|
403
|
+
# are used unchanged. Open list like ``runtime``, not schema-validated.
|
|
404
|
+
"targets": [],
|
|
405
|
+
# Shared INDEPENDENT judge model for the post-gate goal-completion check and
|
|
406
|
+
# the LLM acceptance judge: ``model`` (an id different from the generator so
|
|
407
|
+
# those judgments don't share its blind spots). Empty leaves each judge on the
|
|
408
|
+
# generator's own model (weaker independence, logged). Open dict like
|
|
409
|
+
# ``runtime``, not schema-validated. (The edit-time critic has its own
|
|
410
|
+
# ``critic.model``; this covers the two post-implementation judges.)
|
|
411
|
+
"judge": {},
|
|
412
|
+
"prompt_templates": PROMPT_TEMPLATES,
|
|
413
|
+
"build": asdict(BuildSettings()),
|
|
414
|
+
"orchestrator": asdict(OrchestratorSettings()),
|
|
415
|
+
"build_command": None,
|
|
416
|
+
"test_command": None,
|
|
417
|
+
"lint_command": None,
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
_SCHEMA_FIELDS = {
|
|
422
|
+
section: frozenset(f.name for f in fields(schema))
|
|
423
|
+
for section, schema in _SECTION_SCHEMAS.items()
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def get_section_setting(section: str, section_cfg: Dict[str, Any], key: str) -> Any:
|
|
428
|
+
"""Like :func:`get_setting`, but for a caller that already holds the section
|
|
429
|
+
sub-dict (e.g. the LLM client receives only ``config["llm"]``).
|
|
430
|
+
|
|
431
|
+
Same two guarantees: single-source default (from DEFAULT_CONFIG) and a typo
|
|
432
|
+
in ``key`` raises instead of silently returning a default.
|
|
433
|
+
"""
|
|
434
|
+
known = _SCHEMA_FIELDS.get(section)
|
|
435
|
+
if known is not None and key not in known:
|
|
436
|
+
raise KeyError(
|
|
437
|
+
f"Unknown config key '{section}.{key}' is not a field of "
|
|
438
|
+
f"{_SECTION_SCHEMAS[section].__name__}. Known: {sorted(known)}"
|
|
439
|
+
)
|
|
440
|
+
if key in (section_cfg or {}):
|
|
441
|
+
return section_cfg[key]
|
|
442
|
+
return DEFAULT_CONFIG.get(section, {}).get(key)
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def get_setting(config: Dict[str, Any], section: str, key: str) -> Any:
|
|
446
|
+
"""Read config[section][key], falling back to the canonical DEFAULT_CONFIG.
|
|
447
|
+
|
|
448
|
+
Two guarantees:
|
|
449
|
+
- Single source of truth for defaults: call sites pass no literal default,
|
|
450
|
+
so a key's default lives in exactly one place (the dataclass schema).
|
|
451
|
+
- Typo-safe: for a schema-validated section, an unknown ``key`` raises
|
|
452
|
+
immediately (caught in tests/dev) instead of silently returning a default.
|
|
453
|
+
This is the full-sweep typo protection without replacing the config dict.
|
|
454
|
+
"""
|
|
455
|
+
return get_section_setting(section, config.get(section) or {}, key)
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def warn_unknown_keys(project_config: Dict[str, Any]) -> List[str]:
|
|
459
|
+
"""Warn about keys in a schema-validated section that the schema doesn't know.
|
|
460
|
+
|
|
461
|
+
Catches a user's typo in project.yaml (e.g. ``buildtimeout: 300``) which the
|
|
462
|
+
deep-merge would otherwise silently ignore. Returns the unknown keys found.
|
|
463
|
+
"""
|
|
464
|
+
unknown: List[str] = []
|
|
465
|
+
for section, schema in _SECTION_SCHEMAS.items():
|
|
466
|
+
sub = project_config.get(section)
|
|
467
|
+
if not isinstance(sub, dict):
|
|
468
|
+
continue
|
|
469
|
+
known = {f.name for f in fields(schema)}
|
|
470
|
+
for key in sub:
|
|
471
|
+
if key not in known:
|
|
472
|
+
unknown.append(f"{section}.{key}")
|
|
473
|
+
logger.warning(
|
|
474
|
+
f"Unknown config key '{section}.{key}' in project.yaml is "
|
|
475
|
+
f"ignored (typo? known {section} keys: {sorted(known)})"
|
|
476
|
+
)
|
|
477
|
+
return unknown
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
class ConfigManager:
|
|
481
|
+
def __init__(self):
|
|
482
|
+
import copy
|
|
483
|
+
|
|
484
|
+
self.global_config = copy.deepcopy(DEFAULT_CONFIG)
|
|
485
|
+
|
|
486
|
+
def _deep_update(self, d: Dict[Any, Any], u: Dict[Any, Any]) -> Dict[Any, Any]:
|
|
487
|
+
"""Deep merge two dictionaries."""
|
|
488
|
+
for k, v in u.items():
|
|
489
|
+
if isinstance(v, dict) and k in d and isinstance(d[k], dict):
|
|
490
|
+
d[k] = self._deep_update(d[k], v)
|
|
491
|
+
else:
|
|
492
|
+
d[k] = v
|
|
493
|
+
return d
|
|
494
|
+
|
|
495
|
+
def load_project_config(self, project_path: str | Path) -> Dict[str, Any]:
|
|
496
|
+
"""Loads and merges project.yaml with global defaults."""
|
|
497
|
+
project_dir = Path(project_path)
|
|
498
|
+
yaml_path = project_dir / "project.yaml"
|
|
499
|
+
|
|
500
|
+
project_config = {}
|
|
501
|
+
if yaml_path.exists():
|
|
502
|
+
try:
|
|
503
|
+
with open(yaml_path, "r", encoding="utf-8") as f:
|
|
504
|
+
loaded_config = yaml.safe_load(f)
|
|
505
|
+
if isinstance(loaded_config, dict):
|
|
506
|
+
project_config = loaded_config
|
|
507
|
+
logger.info(f"Loaded project configuration from {yaml_path}")
|
|
508
|
+
except Exception as e:
|
|
509
|
+
logger.error(f"Failed to load project config at {yaml_path}: {e}")
|
|
510
|
+
else:
|
|
511
|
+
logger.warning(f"No project.yaml found at {project_dir}")
|
|
512
|
+
|
|
513
|
+
# Surface typo'd keys before they get silently ignored by the merge.
|
|
514
|
+
warn_unknown_keys(project_config)
|
|
515
|
+
|
|
516
|
+
# Merge defaults with project config (deep copy to avoid mutating defaults)
|
|
517
|
+
import copy
|
|
518
|
+
|
|
519
|
+
merged_config = copy.deepcopy(self.global_config)
|
|
520
|
+
merged_config = self._deep_update(merged_config, project_config)
|
|
521
|
+
return merged_config
|
|
File without changes
|
misterdev/core/audit.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Append-only audit trail (structured JSONL).
|
|
2
|
+
|
|
3
|
+
Records one JSON line per significant event — command run + exit, edit applied,
|
|
4
|
+
gate result, tool call — to ``.orchestrator/audit.jsonl`` under the project
|
|
5
|
+
root. Each line carries an ISO-8601 timestamp, an event ``type``, and
|
|
6
|
+
event-specific details.
|
|
7
|
+
|
|
8
|
+
Append-only and crash-tolerant: :meth:`AuditTrail.record` NEVER raises into the
|
|
9
|
+
caller. An unwritable path, a full disk, or a serialization failure is logged at
|
|
10
|
+
debug and dropped — audit is observability, so a logging failure must never
|
|
11
|
+
break a build. This mirrors the never-hang/never-hard-fail discipline of
|
|
12
|
+
:mod:`misterdev.core.execution.container` and ``lsp``.
|
|
13
|
+
|
|
14
|
+
Defaults ON: it is pure-win observability with no behavioral effect (it only
|
|
15
|
+
appends to a gitignored file under ``.orchestrator/``) and degrades silently if
|
|
16
|
+
the path is unwritable, so enabling it by default cannot regress a build. A
|
|
17
|
+
caller that wants it off passes ``enabled=False`` (or a null trail).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import threading
|
|
22
|
+
from datetime import datetime, timezone
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any, Optional
|
|
25
|
+
|
|
26
|
+
from misterdev.logging_setup import setup_logger
|
|
27
|
+
|
|
28
|
+
logger = setup_logger(__name__)
|
|
29
|
+
|
|
30
|
+
_AUDIT_DIRNAME = ".orchestrator"
|
|
31
|
+
_AUDIT_FILENAME = "audit.jsonl"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _now_iso() -> str:
|
|
35
|
+
return datetime.now(timezone.utc).isoformat()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class AuditTrail:
|
|
39
|
+
"""Append-only JSONL writer for build events.
|
|
40
|
+
|
|
41
|
+
One instance per build, rooted at the project path. Writes are guarded by a
|
|
42
|
+
lock so concurrent gate threads cannot interleave partial lines. Construction
|
|
43
|
+
never raises; the directory is created lazily on first write so an unwritable
|
|
44
|
+
root degrades to a no-op instead of failing at setup.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self, project_path: Path, enabled: bool = True):
|
|
48
|
+
self.enabled = enabled
|
|
49
|
+
self.path = Path(project_path) / _AUDIT_DIRNAME / _AUDIT_FILENAME
|
|
50
|
+
self._lock = threading.Lock()
|
|
51
|
+
|
|
52
|
+
def record(self, event_type: str, **details: Any) -> None:
|
|
53
|
+
"""Append one event line. Never raises.
|
|
54
|
+
|
|
55
|
+
``event_type`` is the event category (``command``, ``edit``, ``gate``,
|
|
56
|
+
``tool``, ``model``, ...); ``details`` are merged into the line. A
|
|
57
|
+
timestamp and the type are always present. Any failure (bad path,
|
|
58
|
+
non-serializable detail, I/O error) is logged at debug and swallowed.
|
|
59
|
+
"""
|
|
60
|
+
if not self.enabled:
|
|
61
|
+
return
|
|
62
|
+
entry = {"ts": _now_iso(), "type": event_type}
|
|
63
|
+
# Merge details, coercing anything non-serializable to a string so a
|
|
64
|
+
# surprising value can never make the whole line unwritable.
|
|
65
|
+
for key, value in details.items():
|
|
66
|
+
try:
|
|
67
|
+
json.dumps(value)
|
|
68
|
+
entry[key] = value
|
|
69
|
+
except (TypeError, ValueError):
|
|
70
|
+
entry[key] = str(value)
|
|
71
|
+
try:
|
|
72
|
+
line = json.dumps(entry, ensure_ascii=False)
|
|
73
|
+
except (TypeError, ValueError) as e:
|
|
74
|
+
logger.debug(f"Audit entry not serializable, dropped: {e}")
|
|
75
|
+
return
|
|
76
|
+
try:
|
|
77
|
+
with self._lock:
|
|
78
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
79
|
+
with open(self.path, "a", encoding="utf-8") as fh:
|
|
80
|
+
fh.write(line + "\n")
|
|
81
|
+
except OSError as e:
|
|
82
|
+
logger.debug(f"Audit write failed (dropped): {e}")
|
|
83
|
+
|
|
84
|
+
def record_command(self, command: str, ok: bool, cwd: Optional[str] = None) -> None:
|
|
85
|
+
self.record("command", command=command, ok=ok, cwd=cwd)
|
|
86
|
+
|
|
87
|
+
def record_edit(self, path: str, action: str = "apply") -> None:
|
|
88
|
+
self.record("edit", path=path, action=action)
|
misterdev/core/config.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Compatibility re-export of the package-level configuration.
|
|
2
|
+
|
|
3
|
+
The canonical configuration lives in ``misterdev.config``. This
|
|
4
|
+
module exposes the same ``DEFAULT_CONFIG`` and ``ConfigManager`` under the
|
|
5
|
+
``core`` namespace so callers can import from either location.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from misterdev.config import DEFAULT_CONFIG, ConfigManager
|
|
9
|
+
|
|
10
|
+
__all__ = ["DEFAULT_CONFIG", "ConfigManager"]
|
|
File without changes
|