Skip to content

Commit fc065be

Browse files
committed
Merge branch 'copilot/content-pipeline-tooling-ci' for Task 8.4.1 - Content Pipeline Tooling & CI
2 parents f32b50c + b3174c0 commit fc065be

78 files changed

Lines changed: 1502 additions & 874 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.pm/tracker.md

Lines changed: 54 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
# Project Task Tracker
22

3-
**Last Updated:** 2025-12-02T06:23:51Z
3+
**Last Updated:** 2025-12-02T18:55:00Z
44

55
## Status Summary
66

77
**Recent Progress (since last update):**
88

9+
- 🎉 **Task 8.4.1 (Content Pipeline Tooling & CI) COMPLETED** - GitHub Issue [#23](https://github.com/TheWizardsCode/GEngine/issues/23)
10+
- Content build script (`scripts/build_content.py`) validates worlds, configs, and sweeps
11+
- CI workflow (`.github/workflows/content-validation.yml`) runs on content file changes
12+
- Designer workflow documented in `docs/gengine/content_designer_workflow.md`
13+
- 17 tests covering all validation paths, all passing
14+
- Clear error messages with entity reference validation
915
- 🎉 **Task 10.1.2 (Strengthen AgentSystem Tests) COMPLETED**
1016
- Refactored `AgentSystem` to extract scoring logic for testability.
1117
- Added unit tests verifying trait influence (empathy, cunning, resolve) on decision scoring.
@@ -92,9 +98,9 @@
9298

9399
**Current Priorities:**
94100

95-
1. 🚀 **Phase 8 Deployment** - Core complete (8.1.1, 8.2.1, 8.3.1), need CI automation (8.3.2) and content pipeline (8.4.1)
101+
1. 🚀 **Phase 8 Deployment** - Core complete (8.1.1, 8.2.1, 8.3.1, 8.4.1), need CI automation (8.3.2) to finish
96102
2. 🤖 **Phase 9 AI Testing** - Observer (9.1.1) and action layer (9.2.1) complete, LLM-enhanced (9.3.1) ready to start
97-
3. 🔧 **CI/CD Gap** - No automated workflows exist; high risk of regressions
103+
3. 🔧 **CI/CD Gap** - K8s validation workflow (8.3.2) still needed for deployment protection
98104

99105
**Recommended Next 3 Parallel Tasks:**
100106

@@ -105,37 +111,30 @@
105111
- Impact: Protects all environments from manifest errors
106112
- Estimated time: 1-2 days
107113

108-
2. **8.3.3 - K8s Resource Tuning** (Priority: MEDIUM, Effort: Low)
109-
- Why: Complete 8.3.1 resource sizing acceptance criteria
110-
- Owner needed: DevOps/SRE-focused agent
111-
- Parallelizable: Configuration work, independent of code
112-
- Impact: Prevents resource exhaustion in production
113-
- Estimated time: 4-6 hours
114-
- Prerequisites: Smoke test data from 8.3.1
115-
116-
3. **9.3.1 - LLM-Enhanced AI Decisions** (Priority: MEDIUM, Effort: High)
114+
2. **9.3.1 - LLM-Enhanced AI Decisions** (Priority: MEDIUM, Effort: High)
117115
- Why: Builds on completed AI foundation (9.1.1, 9.2.1)
118116
- Owner needed: AI/ML-focused agent with LLM experience
119117
- Parallelizable: AI/ML work, independent of infrastructure
120118
- Impact: Enables advanced AI testing capabilities
121119
- Estimated time: 3-5 days
122120

123-
**Alternative (if no AI/ML owner available):**
124-
- **8.4.1 - Content Pipeline Tooling** instead of 9.3.1
125-
- Priority: MEDIUM, Effort: Medium
126-
- Unblocks content designers
127-
- Estimated time: 2-3 days
121+
3. **10.1.3 - Expand SimEngine API Tests** (Priority: HIGH, Effort: Medium)
122+
- Why: Improve core system test coverage
123+
- Owner needed: Test-focused agent
124+
- Parallelizable: Test work, independent of infrastructure
125+
- Impact: Better regression detection for core engine
126+
- Estimated time: 2-3 days
128127

129128
**Key Risks:**
130129

131130
- 🔴 **K8s CI validation missing** - Bad manifests can break deployment (8.3.2) - HIGH IMPACT
132-
- ⚠️ **Phase 8 content pipeline needs ownership** - Task 8.4.1 requires assignment
133131
- ⚠️ **Phase 9 LLM enhancement ready** - Rule-based AI complete, LLM-enhanced (9.3.1) unblocked but needs owner
132+
-**Phase 8 content pipeline complete** - Task 8.4.1 finished with build script, CI workflow, and documentation (2025-12-02)
134133
-**Phase 8 observability complete** - Task 8.3.1 Prometheus annotations and smoke tests added (2025-12-01)
135134
-**Phase 7 delivery risk eliminated** - All core player features complete and tested, per-agent modifiers enabled by default
136135
-**Containerization complete** - Docker/Compose and K8s manifests tested and documented
137136
-**AI player foundation complete** - Observer and action layer shipped with 112 tests
138-
-**Clean repository state** - Issues #21, #24, #25 closed (verified 2025-12-01)
137+
-**Clean repository state** - Issues #21, #23, #24, #25 closed (verified 2025-12-02)
139138

140139
| ID | Task | Status | Priority | Responsible | Updated |
141140
| ----: | ----------------------------------------------- | ----------- | -------- | ------------------ | ---------- |
@@ -168,7 +167,7 @@
168167
| 8.3.3 | K8s Resource Sizing & Tuning (M8.3.y) | completed | Medium | devops-agent | 2025-12-02 |
169168
| 8.3.3 | Gateway/LLM Prometheus Metrics (M8.3.x) | not-started | Medium | TBD (ask Ross) | 2025-12-01 |
170169
| 8.3.4 | Integrate K8s Smoke Test into CI (M8.3.x) | not-started | Medium | TBD (ask Ross) | 2025-12-01 |
171-
| 8.4.1 | Content pipeline tooling & CI (M8.4) | not-started | Medium | TBD (ask Ross) | 2025-11-30 |
170+
| 8.4.1 | Content pipeline tooling & CI (M8.4) | completed | Medium | devops-agent | 2025-12-02 |
172171
| 9.1.1 | AI Observer foundation acceptance (M9.1) | completed | Medium | gamedev-agent | 2025-11-30 |
173172
| 9.2.1 | Rule-based AI action layer (M9.2) | completed | Medium | gamedev-agent | 2025-12-01 |
174173
| 9.3.1 | LLM-enhanced AI decisions (M9.3) | not-started | Medium | TBD (ask Ross) | 2025-11-30 |
@@ -717,16 +716,44 @@
717716
### 8.4.1 — Content Pipeline Tooling & CI (M8.4)
718717
- **GitHub Issue:** [#23](https://github.com/TheWizardsCode/GEngine/issues/23)
719718
- **Description:** Implement content build tooling (`scripts/build_content.py`), CI validation hooks, and documentation so designers can author/test YAML and story seeds efficiently.
720-
- **Acceptance Criteria:** Content build step produces artifacts consumed by simulation; CI validates content on change; designer workflow documented.
719+
- **Acceptance Criteria:**
720+
- ✅ Content build step produces artifacts consumed by simulation
721+
- ✅ CI validates content on change (schema, references, integrity)
722+
- ✅ Designer workflow documented
723+
- ✅ Clear error messages for content validation failures
721724
- **Priority:** Medium
722-
- **Responsible:** TBD (ask Ross)
723-
- **Dependencies:** Stable content schema and directory structure.
725+
- **Responsible:** devops-agent
726+
- **Status:** ✅ COMPLETED
727+
- **Dependencies:** Stable content schema and directory structure (✅ complete).
724728
- **Risks & Mitigations:**
725729
- Risk: Pipeline friction slows content iteration. Mitigation: Optimize for designer ergonomics, provide quick local commands.
726-
- **Next Steps:**
727-
1. Implement build script.
728-
2. Wire into CI.
729-
3. Document designer workflow.
730+
- **Completion Notes:**
731+
- **Build Script** (`scripts/build_content.py`):
732+
- Validates world definitions (`world.yml` and `story_seeds.yml`) with entity reference checking
733+
- Validates simulation configuration (`simulation.yml`) against Pydantic schema
734+
- Validates difficulty sweep configurations (`content/config/sweeps/*/`)
735+
- Outputs JSON manifest with validation results and file lists
736+
- Clear error messages with icons (❌/✓) and bullet-point formatting
737+
- Exit codes: 0 (success), 1 (validation errors), 2 (file/config errors)
738+
- **CI Workflow** (`.github/workflows/content-validation.yml`):
739+
- Triggers on push to main and PRs that modify content files
740+
- Monitors: `content/**/*.yml`, `content/**/*.yaml`, `scripts/build_content.py`, `.github/workflows/content-*.yml`
741+
- Runs validation via `uv run python scripts/build_content.py --verbose --output content-manifest.json`
742+
- Uploads content manifest artifact for debugging
743+
- Blocks PR merge on validation failures
744+
- **Designer Documentation** (`docs/gengine/content_designer_workflow.md`):
745+
- Content types and structure (worlds, configs, sweeps)
746+
- YAML schema examples with annotations
747+
- Local validation instructions with exit codes
748+
- CI/CD validation details and artifact retrieval
749+
- Troubleshooting section with common validation errors
750+
- Best practices for content authors
751+
- **Test Coverage** (`tests/scripts/test_build_content.py`):
752+
- 17 tests covering all validation paths
753+
- Tests for valid content, missing files, invalid schemas, bad entity references
754+
- Integration tests validating real repository content
755+
- All tests passing
756+
- **Last Updated:** 2025-12-02
730757

731758
### 9.1.1 — AI Observer Foundation Acceptance (M9.1)
732759
- **GitHub Issue:** [#19](https://github.com/TheWizardsCode/GEngine/issues/19)

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ build-backend = "setuptools.build_meta"
4747

4848
[tool.ruff]
4949
line-length = 88
50+
51+
[tool.ruff.lint]
5052
select = ["E", "F", "B", "I"]
5153

5254
[tool.pytest.ini_options]

scripts/analyze_difficulty_profiles.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,9 @@ def from_telemetry(cls, preset: str, data: dict[str, Any]) -> "DifficultyProfile
5151
# Calculate faction balance as delta between faction legitimacies
5252
faction_leg = data.get("faction_legitimacy", {})
5353
leg_values = list(faction_leg.values())
54-
faction_balance = max(leg_values) - min(leg_values) if len(leg_values) >= 2 else 0.0
54+
faction_balance = (
55+
max(leg_values) - min(leg_values) if len(leg_values) >= 2 else 0.0
56+
)
5557

5658
# Economic pressure from price volatility
5759
economy = data.get("last_economy", {})
@@ -161,17 +163,16 @@ def compare_profiles(profiles: dict[str, DifficultyProfile]) -> dict[str, Any]:
161163
"✓ Unrest correctly increases with difficulty (harder = more unrest)"
162164
)
163165
else:
164-
findings.append(
165-
"⚠ Unrest does not consistently increase with difficulty"
166-
)
166+
findings.append("⚠ Unrest does not consistently increase with difficulty")
167167

168168
# Check for extreme values
169169
for preset, profile in profiles.items():
170170
if profile.stability_end <= 0.0:
171171
findings.append(f"⚠ {preset}: Stability collapsed to 0 (may be too harsh)")
172172
if profile.anomalies > 100:
173173
findings.append(
174-
f"⚠ {preset}: High anomaly count ({profile.anomalies}) indicates system stress"
174+
f"⚠ {preset}: High anomaly count ({profile.anomalies}) "
175+
"indicates system stress"
175176
)
176177

177178
# Check differentiation between adjacent difficulties
@@ -181,8 +182,8 @@ def compare_profiles(profiles: dict[str, DifficultyProfile]) -> dict[str, Any]:
181182
stability_diff = abs(prof1.stability_end - prof2.stability_end)
182183
if stability_diff < 0.05:
183184
findings.append(
184-
f"⚠ {p1} vs {p2}: Stability difference is minimal ({stability_diff:.3f}), "
185-
"consider widening gap"
185+
f"⚠ {p1} vs {p2}: Stability difference is minimal "
186+
f"({stability_diff:.3f}), consider widening gap"
186187
)
187188

188189
comparison["findings"] = findings

scripts/eoe_dump_state.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
import argparse
66
from pathlib import Path
7-
from typing import Optional
87

98
from gengine.echoes.content import load_world_bundle
109
from gengine.echoes.persistence import save_snapshot

scripts/plot_environment_trajectories.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,15 +50,17 @@ def main(argv: Sequence[str] | None = None) -> int:
5050
runs = _collect_runs(args.run)
5151
if not runs:
5252
raise SystemExit(
53-
"No telemetry files found. Provide --run LABEL=PATH or rerun the sweeps to generate JSON."
53+
"No telemetry files found. Provide --run LABEL=PATH "
54+
"or rerun the sweeps to generate JSON."
5455
)
5556

5657
fig, (ax_pollution, ax_unrest) = plt.subplots(2, 1, sharex=True, figsize=(10, 6))
5758
for label, path in runs.items():
5859
ticks, pollution, unrest = _extract_series(path)
5960
if len(ticks) < 2:
6061
print(
61-
f"Warning: {label} only provided {len(ticks)} sample(s); increase focus.history_length before capturing telemetry."
62+
f"Warning: {label} only provided {len(ticks)} sample(s); "
63+
"increase focus.history_length before capturing telemetry."
6264
)
6365
ax_pollution.plot(ticks, pollution, label=label)
6466
ax_unrest.plot(ticks, unrest, label=label)

scripts/run_difficulty_sweeps.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,14 @@ def run_difficulty_sweeps(
6969
sys.stderr.write(f"[SKIP] Config not found: {config_root}\n")
7070
continue
7171

72-
output_path = output_dir / f"difficulty-{preset}-sweep.json" if output_dir else None
72+
output_path = (
73+
output_dir / f"difficulty-{preset}-sweep.json" if output_dir else None
74+
)
7375

7476
if verbose:
75-
sys.stderr.write(f"\n[START] {preset.upper()} difficulty ({ticks} ticks, seed={seed})\n")
77+
sys.stderr.write(
78+
f"\n[START] {preset.upper()} difficulty ({ticks} ticks, seed={seed})\n"
79+
)
7680

7781
start = perf_counter()
7882
summary = run_headless_sim(
@@ -106,7 +110,9 @@ def run_difficulty_sweeps(
106110

107111
total_elapsed = perf_counter() - start_total
108112
if verbose:
109-
sys.stderr.write(f"\n[COMPLETE] {len(results)} presets in {total_elapsed:.1f}s\n")
113+
sys.stderr.write(
114+
f"\n[COMPLETE] {len(results)} presets in {total_elapsed:.1f}s\n"
115+
)
110116

111117
return results
112118

scripts/run_headless_sim.py

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -58,14 +58,26 @@ def run_headless_sim(
5858
"faction_actions": sum(len(report.faction_actions) for report in reports),
5959
"faction_action_breakdown": _faction_breakdown(reports),
6060
}
61-
summary["suppressed_events"] = sum(len(report.suppressed_events) for report in reports)
61+
summary["suppressed_events"] = sum(
62+
len(report.suppressed_events) for report in reports
63+
)
6264
summary["director_feed"] = dict(engine.state.metadata.get("director_feed", {}))
63-
summary["director_history"] = list(engine.state.metadata.get("director_history") or [])
64-
summary["director_analysis"] = dict(engine.state.metadata.get("director_analysis") or {})
65-
summary["director_events"] = list(engine.state.metadata.get("director_events") or [])
66-
summary["director_pacing"] = dict(engine.state.metadata.get("director_pacing") or {})
65+
summary["director_history"] = list(
66+
engine.state.metadata.get("director_history") or []
67+
)
68+
summary["director_analysis"] = dict(
69+
engine.state.metadata.get("director_analysis") or {}
70+
)
71+
summary["director_events"] = list(
72+
engine.state.metadata.get("director_events") or []
73+
)
74+
summary["director_pacing"] = dict(
75+
engine.state.metadata.get("director_pacing") or {}
76+
)
6777
summary["story_seeds"] = list(engine.state.metadata.get("story_seeds_active") or [])
68-
summary["story_seed_lifecycle"] = dict(engine.state.metadata.get("story_seed_lifecycle") or {})
78+
summary["story_seed_lifecycle"] = dict(
79+
engine.state.metadata.get("story_seed_lifecycle") or {}
80+
)
6981
summary["story_seed_lifecycle_history"] = list(
7082
engine.state.metadata.get("story_seed_lifecycle_history") or []
7183
)
@@ -131,7 +143,9 @@ def _advance_in_batches(
131143
"ticks": len(step_reports),
132144
"ending_tick": last_report.tick if last_report else engine.state.tick,
133145
"agent_actions": sum(len(report.agent_actions) for report in step_reports),
134-
"faction_actions": sum(len(report.faction_actions) for report in step_reports),
146+
"faction_actions": sum(
147+
len(report.faction_actions) for report in step_reports
148+
),
135149
}
136150
if last_report is not None:
137151
batch_payload["tick_ms"] = round(
@@ -213,8 +227,12 @@ def main(argv: Sequence[str] | None = None) -> int:
213227
default=None,
214228
help="Optional snapshot file to load instead of content",
215229
)
216-
parser.add_argument("--ticks", "-t", type=int, default=200, help="Number of ticks to advance")
217-
parser.add_argument("--seed", type=int, default=None, help="RNG seed override for determinism")
230+
parser.add_argument(
231+
"--ticks", "-t", type=int, default=200, help="Number of ticks to advance"
232+
)
233+
parser.add_argument(
234+
"--seed", type=int, default=None, help="RNG seed override for determinism"
235+
)
218236
parser.add_argument(
219237
"--lod",
220238
choices=["detailed", "balanced", "coarse"],

src/gengine/ai_player/actor.py

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -419,9 +419,9 @@ def _create_observation_summary(
419419
start_value=1.0, # Assumed start
420420
end_value=stability,
421421
delta=stability - 1.0,
422-
trend="stable" if abs(stability - 1.0) < 0.01 else (
423-
"increasing" if stability > 1.0 else "decreasing"
424-
),
422+
trend="stable"
423+
if abs(stability - 1.0) < 0.01
424+
else ("increasing" if stability > 1.0 else "decreasing"),
425425
)
426426

427427
# Extract faction swings
@@ -432,9 +432,9 @@ def _create_observation_summary(
432432
start_value=0.5, # Assumed start
433433
end_value=leg,
434434
delta=leg - 0.5,
435-
trend="stable" if abs(leg - 0.5) < 0.05 else (
436-
"increasing" if leg > 0.5 else "decreasing"
437-
),
435+
trend="stable"
436+
if abs(leg - 0.5) < 0.05
437+
else ("increasing" if leg > 0.5 else "decreasing"),
438438
)
439439

440440
return ObservationReport(
@@ -472,9 +472,7 @@ def _build_telemetry(self, final_state: dict[str, Any]) -> dict[str, Any]:
472472

473473
return {
474474
"action_counts": action_counts,
475-
"priority_stats": {
476-
k: round(v, 4) for k, v in priority_stats.items()
477-
},
475+
"priority_stats": {k: round(v, 4) for k, v in priority_stats.items()},
478476
"strategy_type": self._strategy.strategy_type.value,
479477
"final_state": {
480478
"stability": final_state.get("stability", 1.0),

src/gengine/ai_player/llm_strategy.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,7 @@ def __post_init__(self) -> None:
8383
if self.llm_timeout_seconds <= 0:
8484
raise ValueError("llm_timeout_seconds must be positive")
8585
if not 0.0 <= self.rule_priority_scaling <= 1.0:
86-
raise ValueError(
87-
"rule_priority_scaling must be between 0.0 and 1.0"
88-
)
86+
raise ValueError("rule_priority_scaling must be between 0.0 and 1.0")
8987

9088

9189
@dataclass
@@ -270,6 +268,7 @@ def request_decision(
270268
if loop is not None and loop.is_running():
271269
# Already in async context - use thread to avoid nested loops
272270
import concurrent.futures
271+
273272
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
274273
# Create a new event loop in the thread
275274
future = executor.submit(self._run_in_new_loop, request)
@@ -376,7 +375,8 @@ def _build_command_from_context(
376375
if "multiple_stressed_factions" in factors:
377376
factions = request.state.get("faction_legitimacy", {})
378377
low_factions = [
379-
f for f, leg in factions.items()
378+
f
379+
for f, leg in factions.items()
380380
if leg < self._config.complexity_threshold_legitimacy
381381
]
382382
return (
@@ -496,7 +496,8 @@ def evaluate_complexity(
496496
# Check faction stress
497497
faction_legitimacy = state.get("faction_legitimacy", {})
498498
stressed_factions = sum(
499-
1 for leg in faction_legitimacy.values()
499+
1
500+
for leg in faction_legitimacy.values()
500501
if leg < config.complexity_threshold_legitimacy
501502
)
502503
if stressed_factions >= config.complexity_threshold_factions:

0 commit comments

Comments
 (0)