specfuse-loop 0.3.23__tar.gz → 0.3.24__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.23/specfuse_loop.egg-info → specfuse_loop-0.3.24}/PKG-INFO +1 -1
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/pyproject.toml +1 -1
- specfuse_loop-0.3.24/specfuse/loop/data/VERSION +1 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/data/docs/skills.md +5 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/data/roadmap.template.md +18 -2
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/data/templates/PLAN.template.md +1 -1
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/lint_plan.py +1 -1
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/loop.py +33 -2
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24/specfuse_loop.egg-info}/PKG-INFO +1 -1
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_loop_auto_archive.py +40 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_loop_orchestration.py +25 -0
- specfuse_loop-0.3.23/specfuse/loop/data/VERSION +0 -1
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/LICENSE +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/NOTICE +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/README.md +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/setup.cfg +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/__init__.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/_filelock.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/_miniyaml.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/adopt_feature.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/data/LEARNINGS.template.md +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/data/docs/concepts/architecture-addendum-gates-and-iterative-planning.md +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/data/docs/concepts/ralph-lineage.md +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/data/docs/getting-started.md +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/data/docs/methodology.md +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/data/gitignore.snippet +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/data/rules/close-discipline.md +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/data/rules/correlation-ids.md +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/data/rules/never-touch.md +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/data/rules/planning-discipline.md +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/data/rules/result-contract.md +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/data/rules/security-boundaries.md +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/data/rules/verification-discipline.md +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/data/rules-local/README.md +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/data/schemas/event.schema.json +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/data/schemas/events/initiative_created.schema.json +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/data/schemas/events/spec_issue_resolved.schema.json +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/data/schemas/events/spec_issue_routed.schema.json +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/data/schemas/events/spec_validated.schema.json +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/data/templates/GATE.template.md +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/data/templates/WU.template.md +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/data/verification.yml.example +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/gate_eval.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/gh_backend.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/gh_features.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/scaffold.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/validate_event.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse_loop.egg-info/SOURCES.txt +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse_loop.egg-info/dependency_links.txt +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse_loop.egg-info/entry_points.txt +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse_loop.egg-info/requires.txt +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse_loop.egg-info/top_level.txt +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_adopt_feature.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_arm_gate_edits_uncommitted.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_attempt_outcome_emission.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_autoclose_deferral_visibility.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_autosync.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_autosync_consent.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_autosync_firstrun.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_autosync_plugin.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_backend.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_bash_routing.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_bookkeeping_commit_crash_run.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_bookkeeping_commit_hook_crash.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_bookkeeping_no_verify.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_claude_resolution.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_close_wu_self_reference.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_closing_deliverable_guard.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_console_encoding.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_cost_tracking.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_degraded_oracle.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_deliverable_presence_gate.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_doctor.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_driver_integration.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_driver_lock.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_duration_tracking.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_empty_files_escalation.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_ensure_feature_branch.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_ensure_feature_branch_base.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_events_redaction.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_extra_gates.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_filelock_portable.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_force_full_close.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_gate_eval.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_gate_eval_calibration.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_gate_eval_intermediate_wiring.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_gate_eval_terminal_wiring.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_gh_backend.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_gh_features.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_git_env_isolation.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_hashed_denylist.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_hashed_denylist_ci.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_init_integration.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_interpreter_normalization.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_leak_findings_redaction.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_leak_scan.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_leak_scan_content.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_learnings_query.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_legacy_4wu_terminal_flips.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_lifecycle_integration.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_lifetime_accounting.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_lint_bare_produces_path.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_lint_close_intermediate.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_lint_close_wu.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_lint_correlation_id.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_lint_correlation_id_close_intermediate.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_lint_oracle_env.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_lint_plan_errors.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_lint_plan_next_draft.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_lint_plan_relative_import.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_lint_planning_sections.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_lint_produces_driver_helper.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_lint_sections.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_lint_task_graph_yaml_selection.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_lint_verification_reference.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_loop_attempt_evidence.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_loop_caveman_preamble.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_loop_close_intermediate.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_loop_defaults_by_type.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_loop_effort.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_loop_failure_note_cap.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_loop_files_changed_guard.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_loop_gate_budget.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_loop_model_alias.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_loop_post_pass_invariant.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_loop_reset_preserving_events.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_loop_smoke_runner.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_loop_unsandboxed.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_loop_zero_token_guard.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_migrate_legacy.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_miniyaml_equivalence.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_miniyaml_negative.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_planned_cost_lint.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_prepare_feature.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_prepare_scaffold_sync.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_produces_field.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_produces_in_diff.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_redaction_windows_home.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_resolve_base.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_result_block.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_roadmap_add_skill.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_roadmap_archive_skill.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_roadmap_row_parser.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_scaffold_data_in_sync.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_scaffold_doc_hygiene.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_scaffold_docs.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_scaffold_init.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_scaffold_manifest.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_scaffold_resources.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_scaffold_seed_sanity.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_scaffold_upgrade.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_scaffold_wiring.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_skills_vendored_in_sync.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_spinning_rearm_gate.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_squash_commit_hook_crash.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_template_closing_shapes.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_terminal_flip_ownership.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_terminal_flips.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_timeout_kill_portable.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_untracked_feature_folder.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_untracked_file_absorption.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_upgrade_integration.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_upgrade_merge_gate.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_validate_event.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_verdict_coupling.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_verify_empty_gate_set.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_version_consistency.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_version_skew.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/tests/test_windows_gate_exec.py +0 -0
- {specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/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.24"
|
|
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.24
|
|
@@ -48,6 +48,11 @@ roadmap ──/pick-feature──▶ active ──/draft-feature──▶ gate 1
|
|
|
48
48
|
`/draft-feature`.
|
|
49
49
|
- **`/roadmap-add`** — add a new feature row to the roadmap before it's ready to
|
|
50
50
|
pick.
|
|
51
|
+
- **`/block-feature`** — flip a roadmap feature to `blocked` (or clear it) with a
|
|
52
|
+
linked `**Blocked by.**` blocker — an ADR awaiting approval or an upstream
|
|
53
|
+
FEAT-ID that must complete first. Writes the row status, the detail block, and
|
|
54
|
+
PLAN frontmatter, keeping intra-page feature links resolvable.
|
|
55
|
+
`/block-feature FEAT-ID --unblock` clears the block and restores the status.
|
|
51
56
|
|
|
52
57
|
### 2. Draft — turn a picked feature into a runnable gate
|
|
53
58
|
|
|
@@ -23,8 +23,24 @@ the canonical column order below.
|
|
|
23
23
|
|----------------|-------|--------|--------|--------|
|
|
24
24
|
|
|
25
25
|
Status: `planned` → `active` → `done` (or `abandoned`). `deferred` = parked
|
|
26
|
-
pending an external decision/dependency; resumable (a human flips it
|
|
27
|
-
`active` when the blocker clears), distinct from `abandoned` (dead).
|
|
26
|
+
by choice pending an external decision/dependency; resumable (a human flips it
|
|
27
|
+
back to `active` when the blocker clears), distinct from `abandoned` (dead).
|
|
28
|
+
`blocked` = cannot proceed because a *named* dependency is unmet — an ADR
|
|
29
|
+
awaiting approval, or an upstream feature that must complete first. Resumable
|
|
30
|
+
like `deferred`, but where `deferred` is a voluntary park with no named blocker,
|
|
31
|
+
`blocked` always names its blocker so the roadmap shows what it waits on and
|
|
32
|
+
links to it.
|
|
33
|
+
|
|
34
|
+
A `blocked` feature's detail section carries a `**Blocked by.**` block — blank
|
|
35
|
+
line above it, one linked blocker per clause:
|
|
36
|
+
|
|
37
|
+
**Blocked by.** [ADR-0007: event-schema versioning](../docs/adr/0007-event-schema-versioning.md) — awaiting approval; [FEAT-2026-0011](#feat-2026-0011) — scoring data must land first.
|
|
38
|
+
|
|
39
|
+
Each blocker is a Markdown link: an ADR points at its `.md` file (kept under
|
|
40
|
+
`docs/adr/` by convention, so from `.specfuse/roadmap.md` the link is
|
|
41
|
+
`../docs/adr/…`); a feature dependency points at the blocking
|
|
42
|
+
feature's roadmap anchor (`#feat-yyyy-nnnn`). When every blocker clears, a human
|
|
43
|
+
flips `blocked` → `active` (or `planned`) and removes the `**Blocked by.**` block.
|
|
28
44
|
|
|
29
45
|
## Notes
|
|
30
46
|
|
|
@@ -7,7 +7,7 @@ roadmap_goal: <one line copied from the roadmap — the north star this feature
|
|
|
7
7
|
plan-next anchors every drafted gate to this and flags if a retrospective implies
|
|
8
8
|
it should change>
|
|
9
9
|
autonomy_default: review # auto | review | supervised
|
|
10
|
-
status: active # active | deferred | done | abandoned
|
|
10
|
+
status: active # active | blocked | deferred | done | abandoned
|
|
11
11
|
# deferred = parked pending an external decision/dependency; resumable, non-dispatchable
|
|
12
12
|
# planned_cost_usd: 0.00 # OPTIONAL — sum of WU planned costs; lint warns when missing or when delta from WU sum > 10%
|
|
13
13
|
---
|
|
@@ -46,7 +46,7 @@ VALID_STATUS = {"draft", "pending", "ready", "in_progress", "in_review", "done",
|
|
|
46
46
|
# unrecognized status that passes lint and then behaves differently at dispatch
|
|
47
47
|
# is the class #183 (deferred) and #185 exist to close. Keep in sync with
|
|
48
48
|
# find_feature / the gate-status transitions in loop.py.
|
|
49
|
-
VALID_FEATURE_STATUS = {"planned", "active", "deferred", "done", "abandoned"}
|
|
49
|
+
VALID_FEATURE_STATUS = {"planned", "active", "blocked", "deferred", "done", "abandoned"}
|
|
50
50
|
VALID_GATE_STATUS = {"open", "awaiting_review", "passed"}
|
|
51
51
|
CLOSING_SEQUENCE = ["retrospective", "lessons", "docs", "plan-next"]
|
|
52
52
|
# New compact closing shapes (FEAT-2026-0015):
|
|
@@ -64,7 +64,7 @@ SPECFUSE_DIR = Path(".specfuse")
|
|
|
64
64
|
REPO_ROOT = SPECFUSE_DIR.parent
|
|
65
65
|
FEATURES_DIR = SPECFUSE_DIR / "features"
|
|
66
66
|
VERIFICATION_PATH = SPECFUSE_DIR / "verification.yml"
|
|
67
|
-
DRIVER_VERSION = "0.3.
|
|
67
|
+
DRIVER_VERSION = "0.3.24"
|
|
68
68
|
# Oldest scaffold layout this driver can drive. init.sh stamps the scaffold's own
|
|
69
69
|
# version into `.specfuse/VERSION`; check_scaffold_version() fails loud at startup if
|
|
70
70
|
# the consumer's scaffold is older than this, pointing at `specfuse upgrade`. Bump
|
|
@@ -285,10 +285,22 @@ def find_feature(arg: str | None) -> Path:
|
|
|
285
285
|
f"deferred features. Flip its PLAN.md status back to `active` "
|
|
286
286
|
f"when the blocker clears, then re-run."
|
|
287
287
|
)
|
|
288
|
+
# A `blocked` feature waits on a named unmet dependency (an ADR or an
|
|
289
|
+
# upstream feature — see its roadmap `**Blocked by.**` block). Like
|
|
290
|
+
# `deferred` it is non-dispatchable; refuse an explicit --feature
|
|
291
|
+
# targeting one rather than dispatching it anyway.
|
|
292
|
+
if _fm.get("status") == "blocked":
|
|
293
|
+
sys.exit(
|
|
294
|
+
f"{d.name} is blocked — the loop does not dispatch blocked "
|
|
295
|
+
f"features. Clear its blocker(s), then run "
|
|
296
|
+
f"`/block-feature {_fm.get('feature_id', d.name)} --unblock` "
|
|
297
|
+
f"(or flip its PLAN.md status back to `active`) and re-run."
|
|
298
|
+
)
|
|
288
299
|
return d
|
|
289
300
|
actives = []
|
|
290
301
|
done_pending_wrap = []
|
|
291
302
|
deferred = []
|
|
303
|
+
blocked = []
|
|
292
304
|
for d in sorted(FEATURES_DIR.glob("*/")):
|
|
293
305
|
plan = d / "PLAN.md"
|
|
294
306
|
if plan.exists():
|
|
@@ -316,6 +328,11 @@ def find_feature(arg: str | None) -> Path:
|
|
|
316
328
|
# a bare invocation won't pick it up; surfaced below so a lone
|
|
317
329
|
# deferred feature reads as parked, not silently absent.
|
|
318
330
|
deferred.append(d)
|
|
331
|
+
elif fm.get("status") == "blocked":
|
|
332
|
+
# Waiting on a named unmet dependency — non-dispatchable, same
|
|
333
|
+
# as deferred. Surfaced below so a lone blocked feature reads as
|
|
334
|
+
# parked-on-a-blocker, not silently absent.
|
|
335
|
+
blocked.append(d)
|
|
319
336
|
if len(actives) == 1:
|
|
320
337
|
return actives[0]
|
|
321
338
|
if not actives:
|
|
@@ -328,6 +345,13 @@ def find_feature(arg: str | None) -> Path:
|
|
|
328
345
|
f" Flip one's PLAN.md status to `active` to "
|
|
329
346
|
f"resume it.\n"
|
|
330
347
|
)
|
|
348
|
+
if blocked:
|
|
349
|
+
names = ", ".join(d.name for d in blocked[-3:])
|
|
350
|
+
msg += (
|
|
351
|
+
f" - {len(blocked)} blocked feature(s), skipped: {names}\n"
|
|
352
|
+
f" Clear the blocker, then /block-feature "
|
|
353
|
+
f"<id> --unblock to resume.\n"
|
|
354
|
+
)
|
|
331
355
|
if done_pending_wrap:
|
|
332
356
|
names = ", ".join(d.name for d in done_pending_wrap[-3:])
|
|
333
357
|
msg += (
|
|
@@ -2669,7 +2693,14 @@ def auto_archive_feature(feature_id: str, repo_root: Path) -> str:
|
|
|
2669
2693
|
section_m2 = section_re.search(roadmap_text)
|
|
2670
2694
|
if section_m2:
|
|
2671
2695
|
roadmap_text = roadmap_text[:section_m2.start()] + roadmap_text[section_m2.end():]
|
|
2672
|
-
|
|
2696
|
+
# A feature that was ever `blocked` via /block-feature carries an explicit
|
|
2697
|
+
# `<a id="feat-…"></a>` line above its heading (so intra-page `#feat-…`
|
|
2698
|
+
# links resolve). The section regex above does not span it, so removing the
|
|
2699
|
+
# section would strand the anchor pointing at a section that has moved to the
|
|
2700
|
+
# archive. Strip the now-orphaned anchor line; Step 3 already re-emitted the
|
|
2701
|
+
# canonical anchor in roadmap-archive.md, so the link target travels with it.
|
|
2702
|
+
roadmap_text = roadmap_text.replace(f"{anchor}\n", "")
|
|
2703
|
+
roadmap_text = re.sub(r'\n{3,}', '\n\n', roadmap_text)
|
|
2673
2704
|
roadmap_path.write_text(roadmap_text)
|
|
2674
2705
|
|
|
2675
2706
|
return "archived"
|
|
@@ -42,6 +42,27 @@ _ROADMAP_DONE = textwrap.dedent("""\
|
|
|
42
42
|
Some content here.
|
|
43
43
|
""")
|
|
44
44
|
|
|
45
|
+
_ROADMAP_DONE_WITH_ANCHOR = textwrap.dedent("""\
|
|
46
|
+
---
|
|
47
|
+
project: test
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
# Roadmap
|
|
51
|
+
|
|
52
|
+
| Feature ID | Title | Status | Folder | Detail |
|
|
53
|
+
|------------|-------|--------|--------|--------|
|
|
54
|
+
| FEAT-2026-9999 | Test feature | done | — | — |
|
|
55
|
+
|
|
56
|
+
<a id="feat-2026-9999"></a>
|
|
57
|
+
## FEAT-2026-9999 — Test feature
|
|
58
|
+
|
|
59
|
+
Some content here.
|
|
60
|
+
|
|
61
|
+
## Notes
|
|
62
|
+
|
|
63
|
+
Trailing content.
|
|
64
|
+
""")
|
|
65
|
+
|
|
45
66
|
_ROADMAP_DONE_NO_SECTION = textwrap.dedent("""\
|
|
46
67
|
---
|
|
47
68
|
project: test
|
|
@@ -122,6 +143,25 @@ class TestAutoArchiveFeature(unittest.TestCase):
|
|
|
122
143
|
"anchor must appear before section heading in archive")
|
|
123
144
|
self.assertIn('Some content here.', archive_text)
|
|
124
145
|
|
|
146
|
+
def test_preexisting_anchor_not_orphaned(self):
|
|
147
|
+
"""A feature blocked via /block-feature carries an explicit <a id> above
|
|
148
|
+
its heading; archiving must remove it, not strand it in roadmap.md."""
|
|
149
|
+
with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmp:
|
|
150
|
+
repo = _make_repo(tmp, roadmap=_ROADMAP_DONE_WITH_ANCHOR)
|
|
151
|
+
result = loop.auto_archive_feature("FEAT-2026-9999", repo)
|
|
152
|
+
self.assertEqual(result, "archived")
|
|
153
|
+
|
|
154
|
+
roadmap_text = (repo / ".specfuse" / "roadmap.md").read_text()
|
|
155
|
+
archive_text = (repo / ".specfuse" / "roadmap-archive.md").read_text()
|
|
156
|
+
|
|
157
|
+
# No orphan anchor left behind in roadmap.md.
|
|
158
|
+
self.assertNotIn('<a id="feat-2026-9999"></a>', roadmap_text)
|
|
159
|
+
self.assertNotIn('## FEAT-2026-9999 — ', roadmap_text)
|
|
160
|
+
# Unrelated content survives.
|
|
161
|
+
self.assertIn('## Notes', roadmap_text)
|
|
162
|
+
# Archive carries exactly one anchor for the feature.
|
|
163
|
+
self.assertEqual(archive_text.count('<a id="feat-2026-9999"></a>'), 1)
|
|
164
|
+
|
|
125
165
|
def test_idempotent_second_call(self):
|
|
126
166
|
"""Second call returns 'already archived' and makes zero file edits."""
|
|
127
167
|
with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmp:
|
|
@@ -368,6 +368,31 @@ class TestFindFeatureSelection(unittest.TestCase):
|
|
|
368
368
|
good = self._write_plan("FEAT-2026-9502-live", "active")
|
|
369
369
|
self.assertEqual(loop.find_feature(None), good)
|
|
370
370
|
|
|
371
|
+
def test_explicit_blocked_feature_is_refused(self):
|
|
372
|
+
"""`--feature` on a blocked feature must refuse, not dispatch."""
|
|
373
|
+
self._write_plan("FEAT-2026-9502-stuck", "blocked")
|
|
374
|
+
with self.assertRaises(SystemExit) as ctx:
|
|
375
|
+
loop.find_feature("FEAT-2026-9502-stuck")
|
|
376
|
+
msg = str(ctx.exception.code).lower()
|
|
377
|
+
self.assertIn("blocked", msg)
|
|
378
|
+
self.assertIn("unblock", msg) # points at the fix
|
|
379
|
+
|
|
380
|
+
def test_blocked_not_treated_as_active_in_scan(self):
|
|
381
|
+
"""A lone blocked feature yields 'no active', named as blocked."""
|
|
382
|
+
self._write_plan("FEAT-2026-9502-stuck", "blocked")
|
|
383
|
+
with self.assertRaises(SystemExit) as ctx:
|
|
384
|
+
loop.find_feature(None)
|
|
385
|
+
msg = str(ctx.exception.code)
|
|
386
|
+
self.assertIn("No active feature", msg)
|
|
387
|
+
self.assertIn("blocked", msg.lower())
|
|
388
|
+
self.assertIn("FEAT-2026-9502-stuck", msg)
|
|
389
|
+
|
|
390
|
+
def test_blocked_does_not_block_a_real_active(self):
|
|
391
|
+
"""A blocked feature alongside one active resolves to the active."""
|
|
392
|
+
self._write_plan("FEAT-2026-9502-stuck", "blocked")
|
|
393
|
+
good = self._write_plan("FEAT-2026-9502-live", "active")
|
|
394
|
+
self.assertEqual(loop.find_feature(None), good)
|
|
395
|
+
|
|
371
396
|
def test_read_frontmatter_error_names_the_file(self):
|
|
372
397
|
"""#177 fix 1: a parse error carries the file path, not just a
|
|
373
398
|
block-relative line number."""
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.3.23
|
|
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.23 → specfuse_loop-0.3.24}/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
|
{specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/data/rules/planning-discipline.md
RENAMED
|
File without changes
|
|
File without changes
|
{specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/specfuse/loop/data/rules/security-boundaries.md
RENAMED
|
File without changes
|
{specfuse_loop-0.3.23 → specfuse_loop-0.3.24}/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
|
|
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.23 → specfuse_loop-0.3.24}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|