agentic-dev-loop 1.0.0a16__tar.gz → 1.0.0a17__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.

Files changed (74) hide show
  1. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/PKG-INFO +2 -2
  2. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/README.md +1 -1
  3. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/pyproject.toml +1 -1
  4. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/engine/actions.py +1 -1
  5. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/engine/graph.py +16 -7
  6. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/engine/hostboundary.py +1 -1
  7. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/engine/stages.py +53 -14
  8. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop.egg-info/PKG-INFO +2 -2
  9. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/tests/test_actions.py +1 -1
  10. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/tests/test_agents.py +1 -1
  11. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/tests/test_config.py +3 -3
  12. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/tests/test_graph.py +35 -10
  13. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/tests/test_qa_prompt.py +8 -0
  14. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/tests/test_secrets.py +4 -4
  15. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/tests/test_stages.py +64 -11
  16. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/LICENSE +0 -0
  17. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/setup.cfg +0 -0
  18. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/__init__.py +0 -0
  19. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/core/__init__.py +0 -0
  20. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/core/config.py +0 -0
  21. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/core/results.py +0 -0
  22. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/core/verdict.py +0 -0
  23. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/engine/__init__.py +0 -0
  24. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/engine/agents.py +0 -0
  25. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/engine/cli.py +0 -0
  26. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/engine/deps.py +0 -0
  27. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/engine/dryrun.py +0 -0
  28. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/engine/extensions/__init__.py +0 -0
  29. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/engine/extensions/aidlc.py +0 -0
  30. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/engine/isolation.py +0 -0
  31. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/engine/llm.py +0 -0
  32. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/engine/loaders.py +0 -0
  33. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/engine/outputs.py +0 -0
  34. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/engine/proc.py +0 -0
  35. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/engine/registry.py +0 -0
  36. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/engine/seccomp/__init__.py +0 -0
  37. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/engine/seccomp/chromium-userns.json +0 -0
  38. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/engine/secrets.py +0 -0
  39. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/py.typed +0 -0
  40. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/viz/__init__.py +0 -0
  41. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/viz/__main__.py +0 -0
  42. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/viz/server.py +0 -0
  43. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop.egg-info/SOURCES.txt +0 -0
  44. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop.egg-info/dependency_links.txt +0 -0
  45. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop.egg-info/entry_points.txt +0 -0
  46. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop.egg-info/requires.txt +0 -0
  47. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop.egg-info/top_level.txt +0 -0
  48. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/tests/test_aidlc.py +0 -0
  49. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/tests/test_cli.py +0 -0
  50. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/tests/test_deps.py +0 -0
  51. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/tests/test_dry_run.py +0 -0
  52. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/tests/test_e2e_fixtures.py +0 -0
  53. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/tests/test_hostboundary.py +0 -0
  54. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/tests/test_hostboundary_integration.py +0 -0
  55. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/tests/test_isolation.py +0 -0
  56. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/tests/test_isolation_integration.py +0 -0
  57. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/tests/test_isolation_pbt.py +0 -0
  58. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/tests/test_llm.py +0 -0
  59. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/tests/test_loaders.py +0 -0
  60. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/tests/test_module_contract.py +0 -0
  61. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/tests/test_outputs.py +0 -0
  62. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/tests/test_outputs_pbt.py +0 -0
  63. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/tests/test_proc.py +0 -0
  64. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/tests/test_publish.py +0 -0
  65. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/tests/test_registry.py +0 -0
  66. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/tests/test_repo_ascii.py +0 -0
  67. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/tests/test_results.py +0 -0
  68. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/tests/test_sdk_compat.py +0 -0
  69. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/tests/test_secrets_integration.py +0 -0
  70. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/tests/test_secrets_pipeline.py +0 -0
  71. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/tests/test_source_resolution.py +0 -0
  72. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/tests/test_trigger_script.py +0 -0
  73. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/tests/test_verdict.py +0 -0
  74. {agentic_dev_loop-1.0.0a16 → agentic_dev_loop-1.0.0a17}/tests/test_viz.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentic-dev-loop
3
- Version: 1.0.0a16
3
+ Version: 1.0.0a17
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
@@ -231,7 +231,7 @@ secrets: # refs only - never values
231
231
  key: KINDIE_CLIENT_SECRET # provider-specific locator
232
232
  r2-keys:
233
233
  from: env
234
- key: YOSK_R2_KEYS
234
+ key: R2_KEYS
235
235
 
236
236
  steps:
237
237
  - name: qa
@@ -189,7 +189,7 @@ secrets: # refs only - never values
189
189
  key: KINDIE_CLIENT_SECRET # provider-specific locator
190
190
  r2-keys:
191
191
  from: env
192
- key: YOSK_R2_KEYS
192
+ key: R2_KEYS
193
193
 
194
194
  steps:
