systematic-debugging
Use when encountering a bug, test failure, or unexpected behavior, before proposing fixes
Install
npx skills add https://github.com/GanyuanRan/Aegis/tree/main/skills/systematic-debugging
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install ganyuanran-aegis@llmmart
git clone https://github.com/GanyuanRan/Aegis.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole ganyuanran/aegis collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Execute
Bug, failure, or unexpected behavior:
- Isolate — read error, reproduce, inspect the diff, and drill upward through diagnostic layers: L1 symptom → L2 logic → L3 system → L4 architecture → L5 cross-system contract → L6 platform → L7 spec gap. Layers are observation altitudes, not one causal chain; the causal shape at the stop altitude is classified explicitly before any root claim. Stop only when causal proof accounts for the recurrence generator or reaches a T-class boundary.
- Identify owner — compare working behavior, trace the bad value, locate the canonical owner, and treat duplicate owners as a finding.
- Decide before editing — Before fixing, run Patch-Shape Triage and Ripple Signal Triage when shared logic,
contracts, fallbacks, adapters, producer/consumer seams, or source-of-truth
boundaries are involved. Surface Change Necessity for any new source-code
path or non-trivial source edit. Run Minimality Check for a new branch,
fallback, adapter, owner, or compatibility path, and Pre-Edit Complexity
Check for an overloaded owner or complexity growth. After Change Necessity
selects
code-changeand before the first repair edit, own the TDD Route for the repair slice pertest-driven-development(offdefault;stricton behavior/bugfix/shared/contract/persistence/permission/migration risk). - Prove — test one hypothesis with the smallest reproduction or
verification. A failing test first is required only by a recorded
TDD Route: strict; withTDD Mode: off, do not require a failing test or RED/GREEN cycle. Three failed fixes means stop and question architecture. - Repair and close — fix minimally at the canonical owner, verify in proportion to risk, review architecture, and close both repair and retirement tracks. If any symptom remains, stop and diagnose it separately.
Done: confidence ≥ B, causal status matches recurrence evidence or an external terminal, tracks explicit, no H signal, and required D evidence passes.
Core invariant
Find root cause and fix the bug class at its canonical owner. A minimal fix is not the smallest textual diff; it is the smallest sufficient owner-level repair.
Quick bug lane
For a low-risk, reproducible, single-owner bug with no patch-shape signal, keep
the readback compact: Symptom, Reproduction, Root Cause, Change Necessity, Fix Boundary, and Verification. Skip the causal card only when
the causal-proof owner's Quick Exit Proof passes.
Quick bug lane must surface Change Necessity before source edits. One sentence
may cover the user-visible need, no-change/non-code option, why code must
change, minimum boundary, and an explicit decision token such as
Decision: code-change. If shared logic, a contract, fallback, duplicate
owner, consumer patch, or cross-module behavior appears, leave this lane.
Aegis Visibility names the evidence/owner/patch-shape/verification effect.
Pass root cause, avoided misfix, boundary, evidence, complexity, and risk to
verification-before-completion; no separate receipt.
Diagnose before repair
- Read the complete error/stack and record inputs, environment, versions, and success criteria.
- Reproduce consistently. If unstable, read
feedback-loop-construction.mdonly when evidence shows intermittent or timing-dependent reproduction and build a bounded loop. Shrink the repro to load-bearing elements as the test input, never the fix scope: still drill upward; test at the correct seam. - Inspect recent changes and compare a working example. Code is evidence; if
authority, glossary, code, and tests disagree, compose
establishing-project-contextrather than silently redefining a term. - Instrument component boundaries, then trace the bad value toward its source.
Read
root-cause-tracing.mdonly when the observed bad value is several calls or components downstream from its origin. - State one hypothesis and falsify it with one-variable evidence. Do not stack
speculative fixes. End each loop with
Goal | DeeperCause | Evidence | Risk/Unknown | Decision.
Canonical-owner and patch-shape gate
Before editing, continue upward unless evidence proves the local site is the canonical owner when the candidate is any of these signals:
- keyword, phrase, regex, negation-word list, or sample-text exception;
- local guard, extra conditional,
try/catch, early return, or one-off branch; - fallback, adapter, compatibility branch, prompt branch, or legacy path expansion;
- consumer/caller/readiness/presentation-layer patch;
- downstream logic re-parses raw text or re-infers action/state while typed intent, normalized state, contract, or another source-of-truth exists;
- artifact/download/export/readback/cache patch without producer/owner proof.
PatchShape:
CanonicalOwner:
UpwardDrillSignal:
Decision: fix owner | continue investigation | escalate
A locally green test does not erase triage; a renamed carrier is not a new direction.
When a repair may reinterpret or retire existing semantics, responsibility, contract, or relationship, name the behavior to preserve, highest-risk counterexample, and material unknown. For each known explicit anchor or upstream/downstream reference, state its role and disposition: preserve, rebind to the canonical owner, retire with reason, or reject because of conflict. Leave unresolved relationships unknown; do not re-infer them downstream. Bind role before value and retire invalid responsibility, not evidenced carrier capability. This bounded reminder is not a behavior matrix, relationship graph, referential-integrity proof, or exhaustive discovery claim. It adds no artifact, TDD risk signal, or regression scope; the existing TDD route owner and configured/default mode still apply.
If the diagnosis crosses L3, a patch-shape signal fires, a user disputes the
root claim, a prior fix leaves a symptom, compound/root topology is plausible,
two or more anchored manifestations of one incident exist, reproduction
conditions diverge across occurrences, or an
upstream producer/config/default/contract/spec remains unexcluded, read
root-cause-claim-contract.md before claiming a root cause. It is the sole
owner of the Pre-Claim Gate, causal-closure/falsifier proof, layer-ceiling
proof, and Causal Topology Gate.
Change Necessity
This decision is behavior-triggered, not prompt-triggered. It applies to any new source-code path. Before that path or a non-trivial source edit, expose the Change Necessity decision (no-change | docs/config-only | code-change | needs-clarification); field detail lives in advanced-debugging-governance.md.
Minimality and owner fit
For any proposed branch, fallback, adapter, compatibility path, or new owner, run Minimality Check (fields in advanced-debugging-governance.md) with verdict sufficient repair | local patch | needs first-principles review, and retire invalid responsibility: a local patch needs a retention reason and retirement trigger. For a new non-ordinary repair surface, run the Existence Check in docs/current/AEGIS_MINIMALITY_REFERENCE.md. If retirement involves old code,
external compatibility, or persistent-state risk, compose
anti-entropy-governance; it chooses the retirement path but never grants
destructive authority.
Before editing an overloaded or mixed-purpose owner, complete Pre-Edit Complexity Check and Pre-Edit Owner-Fit Decision (templates in advanced-debugging-governance.md).
Use using-aegis/references/complexity-governance.md for pressure signals.
Do not add new-responsibility in place by default. If the safer boundary
changes the approved shape, update the plan/spec first.
Repair and proportional verification
Implement one owner fix; no bundled “while here” work. Under strict TDD, create the smallest failing test first. With TDD off, a reproduction is diagnostic evidence, not a RED gate or a prerequisite for production edits.
Verification must match the risk:
- local single-owner repair: original reproduction plus focused regression;
- shared/contract/cross-module repair: canonical owner plus affected consumers and compatibility boundary;
- fallback/owner retirement: main-path, lingering-reference, negative, and boundary checks;
- timing/concurrency repair: read
condition-based-waiting.mdonly when evidence identifies polling, sleeps, or race timing as part of the cause; - invalid state crossing several trusted boundaries: read
defense-in-depth.mdonly after the root repair is known and evidence shows a second independent validation boundary is required.
Read advanced-debugging-governance.md before another fix for failed/
persistent / divergent repair or three failures; for unclear/disputed stop /
Layer Stop Card / intervention; or plausible compound root. Closeout triggers:
repair-added patch-shape; multi-site/one-regression;
remaining pattern/anomaly/duplicate/wrong-owner/downstream repair;
uninspected same-symptom fix; open recurrence/unsupported root status;
missing compound topology-specific member/anti-disguise proof;
outside-repo authority; unmigrated
published-contract break; undefined spec; missing permission/info. They route H/T/D;
detail is not causal proof.
For non-trivial debugging with configured workspace support:
python <aegis-workspace-helper> init --root <target-project-root>
python <aegis-workspace-helper> new-work --root <target-project-root> ...
python <aegis-workspace-helper> add-evidence --root <target-project-root> --work <YYYY-MM-DD-slug> ...
python <aegis-workspace-helper> check --root <target-project-root>
Failed attempts use <aegis-workspace-helper> add-attempt; add-evidence is terminal-only.
Fast bug fix or quick bug fix pressure does not skip this: if Ripple Signal Triage fires, record it before editing and verify the canonical owner plus affected downstream path. Records are advisory, not completion authority.
Closure
Always report:
- Repair — cause, owner, smallest change, compatibility, verification.
- Retirement — invalid responsibility status, carrier/capability disposition, retention reason/trigger, removal check.
Confirm the reproduction, same-pattern handling, authority, complexity, and
retirement. Prefix debug logs (e.g. [DEBUG-a4f2]); confirm one-grep removal before close. Confidence: A = direct regression evidence; B = strong evidence
with bounded unknowns; C = partial and not resolved.
Trace Digest may summarize audit evidence; never expose chain-of-thought or
replace root-cause, rule-effect, and verification evidence.
Files (aegis)
-
advanced-debugging-governance.md 8.4 KB
# Advanced Debugging Governance Load this reference only for the evidence triggers named in `SKILL.md`. It owns escalation execution cards and expanded governance checks. It does not own deep causal proof: `root-cause-claim-contract.md` remains the sole owner of the Pre-Claim Gate, causal topology proof, falsifier proof, and layer ceiling. ## Contents - Layer-stop and intervention cards - Residual-symptom and compound-root procedure - Architecture escalation after repeated fixes - Expanded H/T/D governance gate - Pre-edit gate detail (Change Necessity, Minimality, Pre-Edit checks) ## Layer-stop procedure Start at L1 and exhaust the current layer's “why” questions before moving up: L1 symptom, L2 logic, L3 system, L4 architecture, L5 cross-system contract, L6 platform constraint, L7 spec gap. Use this card when the stop point changes the repair boundary, contract owner, product/spec decision, or user correction path—not for ordinary factual answers. ```text Layer Stop Card: - Current Stop Layer: L1 | L2 | L3 | L4 | L5 | L6 | L7 | T-class boundary - Checked Path: - Evidence For Stop: - Excluded Layers: - Falsifier: - User Intervention Point: - Next Action: ``` The falsifier here records what observation would reopen the stop decision. When a root-cause claim is in scope, use the proof contract rather than expanding or reinterpreting the falsifier in this reference. If a user provides a fact that contradicts the stop layer, preserve it as evidence, reopen the earliest invalid edge, and produce: ```text Intervention Card: - User-provided fact: - Invalidated assumption or edge: - Earliest layer reopened: - Evidence to reacquire: - Repair paused: yes ``` ## Residual symptoms and compound roots After a repair, any remaining symptom starts a fresh diagnosis before another edit. Compare the old and new causal paths: | Observation | Working classification | Next action | | --- | --- | --- | | Same conditions and same path | incomplete repair | resume upward from the source | | Different conditions, paths converge | repair stopped too low | reopen their shared upstream | | Different conditions, paths diverge | possible independent compound | diagnose each path separately | | Same symptom, reduced only | downstream mitigation | trace back from the remaining source | Treat these as working classifications, not proof. When paths diverge or a compound/cluster is plausible, read `root-cause-claim-contract.md` and apply its topology and member-proof contract. Do not duplicate that table here. Extended topology procedure: 1. Draw one bounded path per observed manifestation using evidence anchors. 2. Mark the first confirmed convergence or divergence point; unknown edges remain unknown rather than being collapsed. 3. Ask whether apparently separate members share an upstream owner; if so, reopen that owner instead of multiplying repairs. 4. Hand the path set to the canonical topology proof owner before claiming `single-root`, `independent-compound`, or a cluster. 5. Carry every proven root into repair scope; an omitted independent root is a residual-risk finding, not a successful closeout. ## Rare intervention and architecture escalation After three failed fixes, do not attempt a fourth. Escalate when fixes expose new shared state/coupling, require broad refactoring, or create symptoms in new places: ```text Architecture Escalation: - Failed repair hypotheses and evidence: - Repeated owner/coupling pattern: - Current canonical owner uncertainty: - Architecture question for the user: - Safe read-only next step: ``` If the correct change is outside the repository, breaks a published contract without migration, depends on undefined behavior, or lacks permission/data, switch from repair to a bounded mitigation and disclose the boundary. ## Expanded governance gate Before claiming debugging complete, classify countable signals. ### H-class: continue upward if any apply - H1: a conditional/exception branch was added. - H2: multiple sites changed but selected regression covers only one. - H3/H9: repair sits in a consumer/caller/presentation layer, not the owner. - H4: the same bug pattern still exists elsewhere. - H5: the original reproduction retains any anomaly. - H6: history shows the symptom was “fixed” before and that diff was not inspected. - H7: candidate fix adds keyword, phrase, regex, negation-word list, or sample-text exception. - H13: candidate fix names only the observed sample wording/input instead of proving the bug class. - H8: a guard, fallback, adapter, compatibility, prompt, or legacy path grew. - H10: downstream logic re-parses raw text or re-infers action/state while typed intent, normalized state, contract, or another source-of-truth exists. - H11: artifact/cache/readback symptoms were patched without producer proof. - H12: duplicate owners remain without a retirement trigger. - H14: topology-specific member proof is incomplete: a `conjunctive-cluster` lacks per-member necessity or set sufficiency, or an `independent-compound` lacks same-incident activity, per-root path proof, independence, or shared-upstream exclusion. - H15: topology was declared compound/cluster without running the anti-disguise check. - H16: an upstream generator or recurrence path remains open while the causal status is reported as `root`. - H17: the quick lane was used without local origin/termination, negative upstream/history/same-pattern evidence, and a bug-class variant counterfactual. ### T-class: stop drilling and disclose mitigation - T1: required change is outside repository authority. - T2: it breaks a published API with no migration path. - T3: correct behavior is undefined by the specification. - T4: required permission or information is unavailable. Record the boundary, root evidence, exposed vulnerability, mitigation, and whether a more resilient contract is possible. ### D-class: depth evidence for closeout Require: the canonical owner was repaired; the original reproduction is clean; same-pattern occurrences are handled; no unnecessary path/fallback grew; and Minimality Check says `sufficient repair` or retains a bounded patch with a deletion trigger. - D6: causal topology is explicitly classified by `root-cause-claim-contract.md`; conjunctive members pass necessity/set sufficiency, while independent roots pass same-incident, per-root path, independence, and shared-upstream proof. - D7: anti-disguise check has been run for any compound/cluster classification. - D8: the recurrence generator is accounted for and causal status matches the remaining evidence; open recurrence is not labeled `root`. - D9: when the full challenge is skipped, the `Quick Exit Proof` is complete. Then rerun `Goal | DeeperCause | Evidence | Risk/Unknown | Decision`, confirm the source rather than the sample was fixed, state retirement delta, and assign confidence A/B/C. H/T/D output is advisory method-pack evidence, never a `GateDecision`, `PolicySnapshot`, or completion authority. ## Pre-edit gate detail Enter only via a `SKILL.md` trigger (new source-code path, new branch / fallback / adapter / compat path / owner, or editing an overloaded owner). ```text Change Necessity: - User-visible need: - No-change / non-code option: - Why code change is necessary: - Minimum change boundary: - Decision: no-change | docs/config-only | code-change | needs-clarification ``` `no-change` blocks source edits; `docs/config-only` narrows them; `needs-clarification` pauses; `code-change` carries the minimum boundary into repair and verification. ```text Minimality Check: - Existing owner / reuse path: - Correct owner and bug class: - New path and existence proof: - Invalid responsibility retired or scheduled: - Legitimate capability on the same carrier retained, if any: - Verdict: sufficient repair | local patch | needs first-principles review ``` For a new non-ordinary repair surface, run the `Existence Check` in `docs/current/AEGIS_MINIMALITY_REFERENCE.md`. ```text Pre-Edit Complexity Check: - Target edit file: - Existing pressure signal: - Owner fit and safer boundary: - Decision: edit-in-place | extract helper | add owner file | split task | pause for plan update Pre-Edit Owner-Fit Decision: - Edit intent: wiring-only | move-out / extract-first | local-fix-without-new-responsibility | new-responsibility | emergency / compatibility patch - Owner fit and safer boundary: - Decision: edit-in-place | extract helper | add owner file | split task | pause for plan update ``` -
condition-based-waiting-example.ts 4.9 KB
// Complete implementation of condition-based waiting utilities // From: Lace test infrastructure improvements (2025-10-03) // Context: Fixed 15 flaky tests by replacing arbitrary timeouts import type { ThreadManager } from '~/threads/thread-manager'; import type { LaceEvent, LaceEventType } from '~/threads/types'; /** * Wait for a specific event type to appear in thread * * @param threadManager - The thread manager to query * @param threadId - Thread to check for events * @param eventType - Type of event to wait for * @param timeoutMs - Maximum time to wait (default 5000ms) * @returns Promise resolving to the first matching event * * Example: * await waitForEvent(threadManager, agentThreadId, 'TOOL_RESULT'); */ export function waitForEvent( threadManager: ThreadManager, threadId: string, eventType: LaceEventType, timeoutMs = 5000 ): Promise<LaceEvent> { return new Promise((resolve, reject) => { const startTime = Date.now(); const check = () => { const events = threadManager.getEvents(threadId); const event = events.find((e) => e.type === eventType); if (event) { resolve(event); } else if (Date.now() - startTime > timeoutMs) { reject(new Error(`Timeout waiting for ${eventType} event after ${timeoutMs}ms`)); } else { setTimeout(check, 10); // Poll every 10ms for efficiency } }; check(); }); } /** * Wait for a specific number of events of a given type * * @param threadManager - The thread manager to query * @param threadId - Thread to check for events * @param eventType - Type of event to wait for * @param count - Number of events to wait for * @param timeoutMs - Maximum time to wait (default 5000ms) * @returns Promise resolving to all matching events once count is reached * * Example: * // Wait for 2 AGENT_MESSAGE events (initial response + continuation) * await waitForEventCount(threadManager, agentThreadId, 'AGENT_MESSAGE', 2); */ export function waitForEventCount( threadManager: ThreadManager, threadId: string, eventType: LaceEventType, count: number, timeoutMs = 5000 ): Promise<LaceEvent[]> { return new Promise((resolve, reject) => { const startTime = Date.now(); const check = () => { const events = threadManager.getEvents(threadId); const matchingEvents = events.filter((e) => e.type === eventType); if (matchingEvents.length >= count) { resolve(matchingEvents); } else if (Date.now() - startTime > timeoutMs) { reject( new Error( `Timeout waiting for ${count} ${eventType} events after ${timeoutMs}ms (got ${matchingEvents.length})` ) ); } else { setTimeout(check, 10); } }; check(); }); } /** * Wait for an event matching a custom predicate * Useful when you need to check event data, not just type * * @param threadManager - The thread manager to query * @param threadId - Thread to check for events * @param predicate - Function that returns true when event matches * @param description - Human-readable description for error messages * @param timeoutMs - Maximum time to wait (default 5000ms) * @returns Promise resolving to the first matching event * * Example: * // Wait for TOOL_RESULT with specific ID * await waitForEventMatch( * threadManager, * agentThreadId, * (e) => e.type === 'TOOL_RESULT' && e.data.id === 'call_123', * 'TOOL_RESULT with id=call_123' * ); */ export function waitForEventMatch( threadManager: ThreadManager, threadId: string, predicate: (event: LaceEvent) => boolean, description: string, timeoutMs = 5000 ): Promise<LaceEvent> { return new Promise((resolve, reject) => { const startTime = Date.now(); const check = () => { const events = threadManager.getEvents(threadId); const event = events.find(predicate); if (event) { resolve(event); } else if (Date.now() - startTime > timeoutMs) { reject(new Error(`Timeout waiting for ${description} after ${timeoutMs}ms`)); } else { setTimeout(check, 10); } }; check(); }); } // Usage example from actual debugging session: // // BEFORE (flaky): // --------------- // const messagePromise = agent.sendMessage('Execute tools'); // await new Promise(r => setTimeout(r, 300)); // Hope tools start in 300ms // agent.abort(); // await messagePromise; // await new Promise(r => setTimeout(r, 50)); // Hope results arrive in 50ms // expect(toolResults.length).toBe(2); // Fails randomly // // AFTER (reliable): // ---------------- // const messagePromise = agent.sendMessage('Execute tools'); // await waitForEventCount(threadManager, threadId, 'TOOL_CALL', 2); // Wait for tools to start // agent.abort(); // await messagePromise; // await waitForEventCount(threadManager, threadId, 'TOOL_RESULT', 2); // Wait for results // expect(toolResults.length).toBe(2); // Always succeeds // // Result: 60% pass rate → 100%, 40% faster execution -
condition-based-waiting.md 3.4 KB
# Condition-Based Waiting ## Overview Flaky tests often guess at timing with arbitrary delays. This creates race conditions where tests pass on fast machines but fail under load or in CI. **Core principle:** Wait for the actual condition you care about, not a guess about how long it takes. ## When to Use ```dot digraph when_to_use { "Test uses setTimeout/sleep?" [shape=diamond]; "Testing timing behavior?" [shape=diamond]; "Document WHY timeout needed" [shape=box]; "Use condition-based waiting" [shape=box]; "Test uses setTimeout/sleep?" -> "Testing timing behavior?" [label="yes"]; "Testing timing behavior?" -> "Document WHY timeout needed" [label="yes"]; "Testing timing behavior?" -> "Use condition-based waiting" [label="no"]; } ``` **Use when:** - Tests have arbitrary delays (`setTimeout`, `sleep`, `time.sleep()`) - Tests are flaky (pass sometimes, fail under load) - Tests timeout when run in parallel - Waiting for async operations to complete **Don't use when:** - Testing actual timing behavior (debounce, throttle intervals) - Always document WHY if using arbitrary timeout ## Core Pattern ```typescript // ❌ BEFORE: Guessing at timing await new Promise(r => setTimeout(r, 50)); const result = getResult(); expect(result).toBeDefined(); // ✅ AFTER: Waiting for condition await waitFor(() => getResult() !== undefined); const result = getResult(); expect(result).toBeDefined(); ``` ## Quick Patterns | Scenario | Pattern | |----------|---------| | Wait for event | `waitFor(() => events.find(e => e.type === 'DONE'))` | | Wait for state | `waitFor(() => machine.state === 'ready')` | | Wait for count | `waitFor(() => items.length >= 5)` | | Wait for file | `waitFor(() => fs.existsSync(path))` | | Complex condition | `waitFor(() => obj.ready && obj.value > 10)` | ## Implementation Generic polling function: ```typescript async function waitFor<T>( condition: () => T | undefined | null | false, description: string, timeoutMs = 5000 ): Promise<T> { const startTime = Date.now(); while (true) { const result = condition(); if (result) return result; if (Date.now() - startTime > timeoutMs) { throw new Error(`Timeout waiting for ${description} after ${timeoutMs}ms`); } await new Promise(r => setTimeout(r, 10)); // Poll every 10ms } } ``` See `condition-based-waiting-example.ts` in this directory for complete implementation with domain-specific helpers (`waitForEvent`, `waitForEventCount`, `waitForEventMatch`) from actual debugging session. ## Common Mistakes **❌ Polling too fast:** `setTimeout(check, 1)` - wastes CPU **✅ Fix:** Poll every 10ms **❌ No timeout:** Loop forever if condition never met **✅ Fix:** Always include timeout with clear error **❌ Stale data:** Cache state before loop **✅ Fix:** Call getter inside loop for fresh data ## When Arbitrary Timeout IS Correct ```typescript // Tool ticks every 100ms - need 2 ticks to verify partial output await waitForEvent(manager, 'TOOL_STARTED'); // First: wait for condition await new Promise(r => setTimeout(r, 200)); // Then: wait for timed behavior // 200ms = 2 ticks at 100ms intervals - documented and justified ``` **Requirements:** 1. First wait for triggering condition 2. Based on known timing (not guessing) 3. Comment explaining WHY ## Real-World Impact From debugging session (2025-10-03): - Fixed 15 flaky tests across 3 files - Pass rate: 60% → 100% - Execution time: 40% faster - No more race conditions -
defense-in-depth.md 3.6 KB
# Defense-in-Depth Validation ## Overview When you fix a bug caused by invalid data, adding validation at one place feels sufficient. But that single check can be bypassed by different code paths, refactoring, or mocks. **Core principle:** Validate at EVERY layer data passes through. Make the bug structurally impossible. ## Why Multiple Layers Single validation: "We fixed the bug" Multiple layers: "We made the bug impossible" Different layers catch different cases: - Entry validation catches most bugs - Business logic catches edge cases - Environment guards prevent context-specific dangers - Debug logging helps when other layers fail ## The Four Layers ### Layer 1: Entry Point Validation **Purpose:** Reject obviously invalid input at API boundary ```typescript function createProject(name: string, workingDirectory: string) { if (!workingDirectory || workingDirectory.trim() === '') { throw new Error('workingDirectory cannot be empty'); } if (!existsSync(workingDirectory)) { throw new Error(`workingDirectory does not exist: ${workingDirectory}`); } if (!statSync(workingDirectory).isDirectory()) { throw new Error(`workingDirectory is not a directory: ${workingDirectory}`); } // ... proceed } ``` ### Layer 2: Business Logic Validation **Purpose:** Ensure data makes sense for this operation ```typescript function initializeWorkspace(projectDir: string, sessionId: string) { if (!projectDir) { throw new Error('projectDir required for workspace initialization'); } // ... proceed } ``` ### Layer 3: Environment Guards **Purpose:** Prevent dangerous operations in specific contexts ```typescript async function gitInit(directory: string) { // In tests, refuse git init outside temp directories if (process.env.NODE_ENV === 'test') { const normalized = normalize(resolve(directory)); const tmpDir = normalize(resolve(tmpdir())); if (!normalized.startsWith(tmpDir)) { throw new Error( `Refusing git init outside temp dir during tests: ${directory}` ); } } // ... proceed } ``` ### Layer 4: Debug Instrumentation **Purpose:** Capture context for forensics ```typescript async function gitInit(directory: string) { const stack = new Error().stack; logger.debug('About to git init', { directory, cwd: process.cwd(), stack, }); // ... proceed } ``` ## Applying the Pattern When you find a bug: 1. **Trace the data flow** - Where does bad value originate? Where used? 2. **Map all checkpoints** - List every point data passes through 3. **Add validation at each layer** - Entry, business, environment, debug 4. **Test each layer** - Try to bypass layer 1, verify layer 2 catches it ## Example from Session Bug: Empty `projectDir` caused `git init` in source code **Data flow:** 1. Test setup → empty string 2. `Project.create(name, '')` 3. `WorkspaceManager.createWorkspace('')` 4. `git init` runs in `process.cwd()` **Four layers added:** - Layer 1: `Project.create()` validates not empty/exists/writable - Layer 2: `WorkspaceManager` validates projectDir not empty - Layer 3: `WorktreeManager` refuses git init outside tmpdir in tests - Layer 4: Stack trace logging before git init **Result:** All 1847 tests passed, bug impossible to reproduce ## Key Insight All four layers were necessary. During testing, each layer caught bugs the others missed: - Different code paths bypassed entry validation - Mocks bypassed business logic checks - Edge cases on different platforms needed environment guards - Debug logging identified structural misuse **Don't stop at one validation point.** Add checks at every layer. -
feedback-loop-construction.md 2.4 KB
# Feedback Loop Construction ## Principle Before forming hypotheses, build a fast, deterministic, agent-runnable pass/fail signal. With one, the cause becomes findable through bisection, hypothesis-testing, and instrumentation. Without one, code inspection alone is insufficient. Spend disproportionate effort here. A 2-second deterministic loop is a debugging superpower. A 30-second flaky loop is barely better than nothing. ## Priority Ladder Try in order. Stop when you have a loop that inspires belief. | # | Method | When to Use | |---|--------|-------------| | 1 | **Failing test** — unit/integration/e2e at the seam reaching the bug | Almost always the best option | | 2 | **Curl/HTTP script** — against running dev server | API/backend bugs | | 3 | **CLI invocation** — fixture input, diff stdout against known-good | CLI tools, build scripts | | 4 | **Headless browser script** — Playwright/Puppeteer with DOM assertions | Frontend UI bugs | | 5 | **Replay captured trace** — save real payload to disk, replay in isolation | Intermittent / hard-to-reproduce | | 6 | **Throwaway harness** — minimal service subset, mocked deps | Multi-service systems | | 7 | **Property/fuzz loop** — 1000 random inputs seeking failure mode | "Sometimes wrong" output | | 8 | **Bisection harness** — `git bisect run` automation | Regression between known commits | | 9 | **Differential loop** — identical input → old vs new version → diff | Performance regressions | | 10 | **HITL bash script** — structured human-in-the-loop | Last resort; requires manual steps | ## Loop Quality Iteration Once a loop exists, improve it: - **Faster?** Cache setup, skip unrelated init, narrow scope - **Sharper signal?** Assert on the specific symptom, not "didn't crash" - **More deterministic?** Pin time, seed RNG, isolate filesystem, freeze network ## Non-Deterministic Bugs Goal shifts from clean reproduction to higher reproduction rate: - Loop the trigger 100×, parallelize, add stress, narrow timing windows - 50% flake → debuggable. 1% flake → not debuggable without more instrumentation ## When No Loop Can Be Built Stop and say so explicitly. List what was tried. Ask the user for: 1. Access to the reproducing environment 2. A captured artifact (HAR file, log dump, core dump, screen recording with timestamps) 3. Permission to add temporary production instrumentation Do not proceed to hypothesis (Phase 3) without a loop that inspires belief. -
find-polluter.sh 1.5 KB
#!/usr/bin/env bash # Bisection script to find which test creates unwanted files/state # Usage: ./find-polluter.sh <file_or_dir_to_check> <test_pattern> # Example: ./find-polluter.sh '.git' 'src/**/*.test.ts' set -e if [ $# -ne 2 ]; then echo "Usage: $0 <file_to_check> <test_pattern>" echo "Example: $0 '.git' 'src/**/*.test.ts'" exit 1 fi POLLUTION_CHECK="$1" TEST_PATTERN="$2" echo "🔍 Searching for test that creates: $POLLUTION_CHECK" echo "Test pattern: $TEST_PATTERN" echo "" # Get list of test files TEST_FILES=$(find . -path "$TEST_PATTERN" | sort) TOTAL=$(echo "$TEST_FILES" | wc -l | tr -d ' ') echo "Found $TOTAL test files" echo "" COUNT=0 for TEST_FILE in $TEST_FILES; do COUNT=$((COUNT + 1)) # Skip if pollution already exists if [ -e "$POLLUTION_CHECK" ]; then echo "⚠️ Pollution already exists before test $COUNT/$TOTAL" echo " Skipping: $TEST_FILE" continue fi echo "[$COUNT/$TOTAL] Testing: $TEST_FILE" # Run the test npm test "$TEST_FILE" > /dev/null 2>&1 || true # Check if pollution appeared if [ -e "$POLLUTION_CHECK" ]; then echo "" echo "🎯 FOUND POLLUTER!" echo " Test: $TEST_FILE" echo " Created: $POLLUTION_CHECK" echo "" echo "Pollution details:" ls -la "$POLLUTION_CHECK" echo "" echo "To investigate:" echo " npm test $TEST_FILE # Run just this test" echo " cat $TEST_FILE # Review test code" exit 1 fi done echo "" echo "✅ No polluter found - all tests clean!" exit 0 -
root-cause-claim-contract.md 13.5 KB
# Root Cause Claim Contract ## Principle Debugging stops too early not because the agent is lazy, but because the stop condition is a self-judged, non-falsifiable claim. The moment an agent decides "no deeper why remains" is the moment it has selected itself as the authority on absence. That is structurally biased toward premature closure. This document defines the **Pre-Claim Gate**: a small set of mechanical, falsifiable checks an agent must satisfy *before* it is allowed to claim a root cause and enter Phase 4 (Implementation). It also defines the **Causal Topology Gate**, which replaces the implicit "single-root" default with an explicit topology classification — because some bugs have more than one root, and some roots form a cluster rather than a chain. The gate is **advisory method-pack discipline**. It is not a `GateDecision`, a `PolicySnapshot`, evidence sufficiency authority, or completion authority. ## When to Use Run the Pre-Claim Gate when **any** of these is true: - the candidate fix matches a Patch-Shape Triage signal (H1 / H3 / H8 / H10 / H11 / H13) — i.e. the fix is a guard, fallback, consumer/caller patch, artifact/cache patch, or sample-only naming - the diagnosis crosses a component or system boundary (L3 and above) - the user has pushed back on a previous root-cause claim ("is that really the root?", "you stopped too early") - a fix was attempted and the symptom persisted or shifted Do **not** run it for the quick bug lane when the fix is at the canonical owner of a single-component, single-owner bug and no patch-shape signal fires. Keep simple tasks cheap. ## Deeper Cause Challenge Before any non-trivial root claim, account for the mechanism that can regenerate the failure. A locally effective intervention proves that the selected point is causal; it does not prove that the point is the recurrence generator. Run the full challenge when an upstream producer, configuration, default, contract, policy, or specification dependency has not been excluded. Record: ```text Deeper Cause Challenge: - Claimed cause: - Causal status: root | proximate | contributing | deepest-confirmed-root-unknown | external-terminal - Upstream generator: - Recurrence path: - Counterfactual intervention: - Plausible deeper candidate: - Rejection evidence: - Recurrence status: closed | open - Topology / anti-disguise proof: ``` The causal statuses mean: - `root`: the recurrence generator is accounted for and closed; - `proximate`: the mechanism directly produces the symptom but can be regenerated upstream; - `contributing`: the mechanism changes likelihood or severity without independently generating the bug class; - `deepest-confirmed-root-unknown`: evidence confirms the deepest observed mechanism but ends before root closure; - `external-terminal`: the actionable chain ends at a proven T-class boundary. A `root` claim requires all of these: the upstream generator is accounted for, the recurrence path is closed, the counterfactual eliminates the relevant bug class rather than one sample, at least one plausible deeper candidate is rejected with evidence, and the topology / anti-disguise proof passes. If any item remains open, use `proximate`, `contributing`, or `deepest-confirmed-root-unknown`; never promote the deepest observed mechanism to root by default. ### Quick Exit Proof The quick bug lane may skip the full challenge only with explicit negative proof: ```text Quick Exit Proof: - Canonical local owner: - Origin and termination: bad value/state originates and terminates here - Upstream producer/config/default/contract/policy/spec: excluded with evidence - History and same-pattern searches: negative - Variant counterfactual: eliminates the bug class - Causal status: root ``` If any quick-exit field is unknown or positive, leave the quick lane and run the full challenge. A familiar file, canonical owner, or green local test is not a substitute for this proof. ## The Five Gate Checks A root-cause claim passes only when all five are satisfied. Each turns a subjective judgment into a mechanical, checkable condition. ### Gate 1 — Causal Closure Every causal edge from symptom back to claimed root has an evidence anchor (file:line, test, log, reproduction). A link that is "probably" or "I think" breaks the chain. The chain is closed or it is not — there is no partial pass. ### Gate 2 — Falsifier Checked The claim must state: "If X were *not* the root cause, then observable F would appear." Then F must have been checked and found absent. No falsifier, or a falsifier that was never checked, blocks the claim. This converts "X is the root" from an assertion into a proposition that could have been wrong. ### Gate 3 — Adversarial Self-Refutation Before claiming, generate the strongest single argument for why this root cause is wrong, and show why it does not hold. Self-refutation activates a different reasoning path than self-confirmation and is the cheapest known correction for the selection bias at the stop point. ### Gate 4 — Causal Topology Gate (see below) The claim must carry an explicit topology classification, not an implicit "single-root" default. ### Gate 5 — Layer Ceiling Proof If the claimed root sits at layer L?, the agent must show why L?+1 is *not* reachable — with a concrete constraint, not "I didn't go there." Reaching L7 (Spec Gap) or a T-class terminal counts as a valid ceiling; stopping at L2 with "looks deep enough" does not. ```text Pre-Claim Gate Pass: Topology: <single-root | single-root-multi-symptom | chain | independent-compound | conjunctive-cluster | disjunctive-or> CausalClosure: <closed | open-edge: <edge>> Falsifier: <if not-X then F; F checked: yes/no> SelfRefutation: <strongest objection> -> <why it does not hold> LayerCeiling: <L?> -> <why L?+1 unreachable> Verdict: pass | fail-<which-gate> ``` This card is an advisory readback. It is not a `GateDecision`, `PolicySnapshot`, or completion authority. ## Causal Topology Gate Bugs have one of six causal topologies. Diagnostic layers (L1-L7) are observation altitudes, not causal shapes: any topology can occur at any stop layer. These topologies assume convergence on discrete mechanisms within an acyclic incident graph; when member-by-member repair keeps regenerating the symptom, treat that as Architecture Escalation evidence rather than adding another cluster member. The topology is not optional metadata — it determines the correct stop condition and the correct repair shape. The default is `unknown`; an agent must actively exclude the multi-root topologies before it may collapse to a single-root claim. | Topology | Structure | Stop condition | Repair shape | | --- | --- | --- | --- | | `single-root` | A → symptom | Layer Ceiling Proof at A | fix A | | `single-root-multi-symptom` | A → B, C, D | Layer Ceiling Proof at A | fix A, symptoms self-resolve | | `chain` | A → B → C → symptom | Layer Ceiling Proof at A | drill to A, fix A | | `independent-compound` | A and Y are both active in the same incident; each independently produces an anchored manifestation; A ⊥ Y | prove same-incident activity, each root's Gate 1/2/5, and no shared upstream | fix **all active roots**; missing one leaves its causal path | | `conjunctive-cluster` | A ∧ B ∧ C → symptom (each necessary, none sufficient) | enumerate members, necessity test each, sufficiency test the set, anti-disguise check | fix **all** members; missing one leaves symptom | | `disjunctive-or` | A ∨ B can cause symptom; current evidence shows one or an unknown active disjunct | identify the incident's active root and enumerate alternatives | fix the observed active root; enumerate alternatives for defense-in-depth | Classification boundary: `independent-compound` requires evidence that two or more causally independent roots are active in the same observed incident. If evidence identifies one active root while other roots are alternative sufficient mechanisms, classify `disjunctive-or`; if activity is unresolved, keep topology `unknown`. Alternative mechanisms alone do not prove simultaneous activity. ```text ClassificationRules: - independent-compound: multiple active roots in same incident - disjunctive-or: one active root plus alternative sufficient roots ``` ### Topology-specific member proof #### Conjunctive cluster proof For `conjunctive-cluster`, each member must pass a **necessity test**: with all other claimed members present, conceptually remove this member. If the symptom still occurs, the member is not necessary for that cluster. The full set must also pass a **sufficiency test** by explaining every observed manifestation, not only the headline failure. #### Independent compound proof For `independent-compound`, do not reuse the conjunctive removal test. Prove that every claimed root is active in the same incident and independently passes Gates 1, 2, and 5: under evidence excluding the other roots, it can produce its anchored manifestation. Removing it must eliminate its own causal path, although the overall symptom may persist through another independent root. Also prove no shared upstream and enumerate conditions/manifestations assigned to every root. ```text TopologyEvidence: - Candidate: independent-compound | disjunctive-or | unknown - Same-incident active roots: - Alternative sufficient roots: - Shared upstream: none | found | unknown - Repair obligation: ``` These are **conceptual proofs**, not empirical removals. Complete topology proof may require runtime coverage that a method pack cannot grant; state that ceiling honestly when member completeness remains uncertain. ### Anti-disguise check (the step most often skipped) A claimed cluster may actually be a chain or single-root-multi-symptom in disguise. Before accepting `conjunctive-cluster`, ask: > Do members X and Y share a deeper common cause Z, such that X and Y are > merely two manifestations of Z? If yes, the topology is not a cluster — it is `single-root-multi-symptom` or `chain`, and Z is the true root. Drill to Z. This single check is what prevents an agent from stopping at a "two causes" story when one deeper cause explains both. The same check in reverse protects `independent-compound`: if two divergent chains share an upstream Z, they are not independent, and the root is Z. ## Real Example: Excel-to-Word Conversion Produced a 10-Row Artifact This is a replay of a real two-turn debugging session, used here to show the gate catching both a premature L4 stop and a hidden second root. **Symptom:** a downloadable Word file generated from an attached 363-row Excel contained only the first 10 rows. **First-turn stop (would be caught):** the agent stopped at L4 — "scope and intention signals exist but were not upgraded into a full-document conversion contract" — and presented this as the root cause. Running the Pre-Claim Gate against that stop: - **Gate 1 Causal Closure:** open edge. No evidence linked the "missing contract" to the specific 10-row cutoff in the projection layer. - **Gate 2 Falsifier:** none stated. No claim of the form "if this were not the cause, F would appear." - **Gate 4 Topology:** implicit `single-root`, but the projection layer's use of visible-answer text as artifact source was an independent causal path — topology had not been classified, let alone reduced to single-root. - **Gate 5 Layer Ceiling:** no proof that L5+ was unreachable. Verdict: `fail-open-edge / fail-topology / fail-layer-ceiling`. The agent may not claim root cause. It must drill upward. **Second-turn stop (also caught, for a different reason):** drilling deeper, the agent found that the typed workflow contract compiler recognized only four regulated families and mapped "convert Excel to Word" into a generic drafting family with no row-completeness obligation. It presented this L6 gap as the root. Running the Topology Gate against that stop: - The agent had **two** causal paths to the 10-row symptom: (A) no full-document conversion family, and (B) the artifact projection sourced content from the visible answer text rather than the parsed rows. - Independent proof for A: with B excluded (projection reads parsed rows), the missing full-document contract still produces the incomplete-row symptom. - Independent proof for B: with A excluded (the contract requires all rows), visible-answer projection still produces the incomplete-row symptom. - Fixing only A or B leaves the other causal path active. This is an `independent-compound` candidate {A, B} — **not** a single root. - **Anti-disguise check:** do A and B share a deeper cause? Yes. Neither exists because the system never defined "whole-file artifact generation" as a first-class workflow with a completeness obligation — an L7 Spec Gap. A and B are two manifestations of that single deeper cause. Final classification: topology collapses from `independent-compound` to `single-root-multi-symptom` rooted at the L7 Spec Gap, with two ripple paths (A, B) that must both be repaired when the spec gap is addressed. ```text ExampleTopologyEvidence: - Candidate: independent-compound - Same-incident active roots: A, B - Anti-disguise result: shared upstream L7 Spec Gap - Final: single-root-multi-symptom ``` Without the gate, the first turn would have shipped an L4 non-root as a root, and the second turn would have shipped an L6 half-root and silently dropped the projection-layer second member. ## Key Principle The stop condition must be something the agent has to *buy*, not something it *defaults to*. Five mechanical checks plus an explicit topology turn "I think this is deep enough" into "here is the evidence chain, the falsifier I checked, the objection I survived, the topology I proved, and the ceiling I reached." That is a claim worth acting on. -
root-cause-tracing.md 5.5 KB
# Root Cause Tracing ## Overview Bugs often manifest deep in the call stack (git init in wrong directory, file created in wrong location, database opened with wrong path). Your instinct is to fix where the error appears, but that's treating a symptom. **Core principle:** Trace backward through the call chain until you find the original trigger, then fix at the source. ## When to Use ```dot digraph when_to_use { "Bug appears deep in stack?" [shape=diamond]; "Can trace backwards?" [shape=diamond]; "Fix at symptom point" [shape=box]; "Trace to original trigger" [shape=box]; "BETTER: Also add defense-in-depth" [shape=box]; "Bug appears deep in stack?" -> "Can trace backwards?" [label="yes"]; "Can trace backwards?" -> "Trace to original trigger" [label="yes"]; "Can trace backwards?" -> "Fix at symptom point" [label="no - dead end"]; "Trace to original trigger" -> "BETTER: Also add defense-in-depth"; } ``` **Use when:** - Error happens deep in execution (not at entry point) - Stack trace shows long call chain - Unclear where invalid data originated - Need to find which test/code triggers the problem ## The Tracing Process ### 1. Observe the Symptom ``` Error: git init failed in /Users/jesse/project/packages/core ``` ### 2. Find Immediate Cause **What code directly causes this?** ```typescript await execFileAsync('git', ['init'], { cwd: projectDir }); ``` ### 3. Ask: What Called This? ```typescript WorktreeManager.createSessionWorktree(projectDir, sessionId) → called by Session.initializeWorkspace() → called by Session.create() → called by test at Project.create() ``` ### 4. Keep Tracing Up **What value was passed?** - `projectDir = ''` (empty string!) - Empty string as `cwd` resolves to `process.cwd()` - That's the source code directory! ### 5. Find Original Trigger **Where did empty string come from?** ```typescript const context = setupCoreTest(); // Returns { tempDir: '' } Project.create('name', context.tempDir); // Accessed before beforeEach! ``` ## Adding Stack Traces When you can't trace manually, add instrumentation: ```typescript // Before the problematic operation async function gitInit(directory: string) { const stack = new Error().stack; console.error('DEBUG git init:', { directory, cwd: process.cwd(), nodeEnv: process.env.NODE_ENV, stack, }); await execFileAsync('git', ['init'], { cwd: directory }); } ``` **Critical:** Use `console.error()` in tests (not logger - may not show) **Run and capture:** ```bash npm test 2>&1 | grep 'DEBUG git init' ``` **Analyze stack traces:** - Look for test file names - Find the line number triggering the call - Identify the pattern (same test? same parameter?) ## Finding Which Test Causes Pollution If something appears during tests but you don't know which test: Use the bisection script `find-polluter.sh` in this directory: ```bash ./find-polluter.sh '.git' 'src/**/*.test.ts' ``` Runs tests one-by-one, stops at first polluter. See script for usage. ## Real Example: Empty projectDir **Symptom:** `.git` created in `packages/core/` (source tree polluted) **5-level trace (mapped to diagnostic layers):** | Level | Diagnostic layer | Finding | | --- | --- | --- | | L1 | Symptom | `.git` directory appears in `packages/core/` (source code) | | L2 | Logic | `git init` received empty `cwd` → `process.cwd()` resolves as fallback | | L3 | System | `WorktreeManager` passed empty `projectDir` across component boundary | | L4 | System | `Session.create()` passed empty string — the ownership seam | | L5 | Root | Test accessed `context.tempDir` before `beforeEach` lifecycle completed | **Root cause:** top-level variable initialization accessing value before lifecycle ready **Fix:** made `tempDir` a getter that throws if accessed before `beforeEach` **Also added defense-in-depth:** - Layer 1: Project.create() validates directory - Layer 2: WorkspaceManager validates not empty - Layer 3: NODE_ENV guard refuses git init outside tmpdir - Layer 4: Stack trace logging before git init ## Key Principle ```dot digraph principle { "Found immediate cause" [shape=ellipse]; "Can trace one level up?" [shape=diamond]; "Trace backwards" [shape=box]; "Is this the source?" [shape=diamond]; "Fix at source" [shape=box]; "Add validation at each layer" [shape=box]; "Bug impossible" [shape=doublecircle]; "NEVER fix just the symptom" [shape=octagon, style=filled, fillcolor=red, fontcolor=white]; "Found immediate cause" -> "Can trace one level up?"; "Can trace one level up?" -> "Trace backwards" [label="yes"]; "Can trace one level up?" -> "NEVER fix just the symptom" [label="no"]; "Trace backwards" -> "Is this the source?"; "Is this the source?" -> "Trace backwards" [label="no - keeps going"]; "Is this the source?" -> "Fix at source" [label="yes"]; "Fix at source" -> "Add validation at each layer"; "Add validation at each layer" -> "Bug impossible"; } ``` **NEVER fix just where the error appears.** Trace back to find the original trigger. ## Stack Trace Tips **In tests:** Use `console.error()` not logger - logger may be suppressed **Before operation:** Log before the dangerous operation, not after it fails **Include context:** Directory, cwd, environment variables, timestamps **Capture stack:** `new Error().stack` shows complete call chain ## Real-World Impact From debugging session (2025-10-03): - Found root cause through 5-level trace - Fixed at source (getter validation) - Added 4 layers of defense - 1847 tests passed, zero pollution -
SKILL.md 10.6 KB
--- name: systematic-debugging description: "Use when encountering a bug, test failure, or unexpected behavior, before proposing fixes" --- # Execute Bug, failure, or unexpected behavior: 1. **Isolate** — read error, reproduce, inspect the diff, and drill upward through diagnostic layers: L1 symptom → L2 logic → L3 system → L4 architecture → L5 cross-system contract → L6 platform → L7 spec gap. Layers are observation altitudes, not one causal chain; the causal shape at the stop altitude is classified explicitly before any root claim. Stop only when causal proof accounts for the recurrence generator or reaches a T-class boundary. 2. **Identify owner** — compare working behavior, trace the bad value, locate the canonical owner, and treat duplicate owners as a finding. 3. **Decide before editing** — Before fixing, run Patch-Shape Triage and Ripple Signal Triage when shared logic, contracts, fallbacks, adapters, producer/consumer seams, or source-of-truth boundaries are involved. Surface Change Necessity for any new source-code path or non-trivial source edit. Run Minimality Check for a new branch, fallback, adapter, owner, or compatibility path, and Pre-Edit Complexity Check for an overloaded owner or complexity growth. After Change Necessity selects `code-change` and before the first repair edit, own the TDD Route for the repair slice per `test-driven-development` (`off` default; `strict` on behavior/bugfix/shared/contract/persistence/permission/migration risk). 4. **Prove** — test one hypothesis with the smallest reproduction or verification. A failing test first is required only by a recorded `TDD Route: strict`; with `TDD Mode: off`, do not require a failing test or RED/GREEN cycle. Three failed fixes means stop and question architecture. 5. **Repair and close** — fix minimally at the canonical owner, verify in proportion to risk, review architecture, and close both repair and retirement tracks. If any symptom remains, stop and diagnose it separately. Done: confidence ≥ B, causal status matches recurrence evidence or an external terminal, tracks explicit, no H signal, and required D evidence passes. ## Core invariant Find root cause and fix the bug class at its canonical owner. A minimal fix is not the smallest textual diff; it is the smallest sufficient owner-level repair. ## Quick bug lane For a low-risk, reproducible, single-owner bug with no patch-shape signal, keep the readback compact: `Symptom`, `Reproduction`, `Root Cause`, `Change Necessity`, `Fix Boundary`, and `Verification`. Skip the causal card only when the causal-proof owner's `Quick Exit Proof` passes. Quick bug lane must surface Change Necessity before source edits. One sentence may cover the user-visible need, no-change/non-code option, why code must change, minimum boundary, and an explicit decision token such as `Decision: code-change`. If shared logic, a contract, fallback, duplicate owner, consumer patch, or cross-module behavior appears, leave this lane. `Aegis Visibility` names the evidence/owner/patch-shape/verification effect. Pass root cause, avoided misfix, boundary, evidence, complexity, and risk to `verification-before-completion`; no separate receipt. ## Diagnose before repair 1. Read the complete error/stack and record inputs, environment, versions, and success criteria. 2. Reproduce consistently. If unstable, read `feedback-loop-construction.md` **only when evidence shows intermittent or timing-dependent reproduction** and build a bounded loop. Shrink the repro to load-bearing elements as the test input, never the fix scope: still drill upward; test at the correct seam. 3. Inspect recent changes and compare a working example. Code is evidence; if authority, glossary, code, and tests disagree, compose `establishing-project-context` rather than silently redefining a term. 4. Instrument component boundaries, then trace the bad value toward its source. Read `root-cause-tracing.md` **only when the observed bad value is several calls or components downstream from its origin**. 5. State one hypothesis and falsify it with one-variable evidence. Do not stack speculative fixes. End each loop with `Goal | DeeperCause | Evidence | Risk/Unknown | Decision`. ### Canonical-owner and patch-shape gate Before editing, continue upward unless evidence proves the local site is the canonical owner when the candidate is any of these signals: - keyword, phrase, regex, negation-word list, or sample-text exception; - local guard, extra conditional, `try`/`catch`, early return, or one-off branch; - fallback, adapter, compatibility branch, prompt branch, or legacy path expansion; - consumer/caller/readiness/presentation-layer patch; - downstream logic re-parses raw text or re-infers action/state while typed intent, normalized state, contract, or another source-of-truth exists; - artifact/download/export/readback/cache patch without producer/owner proof. ```text PatchShape: CanonicalOwner: UpwardDrillSignal: Decision: fix owner | continue investigation | escalate ``` A locally green test does not erase triage; a renamed carrier is not a new direction. When a repair may reinterpret or retire existing semantics, responsibility, contract, or relationship, name the behavior to preserve, highest-risk counterexample, and material unknown. For each known explicit anchor or upstream/downstream reference, state its role and disposition: preserve, rebind to the canonical owner, retire with reason, or reject because of conflict. Leave unresolved relationships unknown; do not re-infer them downstream. Bind role before value and retire invalid responsibility, not evidenced carrier capability. This bounded reminder is not a behavior matrix, relationship graph, referential-integrity proof, or exhaustive discovery claim. It adds no artifact, TDD risk signal, or regression scope; the existing TDD route owner and configured/default mode still apply. If the diagnosis crosses L3, a patch-shape signal fires, a user disputes the root claim, a prior fix leaves a symptom, compound/root topology is plausible, two or more anchored manifestations of one incident exist, reproduction conditions diverge across occurrences, or an upstream producer/config/default/contract/spec remains unexcluded, read `root-cause-claim-contract.md` **before claiming a root cause**. It is the sole owner of the Pre-Claim Gate, causal-closure/falsifier proof, layer-ceiling proof, and Causal Topology Gate. ### Change Necessity This decision is behavior-triggered, not prompt-triggered. It applies to any new source-code path. Before that path or a non-trivial source edit, expose the `Change Necessity` decision (`no-change | docs/config-only | code-change | needs-clarification`); field detail lives in `advanced-debugging-governance.md`. ### Minimality and owner fit For any proposed branch, fallback, adapter, compatibility path, or new owner, run `Minimality Check` (fields in `advanced-debugging-governance.md`) with verdict `sufficient repair | local patch | needs first-principles review`, and retire invalid responsibility: a `local patch` needs a retention reason and retirement trigger. For a new non-ordinary repair surface, run the `Existence Check` in `docs/current/AEGIS_MINIMALITY_REFERENCE.md`. If retirement involves old code, external compatibility, or persistent-state risk, compose `anti-entropy-governance`; it chooses the retirement path but never grants destructive authority. Before editing an overloaded or mixed-purpose owner, complete `Pre-Edit Complexity Check` and `Pre-Edit Owner-Fit Decision` (templates in `advanced-debugging-governance.md`). Use `using-aegis/references/complexity-governance.md` for pressure signals. Do not add `new-responsibility` in place by default. If the safer boundary changes the approved shape, update the plan/spec first. ## Repair and proportional verification Implement one owner fix; no bundled “while here” work. Under strict TDD, create the smallest failing test first. With TDD off, a reproduction is diagnostic evidence, not a RED gate or a prerequisite for production edits. Verification must match the risk: - local single-owner repair: original reproduction plus focused regression; - shared/contract/cross-module repair: canonical owner plus affected consumers and compatibility boundary; - fallback/owner retirement: main-path, lingering-reference, negative, and boundary checks; - timing/concurrency repair: read `condition-based-waiting.md` **only when evidence identifies polling, sleeps, or race timing as part of the cause**; - invalid state crossing several trusted boundaries: read `defense-in-depth.md` **only after the root repair is known and evidence shows a second independent validation boundary is required**. Read `advanced-debugging-governance.md` **before another fix** for failed/ persistent / divergent repair or three failures; for unclear/disputed stop / Layer Stop Card / intervention; or plausible compound root. Closeout triggers: repair-added patch-shape; multi-site/one-regression; remaining pattern/anomaly/duplicate/wrong-owner/downstream repair; uninspected same-symptom fix; open recurrence/unsupported root status; missing compound topology-specific member/anti-disguise proof; outside-repo authority; unmigrated published-contract break; undefined spec; missing permission/info. They route H/T/D; detail is not causal proof. For non-trivial debugging with configured workspace support: ```bash python <aegis-workspace-helper> init --root <target-project-root> python <aegis-workspace-helper> new-work --root <target-project-root> ... python <aegis-workspace-helper> add-evidence --root <target-project-root> --work <YYYY-MM-DD-slug> ... python <aegis-workspace-helper> check --root <target-project-root> ``` Failed attempts use `<aegis-workspace-helper> add-attempt`; `add-evidence` is terminal-only. Fast bug fix or quick bug fix pressure does not skip this: if Ripple Signal Triage fires, record it before editing and verify the canonical owner plus affected downstream path. Records are advisory, not completion authority. ## Closure Always report: - **Repair** — cause, owner, smallest change, compatibility, verification. - **Retirement** — invalid responsibility status, carrier/capability disposition, retention reason/trigger, removal check. Confirm the reproduction, same-pattern handling, authority, complexity, and retirement. Prefix debug logs (e.g. `[DEBUG-a4f2]`); confirm one-grep removal before close. Confidence: A = direct regression evidence; B = strong evidence with bounded unknowns; C = partial and not resolved. `Trace Digest` may summarize audit evidence; never expose chain-of-thought or replace root-cause, rule-effect, and verification evidence.
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.