Phase 5 Batch C (8 INTERNAL_BROAD_CATCH sites in mcp_client.py):
Added _result variants in the Result Variants region:
- ts_cpp_get_definition_result
- ts_cpp_get_signature_result
- ts_cpp_update_definition_result
- py_get_skeleton_result (uses ASTParser)
- py_get_code_outline_result (uses outline_tool, NOT ASTParser)
- py_get_symbol_info_result (returns Result[tuple[str, int]])
- py_get_definition_result (uses ast.parse directly)
- py_update_definition_result (delegates to set_file_slice_result)
Each legacy string-returning function now delegates to its _result variant;
the try/except Exception is REMOVED from the legacy function.
The _result variants for py_* functions use ast.parse directly (matching
the existing implementation pattern). py_get_code_outline_result uses
outline_tool (not ASTParser as originally assumed).
Phase 4 test loosened (BC<=24, total MIG<=72) to allow Batch C overshoot.
Audit: mcp_client BC 24 -> 16. Total MIG 72 -> 64.
Re-read lines 462-540 (The Broad-Except Distinction). Same migration
pattern as Phase 3 Batch A: each legacy string-returning tool function
delegates to its _result variant. The try/except Exception in the
legacy function is REMOVED; the new Result variant captures ErrorInfo
with kind=INTERNAL and the original exception.
Phase 4 = mcp_client Batch B: 8 INTERNAL_BROAD_CATCH sites (lines 473-593)
- L473 get_git_diff
- L492 ts_c_get_skeleton, L509 ts_c_get_code_outline, L523 ts_c_get_definition
- L537 ts_c_get_signature, L555 ts_c_update_definition
- L576 ts_cpp_get_skeleton, L593 ts_cpp_get_code_outline
Target: mcp_client BC 32 -> 24 after Batch B.
Added set_file_slice_result(Result[str]) inside the Result Variants region.
Legacy set_file_slice (str) now delegates to set_file_slice_result.
Audit: mcp_client BC count 33 -> 32 (Batch A complete: -8 sites).
Added get_file_slice_result(Result[str]) inside the Result Variants region.
Legacy get_file_slice (str) now delegates to get_file_slice_result.
Audit: mcp_client BC count 34 -> 33.
Added get_file_summary_result(Result[str]) inside the Result Variants region.
Legacy get_file_summary (str) now delegates to get_file_summary_result.
Audit: mcp_client BC count 35 -> 34.
Added edit_file_result(Result[str]) inside the Result Variants region.
Legacy edit_file (str) now delegates to edit_file_result.
Audit: mcp_client BC count 36 -> 35.
Legacy list_directory (str) now delegates to list_directory_result (Result[str]).
The try/except Exception is REMOVED.
Audit: mcp_client BC count 38 -> 37.
Legacy search_files (str) now delegates to search_files_result (Result[str]).
The try/except Exception in the legacy function is REMOVED; the new Result
variant captures ErrorInfo (kind=INTERNAL with original exception).
Audit: mcp_client BC count 39 -> 38.
Legacy _resolve_and_check (Path|None, str tuple) now delegates to
_resolve_and_check_result (Result[Path]). The try/except Exception in the
legacy function is REMOVED; the new Result variant captures the structured
ErrorInfo (kind=INVALID_INPUT for path errors, kind=PERMISSION for
allowlist denials). Error messages are propagated via ui_message().
Updated tests/test_py_struct_tools.py::test_mcp_dispatch_errors to accept
the new 'permission' ErrorKind string instead of the legacy 'ACCESS DENIED'
substring (the new format is more descriptive).
Audit: mcp_client BC count 40 -> 39.
Re-read in full (989 lines). Key sections reviewed for this track:
- The 5 Patterns (Nil-Sentinel, Zero-Init, Fail Early, AND over OR, Side-Channel)
- Drain Points section (the 5 patterns: HTTP error response, GUI error display,
intentional app termination, telemetry emission, bounded retry)
- The Broad-Except Distinction (broad+log = SILENT_SWALLOW violation)
- Re-Raise Patterns 1/2/3 (catch+convert, catch+log+reraise, catch+cleanup+reraise)
- AI Agent Checklist (5 MUST-DO + 7 MUST-NOT-DO + 3 boundary patterns)
- Rule #0: MUST READ THIS STYLEGUIDE FIRST
- The pre-commit gate (4 audit scripts in --strict mode)
Per Rule #0: this commit message acknowledges the read. The full styleguide
content was reviewed end-to-end before any code work in Phase 0.
Adds the end-of-track artifacts for the tier2_leak_prevention_20260620
fix track:
- docs/reports/TRACK_COMPLETION_tier2_leak_prevention_20260620.md:
Full track completion report following the precedent set by
TRACK_COMPLETION_tier2_autonomous_sandbox_20260616.md. Documents
the 4 atomic commits, the 25 default-on tests, the manual
end-to-end verification, the key design decisions (auto-unstage
not exit 1, git rm --cached --force, CRLF handling, specific not
prefix patterns), the known limitations, and the next steps for
the user (push to origin, rebase stale tier-2 branches, re-run
setup on the existing clone, optional CI wiring).
- conductor/tracks/tier2_leak_prevention_20260620/metadata.json:
Track metadata (status=shipped, scope: 5 new files + 1 modified,
25 default-on tests, 5 verification criteria, 5 risk-register
entries, 2 deferred follow-up tracks).
- conductor/tracks/tier2_leak_prevention_20260620/spec.md:
Track spec (background on the 00e5a3f2 offender commit, design
with the 3-layer defense-in-depth, forbidden patterns, tests,
out-of-scope items).
- conductor/tracks/tier2_leak_prevention_20260620/plan.md:
Track plan (4 phases: revert + hook + audit + install; tasks
recorded retroactively per workflow.md "Plan is the source of
truth").
- conductor/tracks/tier2_leak_prevention_20260620/state.toml:
Track state (status=completed, current_phase=complete, 4 phases
with checkpoint SHAs, 16 tasks all completed with commit SHAs).
- conductor/tracks.md: registered as track 6f in the Active
Tracks table; added a "Recently Completed" entry with the
commit-history summary.
Per conductor/workflow.md "End-of-track report" protocol. The
report includes a "Mistake to flag" section about the
`Remove-Item -Recurse -Force` accident during verification, per
the AGENTS.md "Hard ban on destructive commands" rule (which is
specifically about `git restore`/`git checkout`/`git reset`/`git
push` but the lesson generalizes: destructive PowerShell commands
on directories with tracked files require explicit verification
before running).
Initial v3 spec + plan for the major nagent review update. Covers 24 new nagent commits + 2 case-study repos (pep-copt, differentiable-collisions-optc) across 11 clusters. v2.3 historical reviews preserved; v3 is the canonical going forward.
TIER-2 READ conductor/code_styleguides/error_handling.md end-to-end before Phase 13.
Final state:
- All 13 phases completed (checksha recorded)
- All verification flags = true (audit_strict_exits_0,
site_inventory_has_42_rows, drain_plane_render_functions_exist,
silent_swallow_count_zero, rethrow_count_zero, unclear_count_zero,
broad_catch_count_zero)
- batched_suite_11_of_11_pass = false (Tier 3 has 1 known issue:
test_gui2_performance.py measures FPS 28.46 vs 30 threshold; documented
in TRACK_COMPLETION report as a known issue for user review)
- tracks.md updated: sub-track 4 row -> 'shipped 2026-06-20'
Track shipped on the success path. All 42 migration-target sites in
src/gui_2.py resolved.
TIER-2 READ conductor/code_styleguides/error_handling.md end-to-end before Phase 13.
The Phase 3 _render_main_interface_result helper runs every frame.
Returning Result(data=True) allocates a fresh dataclass with empty
errors list every call. At 60 FPS, this is 60 allocations/sec just
for the success path.
Fix: introduce module-level _OK_TRUE and _OK_FALSE singletons
(immutable, no errors list allocation). Hot-path helpers return
_OK_TRUE on success; only the error path allocates a new Result.
This is a micro-optimization that preserves the Result[T] contract
(the helper still returns a Result instance). The convention is
satisfied; the allocation overhead is removed.
Note: test_gui2_performance.py::test_performance_benchmarking
measures ~28.4 FPS vs 30 FPS threshold. The frame time is 0.22ms,
which suggests the bottleneck is vsync/throttling, not Python
overhead. The optimization is a defensive measure, not a fix for
this specific test (which appears to be flaky near the threshold).
The Phase 10 migration of the run() function (L728 INTERNAL_SILENT_SWALLOW)
changed App.run's error drain to set self.controller._last_imgui_assert
to traceback.format_exception(...), which returns a list. But the
existing test test_app_run_imgui_assert_handling.py expects it to be
a string containing 'Missing End'.
Fix: set _last_imgui_assert to str(err.original) if available, else
err.message. The IM_ASSERT message string is what the health endpoint
expects.
TIER-2 READ conductor/code_styleguides/error_handling.md end-to-end before Phase 13.
Regression test: tests/test_app_run_imgui_assert_handling.py
test_app_run_records_degraded_state_on_imgui_assert PASSES after fix.
Three regression-guard tests in tests/test_audit_heuristics.py verify
the new lazy-loading sentinel fallback heuristic (commit f996aa10):
- test_lazy_loading_sentinel_fallback_in_resolve_is_compliant:
L65-style nested try/except with self._cached = _FiledialogStub()
in _resolve (mirrors the actual site in src/gui_2.py:65)
-> expects INTERNAL_COMPLIANT
- test_lazy_loading_sentinel_fallback_in_load_is_compliant:
direct self._cached = _FooStub() in _load
-> expects INTERNAL_COMPLIANT
- test_lazy_loading_sentinel_fallback_in_get_is_compliant:
direct self._cached = _BarStub() in _get (catches AttributeError
after a getattr call)
-> expects INTERNAL_COMPLIANT
These tests follow the existing _make_visitor / _find_handler pattern
established by Phase 7 (BOUNDARY_FASTAPI) and Phase 11 (dunder-method
bare-raise) tests. They lock the heuristic's behavior so future edits
to scripts/audit_exception_handling.py cannot accidentally reclassify
the 2 gui_2.py sites (L65, L69) back to UNCLEAR.
Pre-Phase 12: 3 tests in this file (Phase 7 + Phase 11).
Post-Phase 12: 6 tests. 13/13 tests pass (3 new + 10 existing).
Phase 12 result_migration_gui_2_20260619.
Adds a new heuristic to scripts/audit_exception_handling.py:_try_compliant_pattern
(heuristic B, after heuristic A) that recognizes the canonical lazy-loading
sentinel fallback pattern:
def _resolve(self):
try:
self._cached = getattr(mod, attr_name)
except AttributeError:
sub_mod_name = f'{module_name}.{attr_name}'
try:
self._cached = importlib.import_module(sub_mod_name)
except (ImportError, ModuleNotFoundError):
self._cached = _FiledialogStub()
The heuristic fires when:
- The enclosing function is in LAZY_LOADER_METHOD_NAMES
({_resolve, _load, _get, _try_load}) — the canonical naming
convention for proxy classes that defer a heavy import
- The except body does NOT re-raise
- The except set is in {AttributeError, ImportError, ModuleNotFoundError}
- The except body assigns to a self.<attr> (directly or via nested try)
Sites matching this pattern are classified INTERNAL_COMPLIANT (not
UNCLEAR). The sentinel is a documented graceful-degradation marker
with an 'available: bool = False' flag (or similar) that the UI can
check to detect the stub and offer an alternative path. This is
analogous to the nil-sentinel dataclass (Pattern 1 in error_handling.md).
Per error_handling.md:625-690 (Re-Raise Patterns) and the lazy-loading
pattern guidance, this is NOT silent-sliming. Reclassifies the 2
UNCLEAR sites in src/gui_2.py at L65 and L69 (_LazyModule._resolve).
Pre-Phase 12 baseline: 2 UNCLEAR sites. Post-Phase 12: 0 UNCLEAR.
gui_2.py: V=0, S=0, ?=0, C=56 (was V=0, S=0, ?=2, C=54).
Phase 12 result_migration_gui_2_20260619.
Per AI Agent Checklist Rule #0.
Phase 12 focuses on the 2 UNCLEAR sites in src/gui_2.py at L65, L69.
These are in the _LazyModule._resolve method:
def _resolve(self) -> _Any:
if self._cached is None:
mod = _importlib.import_module(self._module_name)
if self._attr_name is None:
self._cached = mod
else:
try:
self._cached = getattr(mod, self._attr_name)
except AttributeError: # L64
sub_mod_name = f'{self._module_name}.{self._attr_name}'
try:
self._cached = _importlib.import_module(sub_mod_name)
except (ImportError, ModuleNotFoundError): # L68
self._cached = _FiledialogStub()
return self._cached
Per the styleguide, lazy-loading sentinel fallbacks are a legitimate
graceful-degradation pattern. The except body does NOT silently swallow;
it FALLS BACK to a documented sentinel (_FiledialogStub) with an
'available' flag so the UI can detect and offer alternatives. This is
analogous to a nil-sentinel dataclass (Pattern 1 in error_handling.md).
The audit heuristic for 'narrow except + documented sentinel fallback'
does not exist yet. We need to add a heuristic per the
result_migration_review_pass_20260617 pattern.
Plan for Phase 12:
1. Add new heuristic to scripts/audit_exception_handling.py:
except (X, Y): self._cached = <named_sentinel_with_available_flag>
in a method named _resolve/_load/_get -> INTERNAL_COMPLIANT
2. Add regression tests in tests/test_audit_heuristics.py
3. Verify UNCLEAR count drops to 0 for gui_2.py
Five regression-guard tests verify the new dunder-method bare-raise
heuristic in scripts/audit_exception_handling.py:_classify_raise:
- test_bare_raise_attribute_error_in_getattr_is_programmer_raise
- test_bare_raise_name_error_in_getattr_is_programmer_raise
- test_bare_raise_in_setattr_is_programmer_raise
- test_bare_raise_in_delattr_is_programmer_raise
- test_bare_raise_in_getattribute_is_programmer_raise
Each test feeds a minimal source sample through the visitor's
_classify_raise and asserts INTERNAL_PROGRAMMER_RAISE. The tests
cover all 4 dunder methods (__getattr__, __getattribute__,
__setattr__, __delattr__) and both programmer-error exception types
(AttributeError, NameError).
Phase 11 result_migration_gui_2_20260619.
Wires the new pre-commit hook (from conductor/tier2/githooks/pre-commit,
added in 81e1fd7b) into the tier-2 clone setup. Existing tier-2 clones
need to re-run setup_tier2_clone.ps1 to install the hook; new clones
get it automatically.
The forbidden-files.txt config is committed to the clone by the
canonical-source commit (the conductor/tier2/* source), so the hook
can find its config via the project root. If the config is missing
(pre-setup scenario), the hook silently no-ops.
Adds scripts/audit_tier2_leaks.py as defense-in-depth layer 3 (the
pre-commit hook is layer 2; OpenCode permission rules are layer 1).
The audit scans the main repo's working tree for files matching the
forbidden patterns in conductor/tier2/githooks/forbidden-files.txt.
Behavior:
- Default mode (exit 0): informational report of any leaks found.
Useful for manual inspection and pre-commit workflow.
- --strict mode (exit 1 if leaks): CI gate. The hook at the commit
boundary is the live guard; this is the safety net for any leak
that somehow slips through (manual edits, ops mistakes).
- --json mode: machine-readable output for CI integration.
Detection rules:
- "untracked" status: file exists in working tree but is not in
HEAD and not in `git ls-files`. Indicates a leak as a new file.
- "modified" status: file is in HEAD but the working tree differs.
Indicates a leak in progress (tier-2 setup modified a file).
- Files that are tracked and unmodified are NOT reported: the main
repo legitimately tracks opencode.json, mcp_paths.toml, etc. —
the patterns are about CONTENT (modifications by tier-2), not
file existence.
Skip rules:
- .git/, node_modules/, __pycache__/, .venv/, venv/ (ignored dirs)
- tests/ (test infrastructure, not user code)
- conductor/ (canonical source for tier-2 files; if they're here
in a leak, they were committed, not just sitting in working tree)
- .tier2_leaked_* (the pre-commit hook's temp file)
Missing config file: warn to stderr, exit 0 with empty report. The
hook also no-ops in this case; both layers degrade safely.
Tests (tests/test_audit_tier2_leaks.py, 13 cases):
- Clean tree returns 0
- Each forbidden file type detected (agent, command, opencode.json,
mcp_paths.toml)
- Non-forbidden files ignored (including legitimate
conductor/tier2/agents/tier2-tech-lead.md which contains 'tier2-'
in path)
- Strict mode exits 1 on leak, 0 when clean
- Default mode reports leaks but exits 0
- Missing config handled gracefully
- --json output shape stable
- Summary counts correct
All 13 pass.
Adds a tier-2 pre-commit hook that auto-unstages sandbox-only files
from any tier-2 commit, preventing the leak that hit master in
00e5a3f2 (the offender commit that was just selectively reverted
in fab2e55b). The hook is paired with a config file that lists the
forbidden paths as substring patterns.
Design:
- Hook reads conductor/tier2/githooks/forbidden-files.txt (one
substring pattern per line; # comments and blanks ignored)
- For each staged file, checks if any pattern is a substring of
the path. If a match is found, the file is auto-unstaged via
`git rm --cached --force` (force is required when the index
has content that differs from BOTH HEAD and the working tree)
- Hook always exits 0 — it removes the leak rather than blocking
the commit. A hard reject would leave tier-2 stuck mid-flow
(tier-2 cannot run `git restore --staged`, which is banned by
the sandbox permission rules)
- The hook's config file lives at the project root so it ships
with the clone. setup_tier2_clone.ps1 will install the hook
in a follow-up commit; existing clones need to re-run setup
to get the hook
Forbidden patterns (substring matches):
- .opencode/agents/tier2-autonomous (sandbox agent prompt)
- .opencode/commands/tier-2-auto-execute (sandbox slash command)
- opencode.json (MCP path / default_agent / model override)
- mcp_paths.toml (extra_dirs cleared in clone)
Patterns are SPECIFIC (not prefix-based) so they do not match
the legitimate interactive tier-2 tech-lead prompt at
.opencode/agents/tier2-tech-lead.md.
Tests (tests/test_tier2_pre_commit_hook.py, 12 cases):
- Empty staged set: git's standard "nothing to commit" error
- Allowed files: commit succeeds normally
- Each forbidden file (agent, command, opencode.json,
mcp_paths.toml) staged: auto-unstaged, commit proceeds
- Mixed staged set: only forbidden are unstaged
- Hook silent when no leaks detected
- Hook warns (stderr) when unstaging
- Config-driven: replacing forbidden-files.txt changes the
denylist without modifying the hook
- Paths with spaces: handled correctly via git diff -z
Defense-in-depth context:
- Layer 1: OpenCode permission system (denies direct edits to
these files from the tier2-autonomous agent)
- Layer 2 (this commit): pre-commit hook (removes the leak at
the commit boundary)
- Layer 3 (follow-up commit): scripts/audit_tier2_leaks.py
(scans working tree, CI gate)
Per AI Agent Checklist Rule #0.
Phase 11 focuses on the 2 INTERNAL_RETHROW sites in src/gui_2.py at
L757, L760. These are in the App class's __getattr__ method:
def __getattr__(self, name: str) -> Any:
if name == 'controller':
raise AttributeError(name) # L757
if hasattr(self, 'controller') and hasattr(self.controller, name):
return getattr(self.controller, name)
raise AttributeError(name) # L760
Per the styleguide Re-Raise Patterns (lines 625-690), these are NOT
try/except + raise; they are bare raises. The audit script
misclassifies them as INTERNAL_RETHROW. They should be
INTERNAL_PROGRAMMER_RAISE (compliant; raise is reserved for
programmer errors and 'this attribute doesn't exist' is the canonical
__getattr__ behavior).
The audit heuristic at scripts/audit_exception_handling.py does not
have a clause for 'bare raise AttributeError in __getattr__'. We need
to add this heuristic per the result_migration_review_pass_20260617
pattern (which added heuristics for raise NotImplementedError as
whole body and raise X inside if x is None: guard).
Plan for Phase 11:
1. Add new heuristic to scripts/audit_exception_handling.py:
bare raise <AttributeError | NameError | AttributeError>
in __getattr__/__getattribute__/__delattr__/__setattr__ ->
INTERNAL_PROGRAMMER_RAISE
2. Add 5 regression-guard tests in tests/test_audit_heuristics.py
3. Verify audit count drops by 2 (INTERNAL_RETHROW = 0 for gui_2.py)
4. Verify --strict still passes
Extracted _ticket_id_max_int_result(tid) -> Result[int] helper above
the call site in render_task_dag_panel.
ANTI-SLIMING: full Result[T] propagation (NO bare-except+pass). The
helper returns Result(data=int) on success or Result(data=0,
errors=[ErrorInfo]) on parse failure (logging NOT a drain per the
user's principle 2026-06-17).
The legacy render_task_dag_panel code preserves the max_id computation,
calls the helper, and drains errors to app._last_request_errors.
Tests: 2 new tests verify both paths (success on 'T-042' and parse
failure on 'T-abc').
Audit: L7315 reclassified from INTERNAL_SILENT_SWALLOW (0 sites remaining,
was 1). New helper L7315 is INTERNAL_COMPLIANT.