Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/onenot8/issueLoop/llms.txt

Use this file to discover all available pages before exploring further.

The fix-apply layer lets IssueLoop go beyond finding and cataloguing bugs — it can propose a shell command to repair the problem, check that command against an explicit allowlist before running it, re-execute the associated test, and mark the ticket resolved if the test passes. This layer is off by default: apply_fix will deny every command until you add an entry to config/permission.yaml. This is intentional — running arbitrary shell commands against a codebase is irreversible, and the allowlist is your only gate.

Configure the allowlist

The permission system reads config/permission.yaml. The file is resolved in priority order: the ISSUELOOP_PERMISSION_PATH environment variable → ./config/permission.yaml in your current directory → the bundled package default (which allows nothing). The default file looks like this:
global:
  allowed_exact: []
  allowed_patterns: []

per_repo: {}
There are three ways to allow a command:
Commands in this list are allowed for every repo. The entire command string must match exactly (no wildcards).
global:
  allowed_exact:
    - "black ."
    - "ruff check --fix ."
Patterns here are tested with re.fullmatch against the command string. They apply to every repo.
global:
  allowed_patterns:
    - 'black \S+'
    - 'ruff check --fix \S+'
Scope a pattern to a single repository by placing it under its name in per_repo. This is the recommended approach when a command is specific to one project.
per_repo:
  myrepo:
    allowed_patterns:
      - 'sed -i .* somefile\.py'
      - 'python3 scripts/migrate\.py --auto'
  other-repo:
    allowed_exact:
      - "make lint"
Every command defined in test_manifest.json for a repo is automatically allowed for that repo without any extra configuration — the permission system loads the manifest’s command list as an implicit exact-match set.

propose_fix

Store a proposed shell command on a ticket before attempting to run it. This separates the planning step from the execution step and lets you inspect or modify the proposed command before committing.
import issueloop

ticket = issueloop.get_top_error("myrepo")

issueloop.propose_fix(
    ticket["id"],
    "sed -i 's/old_function/new_function/g' src/utils.py",
)
propose_fix writes the command to the ticket’s proposed_fix field and returns the updated ticket dict. The command is not run at this point — no permission check is performed yet.

apply_fix

apply_fix executes the full fix-verify-resolve cycle:
  1. Look up the ticket and read its proposed_fix.
  2. Check the command against the allowlist — raise PermissionDenied and return "denied" if it is not allowed.
  3. Run the command in the repo’s local_path.
  4. Re-run the ticket’s associated test (test_id) via run_single_test.
  5. If the test passes, mark the ticket done and return "resolved".
  6. If the test fails, increment the attempt counter and retry or escalate.
result = issueloop.apply_fix(ticket["id"])

print(result)
# {"status": "resolved", "ticket_id": "abc-123", "attempts": 1}
The returned dict always contains a status key:

"resolved"

The fix was applied and the associated test now passes. The ticket is marked done.

"retry"

The fix was applied but the test still fails. The ticket is reset to pending and the attempt count is incremented.

"escalated"

The fix failed and max_retries has been reached. The ticket is set to needs_human.

"denied"

The command is not in the allowlist. Nothing was run. Add an entry to config/permission.yaml to permit it.

Retry and escalation

apply_fix accepts a max_retries parameter (default 3). Each failed attempt increments the ticket’s attempts counter. When attempts reaches max_retries, the status is set to needs_human and the escalation summary records how many attempts were made and what the proposed fix was.
# Allow up to 5 attempts before escalating
result = issueloop.apply_fix(ticket["id"], max_retries=5)
To retrieve all tickets currently awaiting human intervention:
human_tickets = issueloop.get_bugs_needing_human("myrepo")
for t in human_tickets:
    print(t["escalation_summary"])

Audit log

Every permission decision — whether the command was allowed by exact match, allowed by pattern, or denied — is written to data/logs/permission_audit.jsonl. Retrieve recent entries with:
log = issueloop.get_permission_audit_log("myrepo", limit=50)

for entry in log:
    print(entry)
Each audit entry has this structure:
{
  "ts": "2024-01-15T10:23:45.123456+00:00",
  "event": "allowed_pattern",
  "repo": "myrepo",
  "command": "sed -i 's/old/new/' somefile.py",
  "detail": "sed -i .* somefile\\.py"
}
ts
string
ISO 8601 timestamp (UTC) of the permission decision.
event
string
One of "allowed_exact", "allowed_pattern", or "denied".
repo
string
The repo the command was checked against.
command
string
The full command string that was checked.
detail
string
For "allowed_pattern" events, the regex pattern that matched. Empty for other events.

check_permission

Check whether a command would be allowed for a repo without actually running anything. Useful for validating your permission.yaml configuration before committing to an automated pipeline.
allowed = issueloop.check_permission(
    "sed -i 's/old/new/' somefile.py",
    "myrepo",
)

if allowed:
    print("Command is in the allowlist")
else:
    print("Command would be denied — update config/permission.yaml")
check_permission performs the same allowlist lookup as apply_fix — including the implicit manifest commands — and writes an audit entry, so every check is logged.
apply_fix is irreversible. Shell commands run by apply_fix modify files on disk. There is no undo mechanism built into IssueLoop. Before enabling the fix-apply workflow, review your allowlist carefully and consider running against a branch or a copy of the repository. The audit log records every command that ran, but it cannot roll back changes.

Build docs developers (and LLMs) love