Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 52 additions & 29 deletions src/autoskillit/recipe/rules/rules_loop_counter.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,14 +197,29 @@ def _check_loop_guard_before_verify(ctx: ValidationContext) -> list[RuleFinding]
return findings


_WRAPPER_LOOP_EXEMPT_COUNTERS: frozenset[str] = frozenset(
{
"group_iteration_count",
}
)
"""Counters exempt from the cross-cycle reset requirement.

These counters guard run-wide safety ceilings (e.g. group_iteration_count caps the
total number of recipe-group iterations across the entire pipeline run) and must
NOT be reset per audit-remediation cycle — doing so would defeat the safety
ceiling and allow indefinite repetition. Counter names are stable across
recipes; step names vary, so we key the exemption on counter variable rather
than step name."""


@semantic_rule(
name="loop-counter-not-reset-on-outer-cycle",
description=(
"An inner check_loop_iteration guard's counter variable is reachable "
"from an outer check_loop_iteration guard's non-max_exceeded route "
"without passing through a step that resets the inner counter"
),
severity=Severity.WARNING,
severity=Severity.ERROR,
)
def _check_loop_counter_not_reset_on_outer_cycle(ctx: ValidationContext) -> list[RuleFinding]:
"""Detect temporal counter sharing across outer audit-remediation cycles.
Expand All @@ -218,6 +233,12 @@ def _check_loop_counter_not_reset_on_outer_cycle(ctx: ValidationContext) -> list
remediation cycle (audit_remediation_count) — other outer/inner guard
relationships (e.g. merge_fix wrapping merge_rebase) have separate
reset mechanisms and are out of scope for this rule.

Bilateral cycle-membership: an inner guard is only considered in-scope
when it is BOTH forward-reachable from the outer guard's non-exit route
AND backward-reachable to the outer guard. This structurally excludes
post-audit terminal guards (CI watch, stall recovery, etc.) that lie
downstream of the audit-remediation cycle but cannot return to it.
"""
findings: list[RuleFinding] = []
recipe = ctx.recipe
Expand All @@ -244,38 +265,39 @@ def _check_loop_counter_not_reset_on_outer_cycle(ctx: ValidationContext) -> list
if not audit_outer_guards:
return findings

reverse_graph: dict[str, set[str]] = {}
for src, targets in graph.items():
for tgt in targets:
reverse_graph.setdefault(tgt, set()).add(src)

for inner_name, inner_counter in guard_steps.items():
if inner_name in audit_outer_guards:
for outer_name in audit_outer_guards:
outer_step = recipe.steps[outer_name]
if outer_step.on_result is None:
continue

for outer_name in audit_outer_guards:
if inner_name == outer_name:
non_exit_target: str | None = None
for cond in outer_step.on_result.conditions:
if cond.when and "max_exceeded" in cond.when:
continue
non_exit_target = cond.route
break

outer_step = recipe.steps[outer_name]
if outer_step.on_result is None:
continue
if non_exit_target is None or non_exit_target not in recipe.steps:
continue

non_exit_target: str | None = None
for cond in outer_step.on_result.conditions:
if cond.when and "max_exceeded" in cond.when:
continue
non_exit_target = cond.route
break
forward_reachable = bfs_reachable(graph, non_exit_target)
forward_reachable.add(non_exit_target)
cycle_candidates = bfs_reachable(ctx.predecessors, outer_name)

if non_exit_target is None or non_exit_target not in recipe.steps:
for inner_name, inner_counter in guard_steps.items():
if inner_name in audit_outer_guards:
continue

if inner_counter in _WRAPPER_LOOP_EXEMPT_COUNTERS:
continue

forward_reachable = bfs_reachable(graph, non_exit_target)
if inner_name not in forward_reachable:
continue

backward_reachable = bfs_reachable(reverse_graph, inner_name)
if inner_name not in cycle_candidates:
continue

