agentic-dev-loop 0.10.4__tar.gz → 0.11.0__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 (138) hide show
  1. {agentic_dev_loop-0.10.4/src/agentic_dev_loop.egg-info → agentic_dev_loop-0.11.0}/PKG-INFO +5 -3
  2. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/README.md +4 -2
  3. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/pyproject.toml +1 -1
  4. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop/core/profiles/__init__.py +4 -6
  5. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop/engine/cli.py +3 -0
  6. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop/engine/graph.py +4 -2
  7. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop/engine/onboarding/checks.py +4 -3
  8. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop/engine/onboarding/templates/trigger-agentic-dev-loop.sh +6 -14
  9. agentic_dev_loop-0.11.0/src/agentic_dev_loop/engine/onboarding/upgrade.py +76 -0
  10. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop/viz/__init__.py +2 -2
  11. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop/viz/__main__.py +5 -3
  12. agentic_dev_loop-0.11.0/src/agentic_dev_loop/viz/layout.py +233 -0
  13. agentic_dev_loop-0.11.0/src/agentic_dev_loop/viz/page.py +9 -0
  14. agentic_dev_loop-0.11.0/src/agentic_dev_loop/viz/server.py +470 -0
  15. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0/src/agentic_dev_loop.egg-info}/PKG-INFO +5 -3
  16. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop.egg-info/SOURCES.txt +8 -1
  17. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/tests/test_029_merge_on_conflict.py +0 -5
  18. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/tests/test_033_brief_step.py +0 -2
  19. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/tests/test_035_finalize_nudge.py +6 -5
  20. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/tests/test_036_banned_commands_and_lane_boundary.py +0 -11
  21. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/tests/test_041_brief_coverage.py +1 -40
  22. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/tests/test_046_brief_prompt.py +1 -15
  23. agentic_dev_loop-0.11.0/tests/test_055_cli.py +23 -0
  24. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/tests/test_055_doctor.py +1 -1
  25. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/tests/test_055_profiles.py +1 -1
  26. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/tests/test_055_templates.py +2 -2
  27. agentic_dev_loop-0.11.0/tests/test_055_upgrade.py +125 -0
  28. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/tests/test_actions.py +0 -6
  29. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/tests/test_agents.py +0 -22
  30. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/tests/test_aidlc.py +0 -15
  31. agentic_dev_loop-0.11.0/tests/test_build_viz_page.py +68 -0
  32. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/tests/test_cli.py +4 -61
  33. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/tests/test_config.py +1 -90
  34. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/tests/test_containers.py +1 -25
  35. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/tests/test_deps.py +2 -3
  36. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/tests/test_dry_run.py +0 -8
  37. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/tests/test_e2e_fixtures.py +0 -28
  38. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/tests/test_graph.py +0 -31
  39. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/tests/test_isolation.py +1 -65
  40. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/tests/test_isolation_pbt.py +3 -50
  41. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/tests/test_llm.py +0 -1
  42. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/tests/test_loaders.py +0 -3
  43. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/tests/test_proc.py +4 -32
  44. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/tests/test_publish.py +137 -4
  45. agentic_dev_loop-0.11.0/tests/test_qa_prompt.py +30 -0
  46. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/tests/test_registry.py +0 -5
  47. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/tests/test_results.py +0 -22
  48. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/tests/test_secrets.py +2 -11
  49. agentic_dev_loop-0.11.0/tests/test_source_resolution.py +62 -0
  50. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/tests/test_stages.py +0 -84
  51. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/tests/test_sync_back.py +0 -9
  52. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/tests/test_trigger_script.py +32 -47
  53. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/tests/test_unit5_pbt.py +10 -32
  54. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/tests/test_viz.py +335 -320
  55. agentic_dev_loop-0.11.0/tests/test_viz_demo.py +126 -0
  56. agentic_dev_loop-0.11.0/tests/test_viz_layout.py +275 -0
  57. agentic_dev_loop-0.10.4/src/agentic_dev_loop/viz/server.py +0 -1273
  58. agentic_dev_loop-0.10.4/tests/test_055_cli.py +0 -51
  59. agentic_dev_loop-0.10.4/tests/test_qa_prompt.py +0 -53
  60. agentic_dev_loop-0.10.4/tests/test_source_resolution.py +0 -21
  61. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/LICENSE +0 -0
  62. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/setup.cfg +0 -0
  63. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop/__init__.py +0 -0
  64. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop/core/__init__.py +0 -0
  65. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop/core/config.py +0 -0
  66. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop/core/paths.py +0 -0
  67. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop/core/profiles/data/dotnet-node.yaml +0 -0
  68. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop/core/profiles/data/python.yaml +0 -0
  69. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop/core/results.py +0 -0
  70. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop/core/verdict.py +0 -0
  71. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop/engine/__init__.py +0 -0
  72. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop/engine/actions.py +0 -0
  73. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop/engine/agent_error_guide.md +0 -0
  74. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop/engine/agent_error_guide.py +0 -0
  75. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop/engine/agents.py +0 -0
  76. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop/engine/allowlist.py +0 -0
  77. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop/engine/brief_coverage.py +0 -0
  78. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop/engine/brief_prompt.md +0 -0
  79. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop/engine/brief_prompt.py +0 -0
  80. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop/engine/containers.py +0 -0
  81. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop/engine/deps.py +0 -0
  82. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop/engine/dryrun.py +0 -0
  83. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop/engine/egress_policy.py +0 -0
  84. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop/engine/egress_proxy.py +0 -0
  85. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop/engine/extensions/__init__.py +0 -0
  86. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop/engine/extensions/aidlc.py +0 -0
  87. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop/engine/forbidden.py +0 -0
  88. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop/engine/hostboundary.py +0 -0
  89. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop/engine/isolation.py +0 -0
  90. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop/engine/llm.py +0 -0
  91. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop/engine/loaders.py +0 -0
  92. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop/engine/mounts.py +0 -0
  93. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop/engine/onboarding/__init__.py +0 -0
  94. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop/engine/onboarding/doctor.py +0 -0
  95. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop/engine/onboarding/init.py +0 -0
  96. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop/engine/onboarding/recipes/dotnet-node.yaml +0 -0
  97. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop/engine/onboarding/recipes/python.yaml +0 -0
  98. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop/engine/onboarding/recipes.py +0 -0
  99. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop/engine/onboarding/templates/dev.agent.md +0 -0
  100. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop/engine/onboarding/templates/env.example +0 -0
  101. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop/engine/onboarding/templates/qa.agent.md +0 -0
  102. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop/engine/onboarding/templates/review.agent.md +0 -0
  103. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop/engine/onboarding/templates.py +0 -0
  104. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop/engine/outputs.py +0 -0
  105. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop/engine/presets.py +0 -0
  106. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop/engine/proc.py +0 -0
  107. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop/engine/registry.py +0 -0
  108. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop/engine/seccomp/__init__.py +0 -0
  109. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop/engine/seccomp/chromium-userns.json +0 -0
  110. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop/engine/secrets.py +0 -0
  111. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop/engine/stages.py +0 -0
  112. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop/engine/step_workspace.py +0 -0
  113. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop/engine/sync_back.py +0 -0
  114. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop/py.typed +0 -0
  115. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop.egg-info/dependency_links.txt +0 -0
  116. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop.egg-info/entry_points.txt +0 -0
  117. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop.egg-info/requires.txt +0 -0
  118. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/src/agentic_dev_loop.egg-info/top_level.txt +0 -0
  119. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/tests/test_055_init.py +0 -0
  120. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/tests/test_055_profile_loading.py +0 -0
  121. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/tests/test_055_recipes.py +0 -0
  122. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/tests/test_055_schema_lock.py +0 -0
  123. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/tests/test_agent_error_guide.py +0 -0
  124. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/tests/test_egress_proxy.py +0 -0
  125. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/tests/test_hostboundary.py +0 -0
  126. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/tests/test_hostboundary_integration.py +0 -0
  127. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/tests/test_isolation_integration.py +0 -0
  128. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/tests/test_module_contract.py +0 -0
  129. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/tests/test_outputs.py +0 -0
  130. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/tests/test_outputs_pbt.py +0 -0
  131. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/tests/test_repo_ascii.py +0 -0
  132. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/tests/test_sdk_compat.py +0 -0
  133. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/tests/test_secrets_integration.py +0 -0
  134. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/tests/test_secrets_pipeline.py +0 -0
  135. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/tests/test_step_workspace.py +0 -0
  136. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/tests/test_unit5_config.py +0 -0
  137. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/tests/test_unit5_pure.py +0 -0
  138. {agentic_dev_loop-0.10.4 → agentic_dev_loop-0.11.0}/tests/test_verdict.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentic-dev-loop