195
195
  - name: qa
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "agentic-dev-loop"
7
- version = "1.0.0a16"
7
+ version = "1.0.0a17"
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"
@@ -1,6 +1,6 @@
1
1
  """Built-in pipeline actions. Node-shaped: (state, params) -> state update.
2
2
 
3
- `commit` policy (ported unchanged from yosk): agents never commit mid-run;
3
+ `commit` policy (ported unchanged from the consumer): agents never commit mid-run;
4
4
  the pipeline commits exactly once, and only when the worktree was clean at
5
5
  run start - a dirty-at-start run is committed by the human, so the
6
6
  auto-commit can never swallow pre-existing unrelated changes (run 004: the
@@ -27,19 +27,27 @@ from .loaders import resolve_docs
27
27
  from .secrets import resolve_secrets, scan_env_files
28
28
  from .registry import (Registry, default_registry, load_extensions,
29
29
  validate_action_network)
30
- from .stages import (Runtime, build_context, clear_attempt_artifacts,
30
+ from .stages import (Runtime, clear_attempt_artifacts,
31
31
  make_action_node, make_agent_node, make_bump_node,
32
32
  make_command_node, make_router, success_node)
33
33
 
34
34
  CHECKPOINT_DB = "loop-checkpoints.sqlite" # under state_dir, one DB per workspace
35
35
 
36
36
 
37
+ def _merge_artifacts(a: dict[str, str], b: dict[str, str]) -> dict[str, str]:
38
+ """Accumulate per-step artifact text across nodes. LangGraph gives
39
+ un-annotated fields last-write-wins, so without this reducer one node's
40
+ entry would silently replace the accumulated dict."""
41
+ return {**a, **b}
42
+
43
+
37
44
  class LoopState(TypedDict):
38
45
  """State threaded through the graph. `notes` accumulates across attempts
39
- (append reducer); every other key is last-write-wins."""
46
+ (append reducer), `artifacts` accumulates across stages (merge reducer);
47
+ every other key is last-write-wins."""
40
48
  subject: str
41
49
  docs: dict[str, str]
42
- context: str # built at attempt start, shared by all stages
50
+ artifacts: Annotated[dict[str, str], _merge_artifacts] # step name -> artifact text
43
51
  attempt: int
44
52
  max_attempts: int
45
53
  notes: Annotated[list[str], operator.add]
@@ -95,7 +103,7 @@ def run_pipeline(cfg_path, args, *, viz_bus=None,
95
103
  registry: Registry | None = None) -> tuple[bool, int]:
96
104
  """Load a pipeline YAML, resolve docs via the selected document loader,
97
105
  and run the graph to completion. Returns (success, attempts) - the
98
- public API, yosk's `construct`/`resume_loop` equivalent.
106
+ public API, the consumer's `construct`/`resume_loop` equivalent.
99
107
 
100
108
  `args` carries the parsed CLI namespace: loader flags, --resume,
101
109
  --max-attempts (which overrides the YAML value when set), and --no-viz.
@@ -163,7 +171,7 @@ def _run_pipeline(cfg, base, registry, state_dir, max_attempts, args, viz_bus,
163
171
  config = {"configurable": {"thread_id": thread_id}}
164
172
  db = str(state_dir / CHECKPOINT_DB)
165
173
  if resume:
166
- initial = None # state (docs/subject/context) comes from the checkpoint
174
+ initial = None # state (docs/subject/artifacts) comes from the checkpoint
167
175
  else:
168
176
  _loader, docs, subject = resolve_docs(cfg, base, registry, args)
169
177
  # A fresh run owns state_dir: drop cross-run leftovers (a previous
@@ -173,7 +181,7 @@ def _run_pipeline(cfg, base, registry, state_dir, max_attempts, args, viz_bus,
173
181
  initial: LoopState = {
174
182
  "subject": subject,
175
183
  "docs": docs,
176
- "context": build_context(runtime, docs, subject, []),
184
+ "artifacts": {},
177
185
  "attempt": 1,
178
186
  "max_attempts": max_attempts,
179
187
  "notes": [],
@@ -221,7 +229,8 @@ def _run_pipeline(cfg, base, registry, state_dir, max_attempts, args, viz_bus,
221
229
  f"Failure details: {state_dir}/last-error.md",
222
230
  flush=True)
223
231
  initial = {**values, "failed": False,
224
- "resume_target": target, "failed_step": None}
232
+ "resume_target": target, "failed_step": None,
233
+ "artifacts": values.get("artifacts", {})}
225
234
 
226
235
  if bus is None and not getattr(args, "no_viz", True):
227
236
  start_state = initial if initial is not None \
@@ -29,7 +29,7 @@ if TYPE_CHECKING:
29
29
  from agentic_dev_loop.core.config import PipelineConfig
30
30
 
31
31
  # Tooling directories are workspace-root-relative fixed names (the same layout
32
- # the dogfood pipeline and yosk use); the other tooling paths are config- or
32
+ # the dogfood pipeline and the consumer repo use); the other tooling paths are config- or
33
33
  # pipeline-file-relative.
34
34
  TOOLING_DIR_NAMES = ("aidlc-docs", "tools")
35
35
 
@@ -1,6 +1,6 @@
1
1
  """Stage nodes, routers, and the run_agent port.
