agentic-development-framework 0.1.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.
@@ -0,0 +1,146 @@
1
+ """Validated YAML configuration for the framework."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from enum import StrEnum
6
+ from pathlib import Path, PurePath
7
+ from typing import Any
8
+
9
+ import yaml
10
+ from pydantic import BaseModel, ConfigDict, Field, ValidationError, field_validator
11
+
12
+ from agentic_framework.exceptions import ConfigurationError
13
+
14
+
15
+ class StrictModel(BaseModel):
16
+ """Base model that rejects misspelled and unsupported configuration keys."""
17
+
18
+ model_config = ConfigDict(extra="forbid", validate_assignment=True)
19
+
20
+
21
+ class ProjectMode(StrEnum):
22
+ """How project discovery should treat the target directory."""
23
+
24
+ AUTO = "auto"
25
+ NEW = "new"
26
+ EXISTING = "existing"
27
+
28
+
29
+ class IdeKind(StrEnum):
30
+ """IDE harnesses known to the initial product specification."""
31
+
32
+ CODEX = "codex"
33
+ ANTIGRAVITY = "antigravity"
34
+ VSCODE = "vscode"
35
+
36
+
37
+ class LogLevel(StrEnum):
38
+ """Supported Python logging levels."""
39
+
40
+ DEBUG = "DEBUG"
41
+ INFO = "INFO"
42
+ WARNING = "WARNING"
43
+ ERROR = "ERROR"
44
+ CRITICAL = "CRITICAL"
45
+
46
+
47
+ class ProjectConfig(StrictModel):
48
+ """Project identification and discovery preferences."""
49
+
50
+ name: str | None = Field(default=None, min_length=1, max_length=100)
51
+ mode: ProjectMode = ProjectMode.AUTO
52
+
53
+ @field_validator("name")
54
+ @classmethod
55
+ def normalize_name(cls, value: str | None) -> str | None:
56
+ if value is None:
57
+ return None
58
+ normalized = value.strip()
59
+ if not normalized:
60
+ raise ValueError("project name cannot be blank")
61
+ return normalized
62
+
63
+
64
+ class RuntimeConfig(StrictModel):
65
+ """Local runtime storage and diagnostic preferences."""
66
+
67
+ directory: Path = Path(".agentic")
68
+ log_level: LogLevel = LogLevel.INFO
69
+
70
+ @field_validator("directory")
71
+ @classmethod
72
+ def require_safe_relative_path(cls, value: Path) -> Path:
73
+ path = PurePath(value)
74
+ if path.is_absolute():
75
+ raise ValueError("runtime directory must be project-relative")
76
+ if not path.parts or any(part == ".." for part in path.parts):
77
+ raise ValueError("runtime directory cannot traverse outside the project")
78
+ return Path(path)
79
+
80
+
81
+ class IdeConfig(StrictModel):
82
+ """IDE adapters requested for a project."""
83
+
84
+ enabled: list[IdeKind] = Field(default_factory=lambda: [IdeKind.CODEX])
85
+
86
+ @field_validator("enabled")
87
+ @classmethod
88
+ def require_unique_adapters(cls, value: list[IdeKind]) -> list[IdeKind]:
89
+ if len(value) != len(set(value)):
90
+ raise ValueError("IDE adapters must not be duplicated")
91
+ return value
92
+
93
+
94
+ class PolicyConfig(StrictModel):
95
+ """Conservative capability defaults for future execution phases."""
96
+
97
+ allow_network: bool = False
98
+ allow_destructive_operations: bool = False
99
+
100
+
101
+ class FrameworkConfig(StrictModel):
102
+ """Root configuration document."""
103
+
104
+ schema_version: int = Field(default=1, ge=1, le=1)
105
+ project: ProjectConfig = Field(default_factory=ProjectConfig)
106
+ runtime: RuntimeConfig = Field(default_factory=RuntimeConfig)
107
+ ide: IdeConfig = Field(default_factory=IdeConfig)
108
+ policies: PolicyConfig = Field(default_factory=PolicyConfig)
109
+
110
+
111
+ def parse_config(data: Any) -> FrameworkConfig:
112
+ """Validate decoded configuration data and present failures consistently."""
113
+
114
+ if data is None:
115
+ data = {}
116
+ if not isinstance(data, dict):
117
+ raise ConfigurationError("configuration root must be a YAML mapping")
118
+ try:
119
+ return FrameworkConfig.model_validate(data)
120
+ except ValidationError as exc:
121
+ raise ConfigurationError(f"configuration validation failed:\n{exc}") from exc
122
+
123
+
124
+ def load_config(path: Path) -> FrameworkConfig:
125
+ """Read and validate a UTF-8 YAML configuration file."""
126
+
127
+ try:
128
+ content = path.read_text(encoding="utf-8")
129
+ except FileNotFoundError as exc:
130
+ raise ConfigurationError(f"configuration file does not exist: {path}") from exc
131
+ except OSError as exc:
132
+ raise ConfigurationError(f"cannot read configuration file {path}: {exc}") from exc
133
+
134
+ try:
135
+ data = yaml.safe_load(content)
136
+ except yaml.YAMLError as exc:
137
+ raise ConfigurationError(f"invalid YAML in {path}: {exc}") from exc
138
+ return parse_config(data)
139
+
140
+
141
+ def dump_config(config: FrameworkConfig) -> str:
142
+ """Serialize a normalized configuration document as stable YAML."""
143
+
144
+ data = config.model_dump(mode="json")
145
+ return yaml.safe_dump(data, sort_keys=False)
146
+
@@ -0,0 +1,241 @@
1
+ """Safe, deterministic project initialization."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import subprocess
6
+ from collections.abc import Callable
7
+ from dataclasses import dataclass, field
8
+ from enum import StrEnum
9
+ from pathlib import Path
10
+
11
+ from agentic_framework.adapters import generated_project_files
12
+ from agentic_framework.core.config import (
13
+ FrameworkConfig,
14
+ ProjectConfig,
15
+ ProjectMode,
16
+ dump_config,
17
+ load_config,
18
+ )
19
+ from agentic_framework.core.project import ProjectInspection, inspect_project
20
+ from agentic_framework.exceptions import ConfigurationError, InitializationError
21
+ from agentic_framework.state import RuntimeStore
22
+
23
+ _DIRECTORIES = (
24
+ ".agentic/database",
25
+ ".agentic/state",
26
+ ".agentic/checkpoints",
27
+ ".agentic/logs",
28
+ ".agentic/reports",
29
+ ".agentic/proposals",
30
+ ".agents/rules",
31
+ ".agents/skills",
32
+ ".codex/agents",
33
+ ".github/agents",
34
+ ".github/instructions",
35
+ ".github/workflows",
36
+ "requirements/inbox",
37
+ "requirements/versions",
38
+ "requirements/reports",
39
+ "docs/architecture",
40
+ "docs/design",
41
+ "docs/decisions",
42
+ "docs/revisions",
43
+ "docs/reports",
44
+ "design-prototype",
45
+ "tests",
46
+ )
47
+
48
+ _AGENTS = """# Project agent instructions
49
+
50
+ This project uses the Agentic Development Framework (ADF).
51
+
52
+ - Read `.agentic/config.yaml` before framework operations.
53
+ - Preserve existing project files and unrelated working-tree changes.
54
+ - Do not expose secrets or perform destructive operations without explicit approval.
55
+ - Treat framework slash commands as workflow requests even when the IDE has no native
56
+ slash-command support.
57
+ - Use the `agentic-commands` skill for `/start`, `/status`, `/pause`, `/resume`, `/continue`,
58
+ `/stop`, `/review`, `/approve`, `/revise`, `/requirements`, and `/tasks`.
59
+ - Do not claim a task or phase is complete until its required checks pass.
60
+ """
61
+
62
+ _REQUIREMENTS_README = """# Project requirements
63
+
64
+ Place source requirement files in `inbox/`, then run `adt requirements ingest`. ADF copies every
65
+ source byte-for-byte into an immutable directory under `versions/`, detects duplicates using
66
+ SHA-256 hashes, and compares each new version with its predecessor. Files in `inbox/` are never
67
+ silently overwritten or removed.
68
+ """
69
+
70
+ _GITIGNORE = """# Agentic Development Framework local runtime data
71
+ .agentic/database/
72
+ .agentic/state/
73
+ .agentic/checkpoints/
74
+ .agentic/logs/
75
+ .agentic/proposals/
76
+ """
77
+
78
+
79
+ class FileStatus(StrEnum):
80
+ """Outcome of safely installing one generated file."""
81
+
82
+ CREATED = "created"
83
+ UNCHANGED = "unchanged"
84
+ CONFLICT = "conflict"
85
+
86
+
87
+ @dataclass(frozen=True, slots=True)
88
+ class FileResult:
89
+ """Outcome and optional proposal for one generated file."""
90
+
91
+ path: Path
92
+ status: FileStatus
93
+ proposal: Path | None = None
94
+
95
+
96
+ @dataclass(slots=True)
97
+ class InitializationResult:
98
+ """Complete result returned to CLI and tests."""
99
+
100
+ inspection: ProjectInspection
101
+ mode: ProjectMode
102
+ files: list[FileResult] = field(default_factory=list)
103
+ git_initialized: bool = False
104
+
105
+ @property
106
+ def conflicts(self) -> tuple[FileResult, ...]:
107
+ return tuple(item for item in self.files if item.status is FileStatus.CONFLICT)
108
+
109
+
110
+ def _proposal_path(root: Path, relative: Path) -> Path:
111
+ proposal_root = root / ".agentic" / "proposals"
112
+ return proposal_root / relative.parent / f"{relative.name}.proposed"
113
+
114
+
115
+ def _safe_install(root: Path, relative: str, content: str) -> FileResult:
116
+ relative_path = Path(relative)
117
+ destination = root / relative_path
118
+ if not destination.exists():
119
+ destination.parent.mkdir(parents=True, exist_ok=True)
120
+ destination.write_text(content, encoding="utf-8")
121
+ return FileResult(path=relative_path, status=FileStatus.CREATED)
122
+
123
+ try:
124
+ existing = destination.read_text(encoding="utf-8")
125
+ except (OSError, UnicodeError) as exc:
126
+ raise InitializationError(f"cannot compare existing file {destination}: {exc}") from exc
127
+ if existing == content:
128
+ return FileResult(path=relative_path, status=FileStatus.UNCHANGED)
129
+
130
+ proposal = _proposal_path(root, relative_path)
131
+ proposal.parent.mkdir(parents=True, exist_ok=True)
132
+ if not proposal.exists() or proposal.read_text(encoding="utf-8") != content:
133
+ proposal.write_text(content, encoding="utf-8")
134
+ return FileResult(
135
+ path=relative_path,
136
+ status=FileStatus.CONFLICT,
137
+ proposal=proposal.relative_to(root),
138
+ )
139
+
140
+
141
+ def _configuration(root: Path, inspection: ProjectInspection) -> tuple[FrameworkConfig, bool]:
142
+ existing = root / ".agentic" / "config.yaml"
143
+ if existing.is_file():
144
+ try:
145
+ return load_config(existing), True
146
+ except ConfigurationError:
147
+ pass
148
+ mode = ProjectMode.NEW if inspection.is_empty else ProjectMode.EXISTING
149
+ return FrameworkConfig(project=ProjectConfig(name=root.name, mode=mode)), False
150
+
151
+
152
+ def _run_git_init(root: Path) -> None:
153
+ try:
154
+ subprocess.run(
155
+ ["git", "init", "--quiet"],
156
+ cwd=root,
157
+ check=True,
158
+ capture_output=True,
159
+ text=True,
160
+ )
161
+ except FileNotFoundError as exc:
162
+ raise InitializationError("Git is not installed or is not available on PATH") from exc
163
+ except subprocess.CalledProcessError as exc:
164
+ detail = exc.stderr.strip() or "unknown Git error"
165
+ raise InitializationError(f"Git initialization failed: {detail}") from exc
166
+
167
+
168
+ def _report(result: InitializationResult) -> str:
169
+ inspection = result.inspection
170
+ technologies = ", ".join(inspection.technologies) or "none detected"
171
+ managers = ", ".join(inspection.package_managers) or "none detected"
172
+ if result.git_initialized:
173
+ git_state = "initialized"
174
+ elif inspection.has_git:
175
+ git_state = "detected"
176
+ else:
177
+ git_state = "skipped"
178
+ lines = [
179
+ "# ADF initialization report",
180
+ "",
181
+ f"- Project root: `{inspection.root}`",
182
+ f"- Project mode: `{result.mode.value}`",
183
+ f"- Git: {git_state}",
184
+ f"- Technologies: {technologies}",
185
+ f"- Package managers: {managers}",
186
+ f"- Conflicts: {len(result.conflicts)}",
187
+ ]
188
+ if result.conflicts:
189
+ lines.extend(["", "## Conflicts", ""])
190
+ lines.extend(
191
+ f"- `{item.path}` preserved; proposed content written to `{item.proposal}`"
192
+ for item in result.conflicts
193
+ )
194
+ return "\n".join(lines) + "\n"
195
+
196
+
197
+ def initialize_project(
198
+ target: Path,
199
+ *,
200
+ initialize_git: bool = True,
201
+ git_initializer: Callable[[Path], None] = _run_git_init,
202
+ ) -> InitializationResult:
203
+ """Initialize a target without overwriting existing project files."""
204
+
205
+ try:
206
+ inspection = inspect_project(target)
207
+ except (OSError, NotADirectoryError) as exc:
208
+ raise InitializationError(str(exc)) from exc
209
+
210
+ root = inspection.root
211
+ root.mkdir(parents=True, exist_ok=True)
212
+ config, existing_config_is_valid = _configuration(root, inspection)
213
+ result = InitializationResult(inspection=inspection, mode=config.project.mode)
214
+
215
+ for relative in _DIRECTORIES:
216
+ (root / relative).mkdir(parents=True, exist_ok=True)
217
+
218
+ if existing_config_is_valid:
219
+ result.files.append(
220
+ FileResult(path=Path(".agentic/config.yaml"), status=FileStatus.UNCHANGED)
221
+ )
222
+ else:
223
+ result.files.append(_safe_install(root, ".agentic/config.yaml", dump_config(config)))
224
+
225
+ generated = {
226
+ ".gitignore": _GITIGNORE,
227
+ "AGENTS.md": _AGENTS,
228
+ "requirements/README.md": _REQUIREMENTS_README,
229
+ }
230
+ generated.update(generated_project_files())
231
+ result.files.extend(_safe_install(root, path, content) for path, content in generated.items())
232
+
233
+ if initialize_git and not inspection.has_git:
234
+ git_initializer(root)
235
+ result.git_initialized = True
236
+
237
+ RuntimeStore(root).initialize()
238
+
239
+ report_path = root / ".agentic" / "reports" / "initialization.md"
240
+ report_path.write_text(_report(result), encoding="utf-8")
241
+ return result
@@ -0,0 +1,93 @@
1
+ """Project discovery used by safe initialization."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+
8
+ _TECHNOLOGY_MARKERS: dict[str, tuple[str, ...]] = {
9
+ "Docker": (
10
+ "Dockerfile",
11
+ "compose.yaml",
12
+ "compose.yml",
13
+ "docker-compose.yaml",
14
+ "docker-compose.yml",
15
+ ),
16
+ "Django": ("manage.py",),
17
+ "JavaScript": ("package.json",),
18
+ "Next.js": ("next.config.js", "next.config.mjs", "next.config.ts"),
19
+ "Python": ("pyproject.toml", "setup.py", "requirements.txt"),
20
+ }
21
+
22
+ _PACKAGE_MANAGER_MARKERS: dict[str, tuple[str, ...]] = {
23
+ "npm": ("package-lock.json",),
24
+ "pip": ("pyproject.toml", "requirements.txt", "setup.py"),
25
+ "pnpm": ("pnpm-lock.yaml",),
26
+ "Poetry": ("poetry.lock",),
27
+ "uv": ("uv.lock",),
28
+ "Yarn": ("yarn.lock",),
29
+ }
30
+
31
+ @dataclass(frozen=True, slots=True)
32
+ class ProjectInspection:
33
+ """Read-only facts discovered about an initialization target."""
34
+
35
+ root: Path
36
+ is_empty: bool
37
+ git_root: Path | None
38
+ technologies: tuple[str, ...]
39
+ package_managers: tuple[str, ...]
40
+
41
+ @property
42
+ def has_git(self) -> bool:
43
+ return self.git_root is not None
44
+
45
+
46
+ def _matching_labels(root: Path, markers: dict[str, tuple[str, ...]]) -> tuple[str, ...]:
47
+ excluded = {".agentic", ".git", ".venv", "node_modules"}
48
+
49
+ def exists(candidate: str) -> bool:
50
+ direct = root / candidate
51
+ if direct.exists():
52
+ return True
53
+ name = Path(candidate).name
54
+ for match in root.rglob(name):
55
+ relative = match.relative_to(root)
56
+ if len(relative.parts) <= 4 and not excluded.intersection(relative.parts):
57
+ return True
58
+ return False
59
+
60
+ return tuple(
61
+ label
62
+ for label, candidates in markers.items()
63
+ if any(exists(candidate) for candidate in candidates)
64
+ )
65
+
66
+
67
+ def find_git_root(root: Path) -> Path | None:
68
+ """Find a repository marker in the target or one of its parents."""
69
+
70
+ for candidate in (root, *root.parents):
71
+ if (candidate / ".git").exists():
72
+ return candidate
73
+ return None
74
+
75
+
76
+ def inspect_project(root: Path) -> ProjectInspection:
77
+ """Inspect a target without modifying it or invoking project tooling."""
78
+
79
+ resolved = root.expanduser().resolve()
80
+ if not resolved.exists():
81
+ is_empty = True
82
+ elif not resolved.is_dir():
83
+ raise NotADirectoryError(f"initialization target is not a directory: {resolved}")
84
+ else:
85
+ is_empty = next(resolved.iterdir(), None) is None
86
+
87
+ return ProjectInspection(
88
+ root=resolved,
89
+ is_empty=is_empty,
90
+ git_root=find_git_root(resolved),
91
+ technologies=_matching_labels(resolved, _TECHNOLOGY_MARKERS),
92
+ package_managers=_matching_labels(resolved, _PACKAGE_MANAGER_MARKERS),
93
+ )
@@ -0,0 +1,94 @@
1
+ """Persistent workflow lifecycle and recovery behavior."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from agentic_framework.adapters.generator import APPROVAL_STAGES, STAGES
10
+ from agentic_framework.exceptions import StateError
11
+ from agentic_framework.state import RuntimeStore
12
+
13
+
14
+ @dataclass(slots=True)
15
+ class WorkflowService:
16
+ """Apply validated lifecycle transitions to a project runtime."""
17
+
18
+ project_root: Path
19
+ store: RuntimeStore = field(init=False)
20
+
21
+ def __post_init__(self) -> None:
22
+ self.store = RuntimeStore(self.project_root)
23
+ if not self.store.database_path.is_file():
24
+ raise StateError(
25
+ f"runtime database does not exist; run adt init: {self.store.database_path}"
26
+ )
27
+
28
+ def status(self) -> dict[str, Any]:
29
+ return self.store.workflow()
30
+
31
+ def start(self) -> dict[str, Any]:
32
+ current = self.status()
33
+ if current["status"] == "paused":
34
+ raise StateError("workflow is paused; use adt resume")
35
+ if current["status"] != "active":
36
+ self.store.update_workflow("active", current["current_stage"])
37
+ return self.status()
38
+
39
+ def pause(self) -> dict[str, Any]:
40
+ current = self.status()
41
+ if current["status"] != "active":
42
+ raise StateError("only an active workflow can be paused")
43
+ self.store.update_workflow("paused", current["current_stage"])
44
+ self.store.create_checkpoint("pause")
45
+ return self.status()
46
+
47
+ def stop(self) -> dict[str, Any]:
48
+ current = self.status()
49
+ if current["status"] not in {"active", "paused"}:
50
+ raise StateError("only an active or paused workflow can be stopped")
51
+ self.store.update_workflow("stopped", current["current_stage"])
52
+ self.store.create_checkpoint("stop")
53
+ return self.status()
54
+
55
+ def resume(self) -> dict[str, Any]:
56
+ current = self.status()
57
+ if current["status"] not in {"paused", "stopped"}:
58
+ raise StateError("only a paused or stopped workflow can be resumed")
59
+ checkpoint = self.store.validate_latest_checkpoint()
60
+ if checkpoint is None:
61
+ raise StateError("cannot resume without a valid checkpoint")
62
+ checkpoint_stage = checkpoint["workflow"]["current_stage"]
63
+ if checkpoint_stage not in STAGES:
64
+ raise StateError(f"checkpoint contains an unknown workflow stage: {checkpoint_stage}")
65
+ self.store.update_workflow("active", checkpoint_stage)
66
+ return self.status()
67
+
68
+ def continue_workflow(self) -> dict[str, Any]:
69
+ current = self.status()
70
+ if current["status"] != "active":
71
+ raise StateError("workflow must be active before it can continue")
72
+ stage = current["current_stage"]
73
+ if stage not in STAGES:
74
+ raise StateError(f"runtime contains an unknown workflow stage: {stage}")
75
+ if stage in APPROVAL_STAGES and self.store.approval_status(stage) != "approved":
76
+ raise StateError(f"stage requires approval before continuing: {stage}")
77
+ index = STAGES.index(stage)
78
+ next_stage = STAGES[min(index + 1, len(STAGES) - 1)]
79
+ next_status = "completed" if next_stage == "completed" else "active"
80
+ self.store.update_workflow(next_status, next_stage)
81
+ self.store.create_checkpoint("stage_transition")
82
+ return self.status()
83
+
84
+ def approve(self, stage: str) -> None:
85
+ if stage not in STAGES:
86
+ raise StateError(f"unknown workflow stage: {stage}")
87
+ self.store.set_approval(stage, "approved")
88
+
89
+ def revise(self, stage: str, feedback: str) -> None:
90
+ if stage not in STAGES:
91
+ raise StateError(f"unknown workflow stage: {stage}")
92
+ if not feedback.strip():
93
+ raise StateError("revision feedback cannot be blank")
94
+ self.store.set_approval(stage, "revision_requested", feedback.strip())
@@ -0,0 +1,22 @@
1
+ """Framework-specific exceptions."""
2
+
3
+
4
+ class AgenticFrameworkError(Exception):
5
+ """Base class for expected framework errors."""
6
+
7
+
8
+ class ConfigurationError(AgenticFrameworkError):
9
+ """Raised when configuration cannot be read or validated."""
10
+
11
+
12
+ class InitializationError(AgenticFrameworkError):
13
+ """Raised when a target project cannot be initialized safely."""
14
+
15
+
16
+ class RequirementsError(AgenticFrameworkError):
17
+ """Raised when requirements cannot be versioned safely."""
18
+
19
+
20
+ class StateError(AgenticFrameworkError):
21
+ """Raised when persistent runtime state is invalid or a transition is unsafe."""
22
+
@@ -0,0 +1,70 @@
1
+ """Console and JSONL event logging utilities."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging
7
+ from datetime import UTC, datetime
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ _MANAGED_HANDLER = "_agentic_framework_handler"
12
+
13
+
14
+ class JsonLineFormatter(logging.Formatter):
15
+ """Render each log record as one machine-readable JSON object."""
16
+
17
+ def format(self, record: logging.LogRecord) -> str:
18
+ payload: dict[str, Any] = {
19
+ "timestamp": datetime.fromtimestamp(record.created, tz=UTC).isoformat(),
20
+ "level": record.levelname,
21
+ "logger": record.name,
22
+ "message": record.getMessage(),
23
+ }
24
+ event_data = getattr(record, "event_data", None)
25
+ if event_data:
26
+ payload["event"] = event_data
27
+ if record.exc_info:
28
+ payload["exception"] = self.formatException(record.exc_info)
29
+ return json.dumps(payload, ensure_ascii=False, separators=(",", ":"), default=str)
30
+
31
+
32
+ def _managed(handler: logging.Handler) -> logging.Handler:
33
+ setattr(handler, _MANAGED_HANDLER, True)
34
+ return handler
35
+
36
+
37
+ def configure_logging(
38
+ *,
39
+ level: str | int = logging.INFO,
40
+ event_log: Path | None = None,
41
+ ) -> logging.Logger:
42
+ """Configure ADF logging without disturbing handlers owned by the host application."""
43
+
44
+ logger = logging.getLogger("agentic_framework")
45
+ logger.setLevel(level)
46
+ logger.propagate = False
47
+
48
+ for handler in list(logger.handlers):
49
+ if getattr(handler, _MANAGED_HANDLER, False):
50
+ handler.close()
51
+ logger.removeHandler(handler)
52
+
53
+ console = _managed(logging.StreamHandler())
54
+ console.setFormatter(logging.Formatter("%(levelname)s %(name)s: %(message)s"))
55
+ logger.addHandler(console)
56
+
57
+ if event_log is not None:
58
+ event_log.parent.mkdir(parents=True, exist_ok=True)
59
+ jsonl = _managed(logging.FileHandler(event_log, encoding="utf-8"))
60
+ jsonl.setFormatter(JsonLineFormatter())
61
+ logger.addHandler(jsonl)
62
+
63
+ return logger
64
+
65
+
66
+ def log_event(logger: logging.Logger, message: str, **event_data: Any) -> None:
67
+ """Record a normal log message with optional structured event fields."""
68
+
69
+ logger.info(message, extra={"event_data": event_data})
70
+
@@ -0,0 +1,8 @@
1
+ """Requirements ingestion, immutable versioning, and comparison."""
2
+
3
+ from agentic_framework.requirements_manager.ingestion import (
4
+ IngestionResult,
5
+ RequirementsManager,
6
+ )
7
+
8
+ __all__ = ["IngestionResult", "RequirementsManager"]