iceboundrock / iceboundrock/AutoForge
EPIC: AutoForge hardening roadmap toward unattended operation
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 0
- Forks
- 0
- Avg merge
- 8h 56m
- Merged PRs (30d)
- 40
Description
Tracking EPIC for all open engineering work on AutoForge / 智铸, derived from the AutoForge Engineering Review (2026-09-06, commit a039525), the PR #1 review follow-ups, the consolidated reviews of PR #27 / #28 / #29 / #32, the PR #38 (hosted CI) review, the eleven review rounds of PR #44 (LOCAL mode), the merge-gate PRs #58 / #60 / #62 / #63, the replan follow-up PRs #66 / #70 / #72, and the REVIEW-payload PRs #76 / #79.
Current recommended autonomous operation level: 2 of 5 — supervised single-issue run to READY_FOR_MERGE, human merges.
MERGE and UPDATE_EPIC are implemented and verified controller-side (#8, #3, #12), the REVIEW/FIX loop is bounded (#9), corrupt state fails loudly (#11), REPLAN_REEXECUTE landed as a durable transaction (PR #32), the controller lock is keyed by the git common dir and held for the whole command (#16), ci is a real required check on main (#31, PR #38, with safety.protected_merge_paths guarding .github/workflows/), and LOCAL mode — feature Markdown → implement → review → fix with no GitHub — landed in PR #44 with its own workspace-identity and filesystem-boundary design (ADR 0001).
Phase 0 is complete as of 2026-09-13. safety.allow_merge is the only key that opens the gate (#18, PR #58), doctor verifies that main really requires ci (#41, PR #60), the protected-path check paginates the changed-files listing (#43, PR #62), and the pre-merge step compares the check definition against the base branch and re-runs merge.verification_commands at the reviewed HEAD (#42, PR #63). #39 (pin CI actions to commit SHAs) was declined. The gate's own preconditions are therefore met; enabling safety.allow_merge for a supervised run is now an operator decision, while unattended merge (Level 4) still needs Level 3 plus the one merge-path follow-up (#68) surfaced by the replan work.
Phase 1 is complete as of 2026-09-16. The persisted replan transaction is schema-validated and fails closed on corruption, with the state protocol bumped to 2 (#35, PR #66); the window rules are scoped, the replan budget is idempotent on replay, and REPLAN steps are accounted (#36, PR #70); runlog metadata is redacted and every crash window, dry-run and activation path is exercised through save → reload → step (#37, PR #72); the REVIEW payload is bounded at the parser — finding count, id, title, location and required_resolution length — with rejection, never clipping (#34, PR #76); and an accepted review's redacted resolutions stay inside the persisted evidence bound, so a replan can proceed without the evidence-truncated block (#33, PR #79). The instruction files were refactored into the scoped AGENTS.md + docs/agent-guides/ layout in the same period (#73, PR #74).
Last roadmap revision: 2026-09-16, after the merge of PR #79 (d4faddb). 33 issues open (excluding this EPIC), 10 of them filed from the Phase 1 reviews.
Dependency chains
Phase 0 (merge gate) — done: #18 #41 #43 #42 landed, #39 declined
├─ #59 reject unknown config keys in every section — done (PR #64)
│ └─ #65 subset YAML parser errors do not name the file (cosmetic, independent)
└─ #68 bind the clean review to the PR identity before MERGE (same shape as #66 P1 on the replan
path; the only item left that gates the merge half of Level 4)
Phase 1 (replan) — done: #35 #36 #37 #34 #33 landed
├─ #71 step budget can block a resume inside the replan write window (from PR #70)
├─ #69 restore automatic resume for the crash-before-close window (from PR #66 R11-F1; low priority)
└─ #67 status --json: derived journal defects beside the raw journal (from PR #66; shares the
output path with #6 — whichever lands second rebases)
Parser bounds (from #34 / PR #76):
#77 bound the FIX payload (rationale length, resolution count, commit_sha shape)
├─ #78 decide whether one-line finding fields may carry control characters
│ (same rejection rule; decide with #77 so the reviewer and fixer bounds land together)
├─ #19 SHA length drift (#77's commit_sha check is the same regex)
└─ #53 executor capture bound + CONTROL_RESULT payload size (same rejection rule)
└─ #17 non-UTF-8 agent output (same file, same change)
PR #44 follow-ups (log tree, executor, LOCAL launch accounting):
#57 charge the LOCAL launch at the launch site (bug, independent)
#55 bound the append-side journal read ─┐
#56 bound the sibling-run directory scan ─┴─ subsumed by #51 journal redesign
#53 bound stdout/stderr capture ── #51 (what a journal line may hold)
#54 narrow the LOCAL no-GitHub wording ── #22 (README claims), #10 (the sandbox half)
#52 O_TMPFILE on Linux (optional hardening, no dependency)
Redaction at the persistence boundary:
#81 REMOTE FIX resolutions persisted unredacted (one line + test; from PR #79 R3-F1)
└─ #6 redact status / status --json output (the output side of the same hole)
└─ #21 redaction patterns: fine-grained PATs, Basic auth, JWTs, URL credentials
REVIEW → FIX handoff:
#80 pass the verified review comment URL to the fixer — mostly already implemented
(`_verify_review_comment` + `{{REVIEW_COMMENT_URL}}` in fix.md); residual scope only
└─ #14 review-comment recovery probe (finding the comment by marker without the agent's URL)
#10 worktree + environment isolation (REMOTE mode)
├─ #14 review-comment / FIX recovery probes
└─ #54's "real sandbox" half (filed there, not in #54)
#13 controller-owned EPIC batching + managed-section splice
└─ absorbs #4 (prompt-side confinement of the roadmap edit)
#2 wire in / remove decide_next_phase ── #22 (the "all transition logic lives in transitions.py" claim)
#75 make check ↔ hosted CI (lockfile check, opt-in Python matrix) ── #40 (the PyYAML branch no
environment runs is the same "what does CI actually exercise" question)
Execution roadmap
Phases are ordered by execution, not by severity. Phase numbering is unchanged from the 2026-09-12 revision: Phase 0 = merge gate, Phase 1 = replan hardening, Phase 2 = PR #44 follow-ups and payload bounds, Phase 3 = reliability, Phase 4 = EPIC ownership, Phase 5 = hygiene. This revision closes Phase 1, files the ten follow-ups from its reviews into their phases, adds #68 as the single Phase 0 follow-up, and triages #80 against what main already does.
Execution order (next 2026-09-16)
The suggested order across phases, smallest-and-most-independent first, dependencies respected. Each line is one PR unless noted.
- #81 — redact REMOTE FIX resolutions at the persistence boundary (one line + one test)
- #57 — charge the LOCAL launch at the launch site (bug, independent)
- #17 + #53 — executor decode errors, head+tail capture bound, CONTROL_RESULT payload size bound (one change)
- #77 + #78 — bound the FIX payload; decide the control-character rule for one-line fields in the same change (closes the parser-bound track opened by #34; touch
_SHA_REonce for #19's SHA-length half) - #55 + #56 — the two stopgap bounds, or #51 if the journal design is decided first (either subsumes both)
- #71 — let a replan journal past the destructive write finish under the step budget
- #68 — bind the clean review to the reviewed PR identity before
MERGE(gates Level 4) - #6 + #67 —
status/status --json: redact state-derived output and add the derived journal-defect field (same output path, one PR) - #15 — re-scope
parse_pr_urlescapes to the remaining call sites - #14 (+ #80 residual) — REVIEW recovery probe, stale-review finding retention, FIX-entry HEAD drift; persist the canonical comment URL from the matched GitHub object while there
- #5 — explicit unblock path for
BLOCKEDruns - #10 — REMOTE worktree + environment allow-list + anchor check (largest item; last of Level 3)
- Phase 4: #13 (absorbing #4), then the rest of #19
- Phase 5 as capacity allows: #21, #20, #65, #75, #40, #22 (after #54), #2, #7, #30, #52, #54
- #69 — last; low priority, recorded so the naive retry is not reintroduced
Resolved by PR #44 (closed 2026-09-12 after verification on main)
PR #44 (148c7f8) closed these by design. Each was re-verified against the merged code and its pinning tests before closing; no further work is planned.
- #45 LOCAL
--allow-dirty: baseline contents not pinned — dissolved: the snapshot content-hashes every regular file, so dirty baseline files are pinned like any other (test_workspace_shape_matrix[tracked-dirty-at-baseline]) - #47 LOCAL mode with a submodule or nested repository — decided: unsupported in LOCAL v1; a
.gitentry below the root refuses the snapshot and names thelocal.excludeentry that would declare the subtree unreviewed (ADR 0001). Re-open as a feature request if recursive snapshots are wanted - #48 crash-shaped runtime artifacts excluded by name — dissolved: LOCAL state moved to
<git common dir>/autoforge/state, so nothing is carved out of the fingerprint andis_runtime_artifactis gone - #49 clean tracked symlink's external target unbound — dissolved: the controller walk classifies every symlink by target text and refuses one resolving outside the tree, clean or dirty
- #50 pathname-based runtime-path safety — answered:
safefs.SafeRootresolves every name withdir_fd=+O_NOFOLLOW; the root pathname is the one stated limit (test_the_root_pathname_is_resolved_normally_and_that_is_the_stated_limit)
Phase 0 — before safety.allow_merge is enabled anywhere (complete 2026-09-13)
- #8 [AF-PROMPT-001]
common.mdforbids merging whilemerge.mdinstructsgh pr merge; decide who owns the merge (closed by #24, merge 27dc6a1) - #3 Controller-side GitHub verification before and after MERGE (AF-WORKFLOW-001 / AF-WORKFLOW-003) (closed by #25)
- #12 [AF-WORKFLOW-001b] Verify
next_issue_urlbefore switching issues in MERGE / UPDATE_EPIC (closed by #26, merge ccb2616) - #9 [AF-WORKFLOW-002] Review-round cap and stagnation detection (closed by #27, merge d16fb92)
- #11 [AF-CODE-001]
runfails loudly on a corruptedstate.json;--forcemoves it aside (closed by #28, merge bbb6afb) - #31 Hosted CI workflow (pytest, ruff, mypy) so the MERGE gate has a real required check (closed by #38;
ciis required onmainvia ruleset 22792049, andsafety.protected_merge_pathsblocks a PR that edits.github/workflows/) - #18 [AF-SEC-001] Deprecated
execution.allow_mergealso opens the merge gate; unknown safety keys ignored — closed by PR #58 (704de3e):execution.allow_mergeis a hardConfigurationError, unknownsafetykeys are rejected,doctorprints the effective gate and its source (Config.allow_merge_source) - #41
doctor: verify the default branch really requires thecicheck — closed by PR #60 (bb71184): read-only ruleset check, including the disabled-ruleset case the effective-rules read cannot see - #39 Pin CI actions to commit SHAs — declined 2026-09-13 (not planned); the mutable
actions/checkout@v7/astral-sh/setup-uv@v10.0.1tags stay as an accepted risk, and #42's check-definition comparison is the compensating control - #43 Pre-merge gate: a PR with more than 100 changed files can never pass the protected-path check — closed by PR #62 (
a784ed2): the listing paginates to the endpoint's cap; thechangedFilescomparison and the incomplete-listing refusal are unchanged - #42 Pre-merge: a green
ciproves the suite ran, not what it asserted — closed by PR #63 (4ca622f):safety.verify_check_definitioncompares the PR's check run jobs/steps against the base branch's own run, andmerge.verification_commandsre-run in a temporary export of the reviewed HEAD (premerge.py); landed without #10's dedicated worktree
Phase 0 follow-up (gates Level 4)
- #68 Bind the clean review to the reviewed PR identity before MERGE, not only to the reviewed HEAD —
_merge_step/_verify_pr_for_mergebind tostate.reviewed_head_shaonly (--match-head-commit); acurrent_pr_urlsubstituted with a same-repository PR at the same HEAD on the same branch but a different base passes every gate and merges a PR whose base no review decided on. Same shape as the #66 round-4 P1 fix on the replan path (decision_pr_url). Persist the reviewed PR identity (and consider the reviewedbase_ref) with the clean review, requirecurrent_pr_urlto be the same PR by identity atREADY_FOR_MERGEand before theMERGEwrite,BLOCKEDon mismatch, never a silent re-bind. Less severe than the replan case (the merged code is the reviewed commit;safety.allow_mergestill applies) but the same class
Phase 1 — replan hardening (follow-ups to PR #32; complete 2026-09-16)
REPLAN_REEXECUTE is the only phase that performs a destructive GitHub write on agent work. Its transaction is sound; these were fail-closed and evidence gaps around it.
- #35 Replan follow-up: persisted-transaction validation, source-branch check, status handling — closed by PR #66 (
c52d878):ReplanTransaction.from_dictschema-validates every field and per-stage requirement, a corrupt journal replays a durableREJECTED+BLOCKEDon everyresume, the sourcehead_refis required atPENDING, the decision is bound to the reviewed PR (decision_pr_url) and to the run, the close is reachable fromVERIFIEDonly (R11-F1), state protocol bumped to 2,statusprintsjournal: CORRUPT (...) - #36 Replan follow-up: trigger semantics, budget idempotence, step accounting — closed by PR #70 (
c18454a):escalation_countandsuperseded_prsare idempotent on replay of a durableSUPERSEDED,stagnation_after_soft_thresholdno longer escalates all-new-findings progress, REPLAN steps are counted, and the fresh-PENDINGbudget block is pinned (test_t2_*) - #37 Replan follow-up: runlog redaction + crash/dry-run/activation test gaps — closed by PR #72 (
cff6241):events.jsonlerror/metadataredaction is pinned, every crash window is exercised through save → reload → step, dry-run, activation source-drift and replacement-side confirmation failure are covered, and replan checkpoints and listings compare by GitHub identity, never canonical strings - #34 Bound the REVIEW payload the parser accepts (finding count and
required_resolutionlength) — closed by PR #76 (c60abea):MAX_FINDINGS_PER_REVIEW,MAX_FINDING_ID_CHARS, title / location /required_resolutionlength bounds atReviewResult.from_payload, rejection with a correctable message that names the size and the limit and never the text; the reviewer is told the bounds throughREVIEW_BOUND_VARIABLES; the FIX prompt size is pinned against them (test_fix_prompt_size_is_bounded_by_the_review_bounds) - #33 Let a replan proceed when review evidence exceeded the persisted bounds — closed by PR #79 (
d4faddb): an accepted review's redacted resolutions stay inside the persisted evidence bound (the bound is applied after redaction inloop_guard), so the parser cap from #34 makes theevidence_truncatedblock unreachable for a controller-accepted round
Phase 1 follow-ups (from the #66 / #70 reviews)
- #71 Step budget can block a resume inside the replan write window, stranding a closed source PR —
_step_oncechecksmax_total_stepsbefore_drive_replan, so aresumeatSUPERSEDE_INTENT(receipt present),SUPERSEDEDorCOMPENSATINGwithstep_count == max_total_stepsenters terminalBLOCKEDwith the plain budget reason: source PR closed, replacement never installed ascurrent_pr_url(or a decided reopen never carried out),superseded_prs/escalation_countuntouched. Preferred: exempt the post-write stages from the gate (the step still counts; no agent is invoked there) so the budget ends the run at the next phase boundary;PENDING/PREPARED/VERIFIEDkeep blocking as pinned bytest_t2_*. Alternatively keep blocking but name the txn id, both PRs and the manual step. State the rule in the loop-bounds section ofworkflow.md - #69 Restore automatic resume for the crash-before-close window with proof-grade evidence — low priority. #66 R11-F1 refuses every OPEN-at-intent resume because "close never ran" and "close landed, crash before receipt, human reopened" are the same evidence; the cost is that a sub-second crash between persisting
SUPERSEDE_INTENTandgh pr closelanding needs a human. The only acceptable design is the timeline-event watermark (monotonic, undeletable event ids read before the close; aclosedevent past the watermark blocks); the controller-posted pre-close marker turns absence of a comment into a licence for a destructive write and is rejected. Needs aGitHubClienttimeline read, a new journal field required atSUPERSEDE_INTENT(protocol bump), fake support; thetest_r11f1_*,test_w6_*and transient-close no-second-close assertions must keep holding - #67
status --json: surface the derived replan journal defects beside the raw journal —statusprintsjournal: CORRUPT (...),--jsonprints the journal as persisted and a consumer cannot tell usable from corrupt without reimplementing the validation. Add a derived sibling field (replan_journal: {readable, stage, defects}) from the samefrom_dictcallstatususes; never merge into the raw object; a non-object journal keeps raisingStateError. Shares the output path with #6
Phase 2 — log tree, executor, LOCAL launch accounting and payload bounds (follow-ups to PR #44 and PR #76)
The R11 statement "a journal refusal lands before a write-capable agent has done work" is true of the recovery read only. These close the remaining unbounded reads, the mis-charged launch, and the two agent payloads (FIX result, raw stdout) that are still accepted unbounded; they apply to both REMOTE and LOCAL runs except where marked.
- #57 LOCAL: a pre-launch refusal inside
_invoke_phaseis charged as a write-agent launch — a refused journal, aConfigurationErrorfromvalidate_profileor a missing template each consume one ofMAX_LOCAL_PHASE_ATTEMPTSwith zero launches, and the eventual block reason is false. Charge at the launch site, immediately beforeprovider.execute(), the way correction re-launches already are (_charge_local_launch) - #55
SafeRoot.append_textreads the existing file unbounded — the one journal read that happens after the agent ran bypassesMAX_EVENT_JOURNAL_BYTESand carries an oversized file forward (safefs.py:622takes nolimit, unlikeread_bytesat:550). Stopgap that holds under any #51 design: alimitparameter onappend_textmapped to the existing_refuse_journalStateError - #56
RunLoggerlists every sibling run inlogs/before skipping it, with no budget — cost of opening the logger is O(entries inlogs/); walklogs.subroot(run_id)one level deep withmax_entries=MAX_EVENT_JOURNAL_RECORDS. The refusal must not consume a LOCAL launch (#57) - #53 Executor: agent stdout/stderr capture is unbounded and reaches the log tree whole — several in-memory copies (decode + redaction) before any bound; a head+tail capture bound with a truncation mark, and a CONTROL_RESULT payload size bound at parse time (rejected, not clipped, as in #34). Do #17 in the same change
- #77 Bound the FIX payload the parser accepts —
rationalehas a minimum but no maximum and is persisted whole instate.last_fix_resolutions(and echoed into a LOCALblock_reason);commit_shais never matched against_SHA_RE;follow_up_issue_urlis unbounded beforeparse_issue_urlquotes it; theresolutionslist has no count bound before every element is parsed. Same policy as #34:MAX_FIX_RATIONALE_CHARS(≤MAX_REQUIRED_RESOLUTION_CHARS, relation pinned),commit_shamust match_SHA_RE, count checked atMAX_FINDINGS_PER_REVIEWbefore parsing elements, URL length bounded, bounds told to the fixer through template variables. LOCAL REVIEWobservationsstay unbounded by design (parsed and discarded) - #78 Decide whether REVIEW one-line finding fields (
title,location) may carry control characters — the render path is safe (escape_inline, growing fence) and the prompt stays bounded, but a control-character-filled title renders at 4–6× and persists at 6× (json.dumps\u00XX) the bytes of a real one, per round, for as many rounds as the loop runs. Decision: reject at parse time (correctable "keep it to one line" error) or keep render-only. The #76 R3 data point extends the question to control characters other than newline insiderequired_resolution. Decide together with #77 so the bound set lands once;escape_inlinestays regardless - #51
RunLogger: the event journal is read whole on resume and rewritten whole on every append — the recovery read is bounded since R11 (64 MiB / record count); the O(journal)-per-event append remains. Choose between a persisted next-seqside record (option 2) and a segmented journal (option 3); whichever lands should subsume #55 and #56 - #54 LOCAL mode: the documented no-GitHub promise is wider than what the controller enforces — README says a local run "never touches GitHub" and
local_common.mdsays the rule list is "enforced by the controller"; only the git anchor (HEAD + branch) is checked. Narrow both to what ADR 0001 §2.2 / §8.1 already say and pin with the local-template lint; the sandbox half belongs to #10 - #52
safefs: close the named-temporary observation window withO_TMPFILEon Linux — not a defect of the boundary (a planted link can only observe bytes the same UID can already read; it can never make a controller write replace anything). Platform-conditional_unnamed_tmp_atwith the nlink re-check kept on both paths; document in ADR 0001 §8.10 (todaysafefs.py:75documents why it is not done)
Phase 3 — reliability for unattended single-issue runs
- #59 Config: unknown keys are rejected under
safetyonly; a typo in any other section is silently ignored — closed by PR #64 (1252655): every section,review.replan, eachprofiles.<name>mapping and the top level reject unknown keys with the message shapesafetyalready used - #81 REMOTE FIX persists agent-authored resolutions unredacted — PR #44 put
redact_dictin front of state at four sites and missed the fourth:engine.py:4296state.last_fix_resolutions = [r.to_dict() ...](LOCAL FIX at:1875is redacted). A valid FIX result whoserationalequotes a token lands instate.jsonandstatus --json. One line plus one REMOTE regression test mirroringtest_findings_and_resolutions_are_redacted_before_they_are_persisted, covering REMOTE REVIEW findings in the same test. #6 is the output side of the same hole - #17 [AF-CODE-004] Executor crashes with
UnicodeDecodeErroron non-UTF-8 agent output —executor.py:108still passestext=Truewith noencoding/errors; a single invalid byte escapes theAutoForgeErrorexit-code mapping and skips the log write. Smallest change in the phase; do it alongside #53 - #15 [AF-CODE-002] Agent-returned URLs raise
ConfigurationErrorand bypass attempt persistence — partly addressed for issue URLs (_verify_issue_selectionraisesVerificationError), butparse_pr_urlstill escapes several replan and merge paths (13 call sites inengine.py; see #35 N3); re-scope to the remaining call sites. #77'sfollow_up_issue_urlbound removes one input from this class - #14 [AF-WORKFLOW-004] Review-comment recovery, stale-review finding retention, FIX-entry HEAD drift detection — REVIEW has no recovery probe analogous to
_try_recover_pr, a stale review with findings consumes the round and emptiesopen_findings, and_prepare_fixcannot tell a landed-then-crashed push from a fresh entry. A recovery probe must find the round's comment by heading + marker without the agent's URL; that is the same lookup #80's residual scope needs - #80 Output the link of the verified review comment and give it to the coding agent — triage 2026-09-16: mostly implemented on
main._verify_review_comment(since339df68) already requires the claimed URL to be a comment on the current PR, found on GitHub, carrying the# AI Code Review — Round Nheading, the<!-- ai-review-result -->marker, the boundreviewed_head_shaand a consistentneeds_fix_round; the verified URL is persisted instate.last_review_comment_url, survivesresume, andfix.mdrenders it as{{REVIEW_COMMENT_URL}}besideREVIEW_ROUND,REVIEWED_HEAD_SHAandHEAD_SHA. Residual scope: persist the canonical URL from the matched GitHub comment object (match.url) rather than the agent-supplied string, state infix.mdthat the linked comment is the authoritative review for this round and no other comment may be substituted, and pin the multi-round / mixed-conversation / stale / resume cases from the issue as tests. Re-scope the issue to that or close it as satisfied - #10 [AF-ARCH-001] Isolate agents: dedicated worktree, allow-listed environment, state dir outside the repo — re-scope to REMOTE mode: PR #44 already gives LOCAL runs a state directory outside the reviewed tree (
<git common dir>/autoforge/state) and a git-anchor check before and after every phase, and #42 landed its pre-merge verification in a temporary export rather than waiting for the worktree. REMOTE agents still run incwdwithdict(os.environ)(executor.py:96). Remaining: the per-issue worktree, the environment allow-list, reusing the LOCAL anchor check for REMOTE, and the sandbox question deferred from #54 - #16 [AF-CODE-003] Controller lock is scoped to state-dir path, not repository, and only held per step — closed 2026-09-10:
locking.repository_lock_pathkeys on the git common dir andcliwraps whole commands inengine.locked() - #5 Explicit unblock path for BLOCKED runs — every bound (round cap, stagnation, the replan limit,
MAX_LOCAL_PHASE_ATTEMPTS,max_total_steps) is terminal, so an operator has no supported way to continue after resolving the cause other than hand-editingstate.json(which now fails loudly on any unknown key and refuses a protocol-1 replan journal). Must re-run recovery inspection against live state and go throughvalidate_transition; #71 is the one budget block that should not need it
Phase 4 — multi-issue / EPIC ownership
- #13 [AF-WORKFLOW-005] Controller must own EPIC batching, counter reset after verified write, and managed-section splice —
merged_since_epic_updateis written and rendered into the prompt (engine.py:845) but never read by the controller to decide anything;record_epic_update()resets the counter before anything confirms the body changed - #4
update_epicprompt must confine edits to the managed roadmap section (absorbed by #13 if the controller performs the splice) - #19 [AF-PROMPT-002] Prompt/parser drift: SHA length (
_SHA_REstill{7,40}atresult_parser.py:38), code-fence rule, unverifiedfinding_ids, reviewer read-only rule — the SHA half is touched by #77 (commit_shamust match_SHA_RE); land the regex change there and keep the prompt-side items here
Phase 5 — maintainability and hygiene
- #20 [AF-CODE-006] PR recovery only searches the first 100 open PRs — scoped to the ANALYZE_EXECUTE adoption lookup only (
GitHubClient.list_open_prs(limit=100),github.py:1080);REPLAN_REEXECUTEalready uses the strict listing. Search byhead:autoforge/<n>/linked:<n>or paginate; recovery should find the PR rather than block - #21 [AF-SEC-002] Redaction misses fine-grained PATs, Basic auth, JWTs and credentials in URLs —
redaction.pycoversAuthorization: Bearer|Token, classicgh*_tokens andsk-/sk-ant-keys; none of the four named patterns is present yet. Do after #81 / #6 so the new patterns reach every boundary at once - #6 Redact state-derived output in
status --json—block_reasonis printed unredacted atcli.py:338,:553,:615and:661, andstatus --jsonprintsstate.to_dict()whole (cli.py:568). Land with #67 (same output path) - #65 Subset YAML parser errors do not name the config file —
_parse_yaml_subsetraisesConfigurationErrordirectly andload_config_filere-raises it unchanged, so thecannot parse config <path>:prefix that wrapstomllib/json/ PyYAML errors never applies; the two backends also word the root-type error differently. RaiseValueErrorfrom the subset parser (as the #64 duplicate-key error already does) or prefix on re-raise. Cosmetic, follow-up to #59 / PR #64 - #75 Bring
make checkcloser to the hosted CI workflow —make checkruns the same four commands as CI on one interpreter (pinned bytest_ci_runs_the_same_commands_as_the_local_make_targets), but CI also installs withuv sync --lockedand runspyteston 3.11 and 3.12 while.python-versionis 3.14. Add alock-checktarget (uv lock --check) tocheck, fix theMakefilecomment, optionally an opt-incheck-matrixtarget reading the versions from one place, and extend the drift guard for any new mirrored target. Follow-up to #73 / PR #74 - #22 [AF-DOC-001] README and docstrings overstate current guarantees — re-audit after #54; the #11 sentence is now true, the "all transition logic lives in
transitions.py" sentence is still false until #2. The instruction files were re-cut in PR #74; README "Development" was corrected there, the rest of README was not re-audited - #2 Wire in or remove
decide_next_phase(dead transition logic attransitions.py:173; referenced only fromtests/test_transitions.py; cannot express REVIEW → REPLAN_REEXECUTE — see #37 N4) - #7 Minor hygiene — re-scoped: lock-file PID growth (the lock now replaces rather than appends), unknown state keys (
State.from_dictrefuses them since PR #44) and directory fsync are done. Remaining:REQUIRED_PROFILESduplicated inengine.py:229anddoctor.py:52, and the unusedAgentInvocationdataclass atexecutor.py:149 - #30
quarantine_state_file: fall back when hard links are unavailable; document the link+unlink crash window - #40 The
yamlextra's PyYAML branch is exercised by no environment — the branch that decides routing, loop bounds andsafety.allow_mergefor apip install autoforge[yaml]operator has zero coverage; add a differential test over the example config or remove the extra. Same question as #75 (what CI actually exercises)
Exit criteria per level
| Level | Requires |
|---|---|
| 3 — unattended single issue, human merge | Phase 1 follow-up #71 + Phase 2 bugs and bounds (#57, #55, #56, #53, #77) + Phase 3 (#81, #17, #15, #14, #10, #5) |
| 4 — unattended single issue incl. merge | Level 3 + #68 (bind the clean review to the PR identity before MERGE) — Phase 0 is otherwise complete (#18, #41, #43, #42 landed; #39 declined) |
| 5 — unattended multi-issue | Level 4 + Phase 4 (#13, #4, #19) |
#69, #67, #78, #80 and the rest of Phase 2 (#51, #54, #52) and Phase 5 improve cost, documentation, operator visibility and hardening but gate no level. LOCAL mode (PR #44) is a separate product surface with its own promise — one working tree, no GitHub from the controller — and is not scored on this scale.
Only the section between the ai-controller-roadmap markers is intended to be machine-maintained. Text outside the markers is maintained by hand.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the execution roadmap and dependency chains in this issue, then choose a specific open child issue rather than working on the EPIC itself. Read that child issue's named files, tests, or pull requests first; the EPIC is complete only when its tracked engineering items and stated phase criteria are resolved.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- ai-infra-agents, backend, devtools
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Needs clarification
- Newbie friendliness
- 10/100