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.
- {specfuse_loop-0.3.13/specfuse_loop.egg-info → specfuse_loop-0.3.15}/PKG-INFO +1 -1
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/pyproject.toml +1 -1
- specfuse_loop-0.3.15/specfuse/loop/_filelock.py +44 -0
- specfuse_loop-0.3.15/specfuse/loop/data/VERSION +1 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/data/docs/getting-started.md +10 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/gh_backend.py +11 -6
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/lint_plan.py +9 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/loop.py +364 -56
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15/specfuse_loop.egg-info}/PKG-INFO +1 -1
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse_loop.egg-info/SOURCES.txt +14 -0
- specfuse_loop-0.3.15/tests/test_autoclose_deferral_visibility.py +67 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_backend.py +6 -1
- specfuse_loop-0.3.15/tests/test_bash_routing.py +110 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_bookkeeping_commit_hook_crash.py +29 -14
- specfuse_loop-0.3.15/tests/test_bookkeeping_no_verify.py +67 -0
- specfuse_loop-0.3.15/tests/test_claude_resolution.py +104 -0
- specfuse_loop-0.3.15/tests/test_console_encoding.py +70 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_cost_tracking.py +6 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_driver_integration.py +18 -1
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_duration_tracking.py +6 -0
- specfuse_loop-0.3.15/tests/test_ensure_feature_branch_base.py +159 -0
- specfuse_loop-0.3.15/tests/test_filelock_portable.py +98 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_gh_backend.py +57 -14
- specfuse_loop-0.3.15/tests/test_interpreter_normalization.py +122 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_lint_plan_errors.py +41 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_loop_files_changed_guard.py +8 -1
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_loop_orchestration.py +6 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_loop_zero_token_guard.py +6 -1
- specfuse_loop-0.3.15/tests/test_redaction_windows_home.py +54 -0
- specfuse_loop-0.3.15/tests/test_resolve_base.py +180 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_squash_commit_hook_crash.py +31 -14
- specfuse_loop-0.3.15/tests/test_timeout_kill_portable.py +113 -0
- specfuse_loop-0.3.15/tests/test_untracked_file_absorption.py +218 -0
- specfuse_loop-0.3.15/tests/test_windows_gate_exec.py +68 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_wu_execution_metadata.py +6 -0
- specfuse_loop-0.3.13/specfuse/loop/data/VERSION +0 -1
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/LICENSE +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/NOTICE +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/README.md +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/setup.cfg +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/__init__.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/_miniyaml.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/adopt_feature.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/data/LEARNINGS.template.md +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/data/docs/concepts/architecture-addendum-gates-and-iterative-planning.md +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/data/docs/concepts/ralph-lineage.md +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/data/docs/methodology.md +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/data/docs/skills.md +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/data/gitignore.snippet +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/data/roadmap.template.md +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/data/rules/correlation-ids.md +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/data/rules/never-touch.md +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/data/rules/result-contract.md +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/data/rules/security-boundaries.md +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/data/rules/verification-discipline.md +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/data/schemas/event.schema.json +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/data/schemas/events/initiative_created.schema.json +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/data/schemas/events/spec_issue_resolved.schema.json +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/data/schemas/events/spec_issue_routed.schema.json +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/data/schemas/events/spec_validated.schema.json +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/data/templates/GATE.template.md +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/data/templates/PLAN.template.md +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/data/templates/WU.template.md +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/data/verification.yml.example +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/gate_eval.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/gh_features.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/scaffold.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse/loop/validate_event.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse_loop.egg-info/dependency_links.txt +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse_loop.egg-info/entry_points.txt +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse_loop.egg-info/requires.txt +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/specfuse_loop.egg-info/top_level.txt +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_adopt_feature.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_arm_gate_edits_uncommitted.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_attempt_outcome_emission.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_autosync.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_autosync_consent.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_autosync_firstrun.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_autosync_plugin.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_bookkeeping_commit_crash_run.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_close_wu_self_reference.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_closing_deliverable_guard.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_degraded_oracle.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_deliverable_presence_gate.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_doctor.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_driver_lock.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_empty_files_escalation.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_ensure_feature_branch.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_events_redaction.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_extra_gates.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_force_full_close.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_gate_eval.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_gate_eval_calibration.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_gate_eval_intermediate_wiring.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_gate_eval_terminal_wiring.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_gh_features.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_git_env_isolation.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_hashed_denylist.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_hashed_denylist_ci.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_init_integration.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_leak_findings_redaction.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_leak_scan.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_leak_scan_content.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_learnings_query.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_legacy_4wu_terminal_flips.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_lifecycle_integration.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_lint_bare_produces_path.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_lint_close_intermediate.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_lint_close_wu.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_lint_correlation_id.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_lint_correlation_id_close_intermediate.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_lint_oracle_env.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_lint_plan_next_draft.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_lint_plan_relative_import.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_lint_produces_driver_helper.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_lint_sections.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_lint_task_graph_yaml_selection.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_loop_auto_archive.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_loop_caveman_preamble.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_loop_close_intermediate.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_loop_defaults_by_type.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_loop_effort.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_loop_failure_note_cap.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_loop_gate_budget.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_loop_model_alias.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_loop_post_pass_invariant.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_loop_reset_preserving_events.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_loop_smoke_runner.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_loop_unsandboxed.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_migrate_legacy.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_miniyaml_equivalence.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_miniyaml_negative.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_planned_cost_lint.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_prepare_feature.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_prepare_scaffold_sync.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_produces_field.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_result_block.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_roadmap_add_skill.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_roadmap_archive_skill.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_roadmap_row_parser.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_scaffold_data_in_sync.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_scaffold_doc_hygiene.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_scaffold_docs.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_scaffold_init.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_scaffold_manifest.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_scaffold_resources.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_scaffold_seed_sanity.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_scaffold_upgrade.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_scaffold_wiring.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_skills_vendored_in_sync.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_template_closing_shapes.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_terminal_flip_ownership.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_terminal_flips.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_untracked_feature_folder.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_upgrade_integration.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_upgrade_merge_gate.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_validate_event.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_verdict_coupling.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_verify_empty_gate_set.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_version_consistency.py +0 -0
- {specfuse_loop-0.3.13 → specfuse_loop-0.3.15}/tests/test_version_skew.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.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
|
|
17
|
-
"""Shell out to gh with the given argument list. Not called in tests.
|
|
18
|
-
|
|
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 =
|
|
63
|
+
check = self._runner(
|
|
59
64
|
["gh", "pr", "view", branch, "--repo", self.repo, "--json", "number"],
|
|
60
|
-
|
|
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",
|
|
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"]
|