agentic-dev-loop 0.2.0__py3-none-any.whl → 0.3.0__py3-none-any.whl

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.
@@ -1,13 +1,17 @@
1
1
  """AI-DLC document-loader extension. Loading IS choosing: name
2
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).
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.
11
15
  """
12
16
 
13
17
  import argparse
@@ -15,59 +19,29 @@ import re
15
19
  from pathlib import Path
16
20
 
17
21
  _DOC_SUFFIXES = (".md", ".yaml", ".yml", ".txt")
18
- _UNIT_DIR_NUMBER = re.compile(r"unit[-_](\d+)")
19
22
 
20
23
 
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]:
24
+ def resolve_unit_docs(docs_dir: Path | str, unit_slug: str) -> dict[str, str]:
36
25
  docs: dict[str, str] = {}
37
26
  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:]
27
+ unit_slug = unit_slug.strip()
41
28
 
42
- unit_patterns = [f"unit-{unit_id}", f"unit_{unit_id}"]
43
- number = _leading_number(unit_id)
44
29
  construction_dir = docs_dir / "construction"
45
- candidates: list[Path] = []
30
+ unit_dir: Path | None = None
46
31
  if construction_dir.is_dir():
47
32
  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.")
33
+ if entry.is_dir() and entry.name == unit_slug:
34
+ unit_dir = entry
35
+ break
62
36
 
63
- unit_dir = candidates[0] if candidates else None
64
37
  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}"
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)})"
68
42
  raise FileNotFoundError(
69
- f"No docs directory found for unit {unit_id} under {construction_dir} "
70
- f"(looked for {look_for})")
43
+ f"No unit directory named '{unit_slug}' under {construction_dir}"
44
+ f"{available or ' (no unit directories found)'}")
71
45
 
72
46
  for file in sorted(unit_dir.rglob("*")):
73
47
  if file.is_file() and file.suffix in _DOC_SUFFIXES:
@@ -94,37 +68,35 @@ def resolve_unit_docs(docs_dir: Path | str, unit_id: str) -> dict[str, str]:
94
68
  # present is how the spec reaches agents.
95
69
  plans_dir = docs_dir / "construction" / "plans"
96
70
  if plans_dir.is_dir():
97
- for file in sorted(plans_dir.glob(f"unit-{unit_id}-*-code-generation-plan.md")):
71
+ for file in sorted(plans_dir.glob(f"{unit_slug}-*-code-generation-plan.md")):
98
72
  docs[f"plans/{file.name}"] = file.read_text()
99
73
 
100
74
  return docs
101
75
 
102
76
 
103
77
  def get_current_unit(docs_dir: Path | str) -> str:
104
- """Read the current unit from the aidlc-state.md Current Stage line."""
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
+ """
105
84
  state_file = Path(docs_dir) / "aidlc-state.md"
106
85
  if not state_file.is_file():
107
86
  raise FileNotFoundError(f"aidlc-state file not found: {state_file}")
108
87
  # Anchor to the Current Stage line — the file accumulates history, so an
109
88
  # 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())
89
+ match = re.search(r"Current Stage[^\n]*Unit ([\w-]+(?:\.[\w-]+)*)", state_file.read_text())
111
90
  if not match:
112
91
  raise RuntimeError(
113
92
  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}"
93
+ "(no 'Unit <slug>' in the Current Stage line)")
94
+ return match.group(1)
123
95
 
124
96
 
125
97
  class AidlcDocumentLoader:
126
- """`--ai-dlc-unit` (any unambiguous spelling), or bare-run
127
- autodetect from aidlc-state.md."""
98
+ """`--aidlc-unit` (a unit directory name, matched verbatim), or bare-run
99
+ autodetect from the Current Stage line of aidlc-state.md."""
128
100
 
129
101
  name = "ai-dlc"
130
102
  defaultable = True
@@ -133,15 +105,15 @@ class AidlcDocumentLoader:
133
105
  self.docs_dir = Path(docs_dir)
134
106
 
135
107
  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")
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")
141
114
 
142
115
  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))
116
+ unit = args.aidlc_unit or get_current_unit(self.docs_dir)
145
117
  return resolve_unit_docs(self.docs_dir, unit), f"unit {unit}"
146
118
 
147
119
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentic-dev-loop
3
- Version: 0.2.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 --ai-dlc-unit U2
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
- `--ai-dlc-unit` accepts any unambiguous spelling of a unit number — `U2`,
88
- `2`, `U002`, `002` — or the full unit slug (`U002-custom-configuration`).
89
- If multiple unit directories match one id, the run fails loudly naming
90
- every candidate; pass the full slug to disambiguate.
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. Unit ids resolve by
262
- exact directory name or by numeric prefix (`unit-004-preset-management`
263
- answers to `4`, `004`, `U4`, `U004`, or its full slug); ambiguous ids fail
264
- loud listing all matches. A selected loader that
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 `--ai-dlc-unit` flag to those docs.
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/`),
@@ -14,10 +14,10 @@ agentic_dev_loop/results.py,sha256=wVB8k9dDgiLbfGxR7N2gaeX4eAMz6bIj19JRjXfe84E,1
14
14
  agentic_dev_loop/stages.py,sha256=Ku9aku90T1S6MPn_wqgZNqLCswkGyQu5LQtwYTHmw1g,19109
15
15
  agentic_dev_loop/viz.py,sha256=eMgfLBEssorTygdzNDqy1VLVxqw4uWjyxLJtcBBQWSI,42633
16
16
  agentic_dev_loop/extensions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
- agentic_dev_loop/extensions/aidlc.py,sha256=sE1DqJjRxfnvkiefyMwWjn1VaYJoA6Ufab385sscHQg,6195
18
- agentic_dev_loop-0.2.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
19
- agentic_dev_loop-0.2.0.dist-info/METADATA,sha256=n3C_ChRzWVaL6Rgl0-NiGhcmhxBm2q0KZh9dgOw2i-8,14833
20
- agentic_dev_loop-0.2.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
21
- agentic_dev_loop-0.2.0.dist-info/entry_points.txt,sha256=9i9TjC5AHjEfv7nM6RFdf06Okr9cj9Cgdfrd7OWSg5g,63
22
- agentic_dev_loop-0.2.0.dist-info/top_level.txt,sha256=n1ktP1gXxGN6DJAnACR0mt276GRJB9PVDezf9DVPwaw,17
23
- agentic_dev_loop-0.2.0.dist-info/RECORD,,
17
+ agentic_dev_loop/extensions/aidlc.py,sha256=S9sUISIwjGJ9HOC3JFnrnPVHcxvW6E2kYkjdkILePH8,5277
18
+ agentic_dev_loop-0.3.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
19
+ agentic_dev_loop-0.3.0.dist-info/METADATA,sha256=jUdGT4sIvnUWyhdRQdH-egLUASQX0TauvwsqZYPz3wY,14937
20
+ agentic_dev_loop-0.3.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
21
+ agentic_dev_loop-0.3.0.dist-info/entry_points.txt,sha256=9i9TjC5AHjEfv7nM6RFdf06Okr9cj9Cgdfrd7OWSg5g,63
22
+ agentic_dev_loop-0.3.0.dist-info/top_level.txt,sha256=n1ktP1gXxGN6DJAnACR0mt276GRJB9PVDezf9DVPwaw,17
23
+ agentic_dev_loop-0.3.0.dist-info/RECORD,,