3
- Version: 0.10.4
3
+ Version: 0.11.0
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. The trigger repairs a pin/install
496
- mismatch exactly once, then fails hard.
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. The trigger repairs a pin/install
452
- mismatch exactly once, then fails hard.
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.4"
7
+ version = "0.11.0"
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"
@@ -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
- ``latest`` is substituted into the fix line by the caller (doctor); when
89
- omitted the floor itself is used.
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
- f'Upgrade: pipx install --force "agentic-dev-loop=={target}"')
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)
@@ -275,13 +275,15 @@ def _run_pipeline(cfg, base, registry, state_dir, max_attempts, args, viz_bus,
275
275
  if bus is None and not getattr(args, "no_viz", True):
276
276
  start_state = initial if initial is not None \
277
277
  else graph.get_state(config).values
278
- topology, nodes = viz.topology_mermaid(cfg)
278
+ topology = viz.topology_graph(cfg)
279
279
  bus = viz.VizBus()
280
280
  if args.viz_port:
281
281
  kill_listeners_on_port(args.viz_port, match="agentic-dev-loop",
282
282
  cwd=runtime.workspace)
283
283
  httpd, _thread = viz.serve_viz(
284
- bus, topology, start_state["subject"], args.viz_port, nodes)
284
+ bus, topology["mermaid"], start_state["subject"],
285
+ args.viz_port, topology["nodes"], topology["flow"],
286
+ topology["edges"])
285
287
  bound = httpd.server_address[1]
