agentic-dev-loop 0.10.4__tar.gz → 0.10.5__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.
- {agentic_dev_loop-0.10.4/src/agentic_dev_loop.egg-info → agentic_dev_loop-0.10.5}/PKG-INFO +5 -3
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/README.md +4 -2
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/pyproject.toml +1 -1
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/core/profiles/__init__.py +4 -6
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/engine/cli.py +3 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/engine/onboarding/checks.py +4 -3
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/engine/onboarding/templates/trigger-agentic-dev-loop.sh +6 -14
- agentic_dev_loop-0.10.5/src/agentic_dev_loop/engine/onboarding/upgrade.py +76 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5/src/agentic_dev_loop.egg-info}/PKG-INFO +5 -3
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop.egg-info/SOURCES.txt +2 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/tests/test_055_cli.py +8 -1
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/tests/test_055_doctor.py +1 -1
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/tests/test_055_profiles.py +1 -1
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/tests/test_055_templates.py +2 -2
- agentic_dev_loop-0.10.5/tests/test_055_upgrade.py +125 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/tests/test_publish.py +160 -4
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/tests/test_trigger_script.py +32 -41
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/LICENSE +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/setup.cfg +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/__init__.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/core/__init__.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/core/config.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/core/paths.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/core/profiles/data/dotnet-node.yaml +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/core/profiles/data/python.yaml +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/core/results.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/core/verdict.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/engine/__init__.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/engine/actions.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/engine/agent_error_guide.md +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/engine/agent_error_guide.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/engine/agents.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/engine/allowlist.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/engine/brief_coverage.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/engine/brief_prompt.md +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/engine/brief_prompt.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/engine/containers.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/engine/deps.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/engine/dryrun.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/engine/egress_policy.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/engine/egress_proxy.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/engine/extensions/__init__.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/engine/extensions/aidlc.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/engine/forbidden.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/engine/graph.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/engine/hostboundary.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/engine/isolation.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/engine/llm.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/engine/loaders.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/engine/mounts.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/engine/onboarding/__init__.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/engine/onboarding/doctor.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/engine/onboarding/init.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/engine/onboarding/recipes/dotnet-node.yaml +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/engine/onboarding/recipes/python.yaml +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/engine/onboarding/recipes.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/engine/onboarding/templates/dev.agent.md +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/engine/onboarding/templates/env.example +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/engine/onboarding/templates/qa.agent.md +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/engine/onboarding/templates/review.agent.md +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/engine/onboarding/templates.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/engine/outputs.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/engine/presets.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/engine/proc.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/engine/registry.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/engine/seccomp/__init__.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/engine/seccomp/chromium-userns.json +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/engine/secrets.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/engine/stages.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/engine/step_workspace.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/engine/sync_back.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/py.typed +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/viz/__init__.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/viz/__main__.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/viz/server.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop.egg-info/dependency_links.txt +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop.egg-info/entry_points.txt +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop.egg-info/requires.txt +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop.egg-info/top_level.txt +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/tests/test_029_merge_on_conflict.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/tests/test_033_brief_step.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/tests/test_035_finalize_nudge.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/tests/test_036_banned_commands_and_lane_boundary.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/tests/test_041_brief_coverage.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/tests/test_046_brief_prompt.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/tests/test_055_init.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/tests/test_055_profile_loading.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/tests/test_055_recipes.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/tests/test_055_schema_lock.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/tests/test_actions.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/tests/test_agent_error_guide.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/tests/test_agents.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/tests/test_aidlc.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/tests/test_cli.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/tests/test_config.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/tests/test_containers.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/tests/test_deps.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/tests/test_dry_run.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/tests/test_e2e_fixtures.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/tests/test_egress_proxy.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/tests/test_graph.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/tests/test_hostboundary.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/tests/test_hostboundary_integration.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/tests/test_isolation.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/tests/test_isolation_integration.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/tests/test_isolation_pbt.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/tests/test_llm.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/tests/test_loaders.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/tests/test_module_contract.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/tests/test_outputs.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/tests/test_outputs_pbt.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/tests/test_proc.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/tests/test_qa_prompt.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/tests/test_registry.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/tests/test_repo_ascii.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/tests/test_results.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/tests/test_sdk_compat.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/tests/test_secrets.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/tests/test_secrets_integration.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/tests/test_secrets_pipeline.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/tests/test_source_resolution.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/tests/test_stages.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/tests/test_step_workspace.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/tests/test_sync_back.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/tests/test_unit5_config.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/tests/test_unit5_pbt.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/tests/test_unit5_pure.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/tests/test_verdict.py +0 -0
- {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/tests/test_viz.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentic-dev-loop
|
|
3
|
-
Version: 0.10.
|
|
3
|
+
Version: 0.10.5
|
|
4
4
|
Summary: Turn an LLM into a gated, sandboxed, resumable construction loop over your codebase.
|
|
5
5
|
Author: Luke Wilson
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -492,8 +492,10 @@ before.
|
|
|
492
492
|
`agentic-dev-loop doctor --fast` is the per-run gate the trigger runs first.
|
|
493
493
|
Hard failures abort the run (engine pin mismatch, engine runtime availability,
|
|
494
494
|
API version, git repo, `.env*` files); warnings print and continue. `doctor
|
|
495
|
-
--json` emits the same results mechanically.
|
|
496
|
-
|
|
495
|
+
--json` emits the same results mechanically. On a hard failure the trigger
|
|
496
|
+
runs `agentic-dev-loop upgrade` exactly once: it installs the latest release
|
|
497
|
+
with pipx and rewrites `.engine-version` to match, then the run fails hard if
|
|
498
|
+
doctor still fails.
|
|
497
499
|
|
|
498
500
|
A consumer repo needs a pipeline file and the agent files it names, both
|
|
499
501
|
committed before the first run (the host-boundary check refuses dirty tooling,
|
|
@@ -448,8 +448,10 @@ before.
|
|
|
448
448
|
`agentic-dev-loop doctor --fast` is the per-run gate the trigger runs first.
|
|
449
449
|
Hard failures abort the run (engine pin mismatch, engine runtime availability,
|
|
450
450
|
API version, git repo, `.env*` files); warnings print and continue. `doctor
|
|
451
|
-
--json` emits the same results mechanically.
|
|
452
|
-
|
|
451
|
+
--json` emits the same results mechanically. On a hard failure the trigger
|
|
452
|
+
runs `agentic-dev-loop upgrade` exactly once: it installs the latest release
|
|
453
|
+
with pipx and rewrites `.engine-version` to match, then the run fails hard if
|
|
454
|
+
doctor still fails.
|
|
453
455
|
|
|
454
456
|
A consumer repo needs a pipeline file and the agent files it names, both
|
|
455
457
|
committed before the first run (the host-boundary check refuses dirty tooling,
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "agentic-dev-loop"
|
|
7
|
-
version = "0.10.
|
|
7
|
+
version = "0.10.5"
|
|
8
8
|
description = "Turn an LLM into a gated, sandboxed, resumable construction loop over your codebase."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.12"
|
{agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/core/profiles/__init__.py
RENAMED
|
@@ -81,18 +81,16 @@ def parse_version(text: str) -> tuple[int, int, int, int, int]:
|
|
|
81
81
|
return (int(major), int(minor), int(patch), _RANK[pre], int(num))
|
|
82
82
|
|
|
83
83
|
|
|
84
|
-
def check_engine_min(engine_min: str, installed: str
|
|
85
|
-
latest: str | None = None) -> None:
|
|
84
|
+
def check_engine_min(engine_min: str, installed: str) -> None:
|
|
86
85
|
"""Raise unless ``installed`` satisfies ``engine_min``.
|
|
87
86
|
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
The fix line names the upgrade command: it installs the latest release,
|
|
88
|
+
which always satisfies the floor.
|
|
90
89
|
"""
|
|
91
90
|
if parse_version(installed) < parse_version(engine_min):
|
|
92
|
-
target = latest or engine_min
|
|
93
91
|
raise ProfileError(
|
|
94
92
|
f"profile requires engine >= {engine_min}; installed {installed}. "
|
|
95
|
-
|
|
93
|
+
"Upgrade: run agentic-dev-loop upgrade")
|
|
96
94
|
|
|
97
95
|
|
|
98
96
|
def _data_dir():
|
|
@@ -171,6 +171,9 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
171
171
|
if argv and argv[0] == "doctor":
|
|
172
172
|
from .onboarding.doctor import run_doctor
|
|
173
173
|
return run_doctor(argv[1:])
|
|
174
|
+
if argv and argv[0] == "upgrade":
|
|
175
|
+
from .onboarding.upgrade import run_upgrade
|
|
176
|
+
return run_upgrade(argv[1:])
|
|
174
177
|
probe = argparse.ArgumentParser(add_help=False)
|
|
175
178
|
probe.add_argument("pipeline", type=Path, nargs="?")
|
|
176
179
|
_add_version_flag(probe)
|
{agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop/engine/onboarding/checks.py
RENAMED
|
@@ -25,6 +25,7 @@ __all__ = ["Check", "HostProbes", "collect_checks"]
|
|
|
25
25
|
ENV_WHITELIST = frozenset({".env.example", ".env.template", ".env.sample"})
|
|
26
26
|
MIN_API = (1, 43)
|
|
27
27
|
PYPI_URL = "https://pypi.org/pypi/agentic-dev-loop/json"
|
|
28
|
+
FIX_UPGRADE = "run agentic-dev-loop upgrade"
|
|
28
29
|
|
|
29
30
|
|
|
30
31
|
@dataclass(frozen=True)
|
|
@@ -171,12 +172,12 @@ def collect_checks(profile: Profile | None, pipeline, installed: str,
|
|
|
171
172
|
if pin is None:
|
|
172
173
|
checks.append(_hard(
|
|
173
174
|
"pin", False, f"no engine pin file at {pin_path}",
|
|
174
|
-
|
|
175
|
+
FIX_UPGRADE))
|
|
175
176
|
else:
|
|
176
177
|
checks.append(_hard(
|
|
177
178
|
"pin", pin == installed,
|
|
178
179
|
f"installed {installed}, pin {pin}",
|
|
179
|
-
|
|
180
|
+
FIX_UPGRADE))
|
|
180
181
|
latest = probes.latest_release()
|
|
181
182
|
if latest is None:
|
|
182
183
|
checks.append(_hard("pin-latest", True,
|
|
@@ -189,7 +190,7 @@ def collect_checks(profile: Profile | None, pipeline, installed: str,
|
|
|
189
190
|
checks.append(_hard(
|
|
190
191
|
"pin-latest", not behind,
|
|
191
192
|
f"pin {pin} is behind latest release {latest}",
|
|
192
|
-
|
|
193
|
+
FIX_UPGRADE))
|
|
193
194
|
|
|
194
195
|
present = probes.docker_present()
|
|
195
196
|
checks.append(_hard(
|
|
@@ -5,7 +5,6 @@ ROOT="$(cd "$(dirname "$0")" && pwd)"
|
|
|
5
5
|
cd "$ROOT"
|
|
6
6
|
|
|
7
7
|
PIPELINE="$ROOT/agentic-dev-loop/pipeline.yaml"
|
|
8
|
-
PIN_FILE="$ROOT/agentic-dev-loop/.engine-version"
|
|
9
8
|
|
|
10
9
|
# Optional host environment file. The default lives outside the workspace so
|
|
11
10
|
# secrets never enter the repo; set AGENTIC_DEV_LOOP_ENV_FILE to override.
|
|
@@ -25,24 +24,17 @@ if ! command -v agentic-dev-loop >/dev/null 2>&1; then
|
|
|
25
24
|
exit 1
|
|
26
25
|
fi
|
|
27
26
|
|
|
28
|
-
if [[ ! -f "$PIN_FILE" ]]; then
|
|
29
|
-
echo "error: missing engine pin file: $PIN_FILE" >&2
|
|
30
|
-
echo " run 'agentic-dev-loop init' in this repo to write it" >&2
|
|
31
|
-
exit 1
|
|
32
|
-
fi
|
|
33
|
-
PIN="$(cat "$PIN_FILE")"
|
|
34
|
-
|
|
35
27
|
# doctor --fast is the per-run gate: hard failures abort, warnings continue.
|
|
36
|
-
# A
|
|
28
|
+
# A failing gate is repaired exactly once by upgrading to the latest engine
|
|
29
|
+
# release (which also rewrites the project pin), then the run fails hard.
|
|
37
30
|
if ! agentic-dev-loop doctor --fast; then
|
|
38
|
-
echo "
|
|
39
|
-
if !
|
|
40
|
-
echo "error:
|
|
41
|
-
echo " the install may not be pipx-managed, or the index is unreachable" >&2
|
|
31
|
+
echo "doctor --fast failed - upgrading to the latest engine release" >&2
|
|
32
|
+
if ! agentic-dev-loop upgrade; then
|
|
33
|
+
echo "error: agentic-dev-loop upgrade failed" >&2
|
|
42
34
|
exit 1
|
|
43
35
|
fi
|
|
44
36
|
if ! agentic-dev-loop doctor --fast; then
|
|
45
|
-
echo "error: doctor --fast still fails after
|
|
37
|
+
echo "error: doctor --fast still fails after upgrading the engine" >&2
|
|
46
38
|
exit 1
|
|
47
39
|
fi
|
|
48
40
|
fi
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""``agentic-dev-loop upgrade``: install the latest engine and sync the pin.
|
|
2
|
+
|
|
3
|
+
Installs the latest release with pipx and rewrites
|
|
4
|
+
``agentic-dev-loop/.engine-version`` to the installed version, so the host
|
|
5
|
+
install and the committed pin converge in one action.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import shutil
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from agentic_dev_loop.core.profiles import ProfileError, parse_version
|
|
17
|
+
|
|
18
|
+
__all__ = ["run_upgrade"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _resolve(name: str, env: dict | None) -> str | None:
|
|
22
|
+
if env is not None:
|
|
23
|
+
return shutil.which(name, path=env.get("PATH", ""))
|
|
24
|
+
return shutil.which(name)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def run_upgrade(argv: list[str], *, workspace: Path | None = None,
|
|
28
|
+
env: dict | None = None) -> int:
|
|
29
|
+
parser = argparse.ArgumentParser(
|
|
30
|
+
prog="agentic-dev-loop upgrade",
|
|
31
|
+
description="Install the latest engine release and rewrite the "
|
|
32
|
+
"project pin file to match.")
|
|
33
|
+
parser.parse_args(argv)
|
|
34
|
+
|
|
35
|
+
root = Path(workspace or Path.cwd()).resolve()
|
|
36
|
+
pipx = _resolve("pipx", env)
|
|
37
|
+
if pipx is None:
|
|
38
|
+
print("error: pipx is not on PATH; the engine install must be "
|
|
39
|
+
"pipx-managed", file=sys.stderr)
|
|
40
|
+
return 1
|
|
41
|
+
|
|
42
|
+
result = subprocess.run(
|
|
43
|
+
[pipx, "install", "--force", "agentic-dev-loop"],
|
|
44
|
+
capture_output=True, text=True, check=False, env=env)
|
|
45
|
+
if result.returncode != 0:
|
|
46
|
+
detail = (result.stderr or "unknown error").strip()
|
|
47
|
+
print(f"error: pipx install failed: {detail}", file=sys.stderr)
|
|
48
|
+
return 1
|
|
49
|
+
|
|
50
|
+
cli = _resolve("agentic-dev-loop", env)
|
|
51
|
+
if cli is None:
|
|
52
|
+
print("error: agentic-dev-loop is not on PATH", file=sys.stderr)
|
|
53
|
+
return 1
|
|
54
|
+
version_result = subprocess.run(
|
|
55
|
+
[cli, "--version"], capture_output=True, text=True,
|
|
56
|
+
check=False, env=env)
|
|
57
|
+
if version_result.returncode != 0:
|
|
58
|
+
detail = (version_result.stderr or "unknown error").strip()
|
|
59
|
+
print(f"error: could not read the installed version: {detail}",
|
|
60
|
+
file=sys.stderr)
|
|
61
|
+
return 1
|
|
62
|
+
|
|
63
|
+
tokens = version_result.stdout.split()
|
|
64
|
+
version = tokens[-1] if tokens else ""
|
|
65
|
+
try:
|
|
66
|
+
parse_version(version)
|
|
67
|
+
except ProfileError:
|
|
68
|
+
print("error: unreadable version from --version output: "
|
|
69
|
+
f"{version_result.stdout.strip()!r}", file=sys.stderr)
|
|
70
|
+
return 1
|
|
71
|
+
|
|
72
|
+
out = root / "agentic-dev-loop"
|
|
73
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
74
|
+
(out / ".engine-version").write_text(version + "\n", encoding="utf-8")
|
|
75
|
+
print(f"upgraded agentic-dev-loop to {version}; .engine-version updated")
|
|
76
|
+
return 0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentic-dev-loop
|
|
3
|
-
Version: 0.10.
|
|
3
|
+
Version: 0.10.5
|
|
4
4
|
Summary: Turn an LLM into a gated, sandboxed, resumable construction loop over your codebase.
|
|
5
5
|
Author: Luke Wilson
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -492,8 +492,10 @@ before.
|
|
|
492
492
|
`agentic-dev-loop doctor --fast` is the per-run gate the trigger runs first.
|
|
493
493
|
Hard failures abort the run (engine pin mismatch, engine runtime availability,
|
|
494
494
|
API version, git repo, `.env*` files); warnings print and continue. `doctor
|
|
495
|
-
--json` emits the same results mechanically.
|
|
496
|
-
|
|
495
|
+
--json` emits the same results mechanically. On a hard failure the trigger
|
|
496
|
+
runs `agentic-dev-loop upgrade` exactly once: it installs the latest release
|
|
497
|
+
with pipx and rewrites `.engine-version` to match, then the run fails hard if
|
|
498
|
+
doctor still fails.
|
|
497
499
|
|
|
498
500
|
A consumer repo needs a pipeline file and the agent files it names, both
|
|
499
501
|
committed before the first run (the host-boundary check refuses dirty tooling,
|
{agentic_dev_loop-0.10.4 → agentic_dev_loop-0.10.5}/src/agentic_dev_loop.egg-info/SOURCES.txt
RENAMED
|
@@ -55,6 +55,7 @@ src/agentic_dev_loop/engine/onboarding/doctor.py
|
|
|
55
55
|
src/agentic_dev_loop/engine/onboarding/init.py
|
|
56
56
|
src/agentic_dev_loop/engine/onboarding/recipes.py
|
|
57
57
|
src/agentic_dev_loop/engine/onboarding/templates.py
|
|
58
|
+
src/agentic_dev_loop/engine/onboarding/upgrade.py
|
|
58
59
|
src/agentic_dev_loop/engine/onboarding/recipes/dotnet-node.yaml
|
|
59
60
|
src/agentic_dev_loop/engine/onboarding/recipes/python.yaml
|
|
60
61
|
src/agentic_dev_loop/engine/onboarding/templates/dev.agent.md
|
|
@@ -81,6 +82,7 @@ tests/test_055_profiles.py
|
|
|
81
82
|
tests/test_055_recipes.py
|
|
82
83
|
tests/test_055_schema_lock.py
|
|
83
84
|
tests/test_055_templates.py
|
|
85
|
+
tests/test_055_upgrade.py
|
|
84
86
|
tests/test_actions.py
|
|
85
87
|
tests/test_agent_error_guide.py
|
|
86
88
|
tests/test_agents.py
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""055 Task 8: CLI dispatch for init and
|
|
1
|
+
"""055 Task 8: CLI dispatch for init, doctor, and upgrade."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -24,6 +24,13 @@ def test_init_help_exits_zero(capsys):
|
|
|
24
24
|
assert "usage" in capsys.readouterr().out.lower()
|
|
25
25
|
|
|
26
26
|
|
|
27
|
+
def test_upgrade_help_exits_zero(capsys):
|
|
28
|
+
with pytest.raises(SystemExit) as exc:
|
|
29
|
+
main(["upgrade", "--help"])
|
|
30
|
+
assert exc.value.code == 0
|
|
31
|
+
assert "usage" in capsys.readouterr().out.lower()
|
|
32
|
+
|
|
33
|
+
|
|
27
34
|
def test_doctor_json_fast_returns_int(monkeypatch, capsys):
|
|
28
35
|
from agentic_dev_loop.engine.onboarding import checks
|
|
29
36
|
|
|
@@ -95,7 +95,7 @@ def test_installed_pin_mismatch_is_hard_with_fix_text(tmp_path: Path, capsys):
|
|
|
95
95
|
out = capsys.readouterr().out
|
|
96
96
|
assert code == 1
|
|
97
97
|
assert "HARD" in out
|
|
98
|
-
assert
|
|
98
|
+
assert "run agentic-dev-loop upgrade" in out
|
|
99
99
|
|
|
100
100
|
|
|
101
101
|
def test_missing_placeholder_on_path_is_hard(tmp_path: Path, capsys):
|
|
@@ -41,7 +41,7 @@ def test_check_engine_min_blocks_with_exact_message():
|
|
|
41
41
|
check_engine_min("0.10.0", "0.9.2")
|
|
42
42
|
assert str(exc.value) == (
|
|
43
43
|
'profile requires engine >= 0.10.0; installed 0.9.2. '
|
|
44
|
-
'Upgrade:
|
|
44
|
+
'Upgrade: run agentic-dev-loop upgrade'
|
|
45
45
|
)
|
|
46
46
|
|
|
47
47
|
|
|
@@ -63,9 +63,9 @@ def test_dev_template_missing_heading_reports_actual_headings():
|
|
|
63
63
|
|
|
64
64
|
def test_trigger_template_contract():
|
|
65
65
|
text = load_template("trigger-agentic-dev-loop.sh")
|
|
66
|
-
assert ".engine-version" in text
|
|
67
66
|
assert "doctor --fast" in text
|
|
68
|
-
assert "
|
|
67
|
+
assert "agentic-dev-loop upgrade" in text
|
|
68
|
+
assert "pipx install --force" not in text
|
|
69
69
|
assert "pipx upgrade" not in text
|
|
70
70
|
assert "AGENTIC_DEV_LOOP_ENV_FILE" in text
|
|
71
71
|
assert "exec agentic-dev-loop" in text
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""055 follow-up: the ``upgrade`` command.
|
|
2
|
+
|
|
3
|
+
``agentic-dev-loop upgrade`` installs the latest engine release with pipx and
|
|
4
|
+
rewrites ``agentic-dev-loop/.engine-version`` to the installed version, so
|
|
5
|
+
the host install and the committed pin converge in one action. Tests drive
|
|
6
|
+
the real function with stub executables on PATH - no network, no mocks of
|
|
7
|
+
the upgrade logic.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
from agentic_dev_loop.engine.onboarding.upgrade import run_upgrade
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _make_stub(bin_dir: Path, name: str, body: str) -> None:
|
|
19
|
+
path = bin_dir / name
|
|
20
|
+
path.write_text(f"#!/bin/bash\n{body}\n")
|
|
21
|
+
path.chmod(0o755)
|
|
22
|
+
if not os.access(path, os.X_OK):
|
|
23
|
+
pytest.skip("tmp filesystem is mounted noexec; stub executables cannot run")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _env_with(bin_dir: Path | None = None) -> dict:
|
|
27
|
+
entries = ["/usr/bin", "/bin"]
|
|
28
|
+
if bin_dir is not None:
|
|
29
|
+
entries.insert(0, str(bin_dir))
|
|
30
|
+
return {**os.environ, "PATH": ":".join(entries)}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _stub_env(tmp_path: Path, *, pipx_body: str, cli_body: str) -> Path:
|
|
34
|
+
bin_dir = tmp_path / "bin"
|
|
35
|
+
bin_dir.mkdir()
|
|
36
|
+
_make_stub(bin_dir, "pipx", pipx_body)
|
|
37
|
+
_make_stub(bin_dir, "agentic-dev-loop", cli_body)
|
|
38
|
+
return bin_dir
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_upgrade_installs_latest_and_writes_pin(tmp_path, capsys):
|
|
42
|
+
pipx_log = tmp_path / "pipx.log"
|
|
43
|
+
bin_dir = _stub_env(
|
|
44
|
+
tmp_path,
|
|
45
|
+
pipx_body=f'echo "$*" >> "{pipx_log}"',
|
|
46
|
+
cli_body='echo "agentic-dev-loop 0.10.4"',
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
code = run_upgrade([], workspace=tmp_path, env=_env_with(bin_dir))
|
|
50
|
+
|
|
51
|
+
assert code == 0
|
|
52
|
+
assert pipx_log.read_text().strip() == "install --force agentic-dev-loop"
|
|
53
|
+
pin = tmp_path / "agentic-dev-loop" / ".engine-version"
|
|
54
|
+
assert pin.read_text(encoding="utf-8") == "0.10.4\n"
|
|
55
|
+
assert "upgraded agentic-dev-loop to 0.10.4" in capsys.readouterr().out
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_upgrade_overwrites_existing_pin(tmp_path):
|
|
59
|
+
out = tmp_path / "agentic-dev-loop"
|
|
60
|
+
out.mkdir()
|
|
61
|
+
(out / ".engine-version").write_text("0.10.2\n", encoding="utf-8")
|
|
62
|
+
bin_dir = _stub_env(
|
|
63
|
+
tmp_path,
|
|
64
|
+
pipx_body="exit 0",
|
|
65
|
+
cli_body='echo "agentic-dev-loop 0.10.4"',
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
code = run_upgrade([], workspace=tmp_path, env=_env_with(bin_dir))
|
|
69
|
+
|
|
70
|
+
assert code == 0
|
|
71
|
+
assert (out / ".engine-version").read_text(encoding="utf-8") == "0.10.4\n"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_upgrade_pipx_failure_is_loud(tmp_path, capsys):
|
|
75
|
+
bin_dir = _stub_env(
|
|
76
|
+
tmp_path,
|
|
77
|
+
pipx_body="echo index-unreachable >&2; exit 1",
|
|
78
|
+
cli_body='echo "agentic-dev-loop 0.10.4"',
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
code = run_upgrade([], workspace=tmp_path, env=_env_with(bin_dir))
|
|
82
|
+
|
|
83
|
+
assert code == 1
|
|
84
|
+
captured = capsys.readouterr()
|
|
85
|
+
assert "index-unreachable" in captured.err
|
|
86
|
+
assert not (tmp_path / "agentic-dev-loop" / ".engine-version").exists()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_upgrade_version_read_failure_is_loud(tmp_path, capsys):
|
|
90
|
+
bin_dir = _stub_env(
|
|
91
|
+
tmp_path,
|
|
92
|
+
pipx_body="exit 0",
|
|
93
|
+
cli_body="echo version-unknown >&2; exit 1",
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
code = run_upgrade([], workspace=tmp_path, env=_env_with(bin_dir))
|
|
97
|
+
|
|
98
|
+
assert code == 1
|
|
99
|
+
assert "could not read the installed version" in capsys.readouterr().err
|
|
100
|
+
assert not (tmp_path / "agentic-dev-loop" / ".engine-version").exists()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_upgrade_unparseable_version_is_loud(tmp_path, capsys):
|
|
104
|
+
bin_dir = _stub_env(
|
|
105
|
+
tmp_path,
|
|
106
|
+
pipx_body="exit 0",
|
|
107
|
+
cli_body='echo "agentic-dev-loop nope"',
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
code = run_upgrade([], workspace=tmp_path, env=_env_with(bin_dir))
|
|
111
|
+
|
|
112
|
+
assert code == 1
|
|
113
|
+
assert "unreadable version" in capsys.readouterr().err
|
|
114
|
+
assert not (tmp_path / "agentic-dev-loop" / ".engine-version").exists()
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_upgrade_missing_pipx_is_loud(tmp_path, capsys):
|
|
118
|
+
bin_dir = tmp_path / "bin"
|
|
119
|
+
bin_dir.mkdir()
|
|
120
|
+
_make_stub(bin_dir, "agentic-dev-loop", 'echo "agentic-dev-loop 0.10.4"')
|
|
121
|
+
|
|
122
|
+
code = run_upgrade([], workspace=tmp_path, env=_env_with(bin_dir))
|
|
123
|
+
|
|
124
|
+
assert code == 1
|
|
125
|
+
assert "pipx is not on PATH" in capsys.readouterr().err
|
|
@@ -73,11 +73,21 @@ def make_repo(tmp_path: Path, version: str = "0.2.0", unreleased: str = "",
|
|
|
73
73
|
return repo, bare
|
|
74
74
|
|
|
75
75
|
|
|
76
|
-
def run_publish(repo: Path, *args: str
|
|
76
|
+
def run_publish(repo: Path, *args: str, yes: bool = True,
|
|
77
|
+
stdin=None, input=None) -> subprocess.CompletedProcess:
|
|
78
|
+
"""Drive the CLI. Real cuts pass --yes by default so they skip the
|
|
79
|
+
release-note confirmation prompt; pass yes=False with stdin=DEVNULL
|
|
80
|
+
(closed) or input='y\\n' (typed answer) to test the prompt."""
|
|
81
|
+
cmd = [sys.executable, str(PUBLISH), "--cwd", str(repo)]
|
|
82
|
+
if yes:
|
|
83
|
+
cmd.append("--yes")
|
|
84
|
+
cmd.extend(args)
|
|
77
85
|
return subprocess.run(
|
|
78
|
-
|
|
86
|
+
cmd,
|
|
79
87
|
capture_output=True,
|
|
80
88
|
text=True,
|
|
89
|
+
stdin=stdin,
|
|
90
|
+
input=input,
|
|
81
91
|
env={**os.environ,
|
|
82
92
|
"GIT_AUTHOR_NAME": "t", "GIT_AUTHOR_EMAIL": "t@example.com",
|
|
83
93
|
"GIT_COMMITTER_NAME": "t", "GIT_COMMITTER_EMAIL": "t@example.com"},
|
|
@@ -207,13 +217,17 @@ class TestChangelog:
|
|
|
207
217
|
assert "feat: alpha" in section
|
|
208
218
|
assert "fix: beta" in section
|
|
209
219
|
|
|
210
|
-
def
|
|
220
|
+
def test_dirty_tree_reported_but_not_in_changelog(self, tmp_path):
|
|
211
221
|
repo, _ = make_repo(tmp_path)
|
|
212
222
|
(repo / "extra.txt").write_text("uncommitted")
|
|
213
223
|
proc = run_publish(repo, "--patch")
|
|
214
224
|
assert proc.returncode == 0, proc.stderr
|
|
215
225
|
text = read_changelog(repo)
|
|
216
|
-
assert "extra.txt" in text[text.index("## [0.2.1]"):]
|
|
226
|
+
assert "extra.txt" not in text[text.index("## [0.2.1]"):]
|
|
227
|
+
assert "extra.txt" in proc.stdout # reported in the cut report only
|
|
228
|
+
assert _git(repo, "status", "--porcelain").stdout == ""
|
|
229
|
+
assert "extra.txt" in _git(repo, "show", "--stat", "--oneline",
|
|
230
|
+
"HEAD").stdout
|
|
217
231
|
|
|
218
232
|
def test_missing_unreleased_heading_fails(self, tmp_path):
|
|
219
233
|
repo, _ = make_repo(tmp_path)
|
|
@@ -225,6 +239,64 @@ class TestChangelog:
|
|
|
225
239
|
assert "Unreleased" in proc.stderr
|
|
226
240
|
assert read_version(repo) == "0.2.0"
|
|
227
241
|
|
|
242
|
+
def test_dry_run_shows_rendered_section(self, tmp_path):
|
|
243
|
+
"""The dry run is the notes preview: it prints the rendered release
|
|
244
|
+
section so the /publish flow can ask for approval before the cut."""
|
|
245
|
+
repo, _ = make_repo(tmp_path, commits=(("a.txt", "a\n", "feat: alpha"),))
|
|
246
|
+
proc = run_publish(repo, "--patch", "--dry-run", yes=False)
|
|
247
|
+
assert proc.returncode == 0, proc.stderr
|
|
248
|
+
assert "Release notes to be written:" in proc.stdout
|
|
249
|
+
assert "## [0.2.1]" in proc.stdout
|
|
250
|
+
assert "feat: alpha" in proc.stdout
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class TestReleaseConfirmation:
|
|
254
|
+
def test_closed_stdin_without_yes_fails(self, tmp_path):
|
|
255
|
+
repo, _ = make_repo(tmp_path)
|
|
256
|
+
proc = run_publish(repo, "--patch", yes=False, stdin=subprocess.DEVNULL)
|
|
257
|
+
assert proc.returncode != 0
|
|
258
|
+
assert "--yes" in proc.stderr
|
|
259
|
+
assert read_version(repo) == "0.2.0"
|
|
260
|
+
assert tags(repo) == []
|
|
261
|
+
assert last_subject(repo) == "chore: initial"
|
|
262
|
+
|
|
263
|
+
def test_declined_confirmation_aborts(self, tmp_path):
|
|
264
|
+
repo, _ = make_repo(tmp_path)
|
|
265
|
+
proc = run_publish(repo, "--patch", yes=False, input="n\n")
|
|
266
|
+
assert proc.returncode != 0
|
|
267
|
+
assert "aborted" in proc.stderr.lower()
|
|
268
|
+
assert read_version(repo) == "0.2.0"
|
|
269
|
+
assert tags(repo) == []
|
|
270
|
+
assert last_subject(repo) == "chore: initial"
|
|
271
|
+
|
|
272
|
+
def test_confirmed_cut_proceeds(self, tmp_path):
|
|
273
|
+
repo, _ = make_repo(tmp_path)
|
|
274
|
+
proc = run_publish(repo, "--patch", yes=False, input="y\n")
|
|
275
|
+
assert proc.returncode == 0, proc.stderr
|
|
276
|
+
assert read_version(repo) == "0.2.1"
|
|
277
|
+
assert tags(repo) == ["v0.2.1"]
|
|
278
|
+
|
|
279
|
+
def test_prompt_shows_rendered_entry(self, tmp_path):
|
|
280
|
+
repo, _ = make_repo(tmp_path, commits=(("a.txt", "a", "feat: alpha"),))
|
|
281
|
+
proc = run_publish(repo, "--patch", yes=False, input="y\n")
|
|
282
|
+
assert proc.returncode == 0, proc.stderr
|
|
283
|
+
assert "## [0.2.1]" in proc.stdout
|
|
284
|
+
assert "feat: alpha" in proc.stdout
|
|
285
|
+
|
|
286
|
+
def test_yes_flag_skips_prompt(self, tmp_path):
|
|
287
|
+
repo, _ = make_repo(tmp_path)
|
|
288
|
+
proc = run_publish(repo, "--patch", yes=True, stdin=subprocess.DEVNULL)
|
|
289
|
+
assert proc.returncode == 0, proc.stderr
|
|
290
|
+
assert read_version(repo) == "0.2.1"
|
|
291
|
+
|
|
292
|
+
def test_dry_run_needs_no_confirmation(self, tmp_path):
|
|
293
|
+
repo, _ = make_repo(tmp_path)
|
|
294
|
+
proc = run_publish(repo, "--patch", "--dry-run", yes=False,
|
|
295
|
+
stdin=subprocess.DEVNULL)
|
|
296
|
+
assert proc.returncode == 0, proc.stderr
|
|
297
|
+
assert read_version(repo) == "0.2.0"
|
|
298
|
+
assert tags(repo) == []
|
|
299
|
+
|
|
228
300
|
|
|
229
301
|
class TestConsumerAdoption:
|
|
230
302
|
def test_marker_without_adoption_section_fails(self, tmp_path):
|
|
@@ -255,6 +327,16 @@ class TestConsumerAdoption:
|
|
|
255
327
|
assert proc.returncode == 0, proc.stderr
|
|
256
328
|
assert read_version(repo) == "0.2.1"
|
|
257
329
|
|
|
330
|
+
def test_plan_number_reference_without_marker_passes(self, tmp_path):
|
|
331
|
+
"""Number-only plan mentions do not trigger the gate: consumers need
|
|
332
|
+
the concrete list of changes in the subsection, not plan references."""
|
|
333
|
+
repo, _ = make_repo(
|
|
334
|
+
tmp_path,
|
|
335
|
+
unreleased="### Changed\n\n- fix: 056 slug matching in the loader\n",
|
|
336
|
+
)
|
|
337
|
+
proc = run_publish(repo, "--patch", "--dry-run")
|
|
338
|
+
assert proc.returncode == 0, proc.stderr
|
|
339
|
+
|
|
258
340
|
def test_no_marker_passes_without_section(self, tmp_path):
|
|
259
341
|
repo, _ = make_repo(
|
|
260
342
|
tmp_path,
|
|
@@ -341,6 +423,35 @@ class TestGitOps:
|
|
|
341
423
|
assert last_subject(repo) == "chore: initial"
|
|
342
424
|
|
|
343
425
|
|
|
426
|
+
class TestNoAdd:
|
|
427
|
+
def test_no_add_excludes_unstaged_files(self, tmp_path):
|
|
428
|
+
"""--no-add commits only the files publish.py writes; the WIP stays
|
|
429
|
+
uncommitted in the tree instead of being swept into the release."""
|
|
430
|
+
repo, _ = make_repo(tmp_path, commits=(("extra.txt", "old\n",
|
|
431
|
+
"feat: alpha"),))
|
|
432
|
+
(repo / "extra.txt").write_text("dirty\n")
|
|
433
|
+
proc = run_publish(repo, "--patch", "--no-add")
|
|
434
|
+
assert proc.returncode == 0, proc.stderr
|
|
435
|
+
assert read_version(repo) == "0.2.1"
|
|
436
|
+
assert "v0.2.1" in tags(repo)
|
|
437
|
+
assert last_subject(repo) == "release: v0.2.1"
|
|
438
|
+
committed = _git(repo, "show", "--pretty=format:", "--name-only",
|
|
439
|
+
"HEAD").stdout.split()
|
|
440
|
+
assert "pyproject.toml" in committed
|
|
441
|
+
assert "CHANGELOG.md" in committed
|
|
442
|
+
assert "extra.txt" not in committed
|
|
443
|
+
assert (repo / "extra.txt").read_text() == "dirty\n"
|
|
444
|
+
assert "extra.txt" in _git(repo, "status", "--porcelain").stdout
|
|
445
|
+
|
|
446
|
+
def test_no_add_dry_run_reports_exclusion(self, tmp_path):
|
|
447
|
+
repo, _ = make_repo(tmp_path)
|
|
448
|
+
(repo / "extra.txt").write_text("dirty\n")
|
|
449
|
+
proc = run_publish(repo, "--patch", "--dry-run", "--no-add",
|
|
450
|
+
yes=False)
|
|
451
|
+
assert proc.returncode == 0, proc.stderr
|
|
452
|
+
assert "excluded from the release commit" in proc.stdout
|
|
453
|
+
|
|
454
|
+
|
|
344
455
|
class TestArgs:
|
|
345
456
|
def test_no_selector_fails(self, tmp_path):
|
|
346
457
|
repo, _ = make_repo(tmp_path)
|
|
@@ -353,3 +464,48 @@ class TestArgs:
|
|
|
353
464
|
proc = run_publish(repo, "--major", "--minor")
|
|
354
465
|
assert proc.returncode != 0
|
|
355
466
|
assert read_version(repo) == "0.2.0"
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
class TestPublishCommandFile:
|
|
470
|
+
"""The /publish command file is the source of truth for the release flow.
|
|
471
|
+
|
|
472
|
+
Shell blocks in a command file run when OpenCode evaluates the command,
|
|
473
|
+
before the agent sees the prompt, so a real (non-dry-run) publish must
|
|
474
|
+
never live in a shell block. Confirmation must route through the
|
|
475
|
+
`question` tool instead of publish.py's stdin prompt, which has no
|
|
476
|
+
stdin to read when the command runs in a session.
|
|
477
|
+
"""
|
|
478
|
+
|
|
479
|
+
COMMAND_FILE = REPO_ROOT / ".opencode" / "commands" / "publish.md"
|
|
480
|
+
|
|
481
|
+
def test_real_publish_never_runs_in_shell_block(self):
|
|
482
|
+
text = self.COMMAND_FILE.read_text()
|
|
483
|
+
for line in text.splitlines():
|
|
484
|
+
if line.strip().startswith("!") and "publish.py" in line:
|
|
485
|
+
assert "--dry-run" in line, (
|
|
486
|
+
"a publish.py shell block without --dry-run would cut "
|
|
487
|
+
"a release before the user is asked: %r" % line
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
def test_confirmation_routes_through_question_tool(self):
|
|
491
|
+
text = self.COMMAND_FILE.read_text()
|
|
492
|
+
assert "`question` tool" in text
|
|
493
|
+
|
|
494
|
+
def test_mandate_present(self):
|
|
495
|
+
text = self.COMMAND_FILE.read_text()
|
|
496
|
+
assert "### Consumer adoption" in text
|
|
497
|
+
assert "CONSUMER_ADOPTION_PLANS" in text
|
|
498
|
+
|
|
499
|
+
def test_unstaged_files_confirmation_present(self):
|
|
500
|
+
"""The flow must ask whether the reported uncommitted files are
|
|
501
|
+
added to the release commit before the cut."""
|
|
502
|
+
text = self.COMMAND_FILE.read_text()
|
|
503
|
+
assert "uncommitted files" in text
|
|
504
|
+
assert "--no-add" in text
|
|
505
|
+
|
|
506
|
+
def test_notes_happiness_confirmation_present(self):
|
|
507
|
+
"""The flow must ask whether the user is happy with the rendered
|
|
508
|
+
release notes, and offer a changes loop."""
|
|
509
|
+
text = self.COMMAND_FILE.read_text()
|
|
510
|
+
assert "happy" in text
|
|
511
|
+
assert "--description" in text
|