Walk-and-Fix Agent — close the loop from finding to patched app
The walker surfaces real bugs (dead buttons, console errors, broken assets, failed requests). Walk-and-Fix turns those findings into FixIntent records, lets you (or an LLM) author a patch, writes the patched file into the deployed app on its tester VM, and restarts the systemd unit, no human redeploy required. Use it on a freshly walked app to clear the blocker/major findings in-place and re-walk to confirm.
Just want to see what regressed since your last walk? Skip the fix loop and run
diff_walksover two walk IDs for a bucketed report (resolved, persisted, regressed, new). Use this page once you have the findings and want to actually patch them.
M1 caveat. This release ships the data plane end-to-end plus the
local-vmapply path. Thegithub-prandprovider-redeploymodes return501 NotImplementedError. LLM-driven patch generation (auto-fillsuggested_patchfrom the finding evidence) is the M2 deliverable; for now the caller supplies the patch body when invokingapply_fix.
How it works
- Walk an app and read its findings — the autonomous
walker produces a walk with deduped
Findingrecords (severity, category, selector, stack, network URL,parent_state_idtrail). - Propose a FixIntent —
propose_fix(finding_id, mode='local-vm')wraps the finding in aFixIntentwithstatus='proposed',target_file=null,suggested_patch=null. The FixIntent is the addressable unit the rest of the loop operates on. - Decide on a patch — read the finding evidence, pick the file that
needs to change, and produce its full new contents. M1 leaves this to
the caller; M2 lands LLM patch generation via
Context.sample(). - Apply the patch —
apply_fix(intent_id, patch, target_file, vm_name)writes the file under/opt/of-apps/<slug>/via the guest agent (file_writewith base64 payload) and restartsof-app-<slug>.servicevia thesystemctl stop→systemctl reset-failed→systemctl restarttriplet. - Re-walk and diff — re-run
walk_appand calldiff_walks(feature 14) against the pre-fix walk. The targeted finding should land in theresolvedbucket; if it persists, the FixIntent’sfix_outcomeispersisted. - Close the loop —
close_finding(finding_id, resolution='fixed', fix_intent_id=...)marks the finding so futurerun_fix_cyclecalls skip it.
Apply mechanism (local-vm mode)
apply_fix is a thin wrapper over two existing primitives. Knowing the
wire shape helps when debugging a failed apply:
1. Locate the deployment root
/opt/of-apps/<App.slug>/
2. Write the patch via the guest agent
guest_command(
vm_name=<App.vm_name>,
command="file_write",
args={
"path": "/opt/of-apps/<slug>/<target_file>",
"content": base64(patch),
"encoding": "base64"
}
)
3. Restart the systemd unit (triplet)
systemctl stop of-app-<slug>.service
systemctl reset-failed of-app-<slug>.service
systemctl restart of-app-<slug>.service
4. Wait for the unit to be active and the health check to return 200,
then mark FixIntent.status = 'applied' and stamp applied_at.The target_file is always a path relative to the app’s deployment root
(src/App.tsx, public/index.html, server/routes.js). The patch is
the full file contents, not a unified diff; M1 keeps the apply path
boring on purpose.
MCP tools
propose_fix
Create a FixIntent from a walker Finding. Captures
severity/category/evidence so the patch step has context. M1
leaves target_file and suggested_patch as null (the caller fills
them in on apply_fix).
propose_fix(finding_id='f-abc123def456', mode='local-vm',
notes='broken image asset')
→ {
"intent_id": "fi-a1b2c3d4e5f6",
"finding_id": "f-abc123def456",
"walk_id": "w-xyz789",
"app_name": "my-shop",
"severity": "minor",
"category": "http_error",
"status": "proposed",
"target_file": null,
"original_code": null,
"suggested_patch": null,
"mode": "local-vm",
"session_token": "user-id-123",
"next": "apply_fix(intent_id='fi-a1b2c3d4e5f6', patch=<full file>, target_file=<rel path>, vm_name=<from App>)"
}apply_fix
Write the patch into the deployed app and restart its systemd unit.
local-vm is the only supported mode in M1; github-pr and
provider-redeploy return 501 NotImplementedError.
apply_fix(intent_id='fi-a1b2c3d4e5f6',
patch='<full index.html content>',
target_file='public/index.html',
vm_name='of-tester-001')
→ {
"intent_id": "fi-a1b2c3d4e5f6",
"mode": "local-vm",
"success": true,
"restart_status": "restarted",
"applied_at": "2026-06-26T10:15:30Z",
"session_token": "user-id-123",
"next": "Fix applied. Re-walk the app to confirm: walk_app(app_name=..., app_url=..., vm_name='of-tester-001') then diff_walks(...) to confirm the finding cleared."
}get_fix_intent
Retrieve one FixIntent by id with its current status, target file, suggested patch, and apply result.
get_fix_intent(intent_id='fi-a1b2c3d4e5f6')
→ {
"intent_id": "fi-a1b2c3d4e5f6",
"finding_id": "f-abc123def456",
"walk_id": "w-xyz789",
"app_name": "my-shop",
"severity": "minor",
"category": "http_error",
"status": "applied",
"target_file": "public/index.html",
"suggested_patch": "<full file content>",
"apply_result": {
"success": true,
"restart_status": "restarted",
"applied_at": "2026-06-26T10:15:30Z"
},
"session_token": "user-id-123"
}list_fix_intents
List your FixIntents newest-first, optionally scoped to a single walk.
list_fix_intents(walk_id='w-xyz789')
→ {
"intents": [{
"intent_id": "fi-a1b2c3d4e5f6",
"finding_id": "f-abc123def456",
"walk_id": "w-xyz789",
"app_name": "my-shop",
"severity": "minor",
"category": "http_error",
"status": "applied",
"created_at": "2026-06-26T10:10:00Z",
"updated_at": "2026-06-26T10:15:30Z"
}],
"session_token": "user-id-123"
}close_finding
Mark a walker Finding as resolved so future run_fix_cycle calls skip
it. Resolutions: fixed, wont_fix, duplicate, not_a_bug.
close_finding(finding_id='f-abc123def456', resolution='fixed',
notes='patched via apply_fix',
fix_intent_id='fi-a1b2c3d4e5f6')
→ {
"finding_id": "f-abc123def456",
"walk_id": "w-xyz789",
"resolution": "fixed",
"notes": "patched via apply_fix",
"fix_intent_id": "fi-a1b2c3d4e5f6",
"closed_at": "2026-06-26T10:20:00Z",
"session_token": "user-id-123"
}run_fix_cycle
M1 stub. Propose FixIntents for every blocker/major finding in a
walk (or every NEW finding when baseline_walk_id is supplied), capped
at max_findings. Returns the plan, does not execute the applies.
Caller drives apply_fix per intent. M2 will auto-execute via
Context.sample().
run_fix_cycle(walk_id='w-xyz789', baseline_walk_id='w-baseline',
max_findings=5)
→ {
"walk_id": "w-xyz789",
"baseline_walk_id": "w-baseline",
"candidate_findings": ["f-finding-1", "f-finding-2"],
"proposed_intents": [
{"intent_id": "fi-111", "finding_id": "f-finding-1", "severity": "blocker", "...": "..."},
{"intent_id": "fi-222", "finding_id": "f-finding-2", "severity": "major", "...": "..."}
],
"skipped": {
"f-finding-3": "severity minor outside filter",
"f-finding-4": "already resolved: fixed"
},
"note": "M1 STUB: plan returned without execution. Use apply_fix(intent_id, patch, target_file) per intent. The LLM-driven auto-apply loop lands in M2 via FastMCP Context.sample().",
"session_token": "user-id-123",
"next": "M1 returned the plan. For each intent in proposed_intents[]: either apply_fix(...) or close_finding(...). M2 will auto-execute."
}REST surface
The MCP tools are thin wrappers over an authenticated REST API. All
routes require user context (X-User-Id or X-User-Email); callers see
only their own intents and findings. Cross-user access returns 403,
unknown IDs return 404.
| Method | Path | Purpose |
|---|---|---|
POST | /api/tests/walk-fix/findings/{finding_id}/propose | Create a FixIntent from a walker Finding |
POST | /api/tests/walk-fix/intents/{intent_id}/apply | Apply the patch and restart the unit (local-vm only; 501 for other modes) |
GET | /api/tests/walk-fix/intents/{intent_id} | Fetch one FixIntent (status, target_file, suggested_patch, apply_result) |
GET | /api/tests/walk-fix/intents | List caller’s FixIntents newest-first (optional walk_id filter) |
POST | /api/tests/walk-fix/findings/{finding_id}/close | Mark a Finding as resolved (fixed/wont_fix/duplicate/not_a_bug) |
POST | /api/tests/walk-fix/walks/{walk_id}/cycle | Run fix_cycle: plan FixIntents for blocker/major (or NEW) findings, capped (M1 returns plan only) |
Putting it together
- A walk deploys a broken app to a tester VM (App Deployment), discovers 3 findings (Autonomous Walker), stores them in the walk + findings JSON layout.
- Operator calls
propose_fix(finding_id='f-abc123', mode='local-vm'). The MCP tool proxies to the libvirt backend, which scans walks to locate the finding and returns a FixIntent withstatus='proposed',target_file=null,suggested_patch=null. - Operator reads the finding evidence (stack, selector, network URL) and decides the fix: a patched HTML/CSS/JS file. They call
apply_fix(intent_id='fi-123', patch='<full file content>', target_file='src/App.tsx', vm_name='of-tester-001'). The backend writes/opt/of-apps/my-app/src/App.tsxviaguest_command(file_write, base64), then runssystemctl stop→reset-failed→restartonof-app-my-app.service. - Once the unit is active and the health check returns 200,
apply_fixreturnssuccess=true,restart_status='restarted'. The FixIntent transitions tostatus='applied'. - Operator re-walks the app via
walk_app(app_url=..., vm_name='of-tester-001')to produce a new walk; getswalk_id='w-new'. - Operator calls
run_fix_cycle(walk_id='w-baseline', baseline_walk_id='w-new')(or invokesdiff_walksdirectly) to bucketize findings into resolved / persisted / regressed / new. If the targeted finding is gone,fix_outcome='resolved'; if it persists,fix_outcome='persisted'. - Operator calls
close_finding(finding_id='f-abc123', resolution='fixed', fix_intent_id='fi-123')so the nextrun_fix_cycleskips it.
Example prompts
For walk w-xyz789, propose a fix for finding f-abc123 (the broken
image asset on /products). I'll supply the patched index.html.List my open FixIntents for walk w-xyz789. For any that are still
status='proposed', show me the finding evidence so I can decide on
the patch.Run a fix cycle over walk w-current with baseline w-yesterday, cap at
5 findings. I want the plan only; I'll drive apply_fix per intent.Related
- Autonomous Walker — produces the
Findingrecords (severity, category, selector, stack, network URL) thatFixIntentwraps. - Walker Diffs and Tickets — bucketize
findings across two walks (resolved / persisted / regressed / new) and
serialize them into Linear / Jira / GitHub tickets. Use after
apply_fixto confirm the finding cleared. - App Deployment — provides the
Appentity (slug,vm_name,port), the deployment root (/opt/of-apps/<slug>), and the systemd unit (of-app-<slug>.service) thatapply_fixreuses. - Prompt-to-App Agent — shares the M1 synchronous-fallback pattern: real data plane, LLM generation deferred to M2.
- MCP Integration — set up the OpenFactory MCP server.