specfuse-loop 0.3.13__tar.gz → 0.3.15__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 (161) hide show
  1. {specfuse_loop-0.3.13/specfuse_loop.egg-info → specfuse_loop-0.3.15}/PKG-INFO +1 -1
  2. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/pyproject.toml +1 -1
  3. specfuse_loop-0.3.15/specfuse/loop/_filelock.py +44 -0
  4. specfuse_loop-0.3.15/specfuse/loop/data/VERSION +1 -0
  5. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/data/docs/getting-started.md +10 -0
  6. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/gh_backend.py +11 -6
  7. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/lint_plan.py +9 -0
  8. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/loop.py +364 -56
  9. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15/specfuse_loop.egg-info}/PKG-INFO +1 -1
  10. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse_loop.egg-info/SOURCES.txt +14 -0
  11. specfuse_loop-0.3.15/tests/test_autoclose_deferral_visibility.py +67 -0
  12. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_backend.py +6 -1
  13. specfuse_loop-0.3.15/tests/test_bash_routing.py +110 -0
  14. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_bookkeeping_commit_hook_crash.py +29 -14
  15. specfuse_loop-0.3.15/tests/test_bookkeeping_no_verify.py +67 -0
  16. specfuse_loop-0.3.15/tests/test_claude_resolution.py +104 -0
  17. specfuse_loop-0.3.15/tests/test_console_encoding.py +70 -0
  18. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_cost_tracking.py +6 -0
  19. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_driver_integration.py +18 -1
  20. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_duration_tracking.py +6 -0
  21. specfuse_loop-0.3.15/tests/test_ensure_feature_branch_base.py +159 -0
  22. specfuse_loop-0.3.15/tests/test_filelock_portable.py +98 -0
  23. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_gh_backend.py +57 -14
  24. specfuse_loop-0.3.15/tests/test_interpreter_normalization.py +122 -0
  25. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_lint_plan_errors.py +41 -0
  26. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_loop_files_changed_guard.py +8 -1
  27. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_loop_orchestration.py +6 -0
  28. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_loop_zero_token_guard.py +6 -1
  29. specfuse_loop-0.3.15/tests/test_redaction_windows_home.py +54 -0
  30. specfuse_loop-0.3.15/tests/test_resolve_base.py +180 -0
  31. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_squash_commit_hook_crash.py +31 -14
  32. specfuse_loop-0.3.15/tests/test_timeout_kill_portable.py +113 -0
  33. specfuse_loop-0.3.15/tests/test_untracked_file_absorption.py +218 -0
  34. specfuse_loop-0.3.15/tests/test_windows_gate_exec.py +68 -0
  35. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_wu_execution_metadata.py +6 -0
  36. specfuse_loop-0.3.13/specfuse/loop/data/VERSION +0 -1
  37. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/LICENSE +0 -0
  38. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/NOTICE +0 -0
  39. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/README.md +0 -0
  40. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/setup.cfg +0 -0
  41. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/__init__.py +0 -0
  42. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/_miniyaml.py +0 -0
  43. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/adopt_feature.py +0 -0
  44. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/data/LEARNINGS.template.md +0 -0
  45. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/data/docs/concepts/architecture-addendum-gates-and-iterative-planning.md +0 -0
  46. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/data/docs/concepts/ralph-lineage.md +0 -0
  47. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/data/docs/methodology.md +0 -0
  48. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/data/docs/skills.md +0 -0
  49. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/data/gitignore.snippet +0 -0
  50. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/data/roadmap.template.md +0 -0
  51. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/data/rules/correlation-ids.md +0 -0
  52. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/data/rules/never-touch.md +0 -0
  53. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/data/rules/result-contract.md +0 -0
  54. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/data/rules/security-boundaries.md +0 -0
  55. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/data/rules/verification-discipline.md +0 -0
  56. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/data/schemas/event.schema.json +0 -0
  57. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/data/schemas/events/initiative_created.schema.json +0 -0
  58. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/data/schemas/events/spec_issue_resolved.schema.json +0 -0
  59. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/data/schemas/events/spec_issue_routed.schema.json +0 -0
  60. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/data/schemas/events/spec_validated.schema.json +0 -0
  61. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/data/templates/GATE.template.md +0 -0
  62. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/data/templates/PLAN.template.md +0 -0
  63. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/data/templates/WU.template.md +0 -0
  64. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/data/verification.yml.example +0 -0
  65. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/gate_eval.py +0 -0
  66. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/gh_features.py +0 -0
  67. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/scaffold.py +0 -0
  68. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/validate_event.py +0 -0
  69. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse_loop.egg-info/dependency_links.txt +0 -0
  70. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse_loop.egg-info/entry_points.txt +0 -0
  71. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse_loop.egg-info/requires.txt +0 -0
  72. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse_loop.egg-info/top_level.txt +0 -0
  73. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_adopt_feature.py +0 -0
  74. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_arm_gate_edits_uncommitted.py +0 -0
  75. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_attempt_outcome_emission.py +0 -0
  76. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_autosync.py +0 -0
  77. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_autosync_consent.py +0 -0
  78. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_autosync_firstrun.py +0 -0
  79. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_autosync_plugin.py +0 -0
  80. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_bookkeeping_commit_crash_run.py +0 -0
  81. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_close_wu_self_reference.py +0 -0
  82. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_closing_deliverable_guard.py +0 -0
  83. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_degraded_oracle.py +0 -0
  84. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_deliverable_presence_gate.py +0 -0
  85. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_doctor.py +0 -0
  86. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_driver_lock.py +0 -0
  87. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_empty_files_escalation.py +0 -0
  88. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_ensure_feature_branch.py +0 -0
  89. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_events_redaction.py +0 -0
  90. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_extra_gates.py +0 -0
  91. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_force_full_close.py +0 -0
  92. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_gate_eval.py +0 -0
  93. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_gate_eval_calibration.py +0 -0
  94. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_gate_eval_intermediate_wiring.py +0 -0
  95. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_gate_eval_terminal_wiring.py +0 -0
  96. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_gh_features.py +0 -0
  97. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_git_env_isolation.py +0 -0
  98. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_hashed_denylist.py +0 -0
  99. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_hashed_denylist_ci.py +0 -0
  100. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_init_integration.py +0 -0
  101. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_leak_findings_redaction.py +0 -0
  102. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_leak_scan.py +0 -0
  103. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_leak_scan_content.py +0 -0
  104. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_learnings_query.py +0 -0
  105. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_legacy_4wu_terminal_flips.py +0 -0
  106. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_lifecycle_integration.py +0 -0
  107. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_lint_bare_produces_path.py +0 -0
  108. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_lint_close_intermediate.py +0 -0
  109. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_lint_close_wu.py +0 -0
  110. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_lint_correlation_id.py +0 -0
  111. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_lint_correlation_id_close_intermediate.py +0 -0
  112. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_lint_oracle_env.py +0 -0
  113. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_lint_plan_next_draft.py +0 -0
  114. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_lint_plan_relative_import.py +0 -0
  115. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_lint_produces_driver_helper.py +0 -0
  116. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_lint_sections.py +0 -0
  117. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_lint_task_graph_yaml_selection.py +0 -0
  118. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_loop_auto_archive.py +0 -0
  119. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_loop_caveman_preamble.py +0 -0
  120. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_loop_close_intermediate.py +0 -0
  121. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_loop_defaults_by_type.py +0 -0
  122. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_loop_effort.py +0 -0
  123. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_loop_failure_note_cap.py +0 -0
  124. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_loop_gate_budget.py +0 -0
  125. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_loop_model_alias.py +0 -0
  126. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_loop_post_pass_invariant.py +0 -0
  127. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_loop_reset_preserving_events.py +0 -0
  128. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_loop_smoke_runner.py +0 -0
  129. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_loop_unsandboxed.py +0 -0
  130. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_migrate_legacy.py +0 -0
  131. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_miniyaml_equivalence.py +0 -0
  132. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_miniyaml_negative.py +0 -0
  133. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_planned_cost_lint.py +0 -0
  134. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_prepare_feature.py +0 -0
  135. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_prepare_scaffold_sync.py +0 -0
  136. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_produces_field.py +0 -0
  137. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_result_block.py +0 -0
  138. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_roadmap_add_skill.py +0 -0
  139. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_roadmap_archive_skill.py +0 -0
  140. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_roadmap_row_parser.py +0 -0
  141. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_scaffold_data_in_sync.py +0 -0
  142. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_scaffold_doc_hygiene.py +0 -0
  143. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_scaffold_docs.py +0 -0
  144. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_scaffold_init.py +0 -0
  145. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_scaffold_manifest.py +0 -0
  146. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_scaffold_resources.py +0 -0
  147. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_scaffold_seed_sanity.py +0 -0
  148. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_scaffold_upgrade.py +0 -0
  149. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_scaffold_wiring.py +0 -0
  150. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_skills_vendored_in_sync.py +0 -0
  151. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_template_closing_shapes.py +0 -0
  152. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_terminal_flip_ownership.py +0 -0
  153. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_terminal_flips.py +0 -0
  154. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_untracked_feature_folder.py +0 -0
  155. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_upgrade_integration.py +0 -0
  156. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_upgrade_merge_gate.py +0 -0
  157. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_validate_event.py +0 -0
  158. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_verdict_coupling.py +0 -0
  159. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_verify_empty_gate_set.py +0 -0
  160. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_version_consistency.py +0 -0
  161. {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_version_skew.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: specfuse-loop
3
- Version: 0.3.13
3
+ Version: 0.3.15
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.13"
7
+ version = "0.3.15"
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,44 @@
1
+ #
2
+ # Copyright 2026 Specfuse contributors
3
+ # Licensed under the Apache License, Version 2.0. See LICENSE.
4
+ #
5
+ """Cross-platform advisory lock for the driver's single-instance working-tree lock.
6
+
7
+ The kernel must auto-release the lock on process exit, including SIGKILL, with
8
+ no cleanup step — this is why POSIX `fcntl.flock` and Windows `msvcrt.locking`
9
+ are used here instead of any userspace primitive. Pidfiles are ruled out: the
10
+ pid-holder can die without a chance to remove its file, leaving a stale lock
11
+ that stalls or is silently skipped by the next launch.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import sys
17
+ from pathlib import Path
18
+
19
+
20
+ def acquire_tree_lock(specfuse_dir: Path):
21
+ """Open .specfuse/.loop.lock and acquire a non-blocking exclusive lock.
22
+
23
+ Returns the open file object; caller keeps it alive for the process
24
+ lifetime — the OS auto-releases on fd/handle close or process death
25
+ (SIGKILL included), so no stale-lock cleanup is ever needed.
26
+ Raises BlockingIOError if another process already holds the lock.
27
+ """
28
+ lock_path = specfuse_dir / ".loop.lock"
29
+ lock_path.parent.mkdir(parents=True, exist_ok=True)
30
+ fd = lock_path.open("w")
31
+ try:
32
+ if sys.platform == "win32":
33
+ import msvcrt
34
+ try:
35
+ msvcrt.locking(fd.fileno(), msvcrt.LK_NBLCK, 1)
36
+ except OSError as exc:
37
+ raise BlockingIOError from exc
38
+ else:
39
+ import fcntl
40
+ fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
41
+ except BlockingIOError:
42
+ fd.close()
43
+ raise
44
+ return fd
@@ -0,0 +1 @@
1
+ 0.3.15
@@ -31,6 +31,16 @@ python3 -m pip install specfuse
31
31
  > venv) — that's what puts `specfuse-loop` / `specfuse-lint` on PATH for the gate
32
32
  > commands to find.
33
33
 
34
+ > **Windows: Git-Bash is required.** The driver runs natively on Windows (no WSL),
35
+ > but it routes gate commands through **Git-Bash** so a target repo's
36
+ > `verification.yml` (`&&`, `exit 1 || exit 0`, `bats`, globs) runs unchanged.
37
+ > Install [Git for Windows](https://gitforwindows.org/) — it ships `bash.exe`, no
38
+ > admin and no WSL feature needed — before running the loop. The driver prefers
39
+ > the Git-for-Windows `bash.exe` over the `C:\Windows\System32\bash.exe` WSL
40
+ > launcher; it also normalizes `python3` gate tokens to your Windows interpreter
41
+ > and resolves the `claude` CLI via `PATHEXT`, so gate commands authored on
42
+ > POSIX run as-is.
43
+
34
44
  Enable the skills plugin in Claude Code (one-time):
35
45
 
36
46
  ```
@@ -13,9 +13,14 @@ from typing import Callable, Optional
13
13
  from . import loop as _loop
14
14
 
15
15
 
16
- def _default_runner(args: list) -> None:
17
- """Shell out to gh with the given argument list. Not called in tests."""
18
- subprocess.run(args, check=True, capture_output=True, text=True)
16
+ def _default_runner(args: list, check: bool = True):
17
+ """Shell out to gh with the given argument list. Not called in tests.
18
+
19
+ Returns the completed process so probing callers (e.g. the `gh pr view`
20
+ idempotency check) can inspect `.returncode` without raising; side-effecting
21
+ callers pass no `check` override and get the prior raise-on-failure behavior.
22
+ """
23
+ return subprocess.run(args, check=check, capture_output=True, text=True)
19
24
 
20
25
 
21
26
  class GitHubBackend(_loop.Backend):
@@ -55,9 +60,9 @@ class GitHubBackend(_loop.Backend):
55
60
  title = feat_fm.get("title", feature_id)
56
61
 
57
62
  # Idempotent: skip PR creation if one already exists for this branch.
58
- check = subprocess.run(
63
+ check = self._runner(
59
64
  ["gh", "pr", "view", branch, "--repo", self.repo, "--json", "number"],
60
- capture_output=True, text=True,
65
+ check=False,
61
66
  )
62
67
  if check.returncode != 0:
63
68
  body = (
@@ -70,7 +75,7 @@ class GitHubBackend(_loop.Backend):
70
75
  "gh", "pr", "create",
71
76
  "--title", f"[{feature_id}] {title}",
72
77
  "--body", body,
73
- "--base", "main",
78
+ "--base", _loop.resolve_base(feat_fm),
74
79
  "--head", branch,
75
80
  ])
76
81
 
@@ -249,6 +249,15 @@ def lint(feature_dir: Path) -> list[str]:
249
249
  if missing:
250
250
  errs.append(f"PLAN.md frontmatter missing keys: {sorted(missing)}")
251
251
 
252
+ if "base" in fm:
253
+ base_val = fm["base"]
254
+ feature_id_val = fm.get("feature_id", feature_dir.name)
255
+ if not isinstance(base_val, str) or not base_val.strip():
256
+ errs.append(
257
+ f"{feature_id_val}: PLAN.md frontmatter 'base' key is present but "
258
+ f"empty/whitespace-only/non-string: {base_val!r}"
259
+ )
260
+
252
261
  graph = _find_task_graph_block(body)
253
262
  if graph is None:
254
263
  return errs + ["PLAN.md has no ```yaml graph block"]