backward_reachable = bfs_reachable(ctx.predecessors, inner_name)
on_path = forward_reachable & backward_reachable
on_path.add(non_exit_target)
on_path.discard(inner_name)
Expand All @@ -293,13 +315,14 @@ def _check_loop_counter_not_reset_on_outer_cycle(ctx: ValidationContext) -> list
rule_name="loop-counter-not-reset-on-outer-cycle",
step_name=inner_name,
message=(
f"Inner guard '{inner_name}' uses counter '{inner_counter}' "
f"but is reachable from audit-remediation guard "
f"'{outer_name}' via '{non_exit_target}' without a reset "
f"step. Add a step using "
f"'autoskillit.smoke_utils.init_counter' to capture "
f"'{inner_counter}' on this path so each audit-remediation "
f"cycle gets a fresh budget."
f"Inner guard '{inner_name}' uses counter "
f"'{inner_counter}' but is reachable from "
f"audit-remediation guard '{outer_name}' via "
f"'{non_exit_target}' without a reset step. Add "
f"a step using "
f"'autoskillit.smoke_utils.init_counter' to "
f"capture '{inner_counter}' on this path so each "
f"audit-remediation cycle gets a fresh budget."
),
)
)
Expand Down
5 changes: 3 additions & 2 deletions src/autoskillit/recipes/diagrams/implementation-groups.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<!-- autoskillit-recipe-hash: sha256:64c9d08f8daa9e5277d160a1e86119a2e4b3e64a84c70ddae6d1db6fed73ea41 -->
<!-- autoskillit-recipe-hash: sha256:da2c052aa5ec001f093b241b6608a88343d341cc9d576e89ae1c20e6d8bfedf8 -->
<!-- autoskillit-diagram-format: v7 -->
## implementation-groups
Group-based implementation with per-group plan/implement/test cycles and PR gates.
Expand All @@ -18,7 +18,8 @@ group → plan → review_approach (optional)
│ fix (on failure) → next_or_done
└────┘
|
+-- audit_impl → reset_test_fix_counter → remediate (optional)
+-- audit_impl → reset_test_fix_counter → reset_merge_test_fix_counter
→ reset_ref_push_counter → remediate (optional)
|
+-- [open-pr] (optional):
| prepare_pr → compose_pr → review_pr → resolve_review
Expand Down
5 changes: 3 additions & 2 deletions src/autoskillit/recipes/diagrams/implementation.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<!-- autoskillit-recipe-hash: sha256:1cf6514f266b3334d7ae8845e5321f62f4d160e6bc25dbe65e1ed13dfd254404 -->
<!-- autoskillit-recipe-hash: sha256:59adb040b882c08afe266b36f46d96a954feca4b0ca86746d7ed1b41f06779b9 -->
<!-- autoskillit-diagram-format: v7 -->
# implementation

