You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The Product Brainstormer (epic #121) needs a typed, validated "north star" describing what the product is for and which axes of value count as real progress. Today no such structured input exists — the brainstormer would either invent goals or have to be re-briefed every run. We need a discovery layer that pulls this from .forge/product-vision.md (free-form prose) and .forge/axes.yaml (structured, schema-checked). Without both, the brainstormer must refuse to start, loudly, rather than silently fabricate direction.
Example: an operator runs the brainstormer in a repo missing .forge/axes.yaml. Today: nothing stops it from spinning up a generic "improve the codebase" agent. Required behaviour: MissingVisionError: axes.yaml not found at /repo/.forge/axes.yaml — and the brainstormer halts before any LLM call.
Acceptance criteria
New module src/forge_loop/product_vision.py exposes ProductVision, Axis, MissingVisionError, and discover(repo_path: Path) -> ProductVision.
Axis is a pydantic v2 BaseModel with required fields: name: str, customer: str, valuable_means: str, acceptable_work: list[str] (min_length=1), rejected_as_cosmetic: list[str] (may be empty).
ProductVision is a pydantic v2 BaseModel holding vision_markdown: str (raw markdown text) and axes: list[Axis] (min_length=1).
discover(repo_path) reads <repo_path>/.forge/product-vision.md + <repo_path>/.forge/axes.yaml, validates both, and returns a typed ProductVision. Returns the object (never None) on success — missing/invalid inputs raise.
MissingVisionError is raised with a specific reason string for each failure mode: missing vision file, missing axes file, axes.yaml not valid YAML, axes.yaml schema-invalid (mention the offending field path).
Error messages include the absolute path of the file that failed so operators can fix without grep.
Empty product-vision.md (zero non-whitespace chars) is treated as invalid with reason "vision markdown is empty".
Public surface is re-exported in src/forge_loop/__init__.py if other modules already follow that pattern (investigate); otherwise leave as submodule import.
Test matrix
Location: tests/test_product_vision.py, fixtures under tests/fixtures/product_vision/.
Unit tests (happy paths):
valid_minimal/ — single axis, minimal required fields → discover() returns ProductVision with one Axis, vision_markdown equals the file content verbatim.
valid_full/ — multi-axis fixture with rich markdown and non-empty rejected_as_cosmetic → validates and round-trips all fields.
Unit tests (sad paths, all must raise MissingVisionError with the documented reason substring):
missing_vision/ — only axes.yaml present → reason mentions "product-vision.md" and the path.
missing_axes/ — only product-vision.md present → reason mentions "axes.yaml".
empty_vision/ — vision file exists but is whitespace only → reason "vision markdown is empty".
invalid_axes_yaml/ — axes.yaml is not parseable YAML (e.g. unbalanced brackets) → reason mentions YAML parse failure.
invalid_axes_schema/ — axes.yaml parses but an axis is missing customer → reason mentions the missing field customer.
empty_axes_list/ — axes.yaml has axes: [] → reason mentions axes list cannot be empty.
acceptable_work_empty/ — axis with acceptable_work: [] → reason mentions acceptable_work min length.
Adversarial:
discover() on a path where .forge/ itself does not exist → MissingVisionError (not FileNotFoundError leakage).
Symlinked .forge/ directory pointing at a valid fixture → resolves and validates.
Extra unknown keys in axes.yaml entries → either ignored or rejected; the test pins the chosen behaviour (recommended: ignore for forward-compat).
No integration/e2e tests required at this stage — the brainstormer caller is out of scope for this ticket.
Auto-discover .forge/product-vision.md (free-form markdown) and .forge/axes.yaml (structured pydantic schema) at brainstormer startup. Missing or invalid files = hard refusal with a clear error.
Acceptance
New forge_loop/product_vision.py module.
ProductVision pydantic model loads .forge/product-vision.md (markdown, free-form) + .forge/axes.yaml (structured).
Axis pydantic model with required fields: name, customer, valuable_means, acceptable_work: list[str], rejected_as_cosmetic: list[str].
Problem
The Product Brainstormer (epic #121) needs a typed, validated "north star" describing what the product is for and which axes of value count as real progress. Today no such structured input exists — the brainstormer would either invent goals or have to be re-briefed every run. We need a discovery layer that pulls this from
.forge/product-vision.md(free-form prose) and.forge/axes.yaml(structured, schema-checked). Without both, the brainstormer must refuse to start, loudly, rather than silently fabricate direction.Example: an operator runs the brainstormer in a repo missing
.forge/axes.yaml. Today: nothing stops it from spinning up a generic "improve the codebase" agent. Required behaviour:MissingVisionError: axes.yaml not found at /repo/.forge/axes.yaml— and the brainstormer halts before any LLM call.Acceptance criteria
src/forge_loop/product_vision.pyexposesProductVision,Axis,MissingVisionError, anddiscover(repo_path: Path) -> ProductVision.Axisis a pydantic v2BaseModelwith required fields:name: str,customer: str,valuable_means: str,acceptable_work: list[str](min_length=1),rejected_as_cosmetic: list[str](may be empty).ProductVisionis a pydantic v2BaseModelholdingvision_markdown: str(raw markdown text) andaxes: list[Axis](min_length=1).discover(repo_path)reads<repo_path>/.forge/product-vision.md+<repo_path>/.forge/axes.yaml, validates both, and returns a typedProductVision. Returns the object (neverNone) on success — missing/invalid inputs raise.MissingVisionErroris raised with a specific reason string for each failure mode: missing vision file, missing axes file, axes.yaml not valid YAML, axes.yaml schema-invalid (mention the offending field path).product-vision.md(zero non-whitespace chars) is treated as invalid with reason"vision markdown is empty".src/forge_loop/__init__.pyif other modules already follow that pattern (investigate); otherwise leave as submodule import.Test matrix
Location:
tests/test_product_vision.py, fixtures undertests/fixtures/product_vision/.Unit tests (happy paths):
valid_minimal/— single axis, minimal required fields →discover()returnsProductVisionwith oneAxis,vision_markdownequals the file content verbatim.valid_full/— multi-axis fixture with rich markdown and non-emptyrejected_as_cosmetic→ validates and round-trips all fields.Unit tests (sad paths, all must raise
MissingVisionErrorwith the documented reason substring):missing_vision/— only axes.yaml present → reason mentions"product-vision.md"and the path.missing_axes/— only product-vision.md present → reason mentions"axes.yaml".empty_vision/— vision file exists but is whitespace only → reason"vision markdown is empty".invalid_axes_yaml/— axes.yaml is not parseable YAML (e.g. unbalanced brackets) → reason mentions YAML parse failure.invalid_axes_schema/— axes.yaml parses but an axis is missingcustomer→ reason mentions the missing fieldcustomer.empty_axes_list/— axes.yaml hasaxes: []→ reason mentions axes list cannot be empty.acceptable_work_empty/— axis withacceptable_work: []→ reason mentionsacceptable_workmin length.Adversarial:
discover()on a path where.forge/itself does not exist →MissingVisionError(notFileNotFoundErrorleakage)..forge/directory pointing at a valid fixture → resolves and validates.No integration/e2e tests required at this stage — the brainstormer caller is out of scope for this ticket.
Out of scope
discover()into the brainstormer CLI / runtime (separate ticket under epic epic: customer-tunable Product Brainstormer / PO Master (vision + axes drive every ticket) #121)..forge/product-vision.mdor.forge/axes.yaml(no scaffolder here).src/forge_loop/briefs/.pyproject.toml.repo_pathargument only.File pointers
src/forge_loop/product_vision.py(new)tests/test_product_vision.py(new)tests/fixtures/product_vision/(new tree — one subdir per fixture above, each containing a.forge/dir)src/forge_loop/__init__.py(investigate — only touch if existing pattern re-exports public modules)pyproject.toml(investigate — confirmpyyamlandpydanticare already deps; ifpyyamlis missing, add it)Original report
Part of #121 — Product Brainstormer epic.
What
Auto-discover
.forge/product-vision.md(free-form markdown) and.forge/axes.yaml(structured pydantic schema) at brainstormer startup. Missing or invalid files = hard refusal with a clear error.Acceptance
forge_loop/product_vision.pymodule.ProductVisionpydantic model loads.forge/product-vision.md(markdown, free-form) +.forge/axes.yaml(structured).Axispydantic model with required fields:name,customer,valuable_means,acceptable_work: list[str],rejected_as_cosmetic: list[str].discover(repo_path) -> ProductVision | Nonereads both, validates, returns the typed object.MissingVisionErrorraised when either file is missing or invalid.File pointers
src/forge_loop/product_vision.py(new)tests/test_product_vision.py(new)tests/fixtures/product_vision/(new)