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.
Files changed (147) hide show
  1. {specfuse_loop-0.3.12/specfuse_loop.egg-info → specfuse_loop-0.3.13}/PKG-INFO +1 -1
  2. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/pyproject.toml +1 -1
  3. specfuse_loop-0.3.13/specfuse/loop/data/VERSION +1 -0
  4. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/loop.py +29 -9
  5. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13/specfuse_loop.egg-info}/PKG-INFO +1 -1
  6. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse_loop.egg-info/SOURCES.txt +1 -0
  7. specfuse_loop-0.3.13/tests/test_close_wu_self_reference.py +115 -0
  8. specfuse_loop-0.3.12/specfuse/loop/data/VERSION +0 -1
  9. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/LICENSE +0 -0
  10. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/NOTICE +0 -0
  11. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/README.md +0 -0
  12. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/setup.cfg +0 -0
  13. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/__init__.py +0 -0
  14. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/_miniyaml.py +0 -0
  15. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/adopt_feature.py +0 -0
  16. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/data/LEARNINGS.template.md +0 -0
  17. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/data/docs/concepts/architecture-addendum-gates-and-iterative-planning.md +0 -0
  18. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/data/docs/concepts/ralph-lineage.md +0 -0
  19. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/data/docs/getting-started.md +0 -0
  20. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/data/docs/methodology.md +0 -0
  21. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/data/docs/skills.md +0 -0
  22. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/data/gitignore.snippet +0 -0
  23. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/data/roadmap.template.md +0 -0
  24. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/data/rules/correlation-ids.md +0 -0
  25. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/data/rules/never-touch.md +0 -0
  26. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/data/rules/result-contract.md +0 -0
  27. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/data/rules/security-boundaries.md +0 -0
  28. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/data/rules/verification-discipline.md +0 -0
  29. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/data/schemas/event.schema.json +0 -0
  30. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/data/schemas/events/initiative_created.schema.json +0 -0
  31. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/data/schemas/events/spec_issue_resolved.schema.json +0 -0
  32. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/data/schemas/events/spec_issue_routed.schema.json +0 -0
  33. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/data/schemas/events/spec_validated.schema.json +0 -0
  34. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/data/templates/GATE.template.md +0 -0
  35. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/data/templates/PLAN.template.md +0 -0
  36. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/data/templates/WU.template.md +0 -0
  37. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/data/verification.yml.example +0 -0
  38. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/gate_eval.py +0 -0
  39. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/gh_backend.py +0 -0
  40. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/gh_features.py +0 -0
  41. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/lint_plan.py +0 -0
  42. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/scaffold.py +0 -0
  43. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse/loop/validate_event.py +0 -0
  44. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse_loop.egg-info/dependency_links.txt +0 -0
  45. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse_loop.egg-info/entry_points.txt +0 -0
  46. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse_loop.egg-info/requires.txt +0 -0
  47. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/specfuse_loop.egg-info/top_level.txt +0 -0
  48. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_adopt_feature.py +0 -0
  49. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_arm_gate_edits_uncommitted.py +0 -0
  50. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_attempt_outcome_emission.py +0 -0
  51. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_autosync.py +0 -0
  52. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_autosync_consent.py +0 -0
  53. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_autosync_firstrun.py +0 -0
  54. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_autosync_plugin.py +0 -0
  55. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_backend.py +0 -0
  56. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_bookkeeping_commit_crash_run.py +0 -0
  57. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_bookkeeping_commit_hook_crash.py +0 -0
  58. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_closing_deliverable_guard.py +0 -0
  59. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_cost_tracking.py +0 -0
  60. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_degraded_oracle.py +0 -0
  61. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_deliverable_presence_gate.py +0 -0
  62. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_doctor.py +0 -0
  63. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_driver_integration.py +0 -0
  64. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_driver_lock.py +0 -0
  65. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_duration_tracking.py +0 -0
  66. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_empty_files_escalation.py +0 -0
  67. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_ensure_feature_branch.py +0 -0
  68. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_events_redaction.py +0 -0
  69. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_extra_gates.py +0 -0
  70. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_force_full_close.py +0 -0
  71. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_gate_eval.py +0 -0
  72. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_gate_eval_calibration.py +0 -0
  73. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_gate_eval_intermediate_wiring.py +0 -0
  74. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_gate_eval_terminal_wiring.py +0 -0
  75. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_gh_backend.py +0 -0
  76. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_gh_features.py +0 -0
  77. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_git_env_isolation.py +0 -0
  78. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_hashed_denylist.py +0 -0
  79. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_hashed_denylist_ci.py +0 -0
  80. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_init_integration.py +0 -0
  81. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_leak_findings_redaction.py +0 -0
  82. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_leak_scan.py +0 -0
  83. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_leak_scan_content.py +0 -0
  84. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_learnings_query.py +0 -0
  85. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_legacy_4wu_terminal_flips.py +0 -0
  86. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_lifecycle_integration.py +0 -0
  87. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_lint_bare_produces_path.py +0 -0
  88. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_lint_close_intermediate.py +0 -0
  89. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_lint_close_wu.py +0 -0
  90. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_lint_correlation_id.py +0 -0
  91. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_lint_correlation_id_close_intermediate.py +0 -0
  92. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_lint_oracle_env.py +0 -0
  93. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_lint_plan_errors.py +0 -0
  94. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_lint_plan_next_draft.py +0 -0
  95. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_lint_plan_relative_import.py +0 -0
  96. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_lint_produces_driver_helper.py +0 -0
  97. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_lint_sections.py +0 -0
  98. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_lint_task_graph_yaml_selection.py +0 -0
  99. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_loop_auto_archive.py +0 -0
  100. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_loop_caveman_preamble.py +0 -0
  101. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_loop_close_intermediate.py +0 -0
  102. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_loop_defaults_by_type.py +0 -0
  103. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_loop_effort.py +0 -0
  104. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_loop_failure_note_cap.py +0 -0
  105. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_loop_files_changed_guard.py +0 -0
  106. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_loop_gate_budget.py +0 -0
  107. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_loop_model_alias.py +0 -0
  108. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_loop_orchestration.py +0 -0
  109. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_loop_post_pass_invariant.py +0 -0
  110. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_loop_reset_preserving_events.py +0 -0
  111. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_loop_smoke_runner.py +0 -0
  112. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_loop_unsandboxed.py +0 -0
  113. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_loop_zero_token_guard.py +0 -0
  114. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_migrate_legacy.py +0 -0
  115. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_miniyaml_equivalence.py +0 -0
  116. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_miniyaml_negative.py +0 -0
  117. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_planned_cost_lint.py +0 -0
  118. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_prepare_feature.py +0 -0
  119. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_prepare_scaffold_sync.py +0 -0
  120. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_produces_field.py +0 -0
  121. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_result_block.py +0 -0
  122. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_roadmap_add_skill.py +0 -0
  123. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_roadmap_archive_skill.py +0 -0
  124. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_roadmap_row_parser.py +0 -0
  125. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_scaffold_data_in_sync.py +0 -0
  126. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_scaffold_doc_hygiene.py +0 -0
  127. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_scaffold_docs.py +0 -0
  128. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_scaffold_init.py +0 -0
  129. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_scaffold_manifest.py +0 -0
  130. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_scaffold_resources.py +0 -0
  131. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_scaffold_seed_sanity.py +0 -0
  132. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_scaffold_upgrade.py +0 -0
  133. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_scaffold_wiring.py +0 -0
  134. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_skills_vendored_in_sync.py +0 -0
  135. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_squash_commit_hook_crash.py +0 -0
  136. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_template_closing_shapes.py +0 -0
  137. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_terminal_flip_ownership.py +0 -0
  138. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_terminal_flips.py +0 -0
  139. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_untracked_feature_folder.py +0 -0
  140. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_upgrade_integration.py +0 -0
  141. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_upgrade_merge_gate.py +0 -0
  142. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_validate_event.py +0 -0
  143. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_verdict_coupling.py +0 -0
  144. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_verify_empty_gate_set.py +0 -0
  145. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_version_consistency.py +0 -0
  146. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_version_skew.py +0 -0
  147. {specfuse_loop-0.3.12 → specfuse_loop-0.3.13}/tests/test_wu_execution_metadata.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: specfuse-loop
3
- Version: 0.3.12
3
+ Version: 0.3.13
4
4
  Summary: Local-first executor for the Specfuse Plan + Work Unit gate-cycle methodology.
5
5
  Author: Specfuse contributors
6
6
  License: Apache-2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "specfuse-loop"
7
- version = "0.3.12"
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.12"
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). Returns _NO_FAILURES_SENTINEL when
2660
- no non-passing attempts match the filter.
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
- summary = summarize_attempt_failure_classes(feature_dir, gate_n)
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 error message.
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"non-passing attempt(s) in {gate_label} but '### Failure-class breakdown' "
2784
- f"subsection absent from RETROSPECTIVE.md",
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: specfuse-loop
3
- Version: 0.3.12
3
+ Version: 0.3.13
4
4
  Summary: Local-first executor for the Specfuse Plan + Work Unit gate-cycle methodology.
5
5
  Author: Specfuse contributors
6
6
  License: Apache-2.0
@@ -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