2
2
 
3
- The yosk loop's hardcoded stages become per-step closures built from the
3
+ The consumer loop's hardcoded stages become per-step closures built from the
4
4
  pipeline config; the fixed policies stay here, not in config:
5
5
 
6
6
  - per-attempt hygiene: every declared artifact/produces file + last-error.md
@@ -49,6 +49,7 @@ if TYPE_CHECKING:
49
49
  DEFAULT_MAX_ITERATIONS = 500
50
50
  DEFAULT_COMMAND_TIMEOUT = 600.0 # seconds - a pytest-class suite budget
51
51
  LAST_ERROR = "last-error.md"
52
+ ARTIFACT_CONTEXT_CAP = 24_000 # chars per artifact injected into a step's context
52
53
 
53
54
 
54
55
  @dataclass
@@ -68,7 +69,7 @@ class Runtime:
68
69
  def prompt_dir(runtime: Runtime) -> str:
69
70
  """state_dir as agents must address it. Agents resolve prompt paths
70
71
  against their workspace, so a bare `state_dir.name` only reaches the real
71
- state_dir when the pipeline file sits at the workspace root - yosk runs
72
+ state_dir when the pipeline file sits at the workspace root - the consumer runs
72
73
  agentic-dev-loop/pipeline.yaml from the repo root, where `.pr` and
73
74
  `agentic-dev-loop/.pr` diverge. Workspace-relative when under the workspace,
74
75
  the absolute path when outside it."""
@@ -196,15 +197,41 @@ def _run_command(command: str, workspace: Path, timeout: float,
196
197
  return None, out.decode("utf-8", "replace"), True
197
198
 
198
199
 
200
+ def _artifact_file(runtime: Runtime, step_name: str) -> str | None:
201
+ """The declared artifact/produces filename a step writes, keyed by step
202
+ name - the artifacts dict only carries step name -> captured text."""
203
+ for step in runtime.cfg.steps:
204
+ if step.name == step_name:
205
+ if step.artifact is not None:
206
+ return step.artifact
207
+ if step.produces:
208
+ return step.produces[0]
209
+ return None
210
+ return None
211
+
212
+
199
213
  def build_context(runtime: Runtime, docs: dict[str, str], subject: str,
200
- notes: list[str] | None = None) -> str:
214
+ notes: list[str] | None = None,
215
+ artifacts: dict[str, str] | None = None) -> str:
201
216
  """Assemble the attempt's shared message: subject, doc bundle, the
202
217
  canonical dependency-policy paragraph (021), the expected-output-paths
203
218
  section generated from step defs (a prompt merely naming files lets agents
204
- skip them), and previous-attempt notes."""
219
+ skip them), accumulated stage artifacts (027), and previous-attempt
220
+ notes."""
205
221
  parts = [f"You are working on {subject}.\n"]
206
222
  for name, content in docs.items():
207
223
  parts.append(f"## {name}\n\n{content}")
224
+ if artifacts:
225
+ parts.append("## Stage artifacts")
226
+ for step_name, content in artifacts.items():
227
+ capped = content[:ARTIFACT_CONTEXT_CAP]
228
+ if len(content) > ARTIFACT_CONTEXT_CAP:
229
+ filename = _artifact_file(runtime, step_name)
230
+ path = (f"{prompt_dir(runtime)}/{filename}" if filename
231
+ else prompt_dir(runtime))
232
+ capped += (f"\n\n[... content truncated - full text at "
233
+ f"`{path}`]")
234
+ parts.append(f"### {step_name}\n\n{capped}")
208
235
  parts.append("## Dependency policy\n\n" + policy_injection(runtime.cfg))
209
236
  lines = []
210
237
  for step in runtime.cfg.steps:
@@ -240,7 +267,7 @@ def clear_attempt_artifacts(runtime: Runtime) -> None:
240
267
 
241
268
  def _unclear_target(runtime: Runtime, step: StepConfig) -> str:
242
269
  """Where the unclear-verdict retry loops to: on_fail's goto when that is
243
- a step, else the entry step (yosk always looped back to dev)."""
270
+ a step, else the entry step (the consumer always looped back to dev)."""
244
271
  target = route_target(step.on_fail)
245
272
  if target is not None and target not in TERMINALS:
246
273
  return target
@@ -295,8 +322,11 @@ def make_agent_node(runtime: Runtime, step: StepConfig):
295
322
  attempt = state["attempt"]