286
288
  url = f"http://127.0.0.1:{bound}"
287
289
  if args.viz_port and bound != args.viz_port:
@@ -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
- "run agentic-dev-loop init (or write agentic-dev-loop/.engine-version)"))
175
+ FIX_UPGRADE))
175
176
  else:
176
177
  checks.append(_hard(
177
178
  "pin", pin == installed,
178
179
  f"installed {installed}, pin {pin}",
179
- f'pipx install --force "agentic-dev-loop=={pin}"'))
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
- f'pipx install --force "agentic-dev-loop=={latest}"'))
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 pin/install mismatch is repaired exactly once, then the run fails hard.
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 "error: doctor --fast failed - repairing the engine pin $PIN once" >&2
39
- if ! pipx install --force "agentic-dev-loop==$PIN"; then
40
- echo "error: could not install agentic-dev-loop==$PIN" >&2
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 repairing the engine pin" >&2
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
@@ -4,10 +4,10 @@ import time # noqa: F401 - re-exported so tests can pin NodeWatch's clock
4
4
 
5
5
  from .server import (MAX_LINE, PAGE, ConsoleCapture, HeartbeatThread,
6
6
  NodeWatch, VizBus, _ReuseSafeServer, _Tee,
7
- _handler_class, serve_viz, topology_mermaid, translate)
7
+ _handler_class, serve_viz, topology_graph, topology_mermaid, translate)
8
8
 
9
9
  __all__ = [
10
10
  "MAX_LINE", "PAGE", "ConsoleCapture", "HeartbeatThread", "NodeWatch",
11
11
  "VizBus", "_ReuseSafeServer", "_Tee", "_handler_class", "serve_viz",
12
- "topology_mermaid", "translate",
12
+ "topology_graph", "topology_mermaid", "translate",
13
13
  ]
@@ -8,7 +8,7 @@ import sys
8
8
 