Expand All @@ -19,7 +19,8 @@
|
+----+
|
+-- audit_impl → reset_test_fix_counter → remediate (optional)
+-- audit_impl → reset_test_fix_counter → reset_merge_test_fix_counter
→ reset_ref_push_counter → remediate (optional)
|
+-- [open-pr] (optional):
| prepare_pr → compose_pr → review_pr → resolve_review
Expand Down
5 changes: 3 additions & 2 deletions src/autoskillit/recipes/diagrams/remediation.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<!-- autoskillit-recipe-hash: sha256:3c77001f20b467137e2be091e3df4208647f6a4f4c0af9a974a55a350176b8f5 -->
<!-- autoskillit-recipe-hash: sha256:057a181a0b203eb19fff416f34fa9b8e29675c46fc3fe09637f26d177a83b5be -->
<!-- autoskillit-diagram-format: v7 -->
## remediation
Investigate, rectify, implement, and merge a bug fix with CI and PR gates.
Expand All @@ -14,7 +14,8 @@ dry_walkthrough → implement ↔ [retry_worktree on context limit]
|
test → assess
|
+-- audit_impl → reset_test_fix_counter → pre_remediation_merge → remediate (optional)
+-- audit_impl → reset_test_fix_counter → reset_merge_test_fix_counter
→ reset_ref_push_counter → pre_remediation_merge → remediate (optional)
|
make_plan → commit_guard → merge → push
|
Expand Down
28 changes: 27 additions & 1 deletion src/autoskillit/recipes/implementation-groups.json
Original file line number Diff line number Diff line change
Expand Up @@ -540,9 +540,35 @@
"capture": {
"test_fix_loop_count": "${{ result.value }}"
},
"on_success": "reset_merge_test_fix_counter",
"on_failure": "reset_merge_test_fix_counter",
"note": "Resets test_fix_loop_count to 0 so each audit-remediation cycle gets a fresh pre-merge fix budget. The outer audit_remediation_count already bounds total cycles."
},
"reset_merge_test_fix_counter": {
"tool": "run_python",
"with": {
"callable": "autoskillit.smoke_utils.init_counter",
"counter_value": ""
},
"capture": {
"merge_test_fix_loop_count": "${{ result.value }}"
},
"on_success": "reset_ref_push_counter",
"on_failure": "reset_ref_push_counter",
"note": "Resets merge_test_fix_loop_count to 0 so each audit-remediation cycle gets a fresh merge-gate fix budget."
},
"reset_ref_push_counter": {
"tool": "run_python",
"with": {
"callable": "autoskillit.smoke_utils.init_counter",
"counter_value": ""
},
"capture": {
"ref_push_count": "${{ result.value }}"
},
"on_success": "remediate",
"on_failure": "remediate",
"note": "Resets test_fix_loop_count to 0 so each audit-remediation cycle gets a fresh pre-merge fix budget. The outer audit_remediation_count already bounds total cycles."
"note": "Resets ref_push_count to 0 so each audit-remediation cycle gets a fresh ref-push budget."
},
"commit_guard": {
"tool": "run_python",
Expand Down
30 changes: 28 additions & 2 deletions src/autoskillit/recipes/implementation-groups.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -500,13 +500,39 @@ steps:
counter_value: ""
capture:
test_fix_loop_count: "${{ result.value }}"
on_success: remediate
on_failure: remediate
on_success: reset_merge_test_fix_counter
on_failure: reset_merge_test_fix_counter
note: >-
Resets test_fix_loop_count to 0 so each audit-remediation cycle gets a
fresh pre-merge fix budget. The outer audit_remediation_count already
bounds total cycles.

reset_merge_test_fix_counter:
tool: run_python
with:
callable: "autoskillit.smoke_utils.init_counter"
counter_value: ""
capture:
merge_test_fix_loop_count: "${{ result.value }}"
on_success: reset_ref_push_counter
on_failure: reset_ref_push_counter
note: >-
Resets merge_test_fix_loop_count to 0 so each audit-remediation cycle
gets a fresh merge-gate fix budget.

reset_ref_push_counter:
tool: run_python
with:
callable: "autoskillit.smoke_utils.init_counter"
counter_value: ""
capture:
ref_push_count: "${{ result.value }}"
on_success: remediate
on_failure: remediate
note: >-
Resets ref_push_count to 0 so each audit-remediation cycle gets a
fresh ref-push budget.


commit_guard:
tool: run_python
Expand Down
28 changes: 27 additions & 1 deletion src/autoskillit/recipes/implementation.json
Original file line number Diff line number Diff line change
Expand Up @@ -582,9 +582,35 @@
"capture": {
"test_fix_loop_count": "${{ result.value }}"
},
"on_success": "reset_merge_test_fix_counter",
"on_failure": "reset_merge_test_fix_counter",
"note": "Resets test_fix_loop_count to 0 so each audit-remediation cycle gets a fresh pre-merge fix budget. The outer audit_remediation_count already bounds total cycles."
},
"reset_merge_test_fix_counter": {
"tool": "run_python",
"with": {
"callable": "autoskillit.smoke_utils.init_counter",
"counter_value": ""
},
"capture": {
"merge_test_fix_loop_count": "${{ result.value }}"
},
"on_success": "reset_ref_push_counter",
"on_failure": "reset_ref_push_counter",
"note": "Resets merge_test_fix_loop_count to 0 so each audit-remediation cycle gets a fresh merge-gate fix budget."
},
"reset_ref_push_counter": {
"tool": "run_python",
"with": {
"callable": "autoskillit.smoke_utils.init_counter",
"counter_value": ""
},
"capture": {
"ref_push_count": "${{ result.value }}"
},
"on_success": "remediate",
"on_failure": "remediate",
"note": "Resets test_fix_loop_count to 0 so each audit-remediation cycle gets a fresh pre-merge fix budget. The outer audit_remediation_count already bounds total cycles."
"note": "Resets ref_push_count to 0 so each audit-remediation cycle gets a fresh ref-push budget."
},
"commit_guard": {
"tool": "run_python",
Expand Down
30 changes: 28 additions & 2 deletions src/autoskillit/recipes/implementation.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -554,13 +554,39 @@ steps:
counter_value: ''
capture:
test_fix_loop_count: ${{ result.value }}
on_success: remediate
on_failure: remediate
on_success: reset_merge_test_fix_counter
on_failure: reset_merge_test_fix_counter
note: >-
Resets test_fix_loop_count to 0 so each audit-remediation cycle gets a
fresh pre-merge fix budget. The outer audit_remediation_count already
bounds total cycles.

reset_merge_test_fix_counter:
tool: run_python
with:
callable: autoskillit.smoke_utils.init_counter
counter_value: ''
capture:
merge_test_fix_loop_count: ${{ result.value }}
on_success: reset_ref_push_counter
on_failure: reset_ref_push_counter
note: >-
Resets merge_test_fix_loop_count to 0 so each audit-remediation cycle
gets a fresh merge-gate fix budget.

reset_ref_push_counter:
tool: run_python
with:
callable: autoskillit.smoke_utils.init_counter
counter_value: ''
capture:
ref_push_count: ${{ result.value }}
on_success: remediate
on_failure: remediate
note: >-
Resets ref_push_count to 0 so each audit-remediation cycle gets a
fresh ref-push budget.

commit_guard:
tool: run_python
with:
Expand Down
28 changes: 27 additions & 1 deletion src/autoskillit/recipes/remediation.json
Original file line number Diff line number Diff line change
Expand Up @@ -619,9 +619,35 @@
"capture": {
"test_fix_loop_count": "${{ result.value }}"
},
"on_success": "reset_merge_test_fix_counter",
"on_failure": "reset_merge_test_fix_counter",
"note": "Resets test_fix_loop_count to 0 so each audit-remediation cycle gets a fresh pre-merge fix budget. Routes to pre_remediation_merge to preserve the worktree-merge step before starting a new remediation cycle."
},
"reset_merge_test_fix_counter": {
"tool": "run_python",
"with": {
"callable": "autoskillit.smoke_utils.init_counter",
"counter_value": ""
},
"capture": {
"merge_test_fix_loop_count": "${{ result.value }}"
},
"on_success": "reset_ref_push_counter",
"on_failure": "reset_ref_push_counter",
"note": "Resets merge_test_fix_loop_count to 0 so each audit-remediation cycle gets a fresh merge-gate fix budget."
},
"reset_ref_push_counter": {
"tool": "run_python",
"with": {
"callable": "autoskillit.smoke_utils.init_counter",
"counter_value": ""
},
"capture": {
"ref_push_count": "${{ result.value }}"
},
"on_success": "pre_remediation_merge",
"on_failure": "pre_remediation_merge",
"note": "Resets test_fix_loop_count to 0 so each audit-remediation cycle gets a fresh pre-merge fix budget. Routes to pre_remediation_merge to preserve the worktree-merge step before starting a new remediation cycle."
"note": "Resets ref_push_count to 0 so each audit-remediation cycle gets a fresh ref-push budget."
},
"pre_remediation_merge": {
"tool": "merge_worktree",
Expand Down
Loading
Loading