From 181327f1df2073978a8361c9e34d67a09d88a425 Mon Sep 17 00:00:00 2001 From: twilightsaurus Date: Sun, 27 Sep 2026 22:51:14 +0300 Subject: [PATCH 1/2] feat: make agent tooling portable and optional --- bootstrap/create_project.py | 23 +++- bootstrap/model.py | 24 ++++ bootstrap/render.py | 2 + bootstrap/template_manifest.py | 52 +++++--- .../harmony-reverse-engineering/SKILL.md | 2 +- .../skills/valheim-modding/SKILL.md | 2 +- .../skills/valheim-networking/SKILL.md | 0 .../skills/valheim-release/SKILL.md | 2 +- tests/bootstrap/test_model.py | 31 +++++ tests/bootstrap/test_render_ts_escaping.py | 1 + tests/fixtures/_helpers.py | 10 +- tests/fixtures/test_agent_integration.py | 126 ++++++++++++++++++ tests/template/test_template_manifest.py | 18 ++- 13 files changed, 265 insertions(+), 28 deletions(-) rename template/{.omp => .agents}/skills/harmony-reverse-engineering/SKILL.md (89%) rename template/{.omp => .agents}/skills/valheim-modding/SKILL.md (93%) rename template/{.omp => .agents}/skills/valheim-networking/SKILL.md (100%) rename template/{.omp => .agents}/skills/valheim-release/SKILL.md (88%) create mode 100644 tests/fixtures/test_agent_integration.py diff --git a/bootstrap/create_project.py b/bootstrap/create_project.py index 01b5db0..afa1f3c 100644 --- a/bootstrap/create_project.py +++ b/bootstrap/create_project.py @@ -18,7 +18,7 @@ from pathlib import Path from . import naming -from .model import ProjectModel, ProjectParams, build_model, identity_lock_dict, solution_text, suite_config_dict, validate_params +from .model import AGENT_ADAPTERS, ProjectModel, ProjectParams, build_model, identity_lock_dict, solution_text, suite_config_dict, validate_params from .render import TEMPLATE_DIR, render_tree, validate_template from .validate_generated import NO_BYTECODE_ENV, ValidationResult, validate_generated @@ -62,6 +62,14 @@ def interactive_params() -> tuple[ProjectParams, str]: include_server_core = _prompt_yes_no("Include ServerCore?") include_client = _prompt_yes_no("Include Client?") include_shared_diagnostics = _prompt_yes_no("Include Shared.Diagnostics?") + include_agent_tooling = _prompt_yes_no("Include portable agent tooling?", default=True) + agent_adapters: tuple[str, ...] = () + if include_agent_tooling: + agent_adapters = tuple( + adapter + for adapter in sorted(AGENT_ADAPTERS) + if _prompt_yes_no(f"Include {adapter.upper()} agent adapter?", default=False) + ) params = ProjectParams( suite_name=suite_name, root_namespace=root_namespace, @@ -72,6 +80,8 @@ def interactive_params() -> tuple[ProjectParams, str]: include_server_core=include_server_core, include_client=include_client, include_shared_diagnostics=include_shared_diagnostics, + include_agent_tooling=include_agent_tooling, + agent_adapters=agent_adapters, ) return params, output_directory @@ -88,6 +98,15 @@ def parse_args(argv: list[str]) -> argparse.Namespace: parser.add_argument("--no-server-core", action="store_true") parser.add_argument("--no-client", action="store_true") parser.add_argument("--no-shared-diagnostics", action="store_true") + parser.add_argument("--no-agent-tooling", action="store_true") + parser.add_argument( + "--agent-adapter", + action="append", + choices=sorted(AGENT_ADAPTERS), + default=[], + metavar="ADAPTER", + help="Add a harness-specific adapter on top of the portable agent tooling; repeatable.", + ) parser.add_argument("--force", action="store_true") return parser.parse_args(argv) @@ -121,6 +140,8 @@ def resolve_params(args: argparse.Namespace) -> tuple[ProjectParams, str]: include_server_core=not args.no_server_core, include_client=not args.no_client, include_shared_diagnostics=not args.no_shared_diagnostics, + include_agent_tooling=not args.no_agent_tooling, + agent_adapters=tuple(args.agent_adapter), ) return params, args.output diff --git a/bootstrap/model.py b/bootstrap/model.py index adae876..4824a3f 100644 --- a/bootstrap/model.py +++ b/bootstrap/model.py @@ -20,6 +20,8 @@ "csharp_language_version": "10", } +AGENT_ADAPTERS = frozenset({"omp"}) + @dataclass(frozen=True) class ProjectParams: @@ -32,6 +34,8 @@ class ProjectParams: include_server_core: bool = True include_client: bool = True include_shared_diagnostics: bool = True + include_agent_tooling: bool = True + agent_adapters: tuple[str, ...] = () def validate_params(p: ProjectParams) -> None: @@ -113,8 +117,28 @@ def has_client_package(self) -> bool: contributes because it always reaches the client side too.""" return self.client is not None or self.shared_diagnostics is not None + @property + def has_agent_tooling(self) -> bool: + return self.params.include_agent_tooling + + @property + def has_omp_adapter(self) -> bool: + return "omp" in self.params.agent_adapters + def build_model(params: ProjectParams) -> ProjectModel: + unknown_adapters = sorted(set(params.agent_adapters) - AGENT_ADAPTERS) + if unknown_adapters: + raise naming.NamingError( + "unsupported agent adapter(s): " + ", ".join(unknown_adapters) + ) + if len(set(params.agent_adapters)) != len(params.agent_adapters): + raise naming.NamingError("agent adapters must not contain duplicates") + if not params.include_agent_tooling and params.agent_adapters: + raise naming.NamingError( + "agent adapters require portable agent tooling; remove --no-agent-tooling " + "or omit the adapter" + ) if not (params.include_server_core or params.include_client or params.include_shared_diagnostics): raise naming.NamingError( "at least one runtime module must be enabled: ServerCore, Client, or Shared.Diagnostics " diff --git a/bootstrap/render.py b/bootstrap/render.py index 2ad0c76..1f3794f 100644 --- a/bootstrap/render.py +++ b/bootstrap/render.py @@ -102,6 +102,8 @@ class TemplateSourceError(OSError): # `has_server_package`/`has_client_package` model properties that drive # `packaging/server/README.md`'s and `packaging/client/README.md`'s own content. OPTIONAL_GROUP_PRESENT = { + "agent_tooling": lambda m: m.has_agent_tooling, + "omp_adapter": lambda m: m.has_omp_adapter, "server_core": lambda m: m.server_core is not None, "client": lambda m: m.client is not None, "shared_diagnostics": lambda m: m.shared_diagnostics is not None, diff --git a/bootstrap/template_manifest.py b/bootstrap/template_manifest.py index 288b8d0..ece9273 100644 --- a/bootstrap/template_manifest.py +++ b/bootstrap/template_manifest.py @@ -21,32 +21,10 @@ # modules are included. REQUIRED_TEMPLATE_FILES: frozenset[str] = frozenset( { - ".context/CONTEXT.md", - ".context/_meta/schema.md", - ".context/_templates/finding.md", - ".context/findings/CONTEXT.md", - ".context/findings/valheim-runtime.md", - ".context/references/CONTEXT.md", - ".context/references/architecture.md", - ".context/references/networking.md", - ".context/references/patching.md", - ".context/references/project.md", - ".context/references/testing.md", - ".context/state/CONTEXT.md", - ".context/state/current.md", ".editorconfig", ".env.example", ".gitignore", - ".omp/extensions/valheim-dev/README.md", - ".omp/extensions/valheim-dev/index.ts", - ".omp/prompts/bootstrap-valheim.md", - ".omp/skills/harmony-reverse-engineering/SKILL.md", - ".omp/skills/valheim-modding/SKILL.md", - ".omp/skills/valheim-networking/SKILL.md", - ".omp/skills/valheim-release/SKILL.md", ".valheim/dev.json.example", - "AGENTS.md", - "BOOTSTRAP_PROMPT.md", "CHANGELOG.md", "Directory.Build.props", "Directory.Packages.props", @@ -113,6 +91,36 @@ # `docs/TEMPLATE_MAINTENANCE.md` for how to add a new optional module or # optional documentation group. OPTIONAL_TEMPLATE_FILES: dict[str, frozenset[str]] = { + "agent_tooling": frozenset( + { + ".agents/skills/harmony-reverse-engineering/SKILL.md", + ".agents/skills/valheim-modding/SKILL.md", + ".agents/skills/valheim-networking/SKILL.md", + ".agents/skills/valheim-release/SKILL.md", + ".context/CONTEXT.md", + ".context/_meta/schema.md", + ".context/_templates/finding.md", + ".context/findings/CONTEXT.md", + ".context/findings/valheim-runtime.md", + ".context/references/CONTEXT.md", + ".context/references/architecture.md", + ".context/references/networking.md", + ".context/references/patching.md", + ".context/references/project.md", + ".context/references/testing.md", + ".context/state/CONTEXT.md", + ".context/state/current.md", + "AGENTS.md", + "BOOTSTRAP_PROMPT.md", + } + ), + "omp_adapter": frozenset( + { + ".omp/extensions/valheim-dev/README.md", + ".omp/extensions/valheim-dev/index.ts", + ".omp/prompts/bootstrap-valheim.md", + } + ), "server_core": frozenset( { "src/__ROOT_NAMESPACE__.ServerCore/Core/FeatureRegistry.cs", diff --git a/template/.omp/skills/harmony-reverse-engineering/SKILL.md b/template/.agents/skills/harmony-reverse-engineering/SKILL.md similarity index 89% rename from template/.omp/skills/harmony-reverse-engineering/SKILL.md rename to template/.agents/skills/harmony-reverse-engineering/SKILL.md index 7a71e8c..923a8e5 100644 --- a/template/.omp/skills/harmony-reverse-engineering/SKILL.md +++ b/template/.agents/skills/harmony-reverse-engineering/SKILL.md @@ -5,7 +5,7 @@ description: Inspect current Valheim assemblies and implement or repair Harmony # Harmony and Reverse Engineering -1. Inspect the actual installed assembly, preferably with `valheim_inspect` or `ilspycmd`. +1. Inspect the actual installed assembly with `ilspycmd` or another inspection tool available in the active environment. 2. Search current Jotunn APIs/events first. 3. Locate the target type and exact method signature. 4. Trace callers/callees where behavior or ownership is unclear. diff --git a/template/.omp/skills/valheim-modding/SKILL.md b/template/.agents/skills/valheim-modding/SKILL.md similarity index 93% rename from template/.omp/skills/valheim-modding/SKILL.md rename to template/.agents/skills/valheim-modding/SKILL.md index d5a2410..c9e6f82 100644 --- a/template/.omp/skills/valheim-modding/SKILL.md +++ b/template/.agents/skills/valheim-modding/SKILL.md @@ -12,7 +12,7 @@ Use this workflow for Valheim feature implementation. 1. Read `.context/CONTEXT.md`, `.context/references/project.md`, `.context/references/architecture.md`, and `.context/state/current.md`. 2. Classify the feature as `SERVER_ONLY`, `SHARED_OPTIONAL`, `SHARED_REQUIRED`, or `CLIENT_ONLY`. 3. Record or update the feature document from `docs/features/TEMPLATE.md`. -4. Consult current Jotunn docs/Context7 first. +4. Consult current Jotunn documentation or another available current documentation source first. 5. Prefer Jotunn APIs/events over Harmony. 6. If Valheim internals are required, use the `harmony-reverse-engineering` skill. 7. Determine which machine owns and executes the behavior. diff --git a/template/.omp/skills/valheim-networking/SKILL.md b/template/.agents/skills/valheim-networking/SKILL.md similarity index 100% rename from template/.omp/skills/valheim-networking/SKILL.md rename to template/.agents/skills/valheim-networking/SKILL.md diff --git a/template/.omp/skills/valheim-release/SKILL.md b/template/.agents/skills/valheim-release/SKILL.md similarity index 88% rename from template/.omp/skills/valheim-release/SKILL.md rename to template/.agents/skills/valheim-release/SKILL.md index 17be3c0..ac459ac 100644 --- a/template/.omp/skills/valheim-release/SKILL.md +++ b/template/.agents/skills/valheim-release/SKILL.md @@ -12,7 +12,7 @@ Before packaging or deployment: 1. Run `python3 scripts/suite_metadata.py check`. 2. Run scaffold and C# unit tests. 3. Build the intended configuration against the verified local Valheim/Jötunn environment. -4. Use `scripts/deploy.py` through the canonical Bash wrapper or OMP extension. Do not recreate side-classification logic elsewhere. +4. Use `scripts/deploy.py` through the canonical Bash wrapper. Optional agent adapters must delegate to the same implementation rather than recreate side-classification logic. 5. For release candidates, perform a local plugin load test and disposable dedicated-server smoke test. 6. Run multiplayer compatibility tests for changed Shared Modules. 7. Review config generation, patch ledger, persistence/migrations, changelog, and `.context/state/current.md`. diff --git a/tests/bootstrap/test_model.py b/tests/bootstrap/test_model.py index 5777a61..527b836 100644 --- a/tests/bootstrap/test_model.py +++ b/tests/bootstrap/test_model.py @@ -28,6 +28,37 @@ def test_build_model_includes_all_modules_by_default(self): {"Sampleheim.Common", "Sampleheim.ServerCore", "Sampleheim.Client", "Sampleheim.Shared.Diagnostics"}, ) + def test_portable_agent_tooling_is_enabled_by_default(self): + model = build_model(make_params()) + self.assertTrue(model.has_agent_tooling) + self.assertEqual((), model.params.agent_adapters) + self.assertFalse(model.has_omp_adapter) + + def test_omp_adapter_is_explicit_and_additive(self): + model = build_model(make_params(agent_adapters=("omp",))) + self.assertTrue(model.has_agent_tooling) + self.assertTrue(model.has_omp_adapter) + + def test_agent_adapter_requires_agent_tooling(self): + with self.assertRaises(naming.NamingError) as ctx: + build_model( + make_params( + include_agent_tooling=False, + agent_adapters=("omp",), + ) + ) + self.assertIn("require portable agent tooling", str(ctx.exception)) + + def test_build_model_rejects_unknown_agent_adapter(self): + with self.assertRaises(naming.NamingError) as ctx: + build_model(make_params(agent_adapters=("unknown",))) + self.assertIn("unsupported agent adapter", str(ctx.exception)) + + def test_build_model_rejects_duplicate_agent_adapters(self): + with self.assertRaises(naming.NamingError) as ctx: + build_model(make_params(agent_adapters=("omp", "omp"))) + self.assertIn("must not contain duplicates", str(ctx.exception)) + def test_build_model_omits_excluded_modules(self): model = build_model(make_params(include_client=False)) self.assertIsNone(model.client) diff --git a/tests/bootstrap/test_render_ts_escaping.py b/tests/bootstrap/test_render_ts_escaping.py index 9f93e9c..1e1bfb1 100644 --- a/tests/bootstrap/test_render_ts_escaping.py +++ b/tests/bootstrap/test_render_ts_escaping.py @@ -80,6 +80,7 @@ def test_generated_index_ts_is_syntactically_valid(self): author="Tai Benvenuti", thunderstore_namespace="TaiBenvenuti", suite_version="1.2.3-alpha+build.1", + agent_adapters=("omp",), ) self.assertTrue(result.ok, result.errors) index_ts = output_dir / ".omp" / "extensions" / "valheim-dev" / "index.ts" diff --git a/tests/fixtures/_helpers.py b/tests/fixtures/_helpers.py index dd7a57c..6b6b3e4 100644 --- a/tests/fixtures/_helpers.py +++ b/tests/fixtures/_helpers.py @@ -116,7 +116,13 @@ def clone_generated_temp(**overrides) -> tuple[ProjectParams, Path, ValidationRe def expected_relpaths( - root_namespace: str, *, server_core: bool = True, client: bool = True, shared_diagnostics: bool = True + root_namespace: str, + *, + server_core: bool = True, + client: bool = True, + shared_diagnostics: bool = True, + include_agent_tooling: bool = True, + agent_adapters: tuple[str, ...] = (), ) -> set[str]: """The exact set of files a successful `generate()` must produce: every manifest-approved path (post `__ROOT_NAMESPACE__` substitution, @@ -132,6 +138,8 @@ def expected_relpaths( has_server_package = server_core or shared_diagnostics has_client_package = client or shared_diagnostics for included, group in ( + (include_agent_tooling, "agent_tooling"), + ("omp" in agent_adapters, "omp_adapter"), (server_core, "server_core"), (client, "client"), (shared_diagnostics, "shared_diagnostics"), diff --git a/tests/fixtures/test_agent_integration.py b/tests/fixtures/test_agent_integration.py new file mode 100644 index 0000000..debe3c5 --- /dev/null +++ b/tests/fixtures/test_agent_integration.py @@ -0,0 +1,126 @@ +import unittest + +from bootstrap.create_project import parse_args, resolve_params +from tests.fixtures._helpers import generate_into_temp + + +AGENT_TOOLING_FILES = { + "AGENTS.md", + "BOOTSTRAP_PROMPT.md", + ".agents/skills/harmony-reverse-engineering/SKILL.md", + ".agents/skills/valheim-modding/SKILL.md", + ".agents/skills/valheim-networking/SKILL.md", + ".agents/skills/valheim-release/SKILL.md", + ".context/CONTEXT.md", + ".context/_meta/schema.md", + ".context/_templates/finding.md", + ".context/findings/CONTEXT.md", + ".context/findings/valheim-runtime.md", + ".context/references/CONTEXT.md", + ".context/references/architecture.md", + ".context/references/networking.md", + ".context/references/patching.md", + ".context/references/project.md", + ".context/references/testing.md", + ".context/state/CONTEXT.md", + ".context/state/current.md", +} + +OMP_ADAPTER_FILES = { + ".omp/extensions/valheim-dev/README.md", + ".omp/extensions/valheim-dev/index.ts", + ".omp/prompts/bootstrap-valheim.md", +} + + +def _files(root): + return { + str(path.relative_to(root)) + for path in root.rglob("*") + if path.is_file() + } + + +def _cli_params(*extra): + args = parse_args( + [ + "--name", "Sampleheim", + "--guid", "org.example-tests.sampleheim", + "--author", "Sample Author", + "--thunderstore-namespace", "SampleNS", + "--output", "/tmp/sampleheim-agent-cli-test", + *extra, + ] + ) + return resolve_params(args)[0] + + +class AgentIntegrationTests(unittest.TestCase): + def test_default_generation_includes_portable_agent_tooling_without_harness_adapter(self): + params, output_dir, result = generate_into_temp() + self.assertTrue(result.ok, result.errors) + self.assertTrue(params.include_agent_tooling) + self.assertEqual((), params.agent_adapters) + + files = _files(output_dir) + self.assertTrue(AGENT_TOOLING_FILES <= files) + self.assertTrue((output_dir / ".agents" / "skills").is_dir()) + self.assertFalse((output_dir / ".omp").exists()) + + def test_no_agent_tooling_removes_only_the_portable_agent_layer(self): + _default_params, default_dir, default_result = generate_into_temp() + self.assertTrue(default_result.ok, default_result.errors) + + params, output_dir, result = generate_into_temp( + include_agent_tooling=False, + ) + self.assertTrue(result.ok, result.errors) + self.assertFalse(params.include_agent_tooling) + self.assertEqual((), params.agent_adapters) + + default_files = _files(default_dir) + bare_files = _files(output_dir) + + self.assertEqual(AGENT_TOOLING_FILES, default_files - bare_files) + self.assertEqual(bare_files, default_files - AGENT_TOOLING_FILES) + self.assertFalse((output_dir / ".agents").exists()) + self.assertFalse((output_dir / ".context").exists()) + self.assertFalse((output_dir / "AGENTS.md").exists()) + self.assertFalse((output_dir / "BOOTSTRAP_PROMPT.md").exists()) + self.assertFalse((output_dir / ".omp").exists()) + + def test_omp_adapter_is_strictly_additive_to_portable_agent_tooling(self): + _default_params, default_dir, default_result = generate_into_temp() + self.assertTrue(default_result.ok, default_result.errors) + + params, output_dir, result = generate_into_temp( + agent_adapters=("omp",), + ) + self.assertTrue(result.ok, result.errors) + self.assertTrue(params.include_agent_tooling) + self.assertEqual(("omp",), params.agent_adapters) + + default_files = _files(default_dir) + omp_files = _files(output_dir) + + self.assertEqual(OMP_ADAPTER_FILES, omp_files - default_files) + self.assertEqual(default_files, omp_files - OMP_ADAPTER_FILES) + + def test_cli_defaults_to_portable_agent_tooling(self): + params = _cli_params() + self.assertTrue(params.include_agent_tooling) + self.assertEqual((), params.agent_adapters) + + def test_cli_can_disable_agent_tooling(self): + params = _cli_params("--no-agent-tooling") + self.assertFalse(params.include_agent_tooling) + self.assertEqual((), params.agent_adapters) + + def test_cli_can_add_omp_adapter(self): + params = _cli_params("--agent-adapter", "omp") + self.assertTrue(params.include_agent_tooling) + self.assertEqual(("omp",), params.agent_adapters) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/template/test_template_manifest.py b/tests/template/test_template_manifest.py index 2fa54cc..7323063 100644 --- a/tests/template/test_template_manifest.py +++ b/tests/template/test_template_manifest.py @@ -317,7 +317,6 @@ class ManifestIndependentSanityTests(unittest.TestCase): _CURATED_REQUIRED_PATHS = frozenset( { - "AGENTS.md", "README.md", "CHANGELOG.md", "LICENSE.todo", @@ -342,6 +341,23 @@ class ManifestIndependentSanityTests(unittest.TestCase): } ) _CURATED_OPTIONAL_PATHS = { + "agent_tooling": frozenset( + { + "AGENTS.md", + "BOOTSTRAP_PROMPT.md", + ".context/CONTEXT.md", + ".agents/skills/harmony-reverse-engineering/SKILL.md", + ".agents/skills/valheim-modding/SKILL.md", + ".agents/skills/valheim-networking/SKILL.md", + ".agents/skills/valheim-release/SKILL.md", + } + ), + "omp_adapter": frozenset( + { + ".omp/extensions/valheim-dev/index.ts", + ".omp/prompts/bootstrap-valheim.md", + } + ), "server_core": frozenset({"src/__ROOT_NAMESPACE__.ServerCore/Plugin.cs"}), "client": frozenset({"src/__ROOT_NAMESPACE__.Client/Plugin.cs"}), "shared_diagnostics": frozenset({"src/__ROOT_NAMESPACE__.Shared.Diagnostics/Plugin.cs"}), From 1de2e8b16e0095aa5319c48fcc6503dcdb8fd1fd Mon Sep 17 00:00:00 2001 From: twilightsaurus Date: Sun, 27 Sep 2026 23:19:52 +0300 Subject: [PATCH 2/2] docs: make generated agent guidance provider-neutral --- BOOTSTRAP_MANIFEST.json | 8 ++--- README.md | 9 +++++ bootstrap/model.py | 4 +-- docs/GENERATOR_ARCHITECTURE.md | 16 ++++++--- docs/TEMPLATE_MAINTENANCE.md | 16 ++++++--- template/.context/references/project.md | 8 ++--- template/.context/state/current.md | 5 ++- template/.omp/prompts/bootstrap-valheim.md | 6 +++- template/AGENTS.md | 4 +-- template/BOOTSTRAP_PROMPT.md | 18 +++++----- template/CHANGELOG.md | 5 ++- template/docs/HARDENING_V3.md | 6 ++-- template/docs/PROJECT_SPEC.md | 2 +- template/docs/development.md | 4 +-- template/tools/README.md | 4 +-- tests/fixtures/test_agent_integration.py | 40 ++++++++++++++++++++++ 16 files changed, 106 insertions(+), 49 deletions(-) diff --git a/BOOTSTRAP_MANIFEST.json b/BOOTSTRAP_MANIFEST.json index b5db9a9..cad1e71 100644 --- a/BOOTSTRAP_MANIFEST.json +++ b/BOOTSTRAP_MANIFEST.json @@ -37,10 +37,10 @@ "generated MSBuild/C# package metadata has synchronization checks", "cross-platform net48 reference assemblies are explicit and private", "local preflight validates WSL/config/Valheim/BepInEx/Jotunn state", - "Bash and OMP deployment share metadata-driven scripts/deploy.py", + "canonical Bash/Python deployment is shared by optional agent adapters", "development deployment removes stale suite DLLs and enforces side membership", - "OMP inspection is confined to Valheim Managed assemblies", - "OMP server logs no longer run arbitrary configured commands", + "the optional OMP adapter confines assembly inspection to Valheim Managed assemblies", + "the optional OMP adapter delegates server logs/status to fixed canonical runtime operations", "deterministic local Release packages and SHA256SUMS are implemented", "scaffold regression tests protect established architecture", "generic bootstrap/template/tests separation replaces the single-instance scaffold", @@ -48,7 +48,7 @@ ], "runtimeVerificationStillRequired": [ "full plugin build in a generated project's WSL/Valheim environment", - "OMP extension load against the user installed OMP package", + "optional agent-adapter loading when an adapter is enabled", "dedicated-server plugin load", "multiplayer compatibility matrix", "Shared Diagnostics CustomRPC", diff --git a/README.md b/README.md index b685e0a..b72d9cd 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,15 @@ Or non-interactively: `ServerCore`, `Client`, and `Shared.Diagnostics` can each be omitted when they are not needed. +Portable coding-agent support is included by default using `AGENTS.md`, +`.context/`, and standard project skills under `.agents/skills/`. It is not +tied to a specific agent provider or harness. + +Use `--no-agent-tooling` to generate a project without that layer. Optional +provider-specific integrations are additive adapters; for example, +`--agent-adapter omp` adds the OMP extension and prompt without replacing the +portable project instructions or skills. + After generation: ```bash diff --git a/bootstrap/model.py b/bootstrap/model.py index 4824a3f..0a8ed78 100644 --- a/bootstrap/model.py +++ b/bootstrap/model.py @@ -395,7 +395,6 @@ def release_package_family_bullets(model: ProjectModel) -> str: def pending_runtime_proof_bullets(model: ProjectModel) -> str: lines = [ "- full Jötunn/Valheim plugin compilation", - "- OMP extension load against the installed OMP package", "- dedicated-server plugin load", "- actual multiplayer compatibility behavior", ] @@ -409,7 +408,6 @@ def hardening_pending_proof_bullets(model: ProjectModel) -> str: lines = [ "- full plugin build against the user's actual Valheim/Jötunn installation", "- Jötunn publicized-reference generation on the user's WSL/Windows setup", - "- OMP extension loading/type compatibility against the installed OMP package", "- Valheim dedicated-server plugin load", "- client/server connection matrix", ] @@ -429,7 +427,7 @@ def project_spec_milestone2_body(model: ProjectModel) -> str: def initial_milestones_bullets(model: ProjectModel) -> str: - lines = ["Repository/harness scaffold.", "Runtime plugin shells and side boundaries."] + lines = ["Repository and portable agent-tooling scaffold.", "Runtime plugin shells and side boundaries."] if model.shared_diagnostics: lines.append("Shared Diagnostics CustomRPC proof.") lines.append("First real feature (see `docs/features/TEMPLATE.md`).") diff --git a/docs/GENERATOR_ARCHITECTURE.md b/docs/GENERATOR_ARCHITECTURE.md index 9772ffa..601229f 100644 --- a/docs/GENERATOR_ARCHITECTURE.md +++ b/docs/GENERATOR_ARCHITECTURE.md @@ -5,8 +5,8 @@ - `bootstrap/naming.py`: regex validators for identity inputs (`NamingError` on failure). Deliberately duplicates the shape of `template/scripts/suite_metadata.py`'s own `SEMVER`/`GUID_ROOT` regexes — see `docs/TEMPLATE_MAINTENANCE.md`. - `bootstrap/model.py`: `ProjectParams` (raw generator inputs), `build_model()` (derives the included `ModuleSpec` set), `suite_config_dict()` and `solution_text()` (programmatically build `suite.config.json` and the `.sln`), and `token_map()` (the substitution dictionary for `template/`). - `bootstrap/update_dependency_baseline.py`: root maintenance command for explicit Jötunn/BepInExPack default-baseline updates. It validates versions with `bootstrap/naming.py`, refuses pre-existing disagreement between `bootstrap/model.py` and `BOOTSTRAP_MANIFEST.json`, updates only those two authorities, and rolls back attempted writes on failure. It does not operate on generated projects or runtime installations; see `docs/TEMPLATE_MAINTENANCE.md`. -- `bootstrap/template_manifest.py`: the explicit, reviewed list of every file `template/` is allowed to contain (`REQUIRED_TEMPLATE_FILES` plus `OPTIONAL_TEMPLATE_FILES` per optional module). This — not `template/`'s filesystem contents — is the template definition; see "Why rendering never walks `template/`" below. `validate_manifest_structure()` checks the manifest itself (every entry a normalized, relative, POSIX path with no `.`/`..`/absolute forms, no path listed in more than one group) so production validation never merely trusts the checked-in manifest to be well-formed. -- `bootstrap/render.py`: `render_tree()` renders exactly the manifest-approved files for a given `ProjectModel` (required files plus each included optional module's files), substituting `{{TOKEN}}` placeholders and `__ROOT_NAMESPACE__` path segments, and preserving the executable bit on `.sh` files. Every source is opened through `_open_source_file()`, which refuses to follow a symlink at any path component — including the final one — and refuses anything that isn't a plain regular file, so a symlink substituted for an approved path (or one of its parent directories) can never redirect rendering outside `template_dir`. `_render_plan()` containment- and collision-checks every rendered destination before anything is written: it must stay beneath the output root, must not collide with another source's destination, and must not collide with a path `generate()` writes programmatically (`suite.config.json`, `.sln`, `build/Suite.Generated.props`, the generated constants file, `packaging/profile-lock.json`). `validate_template()` is the template's own self-check (backs `scripts/validate-template.sh`): every manifest file must exist as a safe, physically-contained regular file, and every filesystem entry under `template/` — file or directory — must be in (or a required ancestor of) the manifest. +- `bootstrap/template_manifest.py`: the explicit, reviewed list of every file `template/` is allowed to contain (`REQUIRED_TEMPLATE_FILES` plus model-gated `OPTIONAL_TEMPLATE_FILES` groups for modules, package-side docs, portable agent tooling, and harness adapters). This — not `template/`'s filesystem contents — is the template definition; see "Why rendering never walks `template/`" below. `validate_manifest_structure()` checks the manifest itself (every entry a normalized, relative, POSIX path with no `.`/`..`/absolute forms, no path listed in more than one group) so production validation never merely trusts the checked-in manifest to be well-formed. +- `bootstrap/render.py`: `render_tree()` renders exactly the manifest-approved files for a given `ProjectModel` (required files plus each optional group whose model predicate is true), substituting `{{TOKEN}}` placeholders and `__ROOT_NAMESPACE__` path segments, and preserving the executable bit on `.sh` files. Every source is opened through `_open_source_file()`, which refuses to follow a symlink at any path component — including the final one — and refuses anything that isn't a plain regular file, so a symlink substituted for an approved path (or one of its parent directories) can never redirect rendering outside `template_dir`. `_render_plan()` containment- and collision-checks every rendered destination before anything is written: it must stay beneath the output root, must not collide with another source's destination, and must not collide with a path `generate()` writes programmatically (`suite.config.json`, `.sln`, `build/Suite.Generated.props`, the generated constants file, `packaging/profile-lock.json`). `validate_template()` is the template's own self-check (backs `scripts/validate-template.sh`): every manifest file must exist as a safe, physically-contained regular file, and every filesystem entry under `template/` — file or directory — must be in (or a required ancestor of) the manifest. - `bootstrap/validate_generated.py`: post-generation checks on a rendered output directory. Every relative path (directory, file, binary or not) is scanned first: it has no legitimate brace syntax at all, so any raw `{{`/`}}` fails outright, alongside an unresolved `__ROOT_NAMESPACE__` path sentinel and leaked bootstrapper implementation identity (`IDENTITY_LEAK_PATTERNS`/`GUID_LEAK_PATTERN`) -- a binary `ValheimSuite.Common.dll` or a `ValheimSuite.ServerCore/` directory fails on its *path* alone, independent of content. Decodable UTF-8 text files are additionally scanned for the same path sentinel/identity categories in their *content*, plus a marker-*shaped* `{{...}}` leftover -- `UNRESOLVED_MARKER_RE` (ordinary balanced marker) or `MALFORMED_MARKER_RE` (one mangled by a single stray interior brace) rather than every doubled brace, since generated text can legitimately contain one (see `docs/TEMPLATE_MAINTENANCE.md`); binary content is never decoded or scanned -- its path was already fully validated, and treating arbitrary bytes as text would be false confidence, not a stronger check. - `bootstrap/create_project.py`: CLI orchestration — interactive prompts or flags, safety checks on the output directory, source-template validation, and the summary printed to the user. `generate()` runs `validate_generated()` against a disposable copy of the rendered staging directory (not staging itself) so build/test commands like `dotnet test` — which create `bin/`/`obj/` wherever they run — can never leave artifacts in the tree that gets promoted; only the pristine, never-executed-against `staging_dir` is promoted. @@ -59,13 +59,19 @@ workflows fail closed without successful MSBuild evaluation. Generated portable preflight still requires dotnet; "portable" skips machine-specific game checks, not semantic project validation. +## Portable agent tooling and harness adapters + +Portable agent tooling is an optional template group enabled by default. It contains `AGENTS.md`, `BOOTSTRAP_PROMPT.md`, `.context/**`, and standard project skills under `.agents/skills/**`. `--no-agent-tooling` omits that complete layer without changing the generated build, metadata, deployment, package, source, or test surface. + +Harness-specific integrations are separate additive adapter groups. `--agent-adapter omp` currently adds only `.omp/extensions/valheim-dev/**` and `.omp/prompts/bootstrap-valheim.md`. Adapters require the portable agent layer, may delegate to the canonical repository scripts, and must not duplicate the portable skills/context or alter provider-neutral core files. + ## Generated game-stack maintenance Every suite renders `scripts/update-game-stack.py` and `scripts/refresh-references.sh`. The Python command owns offline runtime inspection, explicit Jötunn/BepInExPack pin updates, and the pinless `refresh` workflow; it reuses `suite_metadata.py` as the mutable metadata authority and `preflight.py` path parsing rather than duplicating configuration formats. Generated metadata synchronization is transactional across its complete output set. The shell entry point owns only the serialized Jötunn reference refresh: it validates metadata, derives the solution from `suite.identity.lock.json`, and runs prebuild with `-m:1` because Jötunn writes a shared publicized-assembly directory. `scripts/build.sh` remains the normal parallel build path; neither maintenance script owns deployment or lifecycle operations. ## Generated deployment architecture -The generated project parses development-local schema v1/v2 configuration in `scripts/dev_config.py`. `scripts/deploy.py` owns metadata validation, the transport-independent `DeploymentPlan`, the existing hardened retained-directory-FD deployment, and explicit restart orchestration. `scripts/remote_deploy.py` owns SSH/SCP invocation plus isolated POSIX-shell and Windows-PowerShell staging/promotion implementations. `scripts/server_runtime.py` owns the fixed status/log operations and dispatches them beside the configured local or remote Docker lifecycle without exposing transport logic to the extension. +The generated project parses development-local schema v1/v2 configuration in `scripts/dev_config.py`. `scripts/deploy.py` owns metadata validation, the transport-independent `DeploymentPlan`, the existing hardened retained-directory-FD deployment, and explicit restart orchestration. `scripts/remote_deploy.py` owns SSH/SCP invocation plus isolated POSIX-shell and Windows-PowerShell staging/promotion implementations. `scripts/server_runtime.py` owns the fixed status/log operations and dispatches them beside the configured local or remote Docker lifecycle without exposing transport logic to optional agent adapters. Both local and SSH transports receive the same plan. SSH transport uploads into a unique sibling stage, verifies the exact filenames and SHA-256 hashes, compares the live ownership manifest against the pre-upload observation, and promotes only owned entries with a rollback backup. Lifecycle remains independent: `--restart` invokes `docker restart` only after deployment succeeds and reports lifecycle failures separately. @@ -76,8 +82,8 @@ Remote safety is intentionally fail-safe rather than described as equivalent to 1. `validate_params()` — reject malformed suite name, namespace, GUID root, author, Thunderstore namespace, or version; `validate_params()` also calls `build_model()`, which rejects a selection with every optional module (`ServerCore`, `Client`, `Shared.Diagnostics`) disabled -- Common alone builds no runtime BepInEx plugin, so this fails before the approved destination is ever touched (see `docs/TEMPLATE_MAINTENANCE.md`'s "The empty-runtime invariant"). 2. Refuse an unsafe or non-empty output directory without `--force` (see `_safe_output_dir`/`_approve_destination` in `create_project.py`: rejects `/`, `$HOME`, the bootstrapper's own repository, and any ancestor of it). 3. `validate_template()` — the source template must pass its own manifest/forbidden-content check before anything is rendered; a failure raises `GenerationError` before the approved destination is ever touched. -4. `build_model()` — derive the concrete `ModuleSpec` set (`Common` always; `ServerCore`/`Client`/`Shared.Diagnostics` per include flags). -5. `render_tree()` — render the manifest-approved files (filtered to the included optional modules) into a disposable staging directory, substituting tokens, after `_render_plan()` has containment- and collision-checked every destination. +4. `build_model()` — derive the concrete `ModuleSpec` set plus portable-agent and adapter selection (`Common` always; `ServerCore`/`Client`/`Shared.Diagnostics` per include flags; portable agent tooling on by default; adapters explicit). +5. `render_tree()` — render the manifest-approved files whose model predicates are enabled into a disposable staging directory, substituting tokens, after `_render_plan()` has containment- and collision-checked every destination. 6. Write the generated `suite.config.json`, `.sln`, and `suite.identity.lock.json`. 7. Run `python3 scripts/suite_metadata.py sync --structural-only` inside the staging directory to produce `build/Suite.Generated.props`, `src/.Common/SuiteConstants.Generated.cs`, and `packaging/profile-lock.json` — reusing the generated project's own logic rather than duplicating it here. 8. Copy the fully-rendered staging directory to a disposable validation directory and run `validate_generated()` against *that copy* — structural and (when possible) executable proof that the staged output is sound — then discard the copy regardless of outcome. diff --git a/docs/TEMPLATE_MAINTENANCE.md b/docs/TEMPLATE_MAINTENANCE.md index 03b0675..ee5df98 100644 --- a/docs/TEMPLATE_MAINTENANCE.md +++ b/docs/TEMPLATE_MAINTENANCE.md @@ -2,7 +2,7 @@ ## The template manifest -`bootstrap/template_manifest.py` is the template definition: `REQUIRED_TEMPLATE_FILES` (rendered unconditionally) and `OPTIONAL_TEMPLATE_FILES` (rendered per included optional module). `template/`'s filesystem contents are not themselves the template — `render_tree()` renders exactly the manifest's approved paths, and `validate_template()` (backing `scripts/validate-template.sh`) fails if a manifest file is missing, or if `template/` contains a filesystem entry — file *or* directory, empty or not — that isn't in (or a required ancestor of) the manifest. Adding a file under `template/` without adding it here has no effect on generated output and fails validation instead of being silently ignored. +`bootstrap/template_manifest.py` is the template definition: `REQUIRED_TEMPLATE_FILES` (rendered unconditionally) and `OPTIONAL_TEMPLATE_FILES` (rendered when the corresponding `ProjectModel` predicate is enabled). Optional groups cover modules, package-side documentation, portable agent tooling, and harness-specific adapters. `template/`'s filesystem contents are not themselves the template — `render_tree()` renders exactly the manifest's approved paths, and `validate_template()` (backing `scripts/validate-template.sh`) fails if a manifest file is missing, or if `template/` contains a filesystem entry — file *or* directory, empty or not — that isn't in (or a required ancestor of) the manifest. Adding a file under `template/` without adding it here has no effect on generated output and fails validation instead of being silently ignored. Every manifest entry must be a normalized, relative, POSIX-style path: no leading `/`, no backslashes, no `.`/`..` component, and no double slashes. `validate_manifest_structure()` enforces this on the manifest itself (not just on what happens to exist in `template/`), and also rejects a path listed in more than one group (`REQUIRED_TEMPLATE_FILES` and an `OPTIONAL_TEMPLATE_FILES` set, or two different `OPTIONAL_TEMPLATE_FILES` sets). A literal path repeated *within* one `{...}` set is not separately checked for — Python's `frozenset` already collapses that to one element before any validation runs, so there is nothing to observe. @@ -11,7 +11,7 @@ A manifest-approved path is also re-verified at the moment of reading: `render_t ## Adding a new template file 1. Add the file under `template/` at the path it should render to (using `__ROOT_NAMESPACE__` for module-specific `src/`/`tests/` directories). -2. Add its `template/`-relative path to `bootstrap/template_manifest.py`'s `REQUIRED_TEMPLATE_FILES` (or the relevant `OPTIONAL_TEMPLATE_FILES[module]` set if it belongs to an optional module). +2. Add its `template/`-relative path to `bootstrap/template_manifest.py`'s `REQUIRED_TEMPLATE_FILES` or the relevant model-gated `OPTIONAL_TEMPLATE_FILES` group. 3. Run `scripts/validate-template.sh`. ## Licensing boundary @@ -105,9 +105,9 @@ Common is a shared library other plugins reference, not itself a BepInEx plugin. A weaker, per-side form of the same invariant applies to `template/scripts/deploy.py`: `side_has_runtime_module()` rejects `--target server`/`--target client` when the current validated `packages` groups put no non-`common` project on that side (e.g. a ServerCore-only suite's `--target client`), before any destination is created, flocked, or written to -- otherwise deploying only `Common.dll` would create a deployment-ownership manifest for a side with no plugin to load it. This reads `packages` directly (not bootstrap-time module flags), so it stays correct after a supported post-generation structural edit. See `tests/template/test_deploy_side_availability.py`. -## Adding an optional documentation group +## Adding an optional template group -Not every module-dependent template file should be gated on a single module's presence -- `packaging/server/README.md` and `packaging/client/README.md` are gated on `ProjectModel.has_server_package`/`has_client_package` instead, because Shared.Diagnostics (a `sharedOptional` module) reaches *both* package sides even when ServerCore/Client are absent. `bootstrap/template_manifest.py`'s `OPTIONAL_TEMPLATE_FILES` supports this: `server_package_docs`/`client_package_docs` are ordinary optional groups, just keyed by a package-side predicate instead of a module-presence predicate, registered in `bootstrap/render.py`'s `OPTIONAL_GROUP_PRESENT` exactly like a module group. To add another optional documentation group: +Not every optional template file is gated on a module. `packaging/server/README.md` and `packaging/client/README.md` use package-side predicates, `agent_tooling` uses `ProjectModel.has_agent_tooling`, and harness adapters use their own model predicates. `bootstrap/template_manifest.py`'s `OPTIONAL_TEMPLATE_FILES` is therefore a general model-gated grouping mechanism, with every group registered in `bootstrap/render.py`'s `OPTIONAL_GROUP_PRESENT`. To add another optional template group: 1. Add the file(s) under `template/` and a new key + `frozenset` of paths to `OPTIONAL_TEMPLATE_FILES`. 2. Add the matching predicate (a `lambda m: ...` over `ProjectModel`) to `OPTIONAL_GROUP_PRESENT`. Prefer an existing `ProjectModel` property/method over a one-off inline condition so source-inclusion and prose-rendering can never independently drift, which is exactly how the original audited issue happened (source filtering existed; prose rendering didn't follow it). @@ -115,6 +115,12 @@ Not every module-dependent template file should be gated on a single module's pr 4. Add fixture coverage asserting the file exists/is absent for the right combinations, and that its rendered content never names a module/package family this generation doesn't have. 5. Update the token table and this file. +## Portable agent tooling and harness adapters + +Portable agent tooling is enabled by default and is one coherent optional group: `AGENTS.md`, `BOOTSTRAP_PROMPT.md`, `.context/**`, and `.agents/skills/**`. The content of that group must remain provider-neutral. `--no-agent-tooling` omits it completely, so required/core files must never depend on it. + +Harness-specific files belong in separate adapter groups. The current `omp_adapter` contains `.omp/extensions/valheim-dev/**` and `.omp/prompts/bootstrap-valheim.md`; it must delegate operational behavior to the same canonical scripts used without an agent harness. Do not place portable skills or context under a provider-owned directory, and do not let enabling an adapter change bytes in provider-neutral generated files. + ## Adding a new scalar generator input 1. Extend `ProjectParams` and `naming.py` with a validator if the input has format constraints. @@ -135,4 +141,4 @@ Not every module-dependent template file should be gated on a single module's pr ## Keeping `template/` generic -`template/` must stay a generic modular framework: no concrete gameplay features, no project-specific milestones. `docs/module-catalog.md`, `docs/PROJECT_SPEC.md`'s milestones, and `BOOTSTRAP_PROMPT.md` intentionally stop at "prove the scaffold and Shared Diagnostics architecture, then define the first real feature using `docs/features/TEMPLATE.md`" rather than naming any specific feature. +`template/` must stay a generic modular framework: no concrete gameplay features, no project-specific milestones. Portable agent instructions and skills must also remain provider-neutral; provider-specific discovery or tool integration belongs only in an explicit adapter group. `docs/module-catalog.md`, `docs/PROJECT_SPEC.md`'s milestones, and `BOOTSTRAP_PROMPT.md` intentionally stop at "prove the scaffold and Shared Diagnostics architecture, then define the first real feature using `docs/features/TEMPLATE.md`" rather than naming any specific feature. diff --git a/template/.context/references/project.md b/template/.context/references/project.md index 0f79ee8..60119d0 100644 --- a/template/.context/references/project.md +++ b/template/.context/references/project.md @@ -12,11 +12,11 @@ Build a modular Valheim mod suite for a dedicated private server while preservin Canonical development environment: **WSL/Linux**. -- repository, Git, OMP/Pi harness, dotnet, build/test/package scripts: WSL +- repository, Git, dotnet, build/test/package scripts, and coding-agent tooling: WSL - Windows Valheim client: accessed from WSL through `/mnt/c/...` - dedicated test/server environment: Linux/Docker where possible - Bash/WSL scripts define the only canonical project workflow -- `scripts/deploy.py` is the single deployment implementation used by the Bash wrappers and OMP extension +- `scripts/deploy.py` is the single deployment implementation used by the Bash wrappers; optional agent adapters must delegate to it Keep the repository in the WSL filesystem, for example `~/src/{{ROOT_NAMESPACE_LOWER}}`, rather than under `/mnt/c`. @@ -44,8 +44,8 @@ Exact dependency versions and C# language version are authoritative in `suite.co - Keep changes minimal and scoped. YAGNI applies. - Introduce an abstraction only when a concrete consumer needs it. -- Do not duplicate globally configured Context Mode or Context7 configuration in this repository without a project-specific requirement. -- Use native harness capabilities; do not create generic planner, reviewer, scout, or worker agents for this project. +- Keep provider-specific agent configuration out of the repository unless an explicit adapter requires it. +- Prefer portable project instructions and skills; use capabilities of the active agent environment rather than creating redundant project-local agent frameworks. ## Initial non-goals diff --git a/template/.context/state/current.md b/template/.context/state/current.md index 8db0634..87c7731 100644 --- a/template/.context/state/current.md +++ b/template/.context/state/current.md @@ -18,12 +18,12 @@ The module list below is the initial scaffold snapshot. For current project memb - Metadata-driven deployment prevents client/server DLL cross-contamination. - Deterministic local package generation and SHA-256 checksums are implemented. - Portable scaffold tests cover repository invariants. +- Portable agent instructions and project skills are available through `AGENTS.md` and `.agents/skills/`. ## In progress - Local WSL/Valheim environment verification. - First successful full plugin build against installed Valheim/Jotunn. -- OMP extension load verification against the user's installed OMP version. ## Next @@ -32,8 +32,7 @@ The module list below is the initial scaffold snapshot. For current project memb 3. Run `./scripts/bootstrap.sh`. 4. If publicized Valheim references are absent, run `python3 scripts/update-game-stack.py refresh` after verifying `VALHEIM_INSTALL`; this refreshes references, builds, and runs preflight. 5. Otherwise run `./scripts/build.sh Debug` for an ordinary parallel build. -6. Load OMP and verify the `valheim-dev` extension and tools. -7. Design and implement the first real feature using `docs/features/TEMPLATE.md`. +6. Design and implement the first real feature using `docs/features/TEMPLATE.md`. ## Initial milestones diff --git a/template/.omp/prompts/bootstrap-valheim.md b/template/.omp/prompts/bootstrap-valheim.md index 345eebc..d7c6336 100644 --- a/template/.omp/prompts/bootstrap-valheim.md +++ b/template/.omp/prompts/bootstrap-valheim.md @@ -1 +1,5 @@ -Read and execute `BOOTSTRAP_PROMPT.md` completely. Use the repository's project skills when relevant. Do not duplicate global harness agents or global Context Mode/Context7 configuration. +Read and execute `BOOTSTRAP_PROMPT.md` completely. + +Use the portable project skills under `.agents/skills/` when relevant. + +Then verify OMP discovers `.omp/extensions/valheim-dev` and that its tools load correctly. Treat the extension as an adapter over the canonical repository scripts rather than an independent implementation. diff --git a/template/AGENTS.md b/template/AGENTS.md index 35e8cd9..197e803 100644 --- a/template/AGENTS.md +++ b/template/AGENTS.md @@ -1,6 +1,6 @@ # {{SUITE_NAME}} Project Instructions -This repository is a modular Valheim mod suite. Reusable coding-harness behavior belongs in the user's global OMP rules. +This repository is a modular Valheim mod suite. These instructions and the project skills under `.agents/skills/` are provider-neutral and should work from any compatible coding-agent environment. ## Context routing @@ -17,4 +17,4 @@ Detailed rules in the routed context and linked `docs/` files are mandatory when 5. Treat WSL/Linux and the Bash scripts as the canonical development workflow; access the Windows client through `/mnt/c/...`. Do not create a second independent PowerShell implementation of build, deploy, or package behavior. 6. `suite.config.json`, `suite.identity.lock.json`, and the canonical solution define suite structure and identity. Synchronize supported metadata edits; never hand-edit generated metadata. 7. Keep changes minimal and scoped. Add dependencies and abstractions only for demonstrated requirements. -8. Use native harness capabilities; do not create project-local generic agents or duplicate global Context Mode or Context7 configuration. +8. Use capabilities provided by the active coding-agent environment. Do not add provider-specific project configuration or duplicate global agent configuration unless a concrete repository requirement needs it. diff --git a/template/BOOTSTRAP_PROMPT.md b/template/BOOTSTRAP_PROMPT.md index 43125f6..f7793cf 100644 --- a/template/BOOTSTRAP_PROMPT.md +++ b/template/BOOTSTRAP_PROMPT.md @@ -19,19 +19,19 @@ Read, in order: 13. `docs/dependencies.md` 14. `docs/module-catalog.md` -Use the project skills under `.omp/skills/` when their domain applies. +Use the project skills under `.agents/skills/` when their domain applies. ## Preserve these established decisions Do not redesign the high-confidence foundation during bootstrap: -- WSL/Linux is canonical for repo/harness/build/test/package/server tooling. +- WSL/Linux is canonical for repository, build, test, package, and server tooling. - Windows Valheim is the actual client runtime and is accessed through `/mnt/c/...`. - Runtime topology is Common + ServerCore + independent Shared modules + Client. Any of ServerCore, Client, or a Shared.* module may be absent in this repository; `suite.config.json`'s `projects` is authoritative for what was generated, and must always exactly match `.sln`'s real project membership. {{COMPATIBILITY_BOUNDARIES_LIST}} - Jötunn is the common modding/network platform; ServerSync is not added without a demonstrated need. - `suite.config.json` is the editable source of truth for supported mutable suite/project/package metadata. Generation-time suite identity (`suiteName`, `rootNamespace`) is locked in `suite.identity.lock.json` and cannot be changed by editing `suite.config.json`. -- `scripts/deploy.py` is the single deployment implementation for both Bash and the OMP extension. +- `scripts/deploy.py` is the single deployment implementation. Optional agent adapters must delegate to it rather than duplicate deployment behavior. - Do not replace these choices merely to make the scaffold look different. ## First actions @@ -43,9 +43,8 @@ Do not redesign the high-confidence foundation during bootstrap: 5. Inspect the configured development client for its resolved gameplay assembly (`assembly_valheim.dll`, with `Assembly-CSharp.dll` fallback for older layouts), BepInEx, and Jötunn. 6. Verify the pinned Jötunn and BepInExPack versions against current stable releases. Do not silently upgrade. Report newer versions if any and preserve pins unless there is a compatibility reason to change. 7. Run `./scripts/bootstrap.sh`. -8. Verify OMP actually discovers `.omp/skills/`, `.omp/prompts/`, and `.omp/extensions/valheim-dev` using the installed OMP version. -9. Record the inspected game version and configured runtime environment in `.context/findings/valheim-runtime.md`; create separate evidence-backed findings for any implementation-dependent claims. -10. Update `.context/state/current.md` with the resulting verification status. +8. Record the inspected game version and configured runtime environment in `.context/findings/valheim-runtime.md`; create separate evidence-backed findings for any implementation-dependent claims. +9. Update `.context/state/current.md` with the resulting verification status. ## Jötunn reference refresh @@ -72,7 +71,6 @@ Confirm that: - net48 runtime projects have cross-platform reference-assembly support - deterministic packaging code is present rather than a placeholder - deployment classification is metadata-driven and excludes wrong-side DLLs -- OMP extension loads under the user's installed OMP ## Milestone 1 acceptance @@ -94,13 +92,13 @@ Define the project's first real feature using `docs/features/TEMPLATE.md`: - classify it as SERVER_ONLY, SHARED_OPTIONAL, SHARED_REQUIRED, or CLIENT_ONLY - use the `valheim-modding`, `valheim-networking`, and `harmony-reverse-engineering` skills as appropriate -- consult current Jötunn documentation or Context7 before coding +- consult current Jötunn documentation or another available current documentation source before coding - if game internals are required, reverse engineer only what is needed and record every Harmony patch in `docs/patch-ledger.md` - prefer a disposable world for any destructive testing -## Harness discipline +## Agent discipline -Use native harness planning, review, subagents, worktrees, and model routing. Do not create project-specific generic agents duplicating those capabilities. +Use the planning, review, parallelism, and other capabilities of the active coding-agent environment when available. Do not add project-local provider-specific agent frameworks merely to reproduce capabilities the active environment already supplies. Prefer small coherent changes. Avoid speculative frameworks, custom launchers, auto-updaters, web dashboards, cloud services, databases, generic event buses, and custom networking layers. diff --git a/template/CHANGELOG.md b/template/CHANGELOG.md index 0ebf649..7861c76 100644 --- a/template/CHANGELOG.md +++ b/template/CHANGELOG.md @@ -9,7 +9,6 @@ - Preserved per-module Jötunn compatibility boundaries. - Added transport-independent local/SSH server deployment with explicit optional Docker restart. -### Harness +### Development -- OMP project context, domain skills, extension, and bootstrap prompt. -- WSL-first canonical workflow. +- WSL/Linux-first canonical workflow. diff --git a/template/docs/HARDENING_V3.md b/template/docs/HARDENING_V3.md index bdfe1ac..7da630c 100644 --- a/template/docs/HARDENING_V3.md +++ b/template/docs/HARDENING_V3.md @@ -19,11 +19,9 @@ This hardening pass intentionally preserves the established runtime architecture - Generated MSBuild properties, C# suite constants, and package lock are synchronized and checked. - Linux/WSL `net48` builds explicitly reference `Microsoft.NETFramework.ReferenceAssemblies` as a private build dependency. - Preflight now validates local config, path agreement, Valheim assembly presence, BepInEx, recursively located Jötunn, tooling, and Jötunn prebuild/publicized-reference state. -- Client/server development deployment uses one Python implementation shared by Bash and OMP. +- Client/server development deployment uses one canonical Python implementation behind the Bash wrappers; optional agent adapters delegate to it. - Deployment uses exact configured projects/TFMs instead of broad DLL globs and removes stale suite DLLs from the dedicated suite destination. -- OMP assembly inspection is restricted to `valheim_Data/Managed`. -- OMP server logging no longer permits arbitrary configured commands. -- OMP subprocess execution is cancellable and uses argument arrays. +- Canonical assembly-inspection and server-runtime scripts retain their confinement and fixed-operation boundaries independently of any optional agent adapter. - Release packaging is implemented, deterministic, metadata-driven, and checksum-producing. - Public-release metadata has a deliberate fail-closed validation mode. - Standard-library scaffold tests protect high-confidence architectural invariants from accidental regressions. diff --git a/template/docs/PROJECT_SPEC.md b/template/docs/PROJECT_SPEC.md index 153208c..35e64c6 100644 --- a/template/docs/PROJECT_SPEC.md +++ b/template/docs/PROJECT_SPEC.md @@ -88,7 +88,7 @@ Prefer sidecar storage for suite metadata. Namespaced ZDO data is appropriate wh ### Milestone 0 -Repository/harness scaffold, context, skills, extension, build scripts, dependency docs, metadata validation, and deterministic local packaging foundation. +Repository scaffold, build scripts, dependency docs, metadata validation, deterministic local packaging foundation, and optional portable agent tooling. ### Milestone 1 diff --git a/template/docs/development.md b/template/docs/development.md index 2e684b4..c4edb42 100644 --- a/template/docs/development.md +++ b/template/docs/development.md @@ -131,11 +131,11 @@ Deployment never restarts a server implicitly: `--restart` runs the configured lifecycle only after deployment succeeds. `type: "docker"` executes `docker restart` locally for local transport and through the configured SSH host for SSH transport. `type: "none"` has no restart operation. A lifecycle failure is reported separately after the successful deployment remains in place. Docker Compose is not assumed. -The OMP status and log tools delegate to `scripts/server_runtime.py`; the extension never constructs SSH or Docker commands. Schema-v2 Docker status/log operations run beside the configured container, locally or through SSH. `serverLogFile` remains a local bounded-tail source, and schema-v1 Docker Compose status/log behavior remains compatible. +Optional agent adapters that expose status or log operations must delegate to `scripts/server_runtime.py`; adapters do not construct independent SSH or Docker command paths. Schema-v2 Docker status/log operations run beside the configured container, locally or through SSH. `serverLogFile` remains a local bounded-tail source, and schema-v1 Docker Compose status/log behavior remains compatible. ### Schema-v1 compatibility -Existing schema-v1 files remain valid. `serverPluginDir` keeps its legacy local-deployment meaning; legacy Docker Compose/log fields remain available to legacy extension tools. To migrate, set `schemaVersion` to `2`, move `serverPluginDir` to `server.deployment.pluginDir`, select `server.deployment.type`, and add an independent `server.lifecycle` object. +Existing schema-v1 files remain valid. `serverPluginDir` keeps its legacy local-deployment meaning; legacy Docker Compose/log fields remain available to legacy runtime tooling. To migrate, set `schemaVersion` to `2`, move `serverPluginDir` to `server.deployment.pluginDir`, select `server.deployment.type`, and add an independent `server.lifecycle` object. ## Metadata workflow diff --git a/template/tools/README.md b/template/tools/README.md index bb95ac4..04f5138 100644 --- a/template/tools/README.md +++ b/template/tools/README.md @@ -1,5 +1,5 @@ # Tools -Keep project-specific helper tools here only when the canonical scripts or `.omp/extensions/valheim-dev` are insufficient. +Keep project-specific helper tools here only when the canonical scripts and portable project skills are insufficient. -Do not build a second general-purpose harness or duplicate build/deploy/package classification logic. +Do not build a second general-purpose agent framework or duplicate build/deploy/package classification logic. diff --git a/tests/fixtures/test_agent_integration.py b/tests/fixtures/test_agent_integration.py index debe3c5..0bb89a7 100644 --- a/tests/fixtures/test_agent_integration.py +++ b/tests/fixtures/test_agent_integration.py @@ -1,3 +1,4 @@ +import re import unittest from bootstrap.create_project import parse_args, resolve_params @@ -83,6 +84,12 @@ def test_no_agent_tooling_removes_only_the_portable_agent_layer(self): self.assertEqual(AGENT_TOOLING_FILES, default_files - bare_files) self.assertEqual(bare_files, default_files - AGENT_TOOLING_FILES) + for rel in sorted(bare_files): + self.assertEqual( + (default_dir / rel).read_bytes(), + (output_dir / rel).read_bytes(), + rel, + ) self.assertFalse((output_dir / ".agents").exists()) self.assertFalse((output_dir / ".context").exists()) self.assertFalse((output_dir / "AGENTS.md").exists()) @@ -105,6 +112,39 @@ def test_omp_adapter_is_strictly_additive_to_portable_agent_tooling(self): self.assertEqual(OMP_ADAPTER_FILES, omp_files - default_files) self.assertEqual(default_files, omp_files - OMP_ADAPTER_FILES) + for rel in sorted(default_files): + self.assertEqual( + (default_dir / rel).read_bytes(), + (output_dir / rel).read_bytes(), + rel, + ) + + def test_default_generated_tree_has_no_provider_specific_agent_references(self): + _params, output_dir, result = generate_into_temp() + self.assertTrue(result.ok, result.errors) + + forbidden_literals = ( + ".omp/", + "@oh-my-pi", + "oh-my-pi", + "Context7", + "Context Mode", + ) + for path in output_dir.rglob("*"): + if not path.is_file(): + continue + try: + text = path.read_text(encoding="utf-8") + except UnicodeDecodeError: + continue + rel = str(path.relative_to(output_dir)) + + with self.subTest(path=rel, marker="OMP"): + self.assertIsNone(re.search(r"\\bOMP\\b", text)) + + for marker in forbidden_literals: + with self.subTest(path=rel, marker=marker): + self.assertNotIn(marker, text) def test_cli_defaults_to_portable_agent_tooling(self): params = _cli_params()