agentic-dev-loop 1.0.0a49__tar.gz → 1.0.0a51__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.
Potentially problematic release.
This version of agentic-dev-loop might be problematic. Click here for more details.
- {agentic_dev_loop-1.0.0a49/src/agentic_dev_loop.egg-info → agentic_dev_loop-1.0.0a51}/PKG-INFO +5 -2
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/README.md +4 -1
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/pyproject.toml +1 -1
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/engine/graph.py +16 -2
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/engine/isolation.py +35 -16
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/viz/server.py +48 -12
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51/src/agentic_dev_loop.egg-info}/PKG-INFO +5 -2
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/tests/test_containers.py +5 -1
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/tests/test_graph.py +20 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/tests/test_isolation.py +69 -7
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/tests/test_viz.py +113 -6
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/LICENSE +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/setup.cfg +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/core/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/core/config.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/core/paths.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/core/results.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/core/verdict.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/engine/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/engine/actions.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/engine/agent_error_guide.md +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/engine/agent_error_guide.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/engine/agents.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/engine/allowlist.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/engine/brief_coverage.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/engine/brief_prompt.md +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/engine/brief_prompt.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/engine/cli.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/engine/containers.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/engine/deps.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/engine/dryrun.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/engine/egress_policy.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/engine/egress_proxy.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/engine/extensions/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/engine/extensions/aidlc.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/engine/forbidden.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/engine/hostboundary.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/engine/llm.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/engine/loaders.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/engine/mounts.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/engine/outputs.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/engine/presets.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/engine/proc.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/engine/registry.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/engine/seccomp/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/engine/seccomp/chromium-userns.json +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/engine/secrets.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/engine/stages.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/engine/step_workspace.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/engine/sync_back.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/py.typed +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/viz/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/viz/__main__.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop.egg-info/SOURCES.txt +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop.egg-info/dependency_links.txt +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop.egg-info/entry_points.txt +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop.egg-info/requires.txt +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop.egg-info/top_level.txt +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/tests/test_028_declared_caches.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/tests/test_029_merge_on_conflict.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/tests/test_033_brief_step.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/tests/test_035_finalize_nudge.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/tests/test_036_banned_commands_and_lane_boundary.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/tests/test_041_brief_coverage.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/tests/test_046_brief_prompt.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/tests/test_actions.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/tests/test_agent_error_guide.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/tests/test_agents.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/tests/test_aidlc.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/tests/test_cli.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/tests/test_config.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/tests/test_deps.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/tests/test_dry_run.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/tests/test_e2e_fixtures.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/tests/test_egress_proxy.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/tests/test_hostboundary.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/tests/test_hostboundary_integration.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/tests/test_isolation_integration.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/tests/test_isolation_pbt.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/tests/test_llm.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/tests/test_loaders.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/tests/test_module_contract.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/tests/test_outputs.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/tests/test_outputs_pbt.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/tests/test_proc.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/tests/test_publish.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/tests/test_qa_prompt.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/tests/test_registry.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/tests/test_repo_ascii.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/tests/test_results.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/tests/test_sdk_compat.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/tests/test_secrets.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/tests/test_secrets_integration.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/tests/test_secrets_pipeline.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/tests/test_source_resolution.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/tests/test_stages.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/tests/test_step_workspace.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/tests/test_sync_back.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/tests/test_trigger_script.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/tests/test_unit5_config.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/tests/test_unit5_pbt.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/tests/test_unit5_pure.py +0 -0
- {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/tests/test_verdict.py +0 -0
{agentic_dev_loop-1.0.0a49/src/agentic_dev_loop.egg-info → agentic_dev_loop-1.0.0a51}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentic-dev-loop
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.0a51
|
|
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
|
|
@@ -169,7 +169,10 @@ engine-owned object to its owning run and a `live`, `orphaned`,
|
|
|
169
169
|
registry-owned orphans whose owner pid is dead (never unregistered names).
|
|
170
170
|
A SIGTERM to a running pipeline still closes the sandbox (the graph's
|
|
171
171
|
`finally` runs); SIGKILL is unhandleable, so its containers are reclaimed by
|
|
172
|
-
the startup reaper on the next run or by `containers --reap`.
|
|
172
|
+
the startup reaper on the next run or by `containers --reap`. The reaper
|
|
173
|
+
and `--reap` prune a registry entry only after the daemon confirms the
|
|
174
|
+
object is gone; a surviving object (a failed removal) keeps its entry so
|
|
175
|
+
the next startup retries it. Pass
|
|
173
176
|
`--state-dir` to point at a custom `isolation.state_dir`.
|
|
174
177
|
|
|
175
178
|
**What a run looks like** (the shipped self-loop example):
|
|
@@ -125,7 +125,10 @@ engine-owned object to its owning run and a `live`, `orphaned`,
|
|
|
125
125
|
registry-owned orphans whose owner pid is dead (never unregistered names).
|
|
126
126
|
A SIGTERM to a running pipeline still closes the sandbox (the graph's
|
|
127
127
|
`finally` runs); SIGKILL is unhandleable, so its containers are reclaimed by
|
|
128
|
-
the startup reaper on the next run or by `containers --reap`.
|
|
128
|
+
the startup reaper on the next run or by `containers --reap`. The reaper
|
|
129
|
+
and `--reap` prune a registry entry only after the daemon confirms the
|
|
130
|
+
object is gone; a surviving object (a failed removal) keeps its entry so
|
|
131
|
+
the next startup retries it. Pass
|
|
129
132
|
`--state-dir` to point at a custom `isolation.state_dir`.
|
|
130
133
|
|
|
131
134
|
**What a run looks like** (the shipped self-loop example):
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "agentic-dev-loop"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.0a51"
|
|
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-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/engine/graph.py
RENAMED
|
@@ -8,6 +8,7 @@ steer the retry cycle, and a SQLite checkpointer makes runs crash-resumable
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
import operator
|
|
11
|
+
import time
|
|
11
12
|
import uuid
|
|
12
13
|
import webbrowser
|
|
13
14
|
from pathlib import Path
|
|
@@ -279,12 +280,14 @@ def _run_pipeline(cfg, base, registry, state_dir, max_attempts, args, viz_bus,
|
|
|
279
280
|
if bus is not None:
|
|
280
281
|
start_state = initial if initial is not None \
|
|
281
282
|
else graph.get_state(config).values
|
|
283
|
+
run_start = time.monotonic()
|
|
282
284
|
bus.publish({
|
|
283
285
|
"type": "run_started",
|
|
284
286
|
"subject": start_state["subject"],
|
|
285
287
|
"max_attempts": start_state["max_attempts"],
|
|
286
288
|
"thread_id": thread_id,
|
|
287
289
|
"attempt": start_state["attempt"],
|
|
290
|
+
"started_at": time.time(),
|
|
288
291
|
})
|
|
289
292
|
watch = viz.NodeWatch()
|
|
290
293
|
capture = viz.ConsoleCapture(bus, watch)
|
|
@@ -292,6 +295,7 @@ def _run_pipeline(cfg, base, registry, state_dir, max_attempts, args, viz_bus,
|
|
|
292
295
|
heartbeat = viz.HeartbeatThread(watch, bus)
|
|
293
296
|
heartbeat.start()
|
|
294
297
|
final = None
|
|
298
|
+
step_starts: dict[str, float] = {}
|
|
295
299
|
try:
|
|
296
300
|
for part in graph.stream(initial, config,
|
|
297
301
|
stream_mode=["tasks", "updates"],
|
|
@@ -299,8 +303,13 @@ def _run_pipeline(cfg, base, registry, state_dir, max_attempts, args, viz_bus,
|
|
|
299
303
|
for event in viz.translate(part):
|
|
300
304
|
if event["type"] == "node_started":
|
|
301
305
|
watch.start(event["node"])
|
|
306
|
+
step_starts[event["node"]] = time.monotonic()
|
|
302
307
|
elif event["type"] == "node_finished":
|
|
303
308
|
watch.finish(event["node"])
|
|
309
|
+
if event["node"] in step_starts:
|
|
310
|
+
event["elapsed_seconds"] = max(
|
|
311
|
+
0, int(round(time.monotonic()
|
|
312
|
+
- step_starts[event["node"]])))
|
|
304
313
|
bus.publish(event)
|
|
305
314
|
final = graph.get_state(config).values
|
|
306
315
|
finally:
|
|
@@ -317,8 +326,13 @@ def _run_pipeline(cfg, base, registry, state_dir, max_attempts, args, viz_bus,
|
|
|
317
326
|
else:
|
|
318
327
|
success = False
|
|
319
328
|
attempts = 0
|
|
320
|
-
bus.publish({
|
|
321
|
-
|
|
329
|
+
bus.publish({
|
|
330
|
+
"type": "run_finished",
|
|
331
|
+
"success": success,
|
|
332
|
+
"attempts": attempts,
|
|
333
|
+
"elapsed_seconds": max(
|
|
334
|
+
0, int(round(time.monotonic() - run_start))),
|
|
335
|
+
})
|
|
322
336
|
capture.detach()
|
|
323
337
|
else:
|
|
324
338
|
final = graph.invoke(initial, config)
|
{agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/engine/isolation.py
RENAMED
|
@@ -1632,23 +1632,12 @@ class DockerBackend(Sandbox):
|
|
|
1632
1632
|
_write_registry(self._iso_state_dir, remaining)
|
|
1633
1633
|
|
|
1634
1634
|
def _container_gone(self, name: str) -> bool:
|
|
1635
|
-
"""
|
|
1636
|
-
|
|
1637
|
-
res = self._runner.run(
|
|
1638
|
-
["docker", "ps", "-a", "--filter", f"name={name}",
|
|
1639
|
-
"--format", "{{.Names}}"], timeout=30)
|
|
1640
|
-
if res.exit_code != 0:
|
|
1641
|
-
return False
|
|
1642
|
-
return name not in res.stdout.splitlines()
|
|
1635
|
+
"""Thin delegation to the module-level confirm helper (050 D2)."""
|
|
1636
|
+
return _container_gone(self._runner, name)
|
|
1643
1637
|
|
|
1644
1638
|
def _network_gone(self, name: str) -> bool:
|
|
1645
|
-
"""
|
|
1646
|
-
|
|
1647
|
-
res = self._runner.run(
|
|
1648
|
-
["docker", "network", "inspect", name], timeout=30)
|
|
1649
|
-
if res.exit_code == 0:
|
|
1650
|
-
return False
|
|
1651
|
-
return "No such network" in f"{res.stderr} {res.stdout}"
|
|
1639
|
+
"""Thin delegation to the module-level confirm helper (050 D2)."""
|
|
1640
|
+
return _network_gone(self._runner, name)
|
|
1652
1641
|
|
|
1653
1642
|
# -- internal -----------------------------------------------------------
|
|
1654
1643
|
|
|
@@ -1882,6 +1871,28 @@ def _pid_alive_matches(pid: int, recorded_start: str | None) -> bool:
|
|
|
1882
1871
|
return current == recorded_start
|
|
1883
1872
|
|
|
1884
1873
|
|
|
1874
|
+
def _container_gone(runner: CommandRunner, name: str) -> bool:
|
|
1875
|
+
"""True only when the daemon confirms the container no longer exists.
|
|
1876
|
+
A nonzero daemon exit keeps the entry."""
|
|
1877
|
+
res = runner.run(
|
|
1878
|
+
["docker", "ps", "-a", "--filter", f"name={name}",
|
|
1879
|
+
"--format", "{{.Names}}"], timeout=30)
|
|
1880
|
+
if res.exit_code != 0:
|
|
1881
|
+
return False
|
|
1882
|
+
return name not in res.stdout.splitlines()
|
|
1883
|
+
|
|
1884
|
+
|
|
1885
|
+
def _network_gone(runner: CommandRunner, name: str) -> bool:
|
|
1886
|
+
"""True only when the daemon reports the network is missing. Any
|
|
1887
|
+
other inspect failure (daemon unreachable) keeps the entry."""
|
|
1888
|
+
res = runner.run(
|
|
1889
|
+
["docker", "network", "inspect", name], timeout=30)
|
|
1890
|
+
if res.exit_code == 0:
|
|
1891
|
+
return False
|
|
1892
|
+
return "No such network" in f"{res.stderr} {res.stdout}"
|
|
1893
|
+
|
|
1894
|
+
|
|
1895
|
+
|
|
1885
1896
|
def reap_orphaned_containers(state_dir: str | Path,
|
|
1886
1897
|
runner: CommandRunner | None = None,
|
|
1887
1898
|
*, announce: bool = True) -> list[str]:
|
|
@@ -1889,7 +1900,9 @@ def reap_orphaned_containers(state_dir: str | Path,
|
|
|
1889
1900
|
and the per-run internal network whose owning CLI process is gone (crashed
|
|
1890
1901
|
before close()). Exact by construction - only engine registry names are
|
|
1891
1902
|
touched, and a live owner (pid alive with the same start time) is left
|
|
1892
|
-
alone.
|
|
1903
|
+
alone. A surviving object (rm failure) keeps its entry so the next
|
|
1904
|
+
startup reaper retries; only a daemon-confirmed absence prunes it (050
|
|
1905
|
+
D1). Best-effort: never raises."""
|
|
1893
1906
|
runner = runner or _default_runner
|
|
1894
1907
|
entries = _read_registry(Path(state_dir))
|
|
1895
1908
|
if not entries:
|
|
@@ -1909,6 +1922,9 @@ def reap_orphaned_containers(state_dir: str | Path,
|
|
|
1909
1922
|
remaining.append(entry)
|
|
1910
1923
|
continue
|
|
1911
1924
|
runner.run(["docker", "network", "rm", name], timeout=60)
|
|
1925
|
+
if not _network_gone(runner, name):
|
|
1926
|
+
remaining.append(entry)
|
|
1927
|
+
continue
|
|
1912
1928
|
if announce:
|
|
1913
1929
|
_announce(f"reaped orphaned sandbox network {name}")
|
|
1914
1930
|
reaped.append(name)
|
|
@@ -1924,6 +1940,9 @@ def reap_orphaned_containers(state_dir: str | Path,
|
|
|
1924
1940
|
remaining.append(entry)
|
|
1925
1941
|
continue
|
|
1926
1942
|
runner.run(["docker", "rm", "-f", name], timeout=60)
|
|
1943
|
+
if not _container_gone(runner, name):
|
|
1944
|
+
remaining.append(entry)
|
|
1945
|
+
continue
|
|
1927
1946
|
if announce:
|
|
1928
1947
|
_announce(f"reaped orphaned sandbox container {name}")
|
|
1929
1948
|
reaped.append(name)
|
|
@@ -466,7 +466,7 @@ PAGE = """<!doctype html>
|
|
|
466
466
|
#thread code { font: 12px ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
467
467
|
background: #f5f5f5; padding: 2px 6px; border-radius: 4px;
|
|
468
468
|
cursor: pointer; user-select: all; }
|
|
469
|
-
#
|
|
469
|
+
#total { font-size: 14px; color: #1976d2; display: none; }
|
|
470
470
|
#verdicts { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
471
471
|
.badge { font-size: 12px; padding: 3px 8px; border-radius: 10px;
|
|
472
472
|
background: #f0f0f0; color: #333; }
|
|
@@ -477,12 +477,15 @@ PAGE = """<!doctype html>
|
|
|
477
477
|
#stages { flex: 1 1 auto; min-height: 0; overflow-y: auto; display: flex;
|
|
478
478
|
flex-direction: column; gap: 8px; }
|
|
479
479
|
.stage { border: 1px solid #e0e0e0; border-radius: 6px; overflow: hidden; }
|
|
480
|
-
.stage-header { display:
|
|
480
|
+
.stage-header { display: flex; align-items: baseline; width: 100%;
|
|
481
|
+
text-align: left; cursor: pointer;
|
|
481
482
|
font: 13px/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI",
|
|
482
483
|
Roboto, Helvetica, Arial, sans-serif; padding: 6px 10px;
|
|
483
484
|
border: 0; background: #f7f7f7; }
|
|
484
485
|
.stage-glyph { display: inline-block; width: 1.1em; }
|
|
485
486
|
.stage-status { margin-left: 0.4em; }
|
|
487
|
+
.stage-time { margin-left: auto; color: #555;
|
|
488
|
+
font-variant-numeric: tabular-nums; }
|
|
486
489
|
.status-pending { color: #555; }
|
|
487
490
|
.status-running { color: #1976d2; }
|
|
488
491
|
.status-pass { color: #2e7d32; }
|
|
@@ -531,7 +534,7 @@ PAGE = """<!doctype html>
|
|
|
531
534
|
<h2 id="subject">agentic-dev-loop pipeline</h2>
|
|
532
535
|
<div id="attempt">attempt ?/?</div>
|
|
533
536
|
<div id="thread">thread id: <code id="thread-id" title="click to select">\u2014</code></div>
|
|
534
|
-
<div id="
|
|
537
|
+
<div id="total"></div>
|
|
535
538
|
<div id="verdicts"></div>
|
|
536
539
|
<div id="stages"></div>
|
|
537
540
|
<ul id="run-log"></ul>
|
|
@@ -554,6 +557,10 @@ PAGE = """<!doctype html>
|
|
|
554
557
|
var renderCount = 0;
|
|
555
558
|
var mermaidFailed = !!window.__mermaidFailed;
|
|
556
559
|
var pendingCompletion = null;
|
|
560
|
+
var stepTimes = {};
|
|
561
|
+
var liveElapsed = {};
|
|
562
|
+
var runStartWall = null;
|
|
563
|
+
var totalTimer = null;
|
|
557
564
|
var topologyReady = false;
|
|
558
565
|
var pendingEvents = [];
|
|
559
566
|
var MAX_CONSOLE = 500;
|
|
@@ -602,10 +609,16 @@ PAGE = """<!doctype html>
|
|
|
602
609
|
return m + ":" + (s < 10 ? "0" : "") + s;
|
|
603
610
|
}
|
|
604
611
|
|
|
605
|
-
function
|
|
606
|
-
var line = el("
|
|
607
|
-
|
|
608
|
-
|
|
612
|
+
function renderTotal() {
|
|
613
|
+
var line = el("total");
|
|
614
|
+
if (runStartWall === null) {
|
|
615
|
+
line.textContent = "";
|
|
616
|
+
line.style.display = "none";
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
line.textContent = "total " + mmss(Math.max(0,
|
|
620
|
+
Math.round(Date.now() / 1000 - runStartWall)));
|
|
621
|
+
line.style.display = "block";
|
|
609
622
|
}
|
|
610
623
|
|
|
611
624
|
function statusOf(n) {
|
|
@@ -723,6 +736,9 @@ PAGE = """<!doctype html>
|
|
|
723
736
|
header.appendChild(glyph);
|
|
724
737
|
header.appendChild(name);
|
|
725
738
|
header.appendChild(status);
|
|
739
|
+
var time = document.createElement("span");
|
|
740
|
+
time.className = "stage-time";
|
|
741
|
+
header.appendChild(time);
|
|
726
742
|
|
|
727
743
|
var console = document.createElement("pre");
|
|
728
744
|
console.className = "stage-console";
|
|
@@ -768,6 +784,10 @@ PAGE = """<!doctype html>
|
|
|
768
784
|
else if (v === "env") { suffix = " (verdict: ENV)"; }
|
|
769
785
|
}
|
|
770
786
|
status.textContent = (word ? " " + word : "") + suffix;
|
|
787
|
+
header.querySelector(".stage-time").textContent =
|
|
788
|
+
(word === "completed" && stepTimes[node]) ? mmss(stepTimes[node])
|
|
789
|
+
: (word === "running" && liveElapsed[node] !== undefined)
|
|
790
|
+
? mmss(liveElapsed[node]) : "";
|
|
771
791
|
header.className = "stage-header status-" + renderStatusOf(node);
|
|
772
792
|
}
|
|
773
793
|
|
|
@@ -1033,6 +1053,13 @@ PAGE = """<!doctype html>
|
|
|
1033
1053
|
switch (ev.type) {
|
|
1034
1054
|
case "run_started":
|
|
1035
1055
|
flushPending();
|
|
1056
|
+
stepTimes = {};
|
|
1057
|
+
liveElapsed = {};
|
|
1058
|
+
if (totalTimer !== null) { clearInterval(totalTimer); totalTimer = null; }
|
|
1059
|
+
runStartWall = (typeof ev.started_at === "number")
|
|
1060
|
+
? ev.started_at : null;
|
|
1061
|
+
if (runStartWall !== null) { totalTimer = setInterval(renderTotal, 1000); }
|
|
1062
|
+
renderTotal();
|
|
1036
1063
|
clearConsoles();
|
|
1037
1064
|
resetVerdicts();
|
|
1038
1065
|
if (ev.attempt !== undefined) {
|
|
@@ -1051,14 +1078,18 @@ PAGE = """<!doctype html>
|
|
|
1051
1078
|
if (!nodeIds.has(ev.node)) { break; }
|
|
1052
1079
|
nodeStatus[ev.node] = "running";
|
|
1053
1080
|
stageWord[ev.node] = "running";
|
|
1081
|
+
liveElapsed = {};
|
|
1082
|
+
liveElapsed[ev.node] = 0;
|
|
1054
1083
|
Array.from(nodeIds).forEach(function (n) {
|
|
1055
1084
|
if (n === ev.node) { expandStage(n); } else { collapseStage(n); }
|
|
1056
1085
|
});
|
|
1057
1086
|
renderGraph();
|
|
1058
1087
|
break;
|
|
1059
1088
|
case "node_finished":
|
|
1060
|
-
clearElapsed();
|
|
1061
1089
|
if (!nodeIds.has(ev.node)) { break; }
|
|
1090
|
+
if (ev.elapsed_seconds !== undefined) {
|
|
1091
|
+
stepTimes[ev.node] = (stepTimes[ev.node] || 0) + ev.elapsed_seconds;
|
|
1092
|
+
}
|
|
1062
1093
|
if (!fatalNodes.has(ev.node)) {
|
|
1063
1094
|
nodeStatus[ev.node] = ev.ok ? "pass" : "fail";
|
|
1064
1095
|
}
|
|
@@ -1071,9 +1102,9 @@ PAGE = """<!doctype html>
|
|
|
1071
1102
|
renderGraph();
|
|
1072
1103
|
break;
|
|
1073
1104
|
case "heartbeat":
|
|
1074
|
-
if (nodeStatus[ev.node] === "running") {
|
|
1075
|
-
|
|
1076
|
-
|
|
1105
|
+
if (nodeIds.has(ev.node) && nodeStatus[ev.node] === "running") {
|
|
1106
|
+
liveElapsed[ev.node] = ev.elapsed_seconds;
|
|
1107
|
+
renderStageHeader(ev.node);
|
|
1077
1108
|
}
|
|
1078
1109
|
break;
|
|
1079
1110
|
case "state":
|
|
@@ -1106,7 +1137,12 @@ PAGE = """<!doctype html>
|
|
|
1106
1137
|
updateAttempt();
|
|
1107
1138
|
break;
|
|
1108
1139
|
case "run_finished":
|
|
1109
|
-
|
|
1140
|
+
if (totalTimer !== null) { clearInterval(totalTimer); totalTimer = null; }
|
|
1141
|
+
if (ev.elapsed_seconds !== undefined) {
|
|
1142
|
+
runStartWall = null;
|
|
1143
|
+
el("total").textContent = "total " + mmss(ev.elapsed_seconds);
|
|
1144
|
+
el("total").style.display = "block";
|
|
1145
|
+
}
|
|
1110
1146
|
flushPending();
|
|
1111
1147
|
if (!ev.success) {
|
|
1112
1148
|
Object.keys(nodeStatus).forEach(function (n) {
|
{agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51/src/agentic_dev_loop.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentic-dev-loop
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.0a51
|
|
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
|
|
@@ -169,7 +169,10 @@ engine-owned object to its owning run and a `live`, `orphaned`,
|
|
|
169
169
|
registry-owned orphans whose owner pid is dead (never unregistered names).
|
|
170
170
|
A SIGTERM to a running pipeline still closes the sandbox (the graph's
|
|
171
171
|
`finally` runs); SIGKILL is unhandleable, so its containers are reclaimed by
|
|
172
|
-
the startup reaper on the next run or by `containers --reap`.
|
|
172
|
+
the startup reaper on the next run or by `containers --reap`. The reaper
|
|
173
|
+
and `--reap` prune a registry entry only after the daemon confirms the
|
|
174
|
+
object is gone; a surviving object (a failed removal) keeps its entry so
|
|
175
|
+
the next startup retries it. Pass
|
|
173
176
|
`--state-dir` to point at a custom `isolation.state_dir`.
|
|
174
177
|
|
|
175
178
|
**What a run looks like** (the shipped self-loop example):
|
|
@@ -123,7 +123,11 @@ class TestReapReturnValue:
|
|
|
123
123
|
_write(tmp_path / "state", [_entry(name, proc.pid, None)])
|
|
124
124
|
runner = FakeRunner(ok(""))
|
|
125
125
|
assert reap_orphaned_containers(tmp_path / "state", runner) == [name]
|
|
126
|
-
assert runner.calls == [
|
|
126
|
+
assert runner.calls == [
|
|
127
|
+
["docker", "rm", "-f", name],
|
|
128
|
+
["docker", "ps", "-a", "--filter", f"name={name}",
|
|
129
|
+
"--format", "{{.Names}}"],
|
|
130
|
+
]
|
|
127
131
|
|
|
128
132
|
def test_reap_live_owner_returns_empty(self, tmp_path):
|
|
129
133
|
name = f"{'b' * 32}-exec"
|
|
@@ -7,6 +7,7 @@ everything else - files, git, checkpoints - is real."""
|
|
|
7
7
|
|
|
8
8
|
import argparse
|
|
9
9
|
import queue
|
|
10
|
+
import time
|
|
10
11
|
import sqlite3
|
|
11
12
|
import subprocess
|
|
12
13
|
from pathlib import Path
|
|
@@ -453,6 +454,25 @@ class TestVizStream:
|
|
|
453
454
|
assert events[-1]["success"] is True
|
|
454
455
|
assert events[-1]["attempts"] == 1
|
|
455
456
|
|
|
457
|
+
def test_run_events_carry_timings(self, repo, fake_agents):
|
|
458
|
+
script_all_pass(fake_agents)
|
|
459
|
+
before = time.time()
|
|
460
|
+
bus = VizBus()
|
|
461
|
+
ok, attempts = run_pipeline("pipeline.yaml", args(), viz_bus=bus)
|
|
462
|
+
after = time.time()
|
|
463
|
+
assert (ok, attempts) == (True, 1)
|
|
464
|
+
events = drain_events(bus)
|
|
465
|
+
run_started = next(e for e in events if e["type"] == "run_started")
|
|
466
|
+
assert isinstance(run_started["started_at"], (int, float))
|
|
467
|
+
assert before <= run_started["started_at"] <= after
|
|
468
|
+
run_finished = next(e for e in events if e["type"] == "run_finished")
|
|
469
|
+
assert isinstance(run_finished["elapsed_seconds"], int)
|
|
470
|
+
assert run_finished["elapsed_seconds"] >= 0
|
|
471
|
+
for event in events:
|
|
472
|
+
if event["type"] == "node_finished":
|
|
473
|
+
assert isinstance(event["elapsed_seconds"], int)
|
|
474
|
+
assert event["elapsed_seconds"] >= 0
|
|
475
|
+
|
|
456
476
|
def test_stream_path_failure_returns_same(self, repo, fake_agents):
|
|
457
477
|
fake_agents.set("check", [("write", "ci-fix.md", "VERDICT: FAIL")])
|
|
458
478
|
bus = VizBus()
|
|
@@ -2251,7 +2251,11 @@ class TestReaper:
|
|
|
2251
2251
|
runner = FakeRunner()
|
|
2252
2252
|
reaped = reap_orphaned_containers(tmp_path / "state", runner)
|
|
2253
2253
|
assert reaped == [name]
|
|
2254
|
-
assert runner.calls == [
|
|
2254
|
+
assert runner.calls == [
|
|
2255
|
+
["docker", "rm", "-f", name],
|
|
2256
|
+
["docker", "ps", "-a", "--filter", f"name={name}",
|
|
2257
|
+
"--format", "{{.Names}}"],
|
|
2258
|
+
]
|
|
2255
2259
|
assert self._read(tmp_path / "state") == []
|
|
2256
2260
|
|
|
2257
2261
|
def test_live_pid_left_alone(self, tmp_path):
|
|
@@ -2282,9 +2286,13 @@ class TestReaper:
|
|
|
2282
2286
|
"run_id": "r", "name": name, "kind": "network",
|
|
2283
2287
|
"pid": proc.pid, "process_start": None, "created_at": "now",
|
|
2284
2288
|
}])
|
|
2285
|
-
runner = FakeRunner()
|
|
2286
|
-
reap_orphaned_containers(tmp_path / "state", runner)
|
|
2287
|
-
assert
|
|
2289
|
+
runner = FakeRunner(ok(""), fail(1, "No such network"))
|
|
2290
|
+
reaped = reap_orphaned_containers(tmp_path / "state", runner)
|
|
2291
|
+
assert reaped == [name]
|
|
2292
|
+
assert runner.calls == [
|
|
2293
|
+
["docker", "network", "rm", name],
|
|
2294
|
+
["docker", "network", "inspect", name],
|
|
2295
|
+
]
|
|
2288
2296
|
assert self._read(tmp_path / "state") == []
|
|
2289
2297
|
|
|
2290
2298
|
def test_malformed_name_skipped(self, tmp_path):
|
|
@@ -2300,11 +2308,65 @@ class TestReaper:
|
|
|
2300
2308
|
proc.wait()
|
|
2301
2309
|
name = f"{'e' * 32}-exec"
|
|
2302
2310
|
self._write(tmp_path / "state", [self._entry(name, proc.pid)])
|
|
2303
|
-
runner = FakeRunner(fail(1, "No such container"))
|
|
2304
|
-
reap_orphaned_containers(tmp_path / "state", runner)
|
|
2305
|
-
assert
|
|
2311
|
+
runner = FakeRunner(fail(1, "No such container"), ok(""))
|
|
2312
|
+
reaped = reap_orphaned_containers(tmp_path / "state", runner)
|
|
2313
|
+
assert reaped == [name]
|
|
2314
|
+
assert runner.calls == [
|
|
2315
|
+
["docker", "rm", "-f", name],
|
|
2316
|
+
["docker", "ps", "-a", "--filter", f"name={name}",
|
|
2317
|
+
"--format", "{{.Names}}"],
|
|
2318
|
+
]
|
|
2306
2319
|
assert self._read(tmp_path / "state") == []
|
|
2307
2320
|
|
|
2321
|
+
def test_rm_failure_container_survives_retained(self, tmp_path):
|
|
2322
|
+
proc = subprocess.Popen([sys.executable, "-c", "pass"])
|
|
2323
|
+
proc.wait()
|
|
2324
|
+
name = f"{'a' * 32}-exec"
|
|
2325
|
+
entry = self._entry(name, proc.pid)
|
|
2326
|
+
self._write(tmp_path / "state", [entry])
|
|
2327
|
+
runner = FakeRunner(fail(1, "daemon busy"), ok(name + "\n"))
|
|
2328
|
+
reaped = reap_orphaned_containers(tmp_path / "state", runner)
|
|
2329
|
+
assert reaped == []
|
|
2330
|
+
assert self._read(tmp_path / "state") == [entry]
|
|
2331
|
+
assert runner.calls == [
|
|
2332
|
+
["docker", "rm", "-f", name],
|
|
2333
|
+
["docker", "ps", "-a", "--filter", f"name={name}",
|
|
2334
|
+
"--format", "{{.Names}}"],
|
|
2335
|
+
]
|
|
2336
|
+
|
|
2337
|
+
def test_rm_failure_network_survives_retained(self, tmp_path):
|
|
2338
|
+
proc = subprocess.Popen([sys.executable, "-c", "pass"])
|
|
2339
|
+
proc.wait()
|
|
2340
|
+
name = f"{'a' * 32}-net"
|
|
2341
|
+
entry = {"run_id": "r", "name": name, "kind": "network",
|
|
2342
|
+
"pid": proc.pid, "process_start": None, "created_at": "now"}
|
|
2343
|
+
self._write(tmp_path / "state", [entry])
|
|
2344
|
+
runner = FakeRunner(fail(1, "daemon busy"), ok(name))
|
|
2345
|
+
reaped = reap_orphaned_containers(tmp_path / "state", runner)
|
|
2346
|
+
assert reaped == []
|
|
2347
|
+
assert self._read(tmp_path / "state") == [entry]
|
|
2348
|
+
assert runner.calls == [
|
|
2349
|
+
["docker", "network", "rm", name],
|
|
2350
|
+
["docker", "network", "inspect", name],
|
|
2351
|
+
]
|
|
2352
|
+
|
|
2353
|
+
def test_missing_network_tolerated(self, tmp_path):
|
|
2354
|
+
proc = subprocess.Popen([sys.executable, "-c", "pass"])
|
|
2355
|
+
proc.wait()
|
|
2356
|
+
name = f"{'a' * 32}-net"
|
|
2357
|
+
entry = {"run_id": "r", "name": name, "kind": "network",
|
|
2358
|
+
"pid": proc.pid, "process_start": None, "created_at": "now"}
|
|
2359
|
+
self._write(tmp_path / "state", [entry])
|
|
2360
|
+
runner = FakeRunner(fail(1, "No such network"),
|
|
2361
|
+
fail(1, "No such network"))
|
|
2362
|
+
reaped = reap_orphaned_containers(tmp_path / "state", runner)
|
|
2363
|
+
assert reaped == [name]
|
|
2364
|
+
assert self._read(tmp_path / "state") == []
|
|
2365
|
+
assert runner.calls == [
|
|
2366
|
+
["docker", "network", "rm", name],
|
|
2367
|
+
["docker", "network", "inspect", name],
|
|
2368
|
+
]
|
|
2369
|
+
|
|
2308
2370
|
def test_no_registry_is_a_noop(self, tmp_path):
|
|
2309
2371
|
runner = FakeRunner()
|
|
2310
2372
|
reap_orphaned_containers(tmp_path / "state", runner)
|
|
@@ -764,9 +764,22 @@ class TestServer:
|
|
|
764
764
|
assert "elapsed_seconds" in PAGE
|
|
765
765
|
assert "elapsed" in PAGE
|
|
766
766
|
|
|
767
|
-
def
|
|
768
|
-
assert "
|
|
769
|
-
assert
|
|
767
|
+
def test_page_constant_has_total(self):
|
|
768
|
+
assert 'id="total"' in PAGE
|
|
769
|
+
assert "total " in PAGE
|
|
770
|
+
|
|
771
|
+
def test_page_constant_has_step_time(self):
|
|
772
|
+
assert "stepTimes" in PAGE
|
|
773
|
+
assert ".stage-time" in PAGE
|
|
774
|
+
assert "stage-time" in PAGE
|
|
775
|
+
assert "liveElapsed" in PAGE
|
|
776
|
+
|
|
777
|
+
def test_page_constant_has_loop_total(self):
|
|
778
|
+
assert "renderTotal" in PAGE
|
|
779
|
+
assert "started_at" in PAGE
|
|
780
|
+
assert "setInterval" in PAGE
|
|
781
|
+
assert "clearInterval" in PAGE
|
|
782
|
+
assert "total " in PAGE
|
|
770
783
|
|
|
771
784
|
def test_page_constant_buffers_sse_until_topology_ready(self):
|
|
772
785
|
assert "topologyReady" in PAGE
|
|
@@ -933,7 +946,7 @@ El.prototype.querySelectorAll = function (sel) {
|
|
|
933
946
|
};
|
|
934
947
|
El.prototype.scrollIntoView = function () {};
|
|
935
948
|
|
|
936
|
-
var STATIC_IDS = ["subject", "attempt", "thread-id", "
|
|
949
|
+
var STATIC_IDS = ["subject", "attempt", "thread-id", "total", "verdicts",
|
|
937
950
|
"stages", "run-log", "mermaid-container", "run-banner",
|
|
938
951
|
"divider", "sidebar"];
|
|
939
952
|
STATIC_IDS.forEach(function (id) { registry[id] = new El(id); });
|
|
@@ -961,6 +974,7 @@ var esInstance = null;
|
|
|
961
974
|
globalThis.document = document;
|
|
962
975
|
globalThis.window = window;
|
|
963
976
|
var copiedText = null;
|
|
977
|
+
var intervalFn = null;
|
|
964
978
|
// node 21+ ships a read-only `navigator` global, so defineProperty is needed
|
|
965
979
|
Object.defineProperty(globalThis, "navigator", {
|
|
966
980
|
configurable: true,
|
|
@@ -990,6 +1004,10 @@ globalThis.fetch = function () {
|
|
|
990
1004
|
};
|
|
991
1005
|
globalThis.requestAnimationFrame = function (fn) { fn(); };
|
|
992
1006
|
globalThis.setTimeout = function () { return 0; };
|
|
1007
|
+
globalThis.setInterval = function (fn) { intervalFn = fn; return 1; };
|
|
1008
|
+
globalThis.clearInterval = function () {};
|
|
1009
|
+
var fakeNow = 0;
|
|
1010
|
+
Date.now = function () { return fakeNow; };
|
|
993
1011
|
globalThis.EventSource = function (url) {
|
|
994
1012
|
this.url = url;
|
|
995
1013
|
this.onmessage = null;
|
|
@@ -1012,15 +1030,37 @@ function nodeClasses() {
|
|
|
1012
1030
|
}
|
|
1013
1031
|
function fire(ev) { esInstance.onmessage({ data: JSON.stringify(ev) }); }
|
|
1014
1032
|
|
|
1033
|
+
function stageTimes() {
|
|
1034
|
+
var out = {};
|
|
1035
|
+
["dev", "test", "review"].forEach(function (n) {
|
|
1036
|
+
var section = document.getElementById("stage-" + n);
|
|
1037
|
+
var header = section.querySelector(".stage-header");
|
|
1038
|
+
var t = header.querySelector(".stage-time");
|
|
1039
|
+
out[n] = t ? t.textContent : null;
|
|
1040
|
+
});
|
|
1041
|
+
return out;
|
|
1042
|
+
}
|
|
1043
|
+
function totalText() {
|
|
1044
|
+
var total = document.getElementById("total");
|
|
1045
|
+
return { text: total.textContent, display: total.style.display };
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1015
1048
|
var snapshots = [];
|
|
1016
1049
|
async function main() {
|
|
1017
1050
|
// let the topology fetch microtask chain resolve so events are handled
|
|
1018
1051
|
// live (synchronously inside es.onmessage) instead of buffered
|
|
1019
1052
|
await new Promise(function (res) { setImmediate(res); });
|
|
1020
1053
|
EVENTS.forEach(function (ev) {
|
|
1054
|
+
if (ev.type === "__tick") {
|
|
1055
|
+
fakeNow = ev.now;
|
|
1056
|
+
if (intervalFn) { intervalFn(); }
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1021
1059
|
fire(ev);
|
|
1022
1060
|
if (ev.type === "state") {
|
|
1023
|
-
snapshots.push({ ev: ev, badges: badges(), classes: nodeClasses()
|
|
1061
|
+
snapshots.push({ ev: ev, badges: badges(), classes: nodeClasses(),
|
|
1062
|
+
times: stageTimes(), total: totalText(),
|
|
1063
|
+
timer: intervalFn !== null });
|
|
1024
1064
|
}
|
|
1025
1065
|
});
|
|
1026
1066
|
// exercise the copy button on the dev console
|
|
@@ -1029,7 +1069,9 @@ async function main() {
|
|
|
1029
1069
|
copyBtn.listeners["click"]();
|
|
1030
1070
|
await new Promise(function (res) { setImmediate(res); });
|
|
1031
1071
|
snapshots.push({ copy: { text: copiedText,
|
|
1032
|
-
title: copyBtn.getAttribute("title") }
|
|
1072
|
+
title: copyBtn.getAttribute("title") },
|
|
1073
|
+
times: stageTimes(), total: totalText(),
|
|
1074
|
+
timer: intervalFn !== null });
|
|
1033
1075
|
console.log(JSON.stringify(snapshots));
|
|
1034
1076
|
}
|
|
1035
1077
|
main();
|
|
@@ -1167,6 +1209,71 @@ main();
|
|
|
1167
1209
|
assert snaps[-1]["copy"]["title"] == "Nothing to copy"
|
|
1168
1210
|
|
|
1169
1211
|
|
|
1212
|
+
def test_step_header_shows_accumulated_time(self, node, tmp_path):
|
|
1213
|
+
events = [
|
|
1214
|
+
{"type": "run_started", "subject": "s", "max_attempts": 5,
|
|
1215
|
+
"attempt": 1, "thread_id": "t", "started_at": 1000},
|
|
1216
|
+
{"type": "node_started", "node": "dev"},
|
|
1217
|
+
{"type": "node_finished", "node": "dev", "ok": True,
|
|
1218
|
+
"elapsed_seconds": 135},
|
|
1219
|
+
{"type": "state", "attempt": 1, "failed": False},
|
|
1220
|
+
{"type": "node_finished", "node": "dev", "ok": True,
|
|
1221
|
+
"elapsed_seconds": 45},
|
|
1222
|
+
{"type": "state", "attempt": 1, "failed": False},
|
|
1223
|
+
]
|
|
1224
|
+
snaps = self._run_page(node, events, tmp_path)
|
|
1225
|
+
|
|
1226
|
+
assert snaps[-1]["times"]["dev"] == "3:00"
|
|
1227
|
+
assert snaps[-1]["times"]["test"] == ""
|
|
1228
|
+
assert snaps[-1]["times"]["review"] == ""
|
|
1229
|
+
|
|
1230
|
+
def test_running_stage_shows_live_heartbeat_time(self, node, tmp_path):
|
|
1231
|
+
events = [
|
|
1232
|
+
{"type": "run_started", "subject": "s", "max_attempts": 5,
|
|
1233
|
+
"attempt": 1, "thread_id": "t", "started_at": 1000},
|
|
1234
|
+
{"type": "node_started", "node": "dev"},
|
|
1235
|
+
{"type": "heartbeat", "node": "dev", "elapsed_seconds": 27},
|
|
1236
|
+
{"type": "state", "attempt": 1, "failed": False},
|
|
1237
|
+
{"type": "node_finished", "node": "dev", "ok": True,
|
|
1238
|
+
"elapsed_seconds": 28},
|
|
1239
|
+
{"type": "state", "attempt": 1, "failed": False},
|
|
1240
|
+
]
|
|
1241
|
+
snaps = self._run_page(node, events, tmp_path)
|
|
1242
|
+
|
|
1243
|
+
assert snaps[0]["times"]["dev"] == "0:27"
|
|
1244
|
+
assert snaps[1]["times"]["dev"] == "0:28"
|
|
1245
|
+
|
|
1246
|
+
def test_loop_total_ticks_then_freezes(self, node, tmp_path):
|
|
1247
|
+
events = [
|
|
1248
|
+
{"type": "run_started", "subject": "s", "max_attempts": 5,
|
|
1249
|
+
"attempt": 1, "thread_id": "t", "started_at": 1000},
|
|
1250
|
+
{"type": "__tick", "now": 1065000},
|
|
1251
|
+
{"type": "state", "attempt": 1, "failed": False},
|
|
1252
|
+
{"type": "run_finished", "success": True, "attempts": 1,
|
|
1253
|
+
"elapsed_seconds": 200},
|
|
1254
|
+
]
|
|
1255
|
+
snaps = self._run_page(node, events, tmp_path)
|
|
1256
|
+
|
|
1257
|
+
assert snaps[0]["total"]["text"] == "total 1:05"
|
|
1258
|
+
assert snaps[0]["total"]["display"] == "block"
|
|
1259
|
+
assert snaps[-1]["total"]["text"] == "total 3:20"
|
|
1260
|
+
|
|
1261
|
+
def test_loop_total_fallback_without_started_at(self, node, tmp_path):
|
|
1262
|
+
events = [
|
|
1263
|
+
{"type": "run_started", "subject": "s", "max_attempts": 5,
|
|
1264
|
+
"attempt": 1, "thread_id": "t"},
|
|
1265
|
+
{"type": "state", "attempt": 1, "failed": False},
|
|
1266
|
+
{"type": "run_finished", "success": True, "attempts": 1,
|
|
1267
|
+
"elapsed_seconds": 95},
|
|
1268
|
+
]
|
|
1269
|
+
snaps = self._run_page(node, events, tmp_path)
|
|
1270
|
+
|
|
1271
|
+
assert snaps[0]["timer"] is False
|
|
1272
|
+
assert snaps[0]["total"] == {"text": "", "display": "none"}
|
|
1273
|
+
assert snaps[-1]["total"]["text"] == "total 1:35"
|
|
1274
|
+
assert snaps[-1]["total"]["display"] == "block"
|
|
1275
|
+
|
|
1276
|
+
|
|
1170
1277
|
class TestDemo:
|
|
1171
1278
|
def test_run_started_carries_attempt(self, monkeypatch):
|
|
1172
1279
|
import viz_demo
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/core/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/core/results.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/core/verdict.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/engine/__init__.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/engine/actions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/engine/agents.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/engine/allowlist.py
RENAMED
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/engine/brief_prompt.md
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/engine/brief_prompt.py
RENAMED
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/engine/containers.py
RENAMED
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/engine/dryrun.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/engine/egress_policy.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/engine/egress_proxy.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/engine/forbidden.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/engine/hostboundary.py
RENAMED
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/engine/loaders.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/engine/mounts.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/engine/outputs.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/engine/presets.py
RENAMED
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/engine/registry.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/engine/secrets.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/engine/stages.py
RENAMED
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/engine/sync_back.py
RENAMED
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/viz/__init__.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop/viz/__main__.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop.egg-info/requires.txt
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/src/agentic_dev_loop.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a51}/tests/test_hostboundary_integration.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|