9
9
  from agentic_dev_loop.core.config import load_config
10
10
 
11
- from . import VizBus, serve_viz, topology_mermaid
11
+ from . import VizBus, serve_viz, topology_graph
12
12
 
13
13
 
14
14
  def main(argv: list[str] | None = None) -> int:
@@ -23,9 +23,11 @@ def main(argv: list[str] | None = None) -> int:
23
23
  args = parser.parse_args(argv)
24
24
 
25
25
  cfg = load_config(args.pipeline)
26
- topology, nodes = topology_mermaid(cfg)
26
+ topology = topology_graph(cfg)
27
27
  bus = VizBus()
28
- httpd, _thread = serve_viz(bus, topology, cfg.name, args.port, nodes)
28
+ httpd, _thread = serve_viz(bus, topology["mermaid"], cfg.name, args.port,
29
+ topology["nodes"], topology["flow"],
30
+ topology["edges"])
29
31
  bound = httpd.server_address[1]
30
32
  url = f"http://127.0.0.1:{bound}"
31
33
  print(f"Live graph viz: {url}", flush=True)
@@ -0,0 +1,233 @@
1
+ """Pure-Python port of the viz pipeline layout algorithm.
2
+
3
+ Mirrors ``planning-docs/design/pipeline-layout.js`` one-to-one (contract in
4
+ its header and in the unit-9 business-logic model): no DOM, no I/O, same
5
+ input/output shapes and the same crossing-free ordering rules. The reference
6
+ ES5 implementation remains the browser-side copy; this port is the tested
7
+ source of truth (decision Q3=A).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Any
13
+
14
+ NW = 112
15
+ NH = 76
16
+ GAP = 24
17
+ STEP = NW + GAP
18
+ LANE = 18
19
+ LANE_TOP = 14
20
+ LANE_PAD = 16
21
+ CH = 12
22
+
23
+ METRICS = {"NW": NW, "NH": NH, "GAP": GAP, "STEP": STEP, "LANE": LANE}
24
+
25
+
26
+ class _Edge:
27
+ """Mutable routing record, mirroring the JS object the reference mutates."""
28
+
29
+ __slots__ = ("src", "dst", "label", "kind", "si", "ti", "rs", "rt",
30
+ "lane_s", "lane_t", "j", "k", "xe")
31
+
32
+ def __init__(self, src: str, dst: str, label: str | None, kind: str):
33
+ self.src = src
34
+ self.dst = dst
35
+ self.label = label
36
+ self.kind = kind
37
+ self.si = 0
38
+ self.ti = 0
39
+ self.rs = 0
40
+ self.rt = 0
41
+ self.lane_s = 0
42
+ self.lane_t = 0
43
+ self.j = 0
44
+ self.k = 0
45
+ self.xe = 0
46
+
47
+
48
+ def _plan(names: list[str], routed: list[_Edge], idx: dict[str, int], r: int) -> dict[str, Any]:
49
+ n = len(names)
50
+ per = max(1, (n + r - 1) // r)
51
+ rows = max(1, (n + per - 1) // per)
52
+ pos = [{"r": i // per, "c": i % per} for i in range(n)]
53
+ edges = []
54
+ for e in routed:
55
+ rec = _Edge(e.src, e.dst, e.label, e.kind)
56
+ rec.si = idx[e.src]
57
+ rec.ti = idx[e.dst]
58
+ rec.rs = pos[rec.si]["r"]
59
+ rec.rt = pos[rec.ti]["r"]
60
+ edges.append(rec)
61
+ same = sorted((e for e in edges if e.rs == e.rt),
62
+ key=lambda e: abs(e.si - e.ti))
63
+ back = sorted((e for e in edges if e.rs > e.rt),
64
+ key=lambda e: (e.rs, e.si))
65
+ forward = sorted((e for e in edges if e.rs < e.rt),
66
+ key=lambda e: (e.rt, -e.si))
67
+ for j, e in enumerate(back):
68
+ e.j = j
69
+ for k, e in enumerate(forward):
70
+ e.k = k
71
+ multi = rows > 1
72
+ left = CH + CH * len(back) + 8 if multi else 0
73
+ core = per * STEP - GAP
74
+ right = (14 if multi else 0) + (8 + CH * len(forward) if forward else 0)
75
+ return {"per": per, "rows": rows, "pos": pos, "U": same, "C": back,
76
+ "F": forward, "O": left, "core": core, "width": left + core + right}
77
+
78
+
79
+ def layout_pipeline(opts: dict[str, Any]) -> dict[str, Any]:
80
+ """Lay out stations and routes for ``opts`` (contract: FD section 1.1)."""
81
+ names = list(opts.get("nodes") or [])
82
+ n = len(names)
83
+ status = opts.get("status") or {}
84
+ avail = max(0, opts.get("availWidth") or 0)
85
+ flow = opts.get("flow") or []
86
+ cond_edges = opts.get("edges") or []
87
+ idx = {name: i for i, name in enumerate(names)}
88
+
89
+ def reached(name: str) -> bool:
90
+ return (status.get(name) or "pending") != "pending"
91
+
92
+ def failed(name: str) -> bool:
93
+ state = status.get(name) or "pending"
94
+ return state == "fail" or state == "fatal"
95
+
96
+ rails: list[dict[str, Any]] = []
97
+ extra_flow: list[_Edge] = []
98
+ for f in flow:
99
+ if f["src"] not in idx or f["dst"] not in idx:
100
+ continue
101
+ if idx[f["dst"]] == idx[f["src"]] + 1:
102
+ rails.append(f)
103
+ else:
104
+ extra_flow.append(_Edge(f["src"], f["dst"], None, "flow"))
105
+ routed = [
106
+ _Edge(e["src"], e["dst"], e.get("label") or None, "back")
107
+ for e in cond_edges
108
+ if e["src"] in idx and e["dst"] in idx
109
+ ] + extra_flow
110
+
111
+ layout = _plan(names, routed, idx, 1)
112
+ r = 2
113
+ while layout["width"] > avail and layout["per"] > 2:
114
+ layout = _plan(names, routed, idx, r)
115
+ r += 1
116
+ offset = layout["O"]
117
+
118
+ gutters: list[dict[str, int]] = []
119
+ for g in range(layout["rows"]):
120
+ items = 0
121
+
122
+ def take() -> int:
123
+ nonlocal items
124
+ value = items
125
+ items += 1
126
+ return value
127
+
128
+ for e in layout["U"]:
129
+ if e.rt == g:
130
+ e.lane_s = take()
131
+ for e in sorted((e for e in layout["C"] if e.rt == g),
132
+ key=lambda e: -e.j):
133
+ e.lane_t = take()
134
+ for e in layout["F"]:
135
+ if e.rt == g:
136
+ e.lane_t = take()
137
+ for e in sorted((e for e in layout["C"] if e.rs == g),
138
+ key=lambda e: e.si):
139
+ e.lane_s = take()
140
+ for e in layout["F"]:
141
+ if e.rs == g:
142
+ e.lane_s = take()
143
+ wrap = take() if g < layout["rows"] - 1 else -1
144
+ gutters.append({"n": items, "wrapLane": wrap})
145
+
146
+ row_y = [0]
147
+ for g, gutter in enumerate(gutters):
148
+ gutter["h"] = (LANE_TOP + (gutter["n"] - 1) * LANE + LANE_PAD
149
+ if gutter["n"] else 4)
150
+ row_y.append(row_y[g] + NH + gutter["h"])
151
+
152
+ def lane_y(g: int, lane: int) -> int:
153
+ return row_y[g] + NH + LANE_TOP + lane * LANE
154
+
155
+ def node_x(i: int) -> int:
156
+ return offset + layout["pos"][i]["c"] * STEP
157
+
158
+ def node_y(i: int) -> int:
159
+ return row_y[layout["pos"][i]["r"]]
160
+
161
+ entry_count: dict[int, int] = {}
162
+ for e in layout["U"] + layout["C"] + layout["F"]:
163
+ m = entry_count.get(e.ti, 0)
164
+ entry_count[e.ti] = m + 1
165
+ e.xe = max(node_x(e.ti) + 8, node_x(e.ti) + NW - 14 - m * 14)
166
+
167
+ lines: list[dict[str, Any]] = []
168
+ arrows: list[dict[str, Any]] = []
169
+ labels: list[dict[str, Any]] = []
170
+
171
+ def horizontal(x1: int, x2: int, y: int, kind: str, on: bool = False) -> None:
172
+ lines.append({"x": min(x1, x2), "y": y, "w": abs(x2 - x1), "h": 0,
173
+ "dir": "h", "kind": kind, "on": bool(on)})
174
+
175
+ def vertical(x: int, y1: int, y2: int, kind: str, on: bool = False) -> None:
176
+ lines.append({"x": x, "y": min(y1, y2), "w": 0, "h": abs(y2 - y1),
177
+ "dir": "v", "kind": kind, "on": bool(on)})
178
+
179
+ for f in rails:
180
+ i = idx[f["src"]]
181
+ on = reached(f["dst"])
182
+ if layout["pos"][i + 1]["r"] == layout["pos"][i]["r"]:
183
+ horizontal(node_x(i) + NW, node_x(i + 1), node_y(i) + 37, "flow", on)
184
+ arrows.append({"x": node_x(i + 1) - 6, "y": node_y(i) + 33,
185
+ "dir": "right", "kind": "flow", "on": on})
186
+ else:
187
+ r = layout["pos"][i]["r"]
188
+ xr = node_x(i) + NW + 12
189
+ yw = lane_y(r, gutters[r]["wrapLane"])
190
+ yn = node_y(i + 1) + 37
191
+ horizontal(node_x(i) + NW, xr + 2, node_y(i) + 37, "flow", on)
192
+ vertical(xr, node_y(i) + 37, yw, "flow", on)
193
+ horizontal(offset - 12, xr + 2, yw - 1, "flow", on)
194
+ vertical(offset - 12, yw - 1, yn + 2, "flow", on)
195
+ horizontal(offset - 12, offset, yn, "flow", on)
196
+ arrows.append({"x": offset - 6, "y": yn - 4, "dir": "right",
197
+ "kind": "flow", "on": on})
198
+
199
+ for e in layout["U"] + layout["C"] + layout["F"]:
200
+ xs = node_x(e.si) + NW // 2
201
+ top = node_y(e.ti) + NH
202
+ y1 = lane_y(e.rs, e.lane_s)
203
+ on = e.kind == "back" and failed(e.src)
204
+ vertical(xs, node_y(e.si) + NH, y1, e.kind, on)
205
+ if e.rs == e.rt:
206
+ horizontal(e.xe, xs, y1, e.kind, on)
207
+ vertical(e.xe, top + 6, y1, e.kind, on)
208
+ else:
209
+ if e.rs > e.rt:
210
+ xc = offset - CH - CH * (e.j + 1)
211
+ else:
212
+ xc = offset + layout["core"] + 14 + 8 + CH * e.k
213
+ y2 = lane_y(e.rt, e.lane_t)
214
+ horizontal(xc, xs, y1, e.kind, on)
215
+ vertical(xc, y2, y1, e.kind, on)
216
+ horizontal(xc, e.xe, y2, e.kind, on)
217
+ vertical(e.xe, top + 6, y2, e.kind, on)
218
+ arrows.append({"x": e.xe - 5, "y": top, "dir": "up", "kind": e.kind,
219
+ "on": on})
220
+ if e.label:
221
+ labels.append({"x": xs - 6, "y": y1 - 9, "text": e.label})
222
+
223
+ return {
224
+ "width": layout["width"],
225
+ "height": row_y[layout["rows"]],
226
+ "rows": layout["rows"],
227
+ "perRow": layout["per"],
228
+ "nodes": [{"id": name, "index": i, "x": node_x(i), "y": node_y(i),
229
+ "w": NW, "h": NH} for i, name in enumerate(names)],
230
+ "lines": lines,
231
+ "arrows": arrows,
232
+ "labels": labels,
233
+ }