296
323
  subject = state["subject"]
297
324
  max_iterations = step.max_iterations or DEFAULT_MAX_ITERATIONS
325
+ message = build_context(runtime, state["docs"], state["subject"],
326
+ state["notes"],
327
+ artifacts=state.get("artifacts", {}))
298
328
  print(f"[{subject}] Running {step.agent} agent...", flush=True)
299
- result = run_agent(step.agent, state["context"], runtime,
329
+ result = run_agent(step.agent, message, runtime,
300
330
  max_iterations=max_iterations, step=step)
301
331
  if not result.ok:
302
332
  print(f"[{subject}] {step.name} agent failed on attempt {attempt}.", flush=True)
@@ -330,7 +360,10 @@ def make_agent_node(runtime: Runtime, step: StepConfig):
330
360
  if verdicts_map is None:
331
361
  merged = {**state["step_verdicts"], step.name: "pass"} if captures else \
332
362
  state["step_verdicts"]
333
- return {"step_verdicts": merged, "failed": False}
363
+ update = {"step_verdicts": merged, "failed": False}
364
+ if captures and result.response is not None:
365
+ update["artifacts"] = {step.name: result.response}
366
+ return update
334
367
 
335
368
  parsed = Verdict.from_text(result.response or "", verdicts_map)
336
369
  merged = {**state["step_verdicts"], step.name: None}
@@ -340,16 +373,24 @@ def make_agent_node(runtime: Runtime, step: StepConfig):
340
373
  if verdict == "pass":
341
374
  print(f"[{subject}] {step.name} VERDICT: PASS on attempt {attempt}.",
342
375
  flush=True)
343
- return {"step_verdicts": merged, "failed": False}
376
+ update = {"step_verdicts": merged, "failed": False}
377
+ if captures and result.response is not None:
378
+ update["artifacts"] = {step.name: result.response}
379
+ return update
344
380
  if verdict == "unverifiable":
345
381
  print(f"[{subject}] {step.name} qa verdict: UNVERIFIABLE "
346
382
  f"(moves on).", flush=True)
347
- return {"step_verdicts": merged, "failed": False}
383
+ update = {"step_verdicts": merged, "failed": False}
384
+ if captures and result.response is not None:
385
+ update["artifacts"] = {step.name: result.response}
386
+ return update
348
387
  print(f"[{subject}] {step.name} verdict: fail on attempt {attempt}.",
349
388
  flush=True)
350
389
  update = {"step_verdicts": merged,
351
390
  "notes": [f"## {step.name} output (attempt {attempt})\n\n"
352
391
  f"{result.response}"]}
392
+ if captures and result.response is not None:
393
+ update["artifacts"] = {step.name: result.response}
353
394
  if isinstance(step.on_fail, Route) and step.on_fail.retry:
354
395
  update["retry_target"] = step.on_fail.goto
355
396
  return update
@@ -548,16 +589,14 @@ def make_router(runtime: Runtime, step: StepConfig):
548
589
 
549
590
  def make_bump_node(runtime: Runtime):
550
591
  """Close the retry cycle: consume one attempt, clear per-attempt
551
- artifacts, and rebuild the context with the accumulated notes - the
552
- attempt-start duties yosk's dev node owned, generalized to any target."""
592
+ artifacts, and reset verdicts/failed for the next attempt. The retried
593
+ agent's message is rebuilt per run by make_agent_node from the
594
+ accumulated notes and artifacts (027)."""
553
595
 
554
596
  def node(state: dict) -> dict:
555
597
  clear_attempt_artifacts(runtime)
556
- context = build_context(runtime, state["docs"], state["subject"],
557
- state["notes"])
558
598
  return {"attempt": state["attempt"] + 1,
559
599
  "step_verdicts": {},
560
- "context": context,
561
600
  "failed": False}
562
601
 
563
602
  return node
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentic-dev-loop
3
- Version: 1.0.0a16
3
+ Version: 1.0.0a17
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
@@ -231,7 +231,7 @@ secrets: # refs only - never values
231
231
  key: KINDIE_CLIENT_SECRET # provider-specific locator
232
232
  r2-keys:
233
233
  from: env
234
- key: YOSK_R2_KEYS
234
+ key: R2_KEYS
235
235
 
236
236
  steps:
237
237
  - name: qa
@@ -1,4 +1,4 @@
1
- """Action tests - ported from yosk's TestKillListenersOnPort /
1
+ """Action tests - ported from the consumer's TestKillListenersOnPort /
2
2
  TestGitWorktreeClean / TestCommitNode, retargeted at the node-shaped
3
3
  `(state, params) -> dict` API. Real processes and real git repos throughout:
4
4
  git and sockets are local utilities, not network boundaries."""
@@ -1,4 +1,4 @@
1
- """load_agent tests - ported from yosk's TestLoadAgent, retargeted at the
1
+ """load_agent tests - ported from the consumer's TestLoadAgent, retargeted at the
2
2
  library signature which also builds the Agent via the SDK factory. Real
3
3
  agent files on disk (fail-fast file policy); real LLM/Agent SDK objects
4
4
  (constructed locally, no network)."""
@@ -6,14 +6,14 @@ from pydantic import ValidationError
6
6
 
7
7
  from agentic_dev_loop.core.config import Route, default_state_dir, load_config
8
8
 
9
- # The plan's yosk-equivalent reference pipeline (009, YAML schema section),
9
+ # The plan's reference pipeline (consumer-equivalent) (009, YAML schema section),
10
10
  # verbatim except for two substitutions: 012 replaced the agent step named
11
11
  # `test` (artifact ci-fix.md, PASS/FAIL) with the mechanical command step
12
12
  # named `test` - same position, same on_fail wiring; and 009 dropped the
13
13
  # port_sweep step (its sweep moved into engine startup, no longer a pipeline
14
14
  # step). This is the canonical pin: the documented schema must load.
15
15
  FULL_YAML = """\
16
- name: yosk-construction
16
+ name: reference-construction
17
17
 
18
18
  extensions:
19
19
  - agentic_dev_loop.engine.extensions.aidlc:AidlcExtension
@@ -86,7 +86,7 @@ def test_full_schema_example_round_trips(tmp_path):
86
86
  p.write_text(FULL_YAML)
87
87
  cfg = load_config(p)
88
88
 
89
- assert cfg.name == "yosk-construction"
89
+ assert cfg.name == "reference-construction"
90
90
  assert cfg.extensions == ["agentic_dev_loop.engine.extensions.aidlc:AidlcExtension"]
91
91
  assert cfg.additional_files == ["coding-standards-ddd.yaml"]
92
92
  assert cfg.state_dir == ".pr"
@@ -1,6 +1,6 @@
1
1
  """Graph builder + run_pipeline behavioral tests.
2
2
 
3
- The yosk-equivalent YAML below must reproduce the hardcoded loop's behavior:
3
+ The consumer-equivalent YAML below must reproduce the hardcoded loop's behavior:
4
4
  dev → check → review → qa → commit, retries to dev, failure on
5
5
  exhaustion/exception. run_agent is stubbed (the SDK network boundary);
6
6
  everything else - files, git, checkpoints - is real."""
@@ -21,8 +21,8 @@ from agentic_dev_loop.engine.registry import Registry, default_registry, load_ex
21
21
  from agentic_dev_loop.engine.stages import Runtime
22
22
  from agentic_dev_loop.viz import VizBus
23
23
 
24
- YOSK_YAML = """\
25
- name: yosk-construction
24
+ REFERENCE_YAML = """\
25
+ name: reference-construction
26
26
  extensions:
27
27
  - agentic_dev_loop.engine.extensions.aidlc:AidlcExtension
28
28
  additional_files:
@@ -101,10 +101,10 @@ def git(repo: Path, *args: str) -> str:
101
101
 
102
102
  @pytest.fixture
103
103
  def repo(tmp_path, monkeypatch, fake_sandbox):
104
- """A real git repo with the yosk-equivalent pipeline committed clean."""
104
+ """A real git repo with the consumer-equivalent pipeline committed clean."""
105
105
  repo = tmp_path / "repo"
106
106
  repo.mkdir()
107
- (repo / "pipeline.yaml").write_text(YOSK_YAML)
107
+ (repo / "pipeline.yaml").write_text(REFERENCE_YAML)
108
108
  (repo / "coding-standards-ddd.yaml").write_text("STANDARDS")
109
109
  (repo / "plan.md").write_text("THE PLAN")
110
110
  git(repo, "init")
@@ -131,7 +131,7 @@ def script_all_pass(fake_agents):
131
131
 
132
132
 
133
133
  class TestTopology:
134
- def test_yosk_pipeline_node_set(self, repo):
134
+ def test_reference_pipeline_node_set(self, repo):
135
135
  cfg = load_config(Path("pipeline.yaml"))
136
136
  registry = default_registry()
137
137
  load_extensions(cfg.extensions, registry)
@@ -143,7 +143,7 @@ class TestTopology:
143
143
  "bump", "success"} <= nodes
