agentic-dev-loop 0.2.0__tar.gz → 0.3.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.
- {agentic_dev_loop-0.2.0 → agentic_dev_loop-0.3.0}/PKG-INFO +13 -11
- {agentic_dev_loop-0.2.0 → agentic_dev_loop-0.3.0}/README.md +12 -10
- {agentic_dev_loop-0.2.0 → agentic_dev_loop-0.3.0}/pyproject.toml +1 -1
- agentic_dev_loop-0.3.0/src/agentic_dev_loop/extensions/aidlc.py +124 -0
- {agentic_dev_loop-0.2.0 → agentic_dev_loop-0.3.0}/src/agentic_dev_loop.egg-info/PKG-INFO +13 -11
- {agentic_dev_loop-0.2.0 → agentic_dev_loop-0.3.0}/src/agentic_dev_loop.egg-info/SOURCES.txt +1 -0
- {agentic_dev_loop-0.2.0 → agentic_dev_loop-0.3.0}/tests/test_aidlc.py +100 -108
- {agentic_dev_loop-0.2.0 → agentic_dev_loop-0.3.0}/tests/test_cli.py +2 -2
- {agentic_dev_loop-0.2.0 → agentic_dev_loop-0.3.0}/tests/test_e2e_fixtures.py +1 -1
- {agentic_dev_loop-0.2.0 → agentic_dev_loop-0.3.0}/tests/test_loaders.py +7 -7
- agentic_dev_loop-0.3.0/tests/test_source_resolution.py +23 -0
- agentic_dev_loop-0.2.0/src/agentic_dev_loop/extensions/aidlc.py +0 -152
- {agentic_dev_loop-0.2.0 → agentic_dev_loop-0.3.0}/LICENSE +0 -0
- {agentic_dev_loop-0.2.0 → agentic_dev_loop-0.3.0}/setup.cfg +0 -0
- {agentic_dev_loop-0.2.0 → agentic_dev_loop-0.3.0}/src/agentic_dev_loop/__init__.py +0 -0
- {agentic_dev_loop-0.2.0 → agentic_dev_loop-0.3.0}/src/agentic_dev_loop/actions.py +0 -0
- {agentic_dev_loop-0.2.0 → agentic_dev_loop-0.3.0}/src/agentic_dev_loop/agents.py +0 -0
- {agentic_dev_loop-0.2.0 → agentic_dev_loop-0.3.0}/src/agentic_dev_loop/cli.py +0 -0
- {agentic_dev_loop-0.2.0 → agentic_dev_loop-0.3.0}/src/agentic_dev_loop/config.py +0 -0
- {agentic_dev_loop-0.2.0 → agentic_dev_loop-0.3.0}/src/agentic_dev_loop/extensions/__init__.py +0 -0
- {agentic_dev_loop-0.2.0 → agentic_dev_loop-0.3.0}/src/agentic_dev_loop/graph.py +0 -0
- {agentic_dev_loop-0.2.0 → agentic_dev_loop-0.3.0}/src/agentic_dev_loop/llm.py +0 -0
- {agentic_dev_loop-0.2.0 → agentic_dev_loop-0.3.0}/src/agentic_dev_loop/loaders.py +0 -0
- {agentic_dev_loop-0.2.0 → agentic_dev_loop-0.3.0}/src/agentic_dev_loop/outputs.py +0 -0
- {agentic_dev_loop-0.2.0 → agentic_dev_loop-0.3.0}/src/agentic_dev_loop/proc.py +0 -0
- {agentic_dev_loop-0.2.0 → agentic_dev_loop-0.3.0}/src/agentic_dev_loop/py.typed +0 -0
- {agentic_dev_loop-0.2.0 → agentic_dev_loop-0.3.0}/src/agentic_dev_loop/registry.py +0 -0
- {agentic_dev_loop-0.2.0 → agentic_dev_loop-0.3.0}/src/agentic_dev_loop/results.py +0 -0
- {agentic_dev_loop-0.2.0 → agentic_dev_loop-0.3.0}/src/agentic_dev_loop/stages.py +0 -0
- {agentic_dev_loop-0.2.0 → agentic_dev_loop-0.3.0}/src/agentic_dev_loop/viz.py +0 -0
- {agentic_dev_loop-0.2.0 → agentic_dev_loop-0.3.0}/src/agentic_dev_loop.egg-info/dependency_links.txt +0 -0
- {agentic_dev_loop-0.2.0 → agentic_dev_loop-0.3.0}/src/agentic_dev_loop.egg-info/entry_points.txt +0 -0
- {agentic_dev_loop-0.2.0 → agentic_dev_loop-0.3.0}/src/agentic_dev_loop.egg-info/requires.txt +0 -0
- {agentic_dev_loop-0.2.0 → agentic_dev_loop-0.3.0}/src/agentic_dev_loop.egg-info/top_level.txt +0 -0
- {agentic_dev_loop-0.2.0 → agentic_dev_loop-0.3.0}/tests/test_actions.py +0 -0
- {agentic_dev_loop-0.2.0 → agentic_dev_loop-0.3.0}/tests/test_agents.py +0 -0
- {agentic_dev_loop-0.2.0 → agentic_dev_loop-0.3.0}/tests/test_config.py +0 -0
- {agentic_dev_loop-0.2.0 → agentic_dev_loop-0.3.0}/tests/test_graph.py +0 -0
- {agentic_dev_loop-0.2.0 → agentic_dev_loop-0.3.0}/tests/test_llm.py +0 -0
- {agentic_dev_loop-0.2.0 → agentic_dev_loop-0.3.0}/tests/test_outputs.py +0 -0
- {agentic_dev_loop-0.2.0 → agentic_dev_loop-0.3.0}/tests/test_outputs_pbt.py +0 -0
- {agentic_dev_loop-0.2.0 → agentic_dev_loop-0.3.0}/tests/test_proc.py +0 -0
- {agentic_dev_loop-0.2.0 → agentic_dev_loop-0.3.0}/tests/test_publish.py +0 -0
- {agentic_dev_loop-0.2.0 → agentic_dev_loop-0.3.0}/tests/test_registry.py +0 -0
- {agentic_dev_loop-0.2.0 → agentic_dev_loop-0.3.0}/tests/test_results.py +0 -0
- {agentic_dev_loop-0.2.0 → agentic_dev_loop-0.3.0}/tests/test_sdk_compat.py +0 -0
- {agentic_dev_loop-0.2.0 → agentic_dev_loop-0.3.0}/tests/test_stages.py +0 -0
- {agentic_dev_loop-0.2.0 → agentic_dev_loop-0.3.0}/tests/test_verdict.py +0 -0
- {agentic_dev_loop-0.2.0 → agentic_dev_loop-0.3.0}/tests/test_viz.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentic-dev-loop
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Opinionated LangGraph engine for OpenHands agent pipelines
|
|
5
5
|
Author: Luke Wilson
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -65,7 +65,7 @@ Requires Python ≥ 3.12.
|
|
|
65
65
|
```bash
|
|
66
66
|
export OPENAI_API_KEY=... # whatever env var your pipeline's api_key_env names
|
|
67
67
|
agentic-dev-loop agentic-dev-loop/pipeline.yaml --plan plan.md
|
|
68
|
-
agentic-dev-loop agentic-dev-loop/pipeline.yaml --
|
|
68
|
+
agentic-dev-loop agentic-dev-loop/pipeline.yaml --aidlc-unit unit-2
|
|
69
69
|
agentic-dev-loop agentic-dev-loop/pipeline.yaml --resume <thread-id> --max-attempts 5
|
|
70
70
|
./trigger-agentic-dev-loop.sh --plan plan.md # same thing, from the repo root
|
|
71
71
|
```
|
|
@@ -84,10 +84,12 @@ expanded and live-tails its console. To eyeball the viz page without
|
|
|
84
84
|
running a pipeline, `.venv/bin/python viz_demo.py` serves the real topology
|
|
85
85
|
and replays a scripted run (every node state, verdict badges, retry cycle).
|
|
86
86
|
|
|
87
|
-
`--
|
|
88
|
-
`
|
|
89
|
-
|
|
90
|
-
|
|
87
|
+
`--aidlc-unit` takes the unit directory name under
|
|
88
|
+
`aidlc-docs/construction/` verbatim — `unit-2`,
|
|
89
|
+
`unit-002-custom-configuration`, or any slug AI-DLC's own naming uses
|
|
90
|
+
(AI-DLC imposes no naming convention on unit directories). No spelling
|
|
91
|
+
tolerance, no numeric aliases: the slug must name the directory exactly,
|
|
92
|
+
and a wrong slug fails loudly listing the directories that do exist.
|
|
91
93
|
|
|
92
94
|
## What a run looks like
|
|
93
95
|
|
|
@@ -258,10 +260,10 @@ Loader-selection flow: a loader's flag in argv selects it; flags from two
|
|
|
258
260
|
loaders in one invocation error out; with no flag, the sole `defaultable`
|
|
259
261
|
loader autoruns (the bundled `ai-dlc` loader autodetects the current unit
|
|
260
262
|
from `aidlc-docs/aidlc-state.md`), otherwise the run proceeds with no
|
|
261
|
-
injected docs and the pipeline `name:` as subject.
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
263
|
+
injected docs and the pipeline `name:` as subject. A unit id must name
|
|
264
|
+
its directory exactly (`--aidlc-unit unit-3`); there is no numeric
|
|
265
|
+
fallback or spelling tolerance, and an unmatched slug fails loud listing
|
|
266
|
+
the available directories. A selected loader that
|
|
265
267
|
yields zero docs **fails fast** — context is never silently degraded.
|
|
266
268
|
Duplicate CLI option strings across loaders are a load-time error naming both.
|
|
267
269
|
|
|
@@ -303,7 +305,7 @@ The development lifecycle workflow lives in
|
|
|
303
305
|
phase details under `aws-aidlc-rule-details/`). Working docs — plans, user
|
|
304
306
|
stories, units of work, state — live in `aidlc-docs/`, created as the
|
|
305
307
|
workflow runs. The bundled `ai-dlc` document loader (see Extension points)
|
|
306
|
-
wires the pipeline's `--
|
|
308
|
+
wires the pipeline's `--aidlc-unit` flag to those docs.
|
|
307
309
|
|
|
308
310
|
OpenCode commands over the workflow: `/epic-status`, `/ticket-details`,
|
|
309
311
|
`/unit-status`, `/update-subtask` (ticket boards over `aidlc-docs/`),
|
|
@@ -27,7 +27,7 @@ Requires Python ≥ 3.12.
|
|
|
27
27
|
```bash
|
|
28
28
|
export OPENAI_API_KEY=... # whatever env var your pipeline's api_key_env names
|
|
29
29
|
agentic-dev-loop agentic-dev-loop/pipeline.yaml --plan plan.md
|
|
30
|
-
agentic-dev-loop agentic-dev-loop/pipeline.yaml --
|
|
30
|
+
agentic-dev-loop agentic-dev-loop/pipeline.yaml --aidlc-unit unit-2
|
|
31
31
|
agentic-dev-loop agentic-dev-loop/pipeline.yaml --resume <thread-id> --max-attempts 5
|
|
32
32
|
./trigger-agentic-dev-loop.sh --plan plan.md # same thing, from the repo root
|
|
33
33
|
```
|
|
@@ -46,10 +46,12 @@ expanded and live-tails its console. To eyeball the viz page without
|
|
|
46
46
|
running a pipeline, `.venv/bin/python viz_demo.py` serves the real topology
|
|
47
47
|
and replays a scripted run (every node state, verdict badges, retry cycle).
|
|
48
48
|
|
|
49
|
-
`--
|
|
50
|
-
`
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
`--aidlc-unit` takes the unit directory name under
|
|
50
|
+
`aidlc-docs/construction/` verbatim — `unit-2`,
|
|
51
|
+
`unit-002-custom-configuration`, or any slug AI-DLC's own naming uses
|
|
52
|
+
(AI-DLC imposes no naming convention on unit directories). No spelling
|
|
53
|
+
tolerance, no numeric aliases: the slug must name the directory exactly,
|
|
54
|
+
and a wrong slug fails loudly listing the directories that do exist.
|
|
53
55
|
|
|
54
56
|
## What a run looks like
|
|
55
57
|
|
|
@@ -220,10 +222,10 @@ Loader-selection flow: a loader's flag in argv selects it; flags from two
|
|
|
220
222
|
loaders in one invocation error out; with no flag, the sole `defaultable`
|
|
221
223
|
loader autoruns (the bundled `ai-dlc` loader autodetects the current unit
|
|
222
224
|
from `aidlc-docs/aidlc-state.md`), otherwise the run proceeds with no
|
|
223
|
-
injected docs and the pipeline `name:` as subject.
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
225
|
+
injected docs and the pipeline `name:` as subject. A unit id must name
|
|
226
|
+
its directory exactly (`--aidlc-unit unit-3`); there is no numeric
|
|
227
|
+
fallback or spelling tolerance, and an unmatched slug fails loud listing
|
|
228
|
+
the available directories. A selected loader that
|
|
227
229
|
yields zero docs **fails fast** — context is never silently degraded.
|
|
228
230
|
Duplicate CLI option strings across loaders are a load-time error naming both.
|
|
229
231
|
|
|
@@ -265,7 +267,7 @@ The development lifecycle workflow lives in
|
|
|
265
267
|
phase details under `aws-aidlc-rule-details/`). Working docs — plans, user
|
|
266
268
|
stories, units of work, state — live in `aidlc-docs/`, created as the
|
|
267
269
|
workflow runs. The bundled `ai-dlc` document loader (see Extension points)
|
|
268
|
-
wires the pipeline's `--
|
|
270
|
+
wires the pipeline's `--aidlc-unit` flag to those docs.
|
|
269
271
|
|
|
270
272
|
OpenCode commands over the workflow: `/epic-status`, `/ticket-details`,
|
|
271
273
|
`/unit-status`, `/update-subtask` (ticket boards over `aidlc-docs/`),
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""AI-DLC document-loader extension. Loading IS choosing: name
|
|
2
|
+
`agentic_dev_loop.extensions.aidlc:AidlcExtension` in the pipeline's
|
|
3
|
+
`extensions:` list and the pipeline gains `--aidlc-unit` — the unit
|
|
4
|
+
directory name under `aidlc-docs/construction/` (e.g. `unit-3`,
|
|
5
|
+
`support-for-windows`), matched verbatim — plus bare-run autodetect of
|
|
6
|
+
the current unit from the Current Stage line of
|
|
7
|
+
`aidlc-docs/aidlc-state.md`.
|
|
8
|
+
|
|
9
|
+
No input mangling. AI-DLC's own ruleset imposes no naming convention on
|
|
10
|
+
unit directories (`{unit-name}` is a free placeholder), so the extension
|
|
11
|
+
imposes none either: the slug is the directory name, exactly. `U3`, `3`,
|
|
12
|
+
`004` and friends are NOT accepted unless a directory is literally named
|
|
13
|
+
that. The Current Stage line names the unit as its directory name
|
|
14
|
+
("Unit unit-3", "Unit support-for-windows") for autodetect.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import re
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
_DOC_SUFFIXES = (".md", ".yaml", ".yml", ".txt")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def resolve_unit_docs(docs_dir: Path | str, unit_slug: str) -> dict[str, str]:
|
|
25
|
+
docs: dict[str, str] = {}
|
|
26
|
+
docs_dir = Path(docs_dir)
|
|
27
|
+
unit_slug = unit_slug.strip()
|
|
28
|
+
|
|
29
|
+
construction_dir = docs_dir / "construction"
|
|
30
|
+
unit_dir: Path | None = None
|
|
31
|
+
if construction_dir.is_dir():
|
|
32
|
+
for entry in sorted(construction_dir.iterdir()):
|
|
33
|
+
if entry.is_dir() and entry.name == unit_slug:
|
|
34
|
+
unit_dir = entry
|
|
35
|
+
break
|
|
36
|
+
|
|
37
|
+
if unit_dir is None:
|
|
38
|
+
available = ""
|
|
39
|
+
if construction_dir.is_dir():
|
|
40
|
+
names = sorted(p.name for p in construction_dir.iterdir() if p.is_dir())
|
|
41
|
+
available = f" (available: {', '.join(names)})"
|
|
42
|
+
raise FileNotFoundError(
|
|
43
|
+
f"No unit directory named '{unit_slug}' under {construction_dir}"
|
|
44
|
+
f"{available or ' (no unit directories found)'}")
|
|
45
|
+
|
|
46
|
+
for file in sorted(unit_dir.rglob("*")):
|
|
47
|
+
if file.is_file() and file.suffix in _DOC_SUFFIXES:
|
|
48
|
+
key = str(file.relative_to(unit_dir))
|
|
49
|
+
docs[key] = file.read_text()
|
|
50
|
+
|
|
51
|
+
# At this point docs holds only unit-dir files — an empty load means the
|
|
52
|
+
# pipeline would build context from inception docs alone (the 2026-07-19
|
|
53
|
+
# U3 silent-degradation bug). Fail fast instead.
|
|
54
|
+
if not any(not k.startswith("inception/") for k in docs):
|
|
55
|
+
raise RuntimeError(
|
|
56
|
+
f"Unit dir {unit_dir} matched but yielded no docs — "
|
|
57
|
+
"refusing to build context from inception docs alone")
|
|
58
|
+
|
|
59
|
+
# Namespaced keys so an inception doc can never silently overwrite a
|
|
60
|
+
# unit doc with the same filename.
|
|
61
|
+
inception = docs_dir / "inception" / "application-design"
|
|
62
|
+
if inception.is_dir():
|
|
63
|
+
for file in sorted(inception.iterdir()):
|
|
64
|
+
if file.is_file() and file.suffix in (".md", ".txt"):
|
|
65
|
+
docs[f"inception/{file.name}"] = file.read_text()
|
|
66
|
+
|
|
67
|
+
# The unit's code-generation plans are the build spec — absent is fine,
|
|
68
|
+
# present is how the spec reaches agents.
|
|
69
|
+
plans_dir = docs_dir / "construction" / "plans"
|
|
70
|
+
if plans_dir.is_dir():
|
|
71
|
+
for file in sorted(plans_dir.glob(f"{unit_slug}-*-code-generation-plan.md")):
|
|
72
|
+
docs[f"plans/{file.name}"] = file.read_text()
|
|
73
|
+
|
|
74
|
+
return docs
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def get_current_unit(docs_dir: Path | str) -> str:
|
|
78
|
+
"""Read the current unit slug from the aidlc-state.md Current Stage line.
|
|
79
|
+
|
|
80
|
+
The stage line must name the unit as its directory name ("Unit unit-3",
|
|
81
|
+
"Unit support-for-windows"); the slug is returned verbatim for a direct
|
|
82
|
+
directory match.
|
|
83
|
+
"""
|
|
84
|
+
state_file = Path(docs_dir) / "aidlc-state.md"
|
|
85
|
+
if not state_file.is_file():
|
|
86
|
+
raise FileNotFoundError(f"aidlc-state file not found: {state_file}")
|
|
87
|
+
# Anchor to the Current Stage line — the file accumulates history, so an
|
|
88
|
+
# unanchored search could silently pick up an earlier "Unit N" mention.
|
|
89
|
+
match = re.search(r"Current Stage[^\n]*Unit ([\w-]+(?:\.[\w-]+)*)", state_file.read_text())
|
|
90
|
+
if not match:
|
|
91
|
+
raise RuntimeError(
|
|
92
|
+
f"Could not determine current unit from {state_file} "
|
|
93
|
+
"(no 'Unit <slug>' in the Current Stage line)")
|
|
94
|
+
return match.group(1)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class AidlcDocumentLoader:
|
|
98
|
+
"""`--aidlc-unit` (a unit directory name, matched verbatim), or bare-run
|
|
99
|
+
autodetect from the Current Stage line of aidlc-state.md."""
|
|
100
|
+
|
|
101
|
+
name = "ai-dlc"
|
|
102
|
+
defaultable = True
|
|
103
|
+
|
|
104
|
+
def __init__(self, docs_dir: Path | str = "aidlc-docs"):
|
|
105
|
+
self.docs_dir = Path(docs_dir)
|
|
106
|
+
|
|
107
|
+
def add_cli_args(self, parser: argparse.ArgumentParser) -> None:
|
|
108
|
+
parser.add_argument("--aidlc-unit", metavar="SLUG", default=None,
|
|
109
|
+
help="AI-DLC unit: the unit directory name under "
|
|
110
|
+
"aidlc-docs/construction/ (e.g. unit-3 or "
|
|
111
|
+
"support-for-windows), matched verbatim; "
|
|
112
|
+
"defaults to the current unit in "
|
|
113
|
+
"aidlc-docs/aidlc-state.md")
|
|
114
|
+
|
|
115
|
+
def resolve(self, args: argparse.Namespace) -> tuple[dict[str, str], str]:
|
|
116
|
+
unit = args.aidlc_unit or get_current_unit(self.docs_dir)
|
|
117
|
+
return resolve_unit_docs(self.docs_dir, unit), f"unit {unit}"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class AidlcExtension:
|
|
121
|
+
name = "ai-dlc"
|
|
122
|
+
|
|
123
|
+
def register(self, registry) -> None:
|
|
124
|
+
registry.add_document_loader(AidlcDocumentLoader())
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentic-dev-loop
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Opinionated LangGraph engine for OpenHands agent pipelines
|
|
5
5
|
Author: Luke Wilson
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -65,7 +65,7 @@ Requires Python ≥ 3.12.
|
|
|
65
65
|
```bash
|
|
66
66
|
export OPENAI_API_KEY=... # whatever env var your pipeline's api_key_env names
|
|
67
67
|
agentic-dev-loop agentic-dev-loop/pipeline.yaml --plan plan.md
|
|
68
|
-
agentic-dev-loop agentic-dev-loop/pipeline.yaml --
|
|
68
|
+
agentic-dev-loop agentic-dev-loop/pipeline.yaml --aidlc-unit unit-2
|
|
69
69
|
agentic-dev-loop agentic-dev-loop/pipeline.yaml --resume <thread-id> --max-attempts 5
|
|
70
70
|
./trigger-agentic-dev-loop.sh --plan plan.md # same thing, from the repo root
|
|
71
71
|
```
|
|
@@ -84,10 +84,12 @@ expanded and live-tails its console. To eyeball the viz page without
|
|
|
84
84
|
running a pipeline, `.venv/bin/python viz_demo.py` serves the real topology
|
|
85
85
|
and replays a scripted run (every node state, verdict badges, retry cycle).
|
|
86
86
|
|
|
87
|
-
`--
|
|
88
|
-
`
|
|
89
|
-
|
|
90
|
-
|
|
87
|
+
`--aidlc-unit` takes the unit directory name under
|
|
88
|
+
`aidlc-docs/construction/` verbatim — `unit-2`,
|
|
89
|
+
`unit-002-custom-configuration`, or any slug AI-DLC's own naming uses
|
|
90
|
+
(AI-DLC imposes no naming convention on unit directories). No spelling
|
|
91
|
+
tolerance, no numeric aliases: the slug must name the directory exactly,
|
|
92
|
+
and a wrong slug fails loudly listing the directories that do exist.
|
|
91
93
|
|
|
92
94
|
## What a run looks like
|
|
93
95
|
|
|
@@ -258,10 +260,10 @@ Loader-selection flow: a loader's flag in argv selects it; flags from two
|
|
|
258
260
|
loaders in one invocation error out; with no flag, the sole `defaultable`
|
|
259
261
|
loader autoruns (the bundled `ai-dlc` loader autodetects the current unit
|
|
260
262
|
from `aidlc-docs/aidlc-state.md`), otherwise the run proceeds with no
|
|
261
|
-
injected docs and the pipeline `name:` as subject.
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
263
|
+
injected docs and the pipeline `name:` as subject. A unit id must name
|
|
264
|
+
its directory exactly (`--aidlc-unit unit-3`); there is no numeric
|
|
265
|
+
fallback or spelling tolerance, and an unmatched slug fails loud listing
|
|
266
|
+
the available directories. A selected loader that
|
|
265
267
|
yields zero docs **fails fast** — context is never silently degraded.
|
|
266
268
|
Duplicate CLI option strings across loaders are a load-time error naming both.
|
|
267
269
|
|
|
@@ -303,7 +305,7 @@ The development lifecycle workflow lives in
|
|
|
303
305
|
phase details under `aws-aidlc-rule-details/`). Working docs — plans, user
|
|
304
306
|
stories, units of work, state — live in `aidlc-docs/`, created as the
|
|
305
307
|
workflow runs. The bundled `ai-dlc` document loader (see Extension points)
|
|
306
|
-
wires the pipeline's `--
|
|
308
|
+
wires the pipeline's `--aidlc-unit` flag to those docs.
|
|
307
309
|
|
|
308
310
|
OpenCode commands over the workflow: `/epic-status`, `/ticket-details`,
|
|
309
311
|
`/unit-status`, `/update-subtask` (ticket boards over `aidlc-docs/`),
|
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
"""AI-DLC document loader tests
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
"""AI-DLC document loader tests.
|
|
2
|
+
|
|
3
|
+
The loader contract: `--aidlc-unit` takes the unit DIRECTORY NAME verbatim
|
|
4
|
+
(`unit-3`, `support-for-windows`) and matches it by exact string equality
|
|
5
|
+
under `aidlc-docs/construction/`. No input mangling: `U3`, `3`, `004`,
|
|
6
|
+
`unit_3` and friends are NOT accepted unless a directory is literally
|
|
7
|
+
named that. Bare-run autodetect reads the slug from the Current Stage
|
|
8
|
+
line of `aidlc-docs/aidlc-state.md` (`Unit <slug>`).
|
|
9
|
+
"""
|
|
5
10
|
|
|
6
11
|
import argparse
|
|
7
12
|
|
|
@@ -23,31 +28,40 @@ class TestResolveUnitDocs:
|
|
|
23
28
|
other_dir.mkdir(parents=True)
|
|
24
29
|
(other_dir / "other.md").write_text("# Other")
|
|
25
30
|
|
|
26
|
-
docs = resolve_unit_docs(tmp_path, "2")
|
|
31
|
+
docs = resolve_unit_docs(tmp_path, "unit-2")
|
|
27
32
|
|
|
28
33
|
assert "design.md" in docs
|
|
29
34
|
assert "tasks.md" in docs
|
|
30
35
|
assert "other.md" not in docs
|
|
31
36
|
|
|
32
|
-
def
|
|
37
|
+
def test_u_prefixed_id_is_not_mangled(self, tmp_path):
|
|
38
|
+
"""The slug is the directory name — `U2` must NOT resolve `unit-2`."""
|
|
33
39
|
unit_dir = tmp_path / "construction" / "unit-2"
|
|
34
40
|
unit_dir.mkdir(parents=True)
|
|
35
41
|
(unit_dir / "design.md").write_text("# Design")
|
|
36
42
|
|
|
37
|
-
|
|
43
|
+
with pytest.raises(FileNotFoundError, match="unit-2"):
|
|
44
|
+
resolve_unit_docs(tmp_path, "U2")
|
|
38
45
|
|
|
39
|
-
def
|
|
46
|
+
def test_lowercase_u_prefixed_id_is_not_mangled(self, tmp_path):
|
|
40
47
|
unit_dir = tmp_path / "construction" / "unit-3"
|
|
41
48
|
unit_dir.mkdir(parents=True)
|
|
42
49
|
(unit_dir / "design.md").write_text("# Design")
|
|
43
50
|
|
|
44
|
-
|
|
51
|
+
with pytest.raises(FileNotFoundError, match="unit-3"):
|
|
52
|
+
resolve_unit_docs(tmp_path, "u3")
|
|
45
53
|
|
|
46
|
-
def
|
|
54
|
+
def test_raises_for_missing_unit_and_lists_available(self, tmp_path):
|
|
47
55
|
"""Unknown unit must fail fast — silently running the pipeline with
|
|
48
|
-
zero design docs is a forbidden silent fallback.
|
|
49
|
-
|
|
50
|
-
|
|
56
|
+
zero design docs is a forbidden silent fallback. The error names the
|
|
57
|
+
directories that DO exist."""
|
|
58
|
+
(tmp_path / "construction" / "unit-1").mkdir(parents=True)
|
|
59
|
+
(tmp_path / "construction" / "unit-2").mkdir(parents=True)
|
|
60
|
+
|
|
61
|
+
with pytest.raises(FileNotFoundError, match="unit-99") as exc:
|
|
62
|
+
resolve_unit_docs(tmp_path, "unit-99")
|
|
63
|
+
assert "unit-1" in str(exc.value)
|
|
64
|
+
assert "unit-2" in str(exc.value)
|
|
51
65
|
|
|
52
66
|
def test_includes_inception_docs(self, tmp_path):
|
|
53
67
|
unit_dir = tmp_path / "construction" / "unit-1"
|
|
@@ -57,7 +71,7 @@ class TestResolveUnitDocs:
|
|
|
57
71
|
inception.mkdir(parents=True)
|
|
58
72
|
(inception / "architecture.md").write_text("# Arch")
|
|
59
73
|
|
|
60
|
-
docs = resolve_unit_docs(tmp_path, "1")
|
|
74
|
+
docs = resolve_unit_docs(tmp_path, "unit-1")
|
|
61
75
|
|
|
62
76
|
assert "inception/architecture.md" in docs
|
|
63
77
|
assert "Arch" in docs["inception/architecture.md"]
|
|
@@ -72,7 +86,7 @@ class TestResolveUnitDocs:
|
|
|
72
86
|
inception.mkdir(parents=True)
|
|
73
87
|
(inception / "architecture.md").write_text("inception version")
|
|
74
88
|
|
|
75
|
-
docs = resolve_unit_docs(tmp_path, "1")
|
|
89
|
+
docs = resolve_unit_docs(tmp_path, "unit-1")
|
|
76
90
|
|
|
77
91
|
assert docs["architecture.md"] == "unit version"
|
|
78
92
|
assert docs["inception/architecture.md"] == "inception version"
|
|
@@ -85,7 +99,7 @@ class TestResolveUnitDocs:
|
|
|
85
99
|
(unit_dir / "functional-design").mkdir(parents=True)
|
|
86
100
|
(unit_dir / "functional-design" / "business-rules.md").write_text("# Rules")
|
|
87
101
|
|
|
88
|
-
docs = resolve_unit_docs(tmp_path, "
|
|
102
|
+
docs = resolve_unit_docs(tmp_path, "unit-3")
|
|
89
103
|
|
|
90
104
|
assert docs["functional-design/business-rules.md"] == "# Rules"
|
|
91
105
|
|
|
@@ -95,7 +109,7 @@ class TestResolveUnitDocs:
|
|
|
95
109
|
(tmp_path / "construction" / "unit-3" / "functional-design").mkdir(parents=True)
|
|
96
110
|
|
|
97
111
|
with pytest.raises(RuntimeError, match="no.*docs|zero"):
|
|
98
|
-
resolve_unit_docs(tmp_path, "
|
|
112
|
+
resolve_unit_docs(tmp_path, "unit-3")
|
|
99
113
|
|
|
100
114
|
def test_includes_code_generation_plans(self, tmp_path):
|
|
101
115
|
"""The unit's *-code-generation-plan.md files are the build spec."""
|
|
@@ -109,7 +123,7 @@ class TestResolveUnitDocs:
|
|
|
109
123
|
(plans / "unit-3-scan-nfr-design-plan.md").write_text("# NOT a code-gen plan")
|
|
110
124
|
(plans / "unit-2-onboarding-code-generation-plan.md").write_text("# Wrong unit")
|
|
111
125
|
|
|
112
|
-
docs = resolve_unit_docs(tmp_path, "
|
|
126
|
+
docs = resolve_unit_docs(tmp_path, "unit-3")
|
|
113
127
|
|
|
114
128
|
assert docs["plans/unit-3-scan-core-types-code-generation-plan.md"] == "# Core types"
|
|
115
129
|
assert docs["plans/unit-3-scan-frontend-code-generation-plan.md"] == "# Frontend"
|
|
@@ -121,7 +135,44 @@ class TestResolveUnitDocs:
|
|
|
121
135
|
unit_dir.mkdir(parents=True)
|
|
122
136
|
(unit_dir / "design.md").write_text("# Design")
|
|
123
137
|
|
|
124
|
-
assert resolve_unit_docs(tmp_path, "
|
|
138
|
+
assert resolve_unit_docs(tmp_path, "unit-2")["design.md"] == "# Design"
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class TestDirectSlugMatching:
|
|
142
|
+
"""The slug IS the directory name — no spelling tolerance, no numeric
|
|
143
|
+
fallback, no prefix mangling."""
|
|
144
|
+
|
|
145
|
+
def _unit(self, tmp_path, name):
|
|
146
|
+
unit_dir = tmp_path / "construction" / name
|
|
147
|
+
unit_dir.mkdir(parents=True)
|
|
148
|
+
(unit_dir / "design.md").write_text("# Design")
|
|
149
|
+
return unit_dir
|
|
150
|
+
|
|
151
|
+
def test_slug_matches_directory_exactly(self, tmp_path):
|
|
152
|
+
self._unit(tmp_path, "unit-004-preset-management")
|
|
153
|
+
assert resolve_unit_docs(tmp_path, "unit-004-preset-management")["design.md"] == "# Design"
|
|
154
|
+
|
|
155
|
+
def test_bare_slug_directory(self, tmp_path):
|
|
156
|
+
self._unit(tmp_path, "support-for-windows")
|
|
157
|
+
assert resolve_unit_docs(tmp_path, "support-for-windows")["design.md"] == "# Design"
|
|
158
|
+
|
|
159
|
+
def test_number_alone_does_not_match_slugged_dir(self, tmp_path):
|
|
160
|
+
"""The numeric fallback is gone: `004` must not resolve
|
|
161
|
+
`unit-004-preset-management`."""
|
|
162
|
+
self._unit(tmp_path, "unit-004-preset-management")
|
|
163
|
+
with pytest.raises(FileNotFoundError, match="004"):
|
|
164
|
+
resolve_unit_docs(tmp_path, "004")
|
|
165
|
+
|
|
166
|
+
def test_underscore_spelling_does_not_match(self, tmp_path):
|
|
167
|
+
"""The `unit_<id>` variant is gone: the slug must spell the
|
|
168
|
+
directory exactly."""
|
|
169
|
+
self._unit(tmp_path, "unit-3")
|
|
170
|
+
with pytest.raises(FileNotFoundError, match="unit-3"):
|
|
171
|
+
resolve_unit_docs(tmp_path, "unit_3")
|
|
172
|
+
|
|
173
|
+
def test_non_numeric_slug(self, tmp_path):
|
|
174
|
+
self._unit(tmp_path, "unit-scan")
|
|
175
|
+
assert resolve_unit_docs(tmp_path, "unit-scan")["design.md"] == "# Design"
|
|
125
176
|
|
|
126
177
|
|
|
127
178
|
class TestGetCurrentUnit:
|
|
@@ -130,16 +181,22 @@ class TestGetCurrentUnit:
|
|
|
130
181
|
state.write_text(f"# AI-DLC State Tracking\n\n{stage_line}\n")
|
|
131
182
|
return state
|
|
132
183
|
|
|
133
|
-
def
|
|
184
|
+
def test_parses_slug_from_current_stage(self, tmp_path):
|
|
185
|
+
self._write_state(
|
|
186
|
+
tmp_path,
|
|
187
|
+
"- **Current Stage**: CONSTRUCTION — Unit unit-3 (SCAN) — Code Generation in progress.")
|
|
188
|
+
assert get_current_unit(tmp_path) == "unit-3"
|
|
189
|
+
|
|
190
|
+
def test_parses_bare_slug_from_current_stage(self, tmp_path):
|
|
134
191
|
self._write_state(
|
|
135
192
|
tmp_path,
|
|
136
|
-
"- **Current Stage**: CONSTRUCTION — Unit
|
|
137
|
-
assert get_current_unit(tmp_path) == "
|
|
193
|
+
"- **Current Stage**: CONSTRUCTION — Unit support-for-windows.")
|
|
194
|
+
assert get_current_unit(tmp_path) == "support-for-windows"
|
|
138
195
|
|
|
139
|
-
def
|
|
196
|
+
def test_parses_different_slug(self, tmp_path):
|
|
140
197
|
self._write_state(
|
|
141
|
-
tmp_path, "- **Current Stage**: CONSTRUCTION — Unit 12 (REPORT).")
|
|
142
|
-
assert get_current_unit(tmp_path) == "
|
|
198
|
+
tmp_path, "- **Current Stage**: CONSTRUCTION — Unit unit-12 (REPORT).")
|
|
199
|
+
assert get_current_unit(tmp_path) == "unit-12"
|
|
143
200
|
|
|
144
201
|
def test_ignores_unit_mentions_before_the_stage_line(self, tmp_path):
|
|
145
202
|
"""The state file accumulates history; only the Current Stage line
|
|
@@ -148,8 +205,8 @@ class TestGetCurrentUnit:
|
|
|
148
205
|
state.write_text(
|
|
149
206
|
"# AI-DLC State Tracking\n\n"
|
|
150
207
|
"- Finished Unit 9 (REPORT) last week.\n"
|
|
151
|
-
"- **Current Stage**: CONSTRUCTION — Unit 3 (SCAN) — Code Generation.\n")
|
|
152
|
-
assert get_current_unit(tmp_path) == "
|
|
208
|
+
"- **Current Stage**: CONSTRUCTION — Unit unit-3 (SCAN) — Code Generation.\n")
|
|
209
|
+
assert get_current_unit(tmp_path) == "unit-3"
|
|
153
210
|
|
|
154
211
|
def test_stage_line_without_unit_raises_despite_earlier_mention(self, tmp_path):
|
|
155
212
|
"""A 'Unit N' somewhere else in the file is not a Current Stage —
|
|
@@ -182,108 +239,43 @@ class TestAidlcDocumentLoader:
|
|
|
182
239
|
def test_is_defaultable(self):
|
|
183
240
|
assert AidlcDocumentLoader.defaultable is True
|
|
184
241
|
|
|
185
|
-
def
|
|
242
|
+
def test_resolve_with_explicit_slug(self, tmp_path):
|
|
186
243
|
self._unit_docs(tmp_path)
|
|
187
244
|
loader = AidlcDocumentLoader(docs_dir=tmp_path)
|
|
188
245
|
|
|
189
|
-
docs, subject = loader.resolve(argparse.Namespace(
|
|
246
|
+
docs, subject = loader.resolve(argparse.Namespace(aidlc_unit="unit-3"))
|
|
190
247
|
|
|
191
248
|
assert docs["design.md"] == "# Design"
|
|
192
|
-
assert subject == "unit
|
|
249
|
+
assert subject == "unit unit-3"
|
|
193
250
|
|
|
194
251
|
def test_resolve_without_unit_autodetects_current(self, tmp_path):
|
|
195
252
|
self._unit_docs(tmp_path)
|
|
196
253
|
(tmp_path / "aidlc-state.md").write_text(
|
|
197
|
-
"# State\n\n- **Current Stage**: CONSTRUCTION — Unit 3 (SCAN).\n")
|
|
254
|
+
"# State\n\n- **Current Stage**: CONSTRUCTION — Unit unit-3 (SCAN).\n")
|
|
198
255
|
loader = AidlcDocumentLoader(docs_dir=tmp_path)
|
|
199
256
|
|
|
200
|
-
docs, subject = loader.resolve(argparse.Namespace(
|
|
257
|
+
docs, subject = loader.resolve(argparse.Namespace(aidlc_unit=None))
|
|
201
258
|
|
|
202
259
|
assert docs["design.md"] == "# Design"
|
|
203
|
-
assert subject == "unit
|
|
204
|
-
|
|
205
|
-
def test_extension_registers_loader_and_flag(self):
|
|
206
|
-
reg = Registry()
|
|
207
|
-
AidlcExtension().register(reg)
|
|
208
|
-
|
|
209
|
-
assert [l.name for l in reg.document_loaders] == ["ai-dlc"]
|
|
210
|
-
assert reg.cli_option_owners["--ai-dlc-unit"] == "ai-dlc"
|
|
211
|
-
assert reg.cli_dests["ai_dlc_unit"] == "ai-dlc"
|
|
212
|
-
|
|
260
|
+
assert subject == "unit unit-3"
|
|
213
261
|
|
|
214
|
-
|
|
215
|
-
"""Numeric tolerance: U004 / 004 / 4 / U4 must all resolve the same unit.
|
|
216
|
-
|
|
217
|
-
Real-world unit dirs are named unit-NNN-slug (e.g.
|
|
218
|
-
unit-004-preset-management); the loader must accept any unambiguous
|
|
219
|
-
spelling of the unit number and fail loud on ambiguity."""
|
|
220
|
-
|
|
221
|
-
def _unit(self, tmp_path, name):
|
|
222
|
-
unit_dir = tmp_path / "construction" / name
|
|
223
|
-
unit_dir.mkdir(parents=True)
|
|
224
|
-
(unit_dir / "design.md").write_text("# Design")
|
|
225
|
-
return unit_dir
|
|
226
|
-
|
|
227
|
-
@pytest.mark.parametrize("unit_id", [
|
|
228
|
-
"4", "U4", "u4", "004", "U004",
|
|
229
|
-
"004-preset-management", "U004-preset-management",
|
|
230
|
-
])
|
|
231
|
-
def test_accepts_all_spellings_for_slugged_dir(self, tmp_path, unit_id):
|
|
232
|
-
self._unit(tmp_path, "unit-004-preset-management")
|
|
233
|
-
assert resolve_unit_docs(tmp_path, unit_id)["design.md"] == "# Design"
|
|
234
|
-
|
|
235
|
-
def test_plain_number_matches_underscore_dir_form(self, tmp_path):
|
|
236
|
-
self._unit(tmp_path, "unit_004_presets")
|
|
237
|
-
assert resolve_unit_docs(tmp_path, "4")["design.md"] == "# Design"
|
|
238
|
-
|
|
239
|
-
def test_higher_number_does_not_match(self, tmp_path):
|
|
240
|
-
self._unit(tmp_path, "unit-40")
|
|
241
|
-
with pytest.raises(FileNotFoundError, match="unit"):
|
|
242
|
-
resolve_unit_docs(tmp_path, "4")
|
|
243
|
-
|
|
244
|
-
def test_zero_padded_plain_dir(self, tmp_path):
|
|
245
|
-
self._unit(tmp_path, "unit-004")
|
|
246
|
-
assert resolve_unit_docs(tmp_path, "U4")["design.md"] == "# Design"
|
|
247
|
-
|
|
248
|
-
def test_ambiguous_number_raises_loud(self, tmp_path):
|
|
249
|
-
self._unit(tmp_path, "unit-4")
|
|
250
|
-
self._unit(tmp_path, "unit-004-preset-management")
|
|
251
|
-
with pytest.raises(RuntimeError, match="[Aa]mbiguous") as exc:
|
|
252
|
-
resolve_unit_docs(tmp_path, "4")
|
|
253
|
-
assert "unit-4" in str(exc.value)
|
|
254
|
-
assert "unit-004-preset-management" in str(exc.value)
|
|
255
|
-
|
|
256
|
-
def test_ambiguous_u_prefixed_number_raises_loud(self, tmp_path):
|
|
257
|
-
self._unit(tmp_path, "unit-004")
|
|
258
|
-
self._unit(tmp_path, "unit-004-preset-management")
|
|
259
|
-
with pytest.raises(RuntimeError, match="[Aa]mbiguous"):
|
|
260
|
-
resolve_unit_docs(tmp_path, "U004")
|
|
261
|
-
|
|
262
|
-
def test_non_numeric_id_still_uses_exact_match(self, tmp_path):
|
|
263
|
-
self._unit(tmp_path, "unit-scan")
|
|
264
|
-
assert resolve_unit_docs(tmp_path, "scan")["design.md"] == "# Design"
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
class TestFlexibleIdsViaLoader:
|
|
268
|
-
def test_autodetect_resolves_slugged_zero_padded_dir(self, tmp_path):
|
|
262
|
+
def test_autodetect_resolves_slugged_dir(self, tmp_path):
|
|
269
263
|
unit_dir = tmp_path / "construction" / "unit-004-preset-management"
|
|
270
264
|
unit_dir.mkdir(parents=True)
|
|
271
265
|
(unit_dir / "design.md").write_text("# Design")
|
|
272
266
|
(tmp_path / "aidlc-state.md").write_text(
|
|
273
|
-
"# State\n\n- **Current Stage**: CONSTRUCTION — Unit 004 ADL-01.\n")
|
|
267
|
+
"# State\n\n- **Current Stage**: CONSTRUCTION — Unit unit-004-preset-management ADL-01.\n")
|
|
274
268
|
loader = AidlcDocumentLoader(docs_dir=tmp_path)
|
|
275
269
|
|
|
276
|
-
docs, subject = loader.resolve(argparse.Namespace(
|
|
270
|
+
docs, subject = loader.resolve(argparse.Namespace(aidlc_unit=None))
|
|
277
271
|
|
|
278
272
|
assert docs["design.md"] == "# Design"
|
|
279
|
-
assert subject == "unit
|
|
280
|
-
|
|
281
|
-
def test_plain_number_subject(self, tmp_path):
|
|
282
|
-
unit_dir = tmp_path / "construction" / "unit-4"
|
|
283
|
-
unit_dir.mkdir(parents=True)
|
|
284
|
-
(unit_dir / "design.md").write_text("# Design")
|
|
285
|
-
loader = AidlcDocumentLoader(docs_dir=tmp_path)
|
|
273
|
+
assert subject == "unit unit-004-preset-management"
|
|
286
274
|
|
|
287
|
-
|
|
275
|
+
def test_extension_registers_loader_and_flag(self):
|
|
276
|
+
reg = Registry()
|
|
277
|
+
AidlcExtension().register(reg)
|
|
288
278
|
|
|
289
|
-
assert
|
|
279
|
+
assert [l.name for l in reg.document_loaders] == ["ai-dlc"]
|
|
280
|
+
assert reg.cli_option_owners["--aidlc-unit"] == "ai-dlc"
|
|
281
|
+
assert reg.cli_dests["aidlc_unit"] == "ai-dlc"
|
|
@@ -120,11 +120,11 @@ class TestHelp:
|
|
|
120
120
|
with pytest.raises(SystemExit) as e:
|
|
121
121
|
main(["aidlc.yaml", "--help"])
|
|
122
122
|
assert e.value.code == 0
|
|
123
|
-
assert "--
|
|
123
|
+
assert "--aidlc-unit" in capsys.readouterr().out
|
|
124
124
|
|
|
125
125
|
with pytest.raises(SystemExit):
|
|
126
126
|
main(["pipeline.yaml", "--help"])
|
|
127
|
-
assert "--
|
|
127
|
+
assert "--aidlc-unit" not in capsys.readouterr().out
|
|
128
128
|
|
|
129
129
|
def test_no_pipeline_shows_core_surface(self, repo, capsys):
|
|
130
130
|
with pytest.raises(SystemExit) as e:
|
|
@@ -184,7 +184,7 @@ class TestHistoricalFixtures:
|
|
|
184
184
|
/ "unit-99").mkdir(parents=True)
|
|
185
185
|
with pytest.raises(RuntimeError, match="yielded no docs"):
|
|
186
186
|
main([str(consumer_repo / "agentic-dev-loop" / "pipeline.yaml"),
|
|
187
|
-
"--
|
|
187
|
+
"--aidlc-unit", "unit-99", "--no-viz"])
|
|
188
188
|
assert fake_agents.calls == []
|
|
189
189
|
|
|
190
190
|
|
|
@@ -81,28 +81,28 @@ class TestSelectLoader:
|
|
|
81
81
|
|
|
82
82
|
def test_flag_selects_its_loader(self):
|
|
83
83
|
plan = _Loader("plan", "--plan", "plan")
|
|
84
|
-
aidlc = _Loader("ai-dlc", "--
|
|
84
|
+
aidlc = _Loader("ai-dlc", "--aidlc-unit", "aidlc_unit", defaultable=True)
|
|
85
85
|
reg = self._registry_with(plan, aidlc)
|
|
86
86
|
|
|
87
|
-
assert select_loader(reg, _args(plan=Path("p.md"),
|
|
88
|
-
assert select_loader(reg, _args(plan=None,
|
|
87
|
+
assert select_loader(reg, _args(plan=Path("p.md"), aidlc_unit=None)) is plan
|
|
88
|
+
assert select_loader(reg, _args(plan=None, aidlc_unit="U3")) is aidlc
|
|
89
89
|
|
|
90
90
|
def test_flags_from_two_loaders_error_naming_both(self):
|
|
91
91
|
reg = self._registry_with(
|
|
92
92
|
_Loader("plan", "--plan", "plan"),
|
|
93
|
-
_Loader("ai-dlc", "--
|
|
93
|
+
_Loader("ai-dlc", "--aidlc-unit", "aidlc_unit", defaultable=True))
|
|
94
94
|
|
|
95
95
|
with pytest.raises(ValueError) as excinfo:
|
|
96
|
-
select_loader(reg, _args(plan=Path("p.md"),
|
|
96
|
+
select_loader(reg, _args(plan=Path("p.md"), aidlc_unit="U3"))
|
|
97
97
|
|
|
98
98
|
message = str(excinfo.value)
|
|
99
99
|
assert "plan" in message and "ai-dlc" in message
|
|
100
100
|
|
|
101
101
|
def test_sole_defaultable_autoruns_with_no_flag(self):
|
|
102
|
-
aidlc = _Loader("ai-dlc", "--
|
|
102
|
+
aidlc = _Loader("ai-dlc", "--aidlc-unit", "aidlc_unit", defaultable=True)
|
|
103
103
|
reg = self._registry_with(_Loader("plan", "--plan", "plan"), aidlc)
|
|
104
104
|
|
|
105
|
-
assert select_loader(reg, _args(plan=None,
|
|
105
|
+
assert select_loader(reg, _args(plan=None, aidlc_unit=None)) is aidlc
|
|
106
106
|
|
|
107
107
|
def test_none_defaultable_returns_none_with_no_flag(self):
|
|
108
108
|
reg = self._registry_with(_Loader("plan", "--plan", "plan"))
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""The suite must exercise the working tree, not an installed wheel.
|
|
2
|
+
|
|
3
|
+
This repo dogfoods its own published wheel in .venv (deliberate). Plain
|
|
4
|
+
pytest would otherwise import the published code and silently test stale
|
|
5
|
+
behaviour. This test pins the import to src/ — it fails the moment the
|
|
6
|
+
conftest sys.path insertion stops working.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import agentic_dev_loop
|
|
12
|
+
|
|
13
|
+
REPO_SRC = Path(__file__).resolve().parents[1] / "src"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_import_resolves_to_working_tree():
|
|
17
|
+
resolved = Path(agentic_dev_loop.__file__).resolve()
|
|
18
|
+
assert str(resolved).startswith(
|
|
19
|
+
str(REPO_SRC) + "/"
|
|
20
|
+
), (
|
|
21
|
+
f"agentic_dev_loop imported from {resolved}, not the working tree "
|
|
22
|
+
f"({REPO_SRC}) — conftest's src/ sys.path insertion is not effective"
|
|
23
|
+
)
|
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
"""AI-DLC document-loader extension. Loading IS choosing: name
|
|
2
|
-
`agentic_dev_loop.extensions.aidlc:AidlcExtension` in the pipeline's
|
|
3
|
-
`extensions:` list and the pipeline gains `--ai-dlc-unit` (any
|
|
4
|
-
unambiguous unit spelling: U4 / 4 / 004 / U004 / full slug) plus bare-run
|
|
5
|
-
autodetect of the current unit from `aidlc-docs/aidlc-state.md`.
|
|
6
|
-
|
|
7
|
-
Resolvers are verbatim ports of yosk's `resolve_unit_docs` /
|
|
8
|
-
`get_current_unit`, with `docs_dir` parameterised (was the module-level
|
|
9
|
-
AIDLC_DOCS_DIR) and the coding-standards merge dropped (the pipeline's
|
|
10
|
-
`additional_files:` now owns shared files).
|
|
11
|
-
"""
|
|
12
|
-
|
|
13
|
-
import argparse
|
|
14
|
-
import re
|
|
15
|
-
from pathlib import Path
|
|
16
|
-
|
|
17
|
-
_DOC_SUFFIXES = (".md", ".yaml", ".yml", ".txt")
|
|
18
|
-
_UNIT_DIR_NUMBER = re.compile(r"unit[-_](\d+)")
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def _leading_number(text: str) -> int | None:
|
|
22
|
-
"""Leading digits of a unit id (``"004-preset-management"`` -> 4), or
|
|
23
|
-
None for ids that do not start with a number."""
|
|
24
|
-
match = re.match(r"(\d+)", text)
|
|
25
|
-
return int(match.group(1)) if match else None
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def _dir_unit_number(dir_name: str) -> int | None:
|
|
29
|
-
"""Numeric prefix of a unit dir name (``"unit-004-preset-management"``
|
|
30
|
-
-> 4, ``"unit-14"`` -> 14)."""
|
|
31
|
-
match = _UNIT_DIR_NUMBER.match(dir_name.lower())
|
|
32
|
-
return int(match.group(1)) if match else None
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def resolve_unit_docs(docs_dir: Path | str, unit_id: str) -> dict[str, str]:
|
|
36
|
-
docs: dict[str, str] = {}
|
|
37
|
-
docs_dir = Path(docs_dir)
|
|
38
|
-
unit_id = unit_id.strip()
|
|
39
|
-
if unit_id[:1] in ("U", "u"):
|
|
40
|
-
unit_id = unit_id[1:]
|
|
41
|
-
|
|
42
|
-
unit_patterns = [f"unit-{unit_id}", f"unit_{unit_id}"]
|
|
43
|
-
number = _leading_number(unit_id)
|
|
44
|
-
construction_dir = docs_dir / "construction"
|
|
45
|
-
candidates: list[Path] = []
|
|
46
|
-
if construction_dir.is_dir():
|
|
47
|
-
for entry in sorted(construction_dir.iterdir()):
|
|
48
|
-
if not entry.is_dir():
|
|
49
|
-
continue
|
|
50
|
-
name = entry.name.lower()
|
|
51
|
-
if name == unit_patterns[0].lower() or name == unit_patterns[1].lower():
|
|
52
|
-
candidates.append(entry)
|
|
53
|
-
elif number is not None and _dir_unit_number(entry.name) == number:
|
|
54
|
-
candidates.append(entry)
|
|
55
|
-
|
|
56
|
-
candidates = sorted(set(candidates), key=lambda p: p.name.lower())
|
|
57
|
-
if len(candidates) > 1:
|
|
58
|
-
raise RuntimeError(
|
|
59
|
-
f"Ambiguous unit id '{unit_id}': multiple unit directories match "
|
|
60
|
-
f"({', '.join(p.name for p in candidates)}). Pass the full unit "
|
|
61
|
-
"slug to disambiguate.")
|
|
62
|
-
|
|
63
|
-
unit_dir = candidates[0] if candidates else None
|
|
64
|
-
if unit_dir is None:
|
|
65
|
-
look_for = ", ".join(unit_patterns)
|
|
66
|
-
if number is not None:
|
|
67
|
-
look_for += f", or any unit dir numbered {number}"
|
|
68
|
-
raise FileNotFoundError(
|
|
69
|
-
f"No docs directory found for unit {unit_id} under {construction_dir} "
|
|
70
|
-
f"(looked for {look_for})")
|
|
71
|
-
|
|
72
|
-
for file in sorted(unit_dir.rglob("*")):
|
|
73
|
-
if file.is_file() and file.suffix in _DOC_SUFFIXES:
|
|
74
|
-
key = str(file.relative_to(unit_dir))
|
|
75
|
-
docs[key] = file.read_text()
|
|
76
|
-
|
|
77
|
-
# At this point docs holds only unit-dir files — an empty load means the
|
|
78
|
-
# pipeline would build context from inception docs alone (the 2026-07-19
|
|
79
|
-
# U3 silent-degradation bug). Fail fast instead.
|
|
80
|
-
if not any(not k.startswith("inception/") for k in docs):
|
|
81
|
-
raise RuntimeError(
|
|
82
|
-
f"Unit dir {unit_dir} matched but yielded no docs — "
|
|
83
|
-
"refusing to build context from inception docs alone")
|
|
84
|
-
|
|
85
|
-
# Namespaced keys so an inception doc can never silently overwrite a
|
|
86
|
-
# unit doc with the same filename.
|
|
87
|
-
inception = docs_dir / "inception" / "application-design"
|
|
88
|
-
if inception.is_dir():
|
|
89
|
-
for file in sorted(inception.iterdir()):
|
|
90
|
-
if file.is_file() and file.suffix in (".md", ".txt"):
|
|
91
|
-
docs[f"inception/{file.name}"] = file.read_text()
|
|
92
|
-
|
|
93
|
-
# The unit's code-generation plans are the build spec — absent is fine,
|
|
94
|
-
# present is how the spec reaches agents.
|
|
95
|
-
plans_dir = docs_dir / "construction" / "plans"
|
|
96
|
-
if plans_dir.is_dir():
|
|
97
|
-
for file in sorted(plans_dir.glob(f"unit-{unit_id}-*-code-generation-plan.md")):
|
|
98
|
-
docs[f"plans/{file.name}"] = file.read_text()
|
|
99
|
-
|
|
100
|
-
return docs
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
def get_current_unit(docs_dir: Path | str) -> str:
|
|
104
|
-
"""Read the current unit from the aidlc-state.md Current Stage line."""
|
|
105
|
-
state_file = Path(docs_dir) / "aidlc-state.md"
|
|
106
|
-
if not state_file.is_file():
|
|
107
|
-
raise FileNotFoundError(f"aidlc-state file not found: {state_file}")
|
|
108
|
-
# Anchor to the Current Stage line — the file accumulates history, so an
|
|
109
|
-
# unanchored search could silently pick up an earlier "Unit N" mention.
|
|
110
|
-
match = re.search(r"Current Stage[^\n]*Unit (\d+)", state_file.read_text())
|
|
111
|
-
if not match:
|
|
112
|
-
raise RuntimeError(
|
|
113
|
-
f"Could not determine current unit from {state_file} "
|
|
114
|
-
"(no 'Unit N' in the Current Stage line)")
|
|
115
|
-
return f"U{match.group(1)}"
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
def _normalize_unit(unit: str) -> str:
|
|
119
|
-
unit = unit.strip()
|
|
120
|
-
if unit[:1] in ("U", "u"):
|
|
121
|
-
unit = unit[1:]
|
|
122
|
-
return f"U{unit}"
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
class AidlcDocumentLoader:
|
|
126
|
-
"""`--ai-dlc-unit` (any unambiguous spelling), or bare-run
|
|
127
|
-
autodetect from aidlc-state.md."""
|
|
128
|
-
|
|
129
|
-
name = "ai-dlc"
|
|
130
|
-
defaultable = True
|
|
131
|
-
|
|
132
|
-
def __init__(self, docs_dir: Path | str = "aidlc-docs"):
|
|
133
|
-
self.docs_dir = Path(docs_dir)
|
|
134
|
-
|
|
135
|
-
def add_cli_args(self, parser: argparse.ArgumentParser) -> None:
|
|
136
|
-
parser.add_argument("--ai-dlc-unit", metavar="U<N>", default=None,
|
|
137
|
-
help="AI-DLC unit ID: any unambiguous spelling "
|
|
138
|
-
"(U4, 4, 004, U004) or the full unit slug "
|
|
139
|
-
"(U004-preset-management); defaults to the "
|
|
140
|
-
"current unit in aidlc-docs/aidlc-state.md")
|
|
141
|
-
|
|
142
|
-
def resolve(self, args: argparse.Namespace) -> tuple[dict[str, str], str]:
|
|
143
|
-
unit = (_normalize_unit(args.ai_dlc_unit) if args.ai_dlc_unit
|
|
144
|
-
else get_current_unit(self.docs_dir))
|
|
145
|
-
return resolve_unit_docs(self.docs_dir, unit), f"unit {unit}"
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
class AidlcExtension:
|
|
149
|
-
name = "ai-dlc"
|
|
150
|
-
|
|
151
|
-
def register(self, registry) -> None:
|
|
152
|
-
registry.add_document_loader(AidlcDocumentLoader())
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-0.2.0 → agentic_dev_loop-0.3.0}/src/agentic_dev_loop/extensions/__init__.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
|
{agentic_dev_loop-0.2.0 → agentic_dev_loop-0.3.0}/src/agentic_dev_loop.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{agentic_dev_loop-0.2.0 → agentic_dev_loop-0.3.0}/src/agentic_dev_loop.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{agentic_dev_loop-0.2.0 → agentic_dev_loop-0.3.0}/src/agentic_dev_loop.egg-info/requires.txt
RENAMED
|
File without changes
|
{agentic_dev_loop-0.2.0 → agentic_dev_loop-0.3.0}/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
|