a2c-core 0.2.1__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.
Files changed (49) hide show
  1. a2c_core/__init__.py +3 -0
  2. a2c_core/contracts/__init__.py +61 -0
  3. a2c_core/contracts/id_format.py +40 -0
  4. a2c_core/contracts/inventory.py +60 -0
  5. a2c_core/contracts/naming.py +40 -0
  6. a2c_core/contracts/paths.py +48 -0
  7. a2c_core/contracts/task_ids.py +31 -0
  8. a2c_core/errors/__init__.py +62 -0
  9. a2c_core/errors/codes.py +43 -0
  10. a2c_core/errors/issues.py +24 -0
  11. a2c_core/schemas/__init__.py +29 -0
  12. a2c_core/schemas/artifacts.py +54 -0
  13. a2c_core/schemas/config.py +74 -0
  14. a2c_core/schemas/metadata.py +45 -0
  15. a2c_core/schemas/results.py +71 -0
  16. a2c_core/schemas/workflows_io.py +108 -0
  17. a2c_core/services/__init__.py +78 -0
  18. a2c_core/services/discovery.py +23 -0
  19. a2c_core/services/doctor.py +138 -0
  20. a2c_core/services/drafts.py +176 -0
  21. a2c_core/services/epic_drafts.py +155 -0
  22. a2c_core/services/frontmatter.py +130 -0
  23. a2c_core/services/inspection.py +147 -0
  24. a2c_core/services/loader.py +304 -0
  25. a2c_core/services/repo.py +23 -0
  26. a2c_core/services/repository.py +22 -0
  27. a2c_core/services/serialize.py +57 -0
  28. a2c_core/services/task_intake_drafts.py +181 -0
  29. a2c_core/services/validation.py +267 -0
  30. a2c_core/workflows/__init__.py +35 -0
  31. a2c_core/workflows/create_epic.py +356 -0
  32. a2c_core/workflows/create_task.py +570 -0
  33. a2c_core/workflows/decompose_epic.py +406 -0
  34. a2c_core/workflows/json_extract.py +38 -0
  35. a2c_core/workflows/prompting.py +126 -0
  36. a2c_core/workflows/prompts/epic-create.md +30 -0
  37. a2c_core/workflows/prompts/epic-decompose.md +42 -0
  38. a2c_core/workflows/prompts/task-create-bug.md +31 -0
  39. a2c_core/workflows/prompts/task-create.md +30 -0
  40. a2c_core/workflows/provider_errors.py +16 -0
  41. a2c_core/workflows/providers/__init__.py +47 -0
  42. a2c_core/workflows/providers/base.py +58 -0
  43. a2c_core/workflows/providers/mock.py +203 -0
  44. a2c_core/workflows/providers/openai_compatible.py +267 -0
  45. a2c_core/workflows/providers/resolver.py +257 -0
  46. a2c_core-0.2.1.dist-info/METADATA +42 -0
  47. a2c_core-0.2.1.dist-info/RECORD +49 -0
  48. a2c_core-0.2.1.dist-info/WHEEL +5 -0
  49. a2c_core-0.2.1.dist-info/top_level.txt +1 -0
