specfuse-loop 0.3.12__tar.gz → 0.3.13__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {specfuse_loop-0.3.12/specfuse_loop.egg-info → specfuse_loop-0.3.13}/PKG-INFO +1 -1
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/pyproject.toml +1 -1
- specfuse_loop-0.3.13/specfuse/loop/data/VERSION +1 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/loop.py +29 -9
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13/specfuse_loop.egg-info}/PKG-INFO +1 -1
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse_loop.egg-info/SOURCES.txt +1 -0
- specfuse_loop-0.3.13/tests/test_close_wu_self_reference.py +115 -0
- specfuse_loop-0.3.12/specfuse/loop/data/VERSION +0 -1
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/LICENSE +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/NOTICE +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/README.md +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/setup.cfg +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/__init__.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/_miniyaml.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/adopt_feature.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/data/LEARNINGS.template.md +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/data/docs/concepts/architecture-addendum-gates-and-iterative-planning.md +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/data/docs/concepts/ralph-lineage.md +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/data/docs/getting-started.md +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/data/docs/methodology.md +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/data/docs/skills.md +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/data/gitignore.snippet +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/data/roadmap.template.md +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/data/rules/correlation-ids.md +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/data/rules/never-touch.md +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/data/rules/result-contract.md +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/data/rules/security-boundaries.md +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/data/rules/verification-discipline.md +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/data/schemas/event.schema.json +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/data/schemas/events/initiative_created.schema.json +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/data/schemas/events/spec_issue_resolved.schema.json +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/data/schemas/events/spec_issue_routed.schema.json +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/data/schemas/events/spec_validated.schema.json +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/data/templates/GATE.template.md +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/data/templates/PLAN.template.md +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/data/templates/WU.template.md +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/data/verification.yml.example +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/gate_eval.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/gh_backend.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/gh_features.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/lint_plan.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/scaffold.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/validate_event.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse_loop.egg-info/dependency_links.txt +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse_loop.egg-info/entry_points.txt +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse_loop.egg-info/requires.txt +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse_loop.egg-info/top_level.txt +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_adopt_feature.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_arm_gate_edits_uncommitted.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_attempt_outcome_emission.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_autosync.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_autosync_consent.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_autosync_firstrun.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_autosync_plugin.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_backend.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_bookkeeping_commit_crash_run.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_bookkeeping_commit_hook_crash.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_closing_deliverable_guard.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_cost_tracking.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_degraded_oracle.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_deliverable_presence_gate.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_doctor.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_driver_integration.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_driver_lock.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_duration_tracking.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_empty_files_escalation.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_ensure_feature_branch.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_events_redaction.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_extra_gates.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_force_full_close.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_gate_eval.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_gate_eval_calibration.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_gate_eval_intermediate_wiring.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_gate_eval_terminal_wiring.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_gh_backend.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_gh_features.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_git_env_isolation.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_hashed_denylist.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_hashed_denylist_ci.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_init_integration.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_leak_findings_redaction.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_leak_scan.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_leak_scan_content.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_learnings_query.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_legacy_4wu_terminal_flips.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_lifecycle_integration.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_lint_bare_produces_path.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_lint_close_intermediate.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_lint_close_wu.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_lint_correlation_id.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_lint_correlation_id_close_intermediate.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_lint_oracle_env.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_lint_plan_errors.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_lint_plan_next_draft.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_lint_plan_relative_import.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_lint_produces_driver_helper.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_lint_sections.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_lint_task_graph_yaml_selection.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_loop_auto_archive.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_loop_caveman_preamble.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_loop_close_intermediate.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_loop_defaults_by_type.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_loop_effort.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_loop_failure_note_cap.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_loop_files_changed_guard.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_loop_gate_budget.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_loop_model_alias.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_loop_orchestration.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_loop_post_pass_invariant.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_loop_reset_preserving_events.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_loop_smoke_runner.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_loop_unsandboxed.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_loop_zero_token_guard.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_migrate_legacy.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_miniyaml_equivalence.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_miniyaml_negative.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_planned_cost_lint.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_prepare_feature.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_prepare_scaffold_sync.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_produces_field.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_result_block.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_roadmap_add_skill.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_roadmap_archive_skill.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_roadmap_row_parser.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_scaffold_data_in_sync.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_scaffold_doc_hygiene.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_scaffold_docs.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_scaffold_init.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_scaffold_manifest.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_scaffold_resources.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_scaffold_seed_sanity.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_scaffold_upgrade.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_scaffold_wiring.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_skills_vendored_in_sync.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_squash_commit_hook_crash.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_template_closing_shapes.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_terminal_flip_ownership.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_terminal_flips.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_untracked_feature_folder.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_upgrade_integration.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_upgrade_merge_gate.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_validate_event.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_verdict_coupling.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_verify_empty_gate_set.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_version_consistency.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_version_skew.py +0 -0
- {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_wu_execution_metadata.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "specfuse-loop"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.13"
|
|
8
8
|
description = "Local-first executor for the Specfuse Plan + Work Unit gate-cycle methodology."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.3.13
|
|
@@ -62,7 +62,7 @@ SPECFUSE_DIR = Path(".specfuse")
|
|
|
62
62
|
REPO_ROOT = SPECFUSE_DIR.parent
|
|
63
63
|
FEATURES_DIR = SPECFUSE_DIR / "features"
|
|
64
64
|
VERIFICATION_PATH = SPECFUSE_DIR / "verification.yml"
|
|
65
|
-
DRIVER_VERSION = "0.3.
|
|
65
|
+
DRIVER_VERSION = "0.3.13"
|
|
66
66
|
# Oldest scaffold layout this driver can drive. init.sh stamps the scaffold's own
|
|
67
67
|
# version into `.specfuse/VERSION`; check_scaffold_version() fails loud at startup if
|
|
68
68
|
# the consumer's scaffold is older than this, pointing at `specfuse upgrade`. Bump
|
|
@@ -2651,13 +2651,17 @@ _NO_FAILURES_SENTINEL = "### Failure-class breakdown\n\n(no non-passing attempts
|
|
|
2651
2651
|
def summarize_attempt_failure_classes(
|
|
2652
2652
|
feature_dir: Path,
|
|
2653
2653
|
gate_n: int | None = None,
|
|
2654
|
+
exclude_correlation_id: str | None = None,
|
|
2654
2655
|
) -> str:
|
|
2655
2656
|
"""Render a '### Failure-class breakdown' markdown table from events.jsonl.
|
|
2656
2657
|
|
|
2657
2658
|
Reads attempt_outcome events whose outcome != 'passed'. When gate_n is
|
|
2658
2659
|
provided, restricts to events whose correlation_id belongs to that gate
|
|
2659
|
-
(resolved via _gate_number_from_wu_id).
|
|
2660
|
-
|
|
2660
|
+
(resolved via _gate_number_from_wu_id). When exclude_correlation_id is
|
|
2661
|
+
provided, drops events with that exact correlation_id — used to keep a
|
|
2662
|
+
close WU's OWN non-passing attempts out of the breakdown it authors, so the
|
|
2663
|
+
close's stumble does not arm the guard against itself (issue #145). Returns
|
|
2664
|
+
_NO_FAILURES_SENTINEL when no non-passing attempts match the filter.
|
|
2661
2665
|
|
|
2662
2666
|
Pure function — reads events.jsonl; no writes, no side effects.
|
|
2663
2667
|
Malformed JSONL lines are skipped (legacy-event tolerance, AC5).
|
|
@@ -2680,8 +2684,10 @@ def summarize_attempt_failure_classes(
|
|
|
2680
2684
|
payload = evt.get("payload") or {}
|
|
2681
2685
|
if payload.get("outcome") == "passed":
|
|
2682
2686
|
continue
|
|
2687
|
+
cid = evt.get("correlation_id", "")
|
|
2688
|
+
if exclude_correlation_id is not None and cid == exclude_correlation_id:
|
|
2689
|
+
continue
|
|
2683
2690
|
if gate_n is not None:
|
|
2684
|
-
cid = evt.get("correlation_id", "")
|
|
2685
2691
|
if _gate_number_from_wu_id(cid) != gate_n:
|
|
2686
2692
|
continue
|
|
2687
2693
|
non_passing.append(payload)
|
|
@@ -2745,7 +2751,14 @@ def assert_failure_class_breakdown_when_failures_present(
|
|
|
2745
2751
|
return True, ""
|
|
2746
2752
|
|
|
2747
2753
|
gate_n = _gate_number_from_wu_id(wu.wu_id)
|
|
2748
|
-
|
|
2754
|
+
# Exclude the close WU's OWN non-passing attempts: the breakdown documents
|
|
2755
|
+
# SUBSTANTIVE-WU failures, not the close's own stumble. Without this, a
|
|
2756
|
+
# failed first close attempt retroactively requires a new subsection in the
|
|
2757
|
+
# RETROSPECTIVE the close itself authors — and the between-attempt
|
|
2758
|
+
# `reset --hard` wipes the partial each retry → spin → blocked_human on
|
|
2759
|
+
# otherwise-done features (issue #145).
|
|
2760
|
+
summary = summarize_attempt_failure_classes(
|
|
2761
|
+
feature_dir, gate_n, exclude_correlation_id=wu.wu_id)
|
|
2749
2762
|
|
|
2750
2763
|
if summary == _NO_FAILURES_SENTINEL:
|
|
2751
2764
|
return True, ""
|
|
@@ -2753,7 +2766,7 @@ def assert_failure_class_breakdown_when_failures_present(
|
|
|
2753
2766
|
if re.search(r"^#{3} Failure-class breakdown\b", retro.read_text(), re.MULTILINE):
|
|
2754
2767
|
return True, ""
|
|
2755
2768
|
|
|
2756
|
-
# Count non-passing attempts for the
|
|
2769
|
+
# Count non-passing attempts (excluding the close's own) for the message.
|
|
2757
2770
|
events_path = feature_dir / "events.jsonl"
|
|
2758
2771
|
count = 0
|
|
2759
2772
|
if events_path.exists():
|
|
@@ -2770,18 +2783,25 @@ def assert_failure_class_breakdown_when_failures_present(
|
|
|
2770
2783
|
payload = evt.get("payload") or {}
|
|
2771
2784
|
if payload.get("outcome") == "passed":
|
|
2772
2785
|
continue
|
|
2786
|
+
cid = evt.get("correlation_id", "")
|
|
2787
|
+
if cid == wu.wu_id:
|
|
2788
|
+
continue
|
|
2773
2789
|
if gate_n is not None:
|
|
2774
|
-
cid = evt.get("correlation_id", "")
|
|
2775
2790
|
if _gate_number_from_wu_id(cid) != gate_n:
|
|
2776
2791
|
continue
|
|
2777
2792
|
count += 1
|
|
2778
2793
|
|
|
2779
2794
|
gate_label = f"gate {gate_n}" if gate_n is not None else "all gates"
|
|
2795
|
+
# Actionable, self-contained retry note (flows into the next attempt's prompt
|
|
2796
|
+
# via failure_note, issue #145 fix #2): name the moved bar AND embed the exact
|
|
2797
|
+
# table to paste, so the agent doesn't rediscover the requirement.
|
|
2780
2798
|
return (
|
|
2781
2799
|
False,
|
|
2782
2800
|
f"assert_failure_class_breakdown_when_failures_present: {count} "
|
|
2783
|
-
f"
|
|
2784
|
-
f"
|
|
2801
|
+
f"substantive-WU attempt(s) in {gate_label} did not pass, so "
|
|
2802
|
+
f"RETROSPECTIVE.md MUST include a '### Failure-class breakdown' subsection "
|
|
2803
|
+
f"— it is absent. Add exactly this subsection to RETROSPECTIVE.md:\n\n"
|
|
2804
|
+
f"{summary}",
|
|
2785
2805
|
)
|
|
2786
2806
|
|
|
2787
2807
|
|
|
@@ -51,6 +51,7 @@ tests/test_autosync_plugin.py
|
|
|
51
51
|
tests/test_backend.py
|
|
52
52
|
tests/test_bookkeeping_commit_crash_run.py
|
|
53
53
|
tests/test_bookkeeping_commit_hook_crash.py
|
|
54
|
+
tests/test_close_wu_self_reference.py
|
|
54
55
|
tests/test_closing_deliverable_guard.py
|
|
55
56
|
tests/test_cost_tracking.py
|
|
56
57
|
tests/test_degraded_oracle.py
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright 2026 Specfuse contributors
|
|
3
|
+
# Licensed under the Apache License, Version 2.0. See LICENSE.
|
|
4
|
+
#
|
|
5
|
+
"""Issue #145 — a close WU's own failed attempt must not arm the
|
|
6
|
+
failure-class-breakdown guard against that same close WU.
|
|
7
|
+
|
|
8
|
+
`assert_failure_class_breakdown_when_failures_present` requires a
|
|
9
|
+
`### Failure-class breakdown` subsection in RETROSPECTIVE.md once non-passing
|
|
10
|
+
attempts exist in the gate. It counted ALL non-passing attempts — including the
|
|
11
|
+
close WU's OWN. Since the close WU is the one that authors the RETROSPECTIVE the
|
|
12
|
+
guard inspects, its malformed first attempt retroactively added a required
|
|
13
|
+
subsection to its own output, and the between-attempt `reset --hard` wiped the
|
|
14
|
+
partial each retry → spin → blocked_human on otherwise-done features.
|
|
15
|
+
|
|
16
|
+
Fix #3: exclude the close WU's own attempts (correlation_id == its wu_id) from
|
|
17
|
+
the failure set that arms the guard against it. Substantive-WU failures still
|
|
18
|
+
require the breakdown. Fix #2: when the guard does fire, its message names the
|
|
19
|
+
required subsection and embeds the exact expected table (it flows into the retry
|
|
20
|
+
prompt via failure_note), so the agent doesn't rediscover the moved bar.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import json
|
|
26
|
+
import tempfile
|
|
27
|
+
import unittest
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
from tests._loop_loader import load_loop
|
|
31
|
+
|
|
32
|
+
loop = load_loop()
|
|
33
|
+
|
|
34
|
+
CLOSE_ID = "FEAT-2026-0099/G1-CLOSE"
|
|
35
|
+
SUBSTANTIVE_ID = "FEAT-2026-0099/G1-T01"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _close_wu(wu_file: Path) -> "loop.WorkUnit":
|
|
39
|
+
return loop.WorkUnit(
|
|
40
|
+
wu_id=CLOSE_ID, file=wu_file, depends_on=[], type="close",
|
|
41
|
+
model="opus", effort="high", status="pending", attempts=1,
|
|
42
|
+
title="Close gate 1", body="body",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _attempt_event(correlation_id: str, outcome: str = "failed",
|
|
47
|
+
failure_class: str = "other", sig: str = "verdict_malformed") -> str:
|
|
48
|
+
return json.dumps({
|
|
49
|
+
"event_type": "attempt_outcome",
|
|
50
|
+
"correlation_id": correlation_id,
|
|
51
|
+
"timestamp": "2026-07-12T00:00:00Z",
|
|
52
|
+
"payload": {"outcome": outcome, "failure_class": failure_class,
|
|
53
|
+
"failure_signature": sig},
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class TestCloseSelfReference(unittest.TestCase):
|
|
58
|
+
def _feature_dir(self, tmp: str, events: list[str], retro: str = "# Retro\n") -> Path:
|
|
59
|
+
d = Path(tmp)
|
|
60
|
+
(d / "events.jsonl").write_text("\n".join(events) + "\n")
|
|
61
|
+
(d / "RETROSPECTIVE.md").write_text(retro)
|
|
62
|
+
return d
|
|
63
|
+
|
|
64
|
+
def test_close_own_failure_does_not_arm_guard(self):
|
|
65
|
+
# Only the close WU's OWN attempts are non-passing (the observed run).
|
|
66
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
67
|
+
d = self._feature_dir(tmp, [_attempt_event(CLOSE_ID)])
|
|
68
|
+
wu = _close_wu(d / "WU.md")
|
|
69
|
+
ok, msg = loop.assert_failure_class_breakdown_when_failures_present(
|
|
70
|
+
wu, d, Path(tmp), "HEAD~1")
|
|
71
|
+
self.assertTrue(ok, f"close's own failure must not require the breakdown: {msg}")
|
|
72
|
+
|
|
73
|
+
def test_substantive_failure_still_requires_breakdown(self):
|
|
74
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
75
|
+
d = self._feature_dir(
|
|
76
|
+
tmp, [_attempt_event(SUBSTANTIVE_ID), _attempt_event(CLOSE_ID)])
|
|
77
|
+
wu = _close_wu(d / "WU.md")
|
|
78
|
+
ok, msg = loop.assert_failure_class_breakdown_when_failures_present(
|
|
79
|
+
wu, d, Path(tmp), "HEAD~1")
|
|
80
|
+
self.assertFalse(ok, "a substantive-WU failure still requires the breakdown")
|
|
81
|
+
# Fix #2 — message is actionable and embeds the exact table to include.
|
|
82
|
+
self.assertIn("### Failure-class breakdown", msg)
|
|
83
|
+
self.assertIn("| failure_class |", msg)
|
|
84
|
+
|
|
85
|
+
def test_breakdown_table_excludes_close_own_attempts(self):
|
|
86
|
+
# Two substantive failures + one close-own failure; the rendered table
|
|
87
|
+
# the guard offers must count only the two substantive ones.
|
|
88
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
89
|
+
d = self._feature_dir(
|
|
90
|
+
tmp,
|
|
91
|
+
[_attempt_event(SUBSTANTIVE_ID), _attempt_event(SUBSTANTIVE_ID),
|
|
92
|
+
_attempt_event(CLOSE_ID)])
|
|
93
|
+
table = loop.summarize_attempt_failure_classes(
|
|
94
|
+
d, gate_n=1, exclude_correlation_id=CLOSE_ID)
|
|
95
|
+
self.assertIn("**total** | **2**", table)
|
|
96
|
+
|
|
97
|
+
def test_summarize_all_self_returns_sentinel(self):
|
|
98
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
99
|
+
d = self._feature_dir(tmp, [_attempt_event(CLOSE_ID)])
|
|
100
|
+
table = loop.summarize_attempt_failure_classes(
|
|
101
|
+
d, gate_n=1, exclude_correlation_id=CLOSE_ID)
|
|
102
|
+
self.assertEqual(table, loop._NO_FAILURES_SENTINEL)
|
|
103
|
+
|
|
104
|
+
def test_breakdown_present_passes(self):
|
|
105
|
+
retro = "# Retro\n\n### Failure-class breakdown\n\n| x | 1 | y |\n"
|
|
106
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
107
|
+
d = self._feature_dir(tmp, [_attempt_event(SUBSTANTIVE_ID)], retro=retro)
|
|
108
|
+
wu = _close_wu(d / "WU.md")
|
|
109
|
+
ok, _ = loop.assert_failure_class_breakdown_when_failures_present(
|
|
110
|
+
wu, d, Path(tmp), "HEAD~1")
|
|
111
|
+
self.assertTrue(ok)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
if __name__ == "__main__":
|
|
115
|
+
unittest.main()
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.3.12
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/data/docs/concepts/ralph-lineage.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/data/rules/security-boundaries.md
RENAMED
|
File without changes
|
{specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/data/rules/verification-discipline.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_lint_correlation_id_close_intermediate.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|