Remediation loop¶
sdetkit diagnose run takes saved CI logs, or the failed steps of your latest gate run, and carries each failure through diagnosis, a fix in a throwaway copy, an independent proof and a verifier decision. It never edits your working tree. You get a reviewable patch and a hash-linked record of how it was produced, and you decide whether to apply it.
# Diagnose and plan only; nothing is executed.
python -m sdetkit diagnose run --log build/ci-logs/lint/ci_log.txt
# Fix in an isolated copy, score the patch, prove it, and verify it.
python -m sdetkit diagnose run --log build/ci-logs/lint/ci_log.txt --prove
Straight from a failing gate¶
gate fast and gate release save their latest result, with each step's output, to .sdetkit/runs/gate-latest.json. With no --log, diagnose run reads it and turns every failed step into a log under <out-dir>/gate-logs/<step>/output.log. For gate release, the failed steps of its nested gate fast are used (gate_fast.pytest, gate_fast.ruff, ...).
python -m sdetkit gate fast # fails
python -m sdetkit diagnose run # why, and the planned fix
python -m sdetkit diagnose run --prove # the fix, proved in a throwaway copy
If the latest run passed, there is nothing to diagnose (exit 0). If it was recorded on a different commit than HEAD, diagnose run refuses and asks you to run the gate again.
What happens¶
| Stage | Engine | Artifact |
|---|---|---|
| Extract the first real failure from each log | diagnose ci-log (Python, JS/TS, Go, Java, Rust, .NET, C++) |
failed-check-logs.json |
| Classify the failed checks | diagnose checks |
check-intelligence.json |
| Build diagnostic vectors | diagnose vector |
diagnostic-vector.json |
| Plan a remediation | diagnose remediation |
remediation-plan.json |
| Record the SafetyGate decision | SafetyGate | failure-vector.json, safety-gate.json |
Apply the mechanical fix in a throwaway copy (--prove) |
ruff, limited to the SafetyGate's allowed files | proposed.patch |
| Read proven patterns | RepoMemory | pattern-insights.json |
| Score the patch | verify score |
patch-score.json |
| Run the plan's proof in a second isolated copy | verify isolated |
verification-evidence.json |
| Verify independently | verify patch |
protected-verifier-result.json |
| Record the attempt | trajectory store and RepoMemory (both append) | trajectory.jsonl, trajectory-summary.json |
| Report and bind every stage | protected proof chain | loop-report.md, protected-proof-chain.json |
| Hand the decision to a person | decision card | decision-card.json, decision-card.md |
Everything is written to build/sdetkit/remediation-loop/ unless you pass --out-dir. loop.json summarises the run and lists every artifact.
RepoMemory¶
Every run appends its trajectory records to .sdetkit/memory/trajectories.jsonl (pass --memory to choose the file, --no-memory to skip it). Before scoring, the loop reads that history:
- a fix pattern the verifier accepted at least twice is treated as proven, so the patch scorer stops deducting for an unrepeated pattern;
- a failure surface whose fixes the verifier blocked, or people rejected, at least twice is held for review, so later fixes there are blocked.
History can make the scorer more or less cautious; it never grants authority.
python -m sdetkit diagnose memory # records, decisions, proven patterns
python -m sdetkit diagnose memory insights --format json
python -m sdetkit diagnose memory query --decision verified_candidate
Decisions¶
| Decision | Meaning |
|---|---|
verified_candidate |
The fix changed only allowed files, its proof passed in isolation, and the verifier accepted it. Review proposed.patch. |
blocked_review_first |
The fix ran, but the score, the proof or the verifier rejected it. Read protected-verifier-result.json. |
no_change |
The fix changed nothing in a fresh copy, so the log is stale or the failure is already fixed. |
proof_not_run |
A safe candidate exists; rerun with --prove to fix and verify it. |
review_first |
No failure is eligible for a mechanical fix; the report lists why. |
Decision cards and human decisions¶
Every run ends with a decision card: one object (decision-card.json, sdetkit.decision_card.v1) and one PR comment (decision-card.md) holding the failure, the SafetyGate decision, the proposed patch (files, line counts, SHA-256), the score and risk flags, the proof and verifier results, whether the protected proof chain still verifies, the RepoMemory context, and the options a person has. approve is offered only for a verified_candidate whose patch exists and whose chain verifies; reject is always offered.
A person records the decision:
python -m sdetkit diagnose card --loop-dir build/sdetkit/remediation-loop --format md # rebuild or print the card
python -m sdetkit diagnose decide --card build/sdetkit/remediation-loop/decision-card.json --approve --reviewer sam
python -m sdetkit diagnose decide --card build/sdetkit/remediation-loop/decision-card.json --reject --reviewer sam --note "wrong file"
decide writes decision-record.json (sdetkit.decision_record.v1), bound to the card's digest and the patch's SHA-256. It refuses a card edited after it was built, a patch changed on disk, loop artifacts that changed, a proof chain that no longer verifies, and an option the card does not offer. Approving applies nothing: the record names the git apply command the person runs, and its verdict is REVIEW until CI proves the pushed fix. verify merge --evidence decision-record.json reads it like any other verdict. Both decisions are appended to RepoMemory (human_approved, human_rejected).
Posting the card on a pull request¶
The card's Markdown starts with <!-- sdetkit:decision-card --> so a CI job can keep one comment up to date. --prove runs the repository's own proof commands, so run it where the pull request's code has no write token, and post from a separate job that never checks that code out:
# .github/workflows/decision-card.yml: runs on the PR's code, read-only token.
on: pull_request
permissions: { contents: read }
jobs:
card:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: python -m pip install sdetkit ruff pre-commit
- run: |
mkdir -p build/ci-logs/lint
{ echo "Run python -m ruff format --check ."; python -m ruff format --check .; } \
> build/ci-logs/lint/ci_log.txt 2>&1 && exit 0
python -m sdetkit diagnose run --log build/ci-logs/lint/ci_log.txt --prove --no-memory
- uses: actions/upload-artifact@v4
with: { name: decision-card, path: build/sdetkit/remediation-loop/decision-card.md, if-no-files-found: ignore }
# .github/workflows/decision-card-post.yml: posts only; never checks out PR code.
on:
workflow_run: { workflows: [decision-card], types: [completed] }
permissions: { actions: read, pull-requests: write }
jobs:
post:
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with: { name: decision-card, run-id: "${{ github.event.workflow_run.id }}", github-token: "${{ github.token }}" }
continue-on-error: true
- env: { GH_TOKEN: "${{ github.token }}", GH_REPO: "${{ github.repository }}" }
run: |
[ -f decision-card.md ] || exit 0
pr="${{ github.event.workflow_run.pull_requests[0].number }}"
gh pr comment "$pr" --body-file decision-card.md --edit-last || gh pr comment "$pr" --body-file decision-card.md
The comment is advisory: it changes no check, and every authority flag on the card is false. workflow_run.pull_requests is empty for pull requests from forks; look those up by workflow_run.head_sha instead.
Boundaries¶
- Only deterministic, mechanical fixes run:
ruff formatfor formatting drift andruff check --fixfor fixable lint, and only on the files the SafetyGate allows. Test, type, dependency, security and unknown failures stay review-first. - The fix runs in a temporary copy; the proof runs in a second copy whose changed-file list comes from git, not from the fixer.
- The worker that applies the fix never certifies it: the protected proof chain records the proof and the verifier as separate stages.
automation_allowed,patch_application_allowedandmerge_authorizedare alwaysfalse. Applying the patch is your decision: record it withdiagnose decide, thengit apply proposed.patchon a branch.