a2c_core/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """A2C engine — domain logic, schemas, services, and workflow orchestration."""
2
+
3
+ __version__ = "0.2.1"
@@ -0,0 +1,61 @@
1
+ """Repository artifact contracts — paths, naming, inventory."""
2
+
3
+ from a2c_core.contracts.inventory import M2_ARTIFACTS, ArtifactSpec
4
+ from a2c_core.contracts.naming import (
5
+ ADR_FILENAME_PATTERN,
6
+ ADR_STATUS_PATTERN,
7
+ EPIC_ID_PATTERN,
8
+ SPRINT_ID_PATTERN,
9
+ TASK_ID_PATTERN,
10
+ WORKFLOW_ID_PATTERN,
11
+ is_valid_adr_filename,
12
+ is_valid_epic_id,
13
+ is_valid_sprint_id,
14
+ is_valid_task_id,
15
+ is_valid_workflow_id,
16
+ parse_adr_filename,
17
+ )
18
+ from a2c_core.contracts.paths import (
19
+ A2C_DIR,
20
+ ADR_DIR,
21
+ CONFIG_FILE,
22
+ DOCS_DIR,
23
+ EPICS_DIR,
24
+ PLANNING_DIR,
25
+ ROOT_MARKERS,
26
+ SPRINTS_DIR,
27
+ TASKS_DIR,
28
+ WORKFLOW_MANIFEST,
29
+ )
30
+
31
+ __all__ = [
32
+ "A2C_DIR",
33
+ "ADR_DIR",
34
+ "ADR_FILENAME_PATTERN",
35
+ "ADR_STATUS_PATTERN",
36
+ "ARTIFACT_SCHEMA_VERSION",
37
+ "CONFIG_FILE",
38
+ "CONFIG_SCHEMA_VERSION",
39
+ "DOCS_DIR",
40
+ "EPICS_DIR",
41
+ "EPIC_ID_PATTERN",
42
+ "M2_ARTIFACTS",
43
+ "PLANNING_DIR",
44
+ "ROOT_MARKERS",
45
+ "SPRINTS_DIR",
46
+ "SPRINT_ID_PATTERN",
47
+ "TASKS_DIR",
48
+ "TASK_ID_PATTERN",
49
+ "WORKFLOW_ID_PATTERN",
50
+ "WORKFLOW_MANIFEST",
51
+ "ArtifactSpec",
52
+ "is_valid_adr_filename",
53
+ "is_valid_epic_id",
54
+ "is_valid_sprint_id",
55
+ "is_valid_task_id",
56
+ "is_valid_workflow_id",
57
+ "parse_adr_filename",
58
+ ]
59
+
60
+ # Re-export version constants from paths
61
+ from a2c_core.contracts.paths import ARTIFACT_SCHEMA_VERSION, CONFIG_SCHEMA_VERSION # noqa: E402
@@ -0,0 +1,40 @@
1
+ """Planning artifact ID shape and configurable format validation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+ # Shipped default when id_format.enabled is true and pattern is omitted.
8
+ DEFAULT_ID_FORMAT_PATTERN = r"^[A-Z][A-Z0-9]{1,9}-[0-9]+$"
9
+
10
+ # Loose Jira-like recommendation for non-fatal warnings when strict mode is off.
11
+ RECOMMENDED_ID_SHAPE_PATTERN = r"^[A-Z][A-Z0-9]+-[0-9]+$"
12
+
13
+ _RECOMMENDED_SHAPE = re.compile(RECOMMENDED_ID_SHAPE_PATTERN)
14
+
15
+
16
+ def compile_id_pattern(pattern: str) -> re.Pattern[str]:
17
+ """Compile a configured ID format pattern."""
18
+ return re.compile(pattern)
19
+
20
+
21
+ def id_matches_pattern(artifact_id: str, pattern: str) -> bool:
22
+ """Return True when artifact_id matches the configured regex pattern."""
23
+ return bool(compile_id_pattern(pattern).fullmatch(artifact_id))
24
+
25
+
26
+ def is_recommended_id_shape(artifact_id: str) -> bool:
27
+ """Return True when artifact_id matches the documented Jira-like recommendation."""
28
+ return bool(_RECOMMENDED_SHAPE.fullmatch(artifact_id))
29
+
30
+
31
+ def looks_like_odd_id(artifact_id: str) -> bool:
32
+ """Heuristic for IDs that may deserve a shape warning (spaces, empty, etc.)."""
33
+ stripped = artifact_id.strip()
34
+ if not stripped:
35
+ return True
36
+ if stripped != artifact_id:
37
+ return True
38
+ if " " in artifact_id:
39
+ return True
40
+ return not is_recommended_id_shape(artifact_id)
@@ -0,0 +1,60 @@
1
+ """Supported artifact inventory for Milestone 2."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+
8
+ @dataclass(frozen=True, slots=True)
9
+ class ArtifactSpec:
10
+ kind: str
11
+ relative_dir: str
12
+ filename_pattern: str
13
+ schema_file: str
14
+ model: str
15
+
16
+
17
+ M2_ARTIFACTS: tuple[ArtifactSpec, ...] = (
18
+ ArtifactSpec(
19
+ kind="config",
20
+ relative_dir=".a2c",
21
+ filename_pattern="config.yaml",
22
+ schema_file="config.schema.json",
23
+ model="a2c_core.schemas.config.A2CConfig",
24
+ ),
25
+ ArtifactSpec(
26
+ kind="workflow_manifest",
27
+ relative_dir="docs",
28
+ filename_pattern="workflow-manifest.yaml",
29
+ schema_file="workflow-manifest.schema.json",
30
+ model="a2c_core.schemas.metadata.WorkflowManifest",
31
+ ),
32
+ ArtifactSpec(
33
+ kind="epic",
34
+ relative_dir="planning/epics",
35
+ filename_pattern="*.md",
36
+ schema_file="epic.schema.json",
37
+ model="a2c_core.schemas.artifacts.Epic",
38
+ ),
39
+ ArtifactSpec(
40
+ kind="task",
41
+ relative_dir="planning/tasks",
42
+ filename_pattern="*.md",
43
+ schema_file="task.schema.json",
44
+ model="a2c_core.schemas.artifacts.Task",
45
+ ),
46
+ ArtifactSpec(
47
+ kind="sprint",
48
+ relative_dir="planning/sprints",
49
+ filename_pattern="*.md",
50
+ schema_file="sprint.schema.json",
51
+ model="a2c_core.schemas.artifacts.Sprint",
52
+ ),
53
+ ArtifactSpec(
54
+ kind="adr",
55
+ relative_dir="docs/adr",
56
+ filename_pattern="NNNN-*.md",
57
+ schema_file="",
58
+ model="a2c_core.schemas.metadata.AdrRecord",
59
+ ),
60
+ )
@@ -0,0 +1,40 @@
1
+ """Identifier and filename conventions (contract C2)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+ WORKFLOW_ID_PATTERN = re.compile(r"^[a-z][a-z0-9_]*$")
8
+ EPIC_ID_PATTERN = re.compile(r"^epic-[a-z0-9]+(?:-[a-z0-9]+)*$")
9
+ TASK_ID_PATTERN = re.compile(r"^task-[a-z0-9]+(?:-[a-z0-9]+)*$")
10
+ SPRINT_ID_PATTERN = re.compile(r"^sprint-[a-z0-9]+(?:-[a-z0-9]+)*$")
11
+ ADR_FILENAME_PATTERN = re.compile(r"^\d{4}-[a-z0-9]+(?:-[a-z0-9]+)*\.md$")
12
+ ADR_STATUS_PATTERN = re.compile(r"^\s*(?:-\s*)?\*\*Status:\*\*\s*(.+)$", re.MULTILINE)
13
+
14
+
15
+ def is_valid_workflow_id(value: str) -> bool:
16
+ return bool(WORKFLOW_ID_PATTERN.fullmatch(value))
17
+
18
+
19
+ def is_valid_epic_id(value: str) -> bool:
20
+ return bool(EPIC_ID_PATTERN.fullmatch(value))
21
+
22
+
23
+ def is_valid_task_id(value: str) -> bool:
24
+ return bool(TASK_ID_PATTERN.fullmatch(value))
25
+
26
+
27
+ def is_valid_sprint_id(value: str) -> bool:
28
+ return bool(SPRINT_ID_PATTERN.fullmatch(value))
29
+
30
+
31
+ def is_valid_adr_filename(name: str) -> bool:
32
+ return bool(ADR_FILENAME_PATTERN.fullmatch(name))
33
+
34
+
35
+ def parse_adr_filename(name: str) -> tuple[int, str] | None:
36
+ if not is_valid_adr_filename(name):
37
+ return None
38
+ number_text, slug_with_ext = name.split("-", 1)
39
+ slug = slug_with_ext.removesuffix(".md")
40
+ return int(number_text), slug
@@ -0,0 +1,48 @@
1
+ """Repository path conventions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ # Tool-internal storage (config, caches, indexes — not human-facing planning artifacts)
8
+ A2C_DIR = ".a2c"
9
+ CONFIG_FILE = Path(A2C_DIR) / "config.yaml"
10
+ A2C_CACHE_DIR = Path(A2C_DIR) / "cache"
11
+ DECOMPOSE_EPIC_CACHE_DIR = A2C_CACHE_DIR / "decompose-epic"
12
+ CREATE_EPIC_CACHE_DIR = A2C_CACHE_DIR / "create-epic"
13
+ CREATE_TASK_CACHE_DIR = A2C_CACHE_DIR / "create-task"
14
+ A2C_INDEXES_DIR = Path(A2C_DIR) / "indexes"
15
+
16
+ # Human-facing planning artifacts (Markdown + YAML front matter)
17
+ PLANNING_DIR = Path("planning")
18
+ EPICS_DIR = PLANNING_DIR / "epics"
19
+ TASKS_DIR = PLANNING_DIR / "tasks"
20
+ SPRINTS_DIR = PLANNING_DIR / "sprints"
21
+
22
+ PLANNING_ARTIFACT_EXTENSION = ".md"
23
+ LEGACY_PLANNING_ARTIFACT_EXTENSION = ".yaml"
24
+
25
+ # Deprecated location — transitional loader support only
26
+ LEGACY_PLANNING_DIR = Path(A2C_DIR) / "planning"
27
+ LEGACY_EPICS_DIR = LEGACY_PLANNING_DIR / "epics"
28
+ LEGACY_TASKS_DIR = LEGACY_PLANNING_DIR / "tasks"
29
+ LEGACY_SPRINTS_DIR = LEGACY_PLANNING_DIR / "sprints"
30
+
31
+ DOCS_DIR = Path("docs")
32
+ WORKFLOW_MANIFEST = DOCS_DIR / "workflow-manifest.yaml"
33
+ ADR_DIR = DOCS_DIR / "adr"
34
+
35
+ # Markers used to recognize an A2C-governed repository root
36
+ ROOT_MARKERS = (
37
+ Path("AGENTS.md"),
38
+ WORKFLOW_MANIFEST,
39
+ CONFIG_FILE,
40
+ )
41
+
42
+ ARTIFACT_SCHEMA_VERSION = 1
43
+ CONFIG_SCHEMA_VERSION = 1
44
+
45
+
46
+ def planning_artifact_path(directory: Path, artifact_id: str) -> str:
47
+ """Canonical relative path for a planning artifact Markdown file."""
48
+ return str(directory / f"{artifact_id}{PLANNING_ARTIFACT_EXTENSION}")
@@ -0,0 +1,31 @@
1
+ """Task ID helpers for single-task intake."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+ PENDING_TASK_ID = "draft-pending"
8
+ _SLUG_WORD_RE = re.compile(r"[a-z0-9]+")
9
+
10
+
11
+ def slug_from_text(text: str, *, max_parts: int = 5) -> str:
12
+ words = _SLUG_WORD_RE.findall(text.lower())
13
+ if not words:
14
+ return "task"
15
+ return "-".join(words[:max_parts])
16
+
17
+
18
+ def draft_key_from_title(title: str) -> str:
19
+ """Stable cache key for a task intake draft (not the final task id)."""
20
+ return slug_from_text(title)
21
+
22
+
23
+ def allocate_task_id(title: str, *, existing_ids: set[str]) -> str:
24
+ """Allocate the next collision-free task id from a title slug."""
25
+ base = f"task-{slug_from_text(title)}"
26
+ candidate = base
27
+ suffix = 2
28
+ while candidate in existing_ids:
29
+ candidate = f"{base}-{suffix}"
30
+ suffix += 1
31
+ return candidate
@@ -0,0 +1,62 @@
1
+ """Typed errors and structured issue records."""
2
+
3
+ from a2c_core.errors.codes import (
4
+ A2C_CONFIG_001,
5
+ A2C_FORMAT_001,
6
+ A2C_FORMAT_002,
7
+ A2C_ID_001,
8
+ A2C_ID_002,
9
+ A2C_ID_003,
10
+ A2C_NAME_001,
11
+ A2C_NAME_002,
12
+ A2C_NAME_003,
13
+ A2C_NAME_004,
14
+ A2C_PROVIDER_001,
15
+ A2C_PROVIDER_002,
16
+ A2C_PROVIDER_003,
17
+ A2C_PROVIDER_004,
18
+ A2C_PROVIDER_005,
19
+ A2C_REPO_001,
20
+ A2C_REPO_002,
21
+ A2C_REPO_003,
22
+ A2C_SCHEMA_001,
23
+ A2C_SCHEMA_002,
24
+ A2C_SCHEMA_003,
25
+ A2C_SCHEMA_004,
26
+ A2C_WORKFLOW_001,
27
+ A2C_WORKFLOW_002,
28
+ A2C_WORKFLOW_003,
29
+ A2C_WORKFLOW_004,
30
+ )
31
+ from a2c_core.errors.issues import Severity, ValidationIssue
32
+
33
+ __all__ = [
34
+ "A2C_CONFIG_001",
35
+ "A2C_FORMAT_001",
36
+ "A2C_FORMAT_002",
37
+ "A2C_ID_001",
38
+ "A2C_ID_002",
39
+ "A2C_ID_003",
40
+ "A2C_NAME_001",
41
+ "A2C_NAME_002",
42
+ "A2C_NAME_003",
43
+ "A2C_NAME_004",
44
+ "A2C_REPO_001",
45
+ "A2C_REPO_002",
46
+ "A2C_REPO_003",
47
+ "A2C_SCHEMA_001",
48
+ "A2C_SCHEMA_002",
49
+ "A2C_SCHEMA_003",
50
+ "A2C_SCHEMA_004",
51
+ "A2C_WORKFLOW_001",
52
+ "A2C_WORKFLOW_002",
53
+ "A2C_WORKFLOW_003",
54
+ "A2C_WORKFLOW_004",
55
+ "A2C_PROVIDER_001",
56
+ "A2C_PROVIDER_002",
57
+ "A2C_PROVIDER_003",
58
+ "A2C_PROVIDER_004",
59
+ "A2C_PROVIDER_005",
60
+ "Severity",
61
+ "ValidationIssue",
62
+ ]
@@ -0,0 +1,43 @@
1
+ """Stable A2C error code constants (contract C2)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ # Schema / parsing
6
+ A2C_SCHEMA_001 = "A2C_SCHEMA_001" # invalid document shape
7
+ A2C_SCHEMA_002 = "A2C_SCHEMA_002" # unsupported document version
8
+ A2C_SCHEMA_003 = "A2C_SCHEMA_003" # YAML/JSON parse failure
9
+ A2C_FORMAT_001 = "A2C_FORMAT_001" # legacy pure-YAML planning artifact
10
+ A2C_FORMAT_002 = "A2C_FORMAT_002" # deprecated .a2c/planning/ location
11
+
12
+ # Naming / identifiers
13
+ A2C_NAME_001 = "A2C_NAME_001" # invalid artifact id
14
+ A2C_NAME_002 = "A2C_NAME_002" # invalid ADR filename
15
+ A2C_NAME_003 = "A2C_NAME_003" # invalid workflow id
16
+ A2C_NAME_004 = "A2C_NAME_004" # filename does not match artifact id (warning)
17
+
18
+ # Planning artifact IDs
19
+ A2C_ID_001 = "A2C_ID_001" # id does not match configured pattern
20
+ A2C_ID_002 = "A2C_ID_002" # duplicate planning artifact id
21
+ A2C_ID_003 = "A2C_ID_003" # recommended id shape suggestion (warning)
22
+ A2C_SCHEMA_004 = "A2C_SCHEMA_004" # planning artifact type mismatch
23
+
24
+ # Repository layout
25
+ A2C_REPO_001 = "A2C_REPO_001" # not an A2C repository root
26
+ A2C_REPO_002 = "A2C_REPO_002" # required artifact missing
27
+ A2C_REPO_003 = "A2C_REPO_003" # artifact reference broken
28
+
29
+ # Config
30
+ A2C_CONFIG_001 = "A2C_CONFIG_001" # invalid config file
31
+
32
+ # AI workflow / decomposition
33
+ A2C_WORKFLOW_001 = "A2C_WORKFLOW_001" # no valid proposal available
34
+ A2C_WORKFLOW_002 = "A2C_WORKFLOW_002" # invalid AI / proposal structure
35
+ A2C_WORKFLOW_003 = "A2C_WORKFLOW_003" # task id conflict on apply
36
+ A2C_WORKFLOW_004 = "A2C_WORKFLOW_004" # epic not found for decomposition
37
+
38
+ # LLM provider integration
39
+ A2C_PROVIDER_001 = "A2C_PROVIDER_001" # provider misconfiguration
40
+ A2C_PROVIDER_002 = "A2C_PROVIDER_002" # provider HTTP / transport failure
41
+ A2C_PROVIDER_003 = "A2C_PROVIDER_003" # provider timeout
42
+ A2C_PROVIDER_004 = "A2C_PROVIDER_004" # unknown provider name
43
+ A2C_PROVIDER_005 = "A2C_PROVIDER_005" # malformed model response
@@ -0,0 +1,24 @@
1
+ """Structured validation and service issues."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Literal
7
+
8
+ Severity = Literal["error", "warning"]
9
+
10
+
11
+ @dataclass(frozen=True, slots=True)
12
+ class ValidationIssue:
13
+ """Machine-consumable issue for CLI/TUI presentation."""
14
+
15
+ code: str
16
+ message: str
17
+ path: str | None = None
18
+ severity: Severity = "error"
19
+
20
+ def to_dict(self) -> dict[str, str]:
21
+ data: dict[str, str] = {"code": self.code, "message": self.message}
22
+ if self.path is not None:
23
+ data["path"] = self.path
24
+ return data
@@ -0,0 +1,29 @@
1
+ """Pydantic models for A2C repository artifacts and results."""
2
+
3
+ from a2c_core.schemas.artifacts import Epic, EpicStatus, Sprint, SprintStatus, Task, TaskStatus
4
+ from a2c_core.schemas.config import A2CConfig, CoreSettings, LogLevel, WorkflowSettings
5
+ from a2c_core.schemas.metadata import AdrRecord, ProfilePointer, WorkflowManifest, WorkflowOwner
6
+ from a2c_core.schemas.results import CliResultEnvelope, ServiceResult
7
+ from a2c_core.schemas.workflows_io import ApprovedProposal, CommitPlanItem, CommitPlanProposal
8
+
9
+ __all__ = [
10
+ "A2CConfig",
11
+ "AdrRecord",
12
+ "ApprovedProposal",
13
+ "CliResultEnvelope",
14
+ "CommitPlanItem",
15
+ "CommitPlanProposal",
16
+ "CoreSettings",
17
+ "Epic",
18
+ "EpicStatus",
19
+ "LogLevel",
20
+ "ProfilePointer",
21
+ "ServiceResult",
22
+ "Sprint",
23
+ "SprintStatus",
24
+ "Task",
25
+ "TaskStatus",
26
+ "WorkflowManifest",
27
+ "WorkflowOwner",
28
+ "WorkflowSettings",
29
+ ]
@@ -0,0 +1,54 @@
1
+ """Planning artifact models — Epic, Task, Sprint (Milestone 2 minimum set)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Literal
6
+
7
+ from pydantic import BaseModel, ConfigDict, Field
8
+
9
+ EpicStatus = Literal["draft", "active", "complete", "cancelled"]
10
+ TaskStatus = Literal["todo", "in_progress", "done", "blocked"]
11
+ SprintStatus = Literal["planned", "active", "closed"]
12
+
13
+
14
+ class Epic(BaseModel):
15
+ """Epic artifact (`planning/epics/<id>.md` with YAML front matter)."""
16
+
17
+ model_config = ConfigDict(extra="ignore")
18
+
19
+ version: Literal[1] = 1
20
+ type: Literal["epic"]
21
+ id: str = Field(min_length=1)
22
+ title: str = Field(min_length=1)
23
+ status: EpicStatus = "draft"
24
+ summary: str | None = None
25
+ task_ids: list[str] = Field(default_factory=list)
26
+ body: str = ""
27
+
28
+
29
+ class Task(BaseModel):
30
+ """Task artifact (`planning/tasks/<id>.md` with YAML front matter)."""
31
+
32
+ model_config = ConfigDict(extra="ignore")
33
+
34
+ version: Literal[1] = 1
35
+ type: Literal["task"]
36
+ id: str = Field(min_length=1)
37
+ title: str = Field(min_length=1)
38
+ status: TaskStatus = "todo"
39
+ epic_id: str | None = None
40
+ body: str = ""
41
+
42
+
43
+ class Sprint(BaseModel):
44
+ """Sprint artifact (`planning/sprints/<id>.md` with YAML front matter)."""
45
+
46
+ model_config = ConfigDict(extra="ignore")
47
+
48
+ version: Literal[1] = 1
49
+ type: Literal["sprint"]
50
+ id: str = Field(min_length=1)
51
+ title: str = Field(min_length=1)
52
+ status: SprintStatus = "planned"
53
+ task_ids: list[str] = Field(default_factory=list)
54
+ body: str = ""
@@ -0,0 +1,74 @@
1
+ """Repository configuration models (contract C3)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from typing import Literal
7
+
8
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
9
+
10
+ from a2c_core.contracts.id_format import DEFAULT_ID_FORMAT_PATTERN
11
+
12
+ LogLevel = Literal["debug", "info", "warning", "error"]
13
+ DecompositionProviderName = Literal["mock", "openai", "local", "ollama"]
14
+
15
+
16
+ class CoreSettings(BaseModel):
17
+ """Core runtime preferences."""
18
+
19
+ model_config = ConfigDict(extra="forbid")
20
+
21
+ log_level: LogLevel = "info"
22
+
23
+
24
+ class WorkflowSettings(BaseModel):
25
+ """Workflow execution preferences."""
26
+
27
+ model_config = ConfigDict(extra="forbid")
28
+
29
+ approval_required: bool = True
30
+ default_profile: str | None = None
31
+
32
+
33
+ class IdFormatSettings(BaseModel):
34
+ """Optional strict planning artifact ID regex validation."""
35
+
36
+ model_config = ConfigDict(extra="forbid")
37
+
38
+ enabled: bool = False
39
+ pattern: str = DEFAULT_ID_FORMAT_PATTERN
40
+
41
+ @field_validator("pattern")
42
+ @classmethod
43
+ def pattern_must_compile(cls, value: str) -> str:
44
+ re.compile(value)
45
+ return value
46
+
47
+
48
+ class DecompositionSettings(BaseModel):
49
+ """Epic decomposition LLM provider settings."""
50
+
51
+ model_config = ConfigDict(extra="forbid")
52
+
53
+ provider: DecompositionProviderName = "mock"
54
+ model: str | None = None
55
+ endpoint: str | None = None
56
+ timeout_seconds: float = Field(default=120.0, gt=0)
57
+ max_tokens: int | None = Field(default=4096, gt=0)
58
+ temperature: float = Field(default=0.2, ge=0.0, le=2.0)
59
+
60
+
61
+ class A2CConfig(BaseModel):
62
+ """Project-local `.a2c/config.yaml` (version 1)."""
63
+
64
+ model_config = ConfigDict(extra="forbid")
65
+
66
+ version: Literal[1] = 1
67
+ core: CoreSettings = Field(default_factory=CoreSettings)
68
+ workflows: WorkflowSettings = Field(default_factory=WorkflowSettings)
69
+ id_format: IdFormatSettings = Field(default_factory=IdFormatSettings)
70
+ decomposition: DecompositionSettings = Field(default_factory=DecompositionSettings)
71
+
72
+ @classmethod
73
+ def defaults(cls) -> A2CConfig:
74
+ return cls()
@@ -0,0 +1,45 @@
1
+ """Workflow and repository metadata models (contract C1)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pydantic import BaseModel, ConfigDict, Field
6
+
7
+
8
+ class WorkflowOwner(BaseModel):
9
+ model_config = ConfigDict(extra="forbid")
10
+
11
+ contact: str | None = None
12
+ gitlab_group: str | None = None
13
+
14
+
15
+ class WorkflowManifest(BaseModel):
16
+ """`docs/workflow-manifest.yaml` shape (subset for validation)."""
17
+
18
+ model_config = ConfigDict(extra="allow")
19
+
20
+ version: int
21
+ workflow_id: str
22
+ display_title: str | None = None
23
+ source_docs_path: str = "docs"
24
+ required_metadata_files: list[str] = Field(default_factory=list)
25
+
26
+
27
+ class AdrRecord(BaseModel):
28
+ """Parsed ADR file metadata."""
29
+
30
+ model_config = ConfigDict(extra="forbid")
31
+
32
+ number: int
33
+ slug: str
34
+ filename: str
35
+ status: str | None = None
36
+ title: str | None = None
37
+
38
+
39
+ class ProfilePointer(BaseModel):
40
+ """Reference to a stack profile."""
41
+
42
+ model_config = ConfigDict(extra="forbid")
43
+
44
+ profile_id: str
45
+ overrides: dict[str, str] = Field(default_factory=dict)