144
144
 
145
145
  def test_unknown_action_fails_at_build(self, repo):
146
- (repo / "bad.yaml").write_text(YOSK_YAML.replace(
146
+ (repo / "bad.yaml").write_text(REFERENCE_YAML.replace(
147
147
  "action: commit", "action: no_such_action"))
148
148
  cfg = load_config(Path("bad.yaml"))
149
149
  runtime = Runtime(cfg=cfg, registry=default_registry(),
@@ -178,6 +178,31 @@ class TestRunPipeline:
178
178
  second_dev = fake_agents.messages_for("dev")[1]
179
179
  assert "broken build" in second_dev # fail note reached the next attempt
180
180
 
181
+ def test_downstream_agent_sees_prior_artifact(self, repo, fake_agents):
182
+ script_all_pass(fake_agents)
183
+ ok, attempts = run_pipeline("pipeline.yaml", args())
184
+ assert (ok, attempts) == (True, 1)
185
+ (dev_msg,) = fake_agents.messages_for("dev")
186
+ (check_msg,) = fake_agents.messages_for("check")
187
+ assert "## Stage artifacts" not in dev_msg # runs before anything accumulates
188
+ assert "## Stage artifacts" in check_msg
189
+ assert "built it" in check_msg # dev's captured response reached check
190
+
191
+ def test_retry_injects_review_artifact_notes_and_prior_dev(self, repo,
192
+ fake_agents):
193
+ script_all_pass(fake_agents)
194
+ fake_agents.set("review", [
195
+ ("write", "review-findings.md", "VERDICT: NEEDS CHANGES\nfix the thing"),
196
+ ("write", "review-findings.md", "VERDICT: APPROVED"),
197
+ ])
198
+ ok, attempts = run_pipeline("pipeline.yaml", args())
199
+ assert (ok, attempts) == (True, 2)
200
+ second_dev = fake_agents.messages_for("dev")[1]
201
+ assert "## Stage artifacts" in second_dev
202
+ assert "built it" in second_dev # dev's own attempt-1 artifact
203
+ assert "fix the thing" in second_dev # review artifact + fail note
204
+ assert "## Previous attempt feedback" in second_dev
205
+
181
206
  def test_exhaustion_fails(self, repo, fake_agents):
182
207
  script_all_pass(fake_agents)
183
208
  fake_agents.set("check", [("write", "ci-fix.md", "VERDICT: FAIL")])
@@ -248,7 +273,7 @@ class TestRunPipeline:
248
273
  # is not a tooling modification - the loader's missing-file guard is
249
274
  # what catches it.
250
275
  (repo / "pipeline.yaml").write_text(
251
- YOSK_YAML.replace("coding-standards-ddd.yaml",
276
+ REFERENCE_YAML.replace("coding-standards-ddd.yaml",
252
277
  "no-such-shared-file.yaml"))
253
278
  git(repo, "add", "-A")
254
279
  git(repo, "commit", "-m", "reference missing additional file")
@@ -257,7 +282,7 @@ class TestRunPipeline:
257
282
  assert fake_agents.calls == []
258
283
 
259
284
  def test_empty_loader_docs_fail_fast(self, repo, fake_agents):
260
- (repo / "empty.yaml").write_text(YOSK_YAML.replace(
285
+ (repo / "empty.yaml").write_text(REFERENCE_YAML.replace(
261
286
  "agentic_dev_loop.engine.extensions.aidlc:AidlcExtension",
262
287
  "sample_ext:EmptyLoaderExtension"))
263
288
  git(repo, "add", "-A")
@@ -267,7 +292,7 @@ class TestRunPipeline:
267
292
  assert fake_agents.calls == []
268
293
 
269
294
 
270
- LOCKED_YAML = YOSK_YAML.replace(
295
+ LOCKED_YAML = REFERENCE_YAML.replace(
271
296
  "deps_policy: open\n",
272
297
  "deps_policy: locked\nlockfiles: [uv.lock]\n",
273
298
  1)
@@ -27,3 +27,11 @@ def test_unverifiable_verdict_rules_present():
27
27
 
28
28
  def test_inconclusive_budget_rule_retained():
29
29
  assert "INCONCLUSIVE" in _text()
30
+
31
+
32
+ def test_probe_plan_rules_present():
33
+ text = _text()
34
+ assert "**Variant matrix.**" in text
35
+ assert "**Outcome probes.**" in text
36
+ assert "**Concurrency probes.**" in text
37
+ assert "**Error messages are assertions.**" in text
@@ -38,7 +38,7 @@ def _cfg_with_secrets(tmp_path) -> PipelineConfig:
38
38
  data = _base()
39
39
  data["secrets"] = {
40
40
  "kinde-client-secret": {"from": "env", "key": "KINDIE_CLIENT_SECRET"},
41
- "r2-keys": {"from": "env", "key": "YOSK_R2_KEYS"},
41
+ "r2-keys": {"from": "env", "key": "R2_KEYS"},
42
42
  }
43
43
  data["steps"] = [
44
44
  {"name": "qa", "agent": "qa", "secrets": ["kinde-client-secret"],
@@ -127,7 +127,7 @@ class TestProviderResolution:
127
127
  def test_resolve_secrets_returns_ref_keyed_values(self, tmp_path):
128
128
  cfg = _cfg_with_secrets(tmp_path)
129
129
  resolved = resolve_secrets(cfg, {"KINDIE_CLIENT_SECRET": "kinde-secret",
130
- "YOSK_R2_KEYS": "r2-keys"})
130
+ "R2_KEYS": "r2-keys"})
131
131
  assert resolved == {"kinde-client-secret": "kinde-secret",
132
132
  "r2-keys": "r2-keys"}
133
133
 
@@ -137,14 +137,14 @@ class TestProviderResolution:
137
137
  resolve_secrets(cfg, {"KINDIE_CLIENT_SECRET": "kinde-secret"})
138
138
  msg = str(e.value)
139
139
  assert "r2-keys" in msg
140
- assert "YOSK_R2_KEYS" in msg
140
+ assert "R2_KEYS" in msg
141
141
  assert "env" in msg
142
142
  assert "['<none>']" in msg # r2-keys is declared but unused by any step
143
143
 
144
144
  def test_resolve_secrets_missing_used_ref_names_step(self, tmp_path):
145
145
  cfg = _cfg_with_secrets(tmp_path)
146
146
  with pytest.raises(SecretResolutionError) as e:
147
- resolve_secrets(cfg, {"YOSK_R2_KEYS": "r2-keys"})
147
+ resolve_secrets(cfg, {"R2_KEYS": "r2-keys"})
148
148
  assert "kinde-client-secret" in str(e.value)
149
149
  assert "['qa']" in str(e.value)
150
150
 
@@ -1,6 +1,6 @@
1
1
  """Stage nodes, routers, context assembly, and the run_agent port.
2
2
 
3
- The yosk hardcoded stages (dev/test/review/qa + bump) become per-step node
3
+ The consumer hardcoded stages (dev/test/review/qa + bump) become per-step node
4
4
  and router closures built from the pipeline config; the fixed policies
5
5
  (hygiene, verdict-first truncation, unclear-verdict retry, notes) stay here."""
6
6
 
@@ -17,8 +17,9 @@ from openhands.sdk.conversation.state import ConversationExecutionStatus
17
17
  from agentic_dev_loop.core.config import PipelineConfig
18
18
  from agentic_dev_loop.engine.registry import Registry, default_registry
19
19
  from agentic_dev_loop.engine.stages import (
20
- Runtime, _run_command, build_context, make_action_node, make_agent_node,
21
- make_bump_node, make_command_node, make_router, run_agent,
20
+ ARTIFACT_CONTEXT_CAP, Runtime, _run_command, build_context,
21
+ make_action_node, make_agent_node, make_bump_node, make_command_node,
22
+ make_router, run_agent,
22
23
  )
23
24
 
24
25
 
@@ -53,7 +54,7 @@ def runtime(tmp_path, fake_sandbox):
53
54
 
54
55
  def make_state(**over):
55
56
  state = {"subject": "plan plan.md", "docs": {"plan.md": "THE PLAN"},
56
- "context": "CTX", "attempt": 1, "max_attempts": 3, "notes": [],
57
+ "artifacts": {}, "attempt": 1, "max_attempts": 3, "notes": [],
57
58
  "failed": False, "outcome": None, "commit_allowed": False,
58
59
  "step_verdicts": {}, "retry_target": None, "thread_id": "t1"}
59
60
  state.update(over)
@@ -78,7 +79,7 @@ class TestBuildContext:
78
79
  assert "## Previous attempt feedback\n\nnote one\n\nnote two" in ctx
79
80
 
80
81
  def test_output_paths_workspace_relative_for_subdirectory_pipeline(self, tmp_path):
81
- """yosk layout: the pipeline YAML lives in a subdirectory while the
82
+ """consumer layout: the pipeline YAML lives in a subdirectory while the
82
83
  workspace is the repo root - the prompt path must reach the real
83
84
  state_dir; state_dir.name alone sends agents to the wrong directory
84
85
  (U18 burned 3 attempts on an unfindable ci-fix.md)."""
@@ -114,6 +115,27 @@ class TestBuildContext:
114
115
  assert "dev agent writes:" in ctx
115
116
  assert "test agent writes:" not in ctx
116
117
 
118
+ def test_artifacts_render_one_subsection_per_artifact_in_order(self, runtime):
119
+ ctx = build_context(runtime, {}, "s", [],
120
+ artifacts={"dev": "summary text",
121
+ "review": "findings text"})
122
+ assert "## Stage artifacts" in ctx
123
+ assert "### dev\n\nsummary text" in ctx
124
+ assert "### review\n\nfindings text" in ctx
125
+ assert ctx.index("### dev") < ctx.index("### review") # dict order
126
+
127
+ def test_artifact_content_capped_with_run_state_path_marker(self, runtime):
128
+ big = "x" * (ARTIFACT_CONTEXT_CAP + 100)
129
+ ctx = build_context(runtime, {}, "s", [], artifacts={"dev": big})
130
+ assert "x" * ARTIFACT_CONTEXT_CAP in ctx
131
+ assert "x" * (ARTIFACT_CONTEXT_CAP + 100) not in ctx
132
+ assert ("[... content truncated - full text at "
133
+ "`.pr/implementation-summary.md`]") in ctx
134
+
135
+ def test_no_artifacts_renders_no_section(self, runtime):
136
+ ctx = build_context(runtime, {}, "s", [])
137
+ assert "## Stage artifacts" not in ctx
138
+
117
139
 
118
140
  class TestDepsPolicyContext:
119
141
  def test_locked_policy_injected(self, runtime):
@@ -182,6 +204,40 @@ class TestAgentNode:
182
204
  (note,) = update["notes"]
183
205
  assert "broken build" in note and "attempt 2" in note
184
206
 
207
+ def test_no_verdicts_branch_captures_artifact(self, runtime, fake_agents):
208
+ fake_agents.set("dev", [("write", "implementation-summary.md", "MARKER-DEV")])
209
+ update = make_agent_node(runtime, runtime.cfg.steps[0])(make_state())
210
+ assert update["artifacts"] == {"dev": "MARKER-DEV"}
211
+
212
+ def test_pass_verdict_captures_artifact(self, runtime, fake_agents):
213
+ fake_agents.set("check", [("write", "ci-fix.md", "MARKER-CHECK\nVERDICT: PASS\n")])
214
+ update = make_agent_node(runtime, runtime.cfg.steps[1])(make_state())
215
+ assert update["artifacts"] == {"check": "MARKER-CHECK\nVERDICT: PASS\n"}
216
+
217
+ def test_fail_verdict_captures_artifact(self, runtime, fake_agents):
218
+ fake_agents.set("check", [("write", "ci-fix.md", "MARKER-FAIL\nVERDICT: FAIL\n")])
219
+ update = make_agent_node(runtime, runtime.cfg.steps[1])(make_state())
220
+ assert update["artifacts"] == {"check": "MARKER-FAIL\nVERDICT: FAIL\n"}
221
+
222
+ def test_unverifiable_verdict_captures_artifact(self, runtime, fake_agents):
223
+ step = make_cfg([
224
+ {"name": "qa", "agent": "qa", "artifact": "qa-report.md",
225
+ "verdicts": {"pass": "PASS", "fail": "FAIL",
226
+ "unverifiable": "UNVERIFIABLE"},
227
+ "on_pass": "success"}]).steps[0]
228
+ fake_agents.set("qa", [("write", "qa-report.md", "MARKER-UNVER\nVERDICT: UNVERIFIABLE\n")])
229
+ update = make_agent_node(runtime, step)(make_state())
230
+ assert update["artifacts"] == {"qa": "MARKER-UNVER\nVERDICT: UNVERIFIABLE\n"}
231
+
232
+ def test_capture_error_branch_returns_no_artifacts(self, runtime, fake_agents):
233
+ update = make_agent_node(runtime, runtime.cfg.steps[1])(make_state())
234
+ assert "artifacts" not in update
235
+
236
+ def test_unclear_branch_returns_no_artifacts(self, runtime, fake_agents):
237
+ fake_agents.set("check", [("write", "ci-fix.md", "undecided")])
238
+ update = make_agent_node(runtime, runtime.cfg.steps[1])(make_state())
239
+ assert "artifacts" not in update
240
+
185
241
  def test_verdicts_merge_with_prior_steps(self, runtime, fake_agents):
186
242
  fake_agents.set("check", [("write", "ci-fix.md", "VERDICT: PASS")])
187
243
  state = make_state(step_verdicts={"dev": "pass"})
@@ -528,14 +584,11 @@ class TestBumpNode:
528
584
  def test_consumes_attempt_resets_verdicts_rebuilds_cleans(self, runtime):
529
585
  for stale in ("ci-fix.md", "implementation-summary.md", "last-error.md"):
530
586
  (runtime.state_dir / stale).write_text("stale")
531
- state = make_state(attempt=1, notes=["N1"],
587
+ state = make_state(attempt=1, notes=["N1"], artifacts={"dev": "D1"},
532
588
  step_verdicts={"check": "fail"})
533
589
  update = make_bump_node(runtime)(state)
534
- assert update["attempt"] == 2
535
- assert update["step_verdicts"] == {}
536
- assert update["failed"] is False
537
- assert "## Previous attempt feedback\n\nN1" in update["context"]
538
- assert "## plan.md\n\nTHE PLAN" in update["context"]
590
+ assert update == {"attempt": 2, "step_verdicts": {}, "failed": False}
591
+ assert "context" not in update # message is rebuilt per run (027)
539
592
  for stale in ("ci-fix.md", "implementation-summary.md", "last-error.md"):
540
593
  assert not (runtime.state_dir / stale).exists()
541
594