From 3adcdbd27a0a09577cdd5430d46f5379a8e5c002 Mon Sep 17 00:00:00 2001 From: Chris Busillo Date: Thu, 30 Apr 2026 09:09:06 -0400 Subject: [PATCH] Teach workspaces about local overrides --- docs/ARCHITECTURE.md | 8 ++++++-- docs/tooling/command-patterns.md | 2 ++ docs/tooling/workspace-cli.md | 4 ++++ odoo_devkit/workspace_cockpit.py | 19 ++++++++++--------- odoo_devkit/workspace_surface.py | 15 ++++++++------- templates/tenant-overlay/AGENTS.md | 2 +- templates/tenant-overlay/docs/README.md | 4 ++-- .../workspace-cockpit/workspace-cockpit.toml | 17 +++++++++-------- tests/test_scaffold.py | 9 +++++++-- tests/test_workspace.py | 14 ++++++++------ 10 files changed, 57 insertions(+), 37 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 020d99c..67a8803 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -54,7 +54,10 @@ When - Materialized `sources/tenant`, `sources/devkit`, and optional `sources/shared-addons`. - Generated runtime output under `.generated/`. -- Disposable local state under `state/`. +- Legacy or disposable local runtime output under `state/`, when an older + workspace still has it. +- Optional local `AGENTS.override.md` for non-secret implementation facts that + should stay out of generated docs. Secrets still belong in `.env`. ### Control plane @@ -81,11 +84,12 @@ When README.md workspace.lock.toml .generated/ + AGENTS.override.md # optional local, untracked, non-secret operator notes sources/ tenant/ devkit/ shared-addons/ - state/ + state/ # legacy or disposable local runtime output when present ``` ## Design Goal diff --git a/docs/tooling/command-patterns.md b/docs/tooling/command-patterns.md index 68f3a49..bd71bdc 100644 --- a/docs/tooling/command-patterns.md +++ b/docs/tooling/command-patterns.md @@ -70,5 +70,7 @@ uv run platform workspace scaffold-tenant-overlay \ - Do not hand-edit generated workspace-root cockpit files. - If the workspace surface is wrong, fix `odoo-devkit` and re-sync. +- Keep implementation-specific, non-secret local facts in an untracked + `AGENTS.override.md`; keep credentials in `.env`. - Keep tenant repo docs thin; use the generated workspace docs index for shared guidance. diff --git a/docs/tooling/workspace-cli.md b/docs/tooling/workspace-cli.md index ca37748..2fa8773 100644 --- a/docs/tooling/workspace-cli.md +++ b/docs/tooling/workspace-cli.md @@ -95,6 +95,8 @@ Purpose of hand-maintaining root markdown files. - Generate `AGENTS.md`, `docs/README.md`, and `docs/session-prompt.md` from that config. +- Point operators to an optional local `AGENTS.override.md` for non-secret + implementation facts that must not be baked into generated shared docs. - Keep non-repo workspace roots thin, link-heavy, and synced from `odoo-devkit` instead of hand-maintaining the same entrypoint docs. @@ -107,6 +109,8 @@ Purpose tenant `workspace sync` surface. - Re-render both repo listings and section-level guidance bullets from the tracked cockpit config. +- Preserve local-only notes by linking to `AGENTS.override.md` instead of + copying implementation details into generated markdown. ## `workspace status-cockpit-root` diff --git a/odoo_devkit/workspace_cockpit.py b/odoo_devkit/workspace_cockpit.py index ed9e519..fe5d2d6 100644 --- a/odoo_devkit/workspace_cockpit.py +++ b/odoo_devkit/workspace_cockpit.py @@ -184,7 +184,7 @@ def _render_workspace_agents(manifest: WorkspaceCockpitManifest) -> str: primary_repos = _repos_for_group(manifest, "primary") upstream_repos = _repos_for_group(manifest, "upstream_image") devkit_repo = _repo_for_role(manifest, "devkit") - sync_command = f"uv --directory {devkit_repo.path} run platform workspace sync-cockpit-root --config workspace-cockpit.toml" + sync_command = f"uv --project {devkit_repo.path} run platform workspace sync-cockpit-root --config workspace-cockpit.toml" repo_map_lines = "\n".join(_format_repo_map_line(repo) for repo in primary_repos) upstream_lines = "\n".join(_format_repo_map_line(repo) for repo in upstream_repos) first_read_lines = _render_markdown_bullets(manifest.agents_first_read_lines) @@ -225,7 +225,7 @@ def _render_workspace_docs_index(manifest: WorkspaceCockpitManifest) -> str: upstream_lines = "\n".join( f"- {repo.label}: [{_docs_link_target(repo.path)}]({_docs_link_target(repo.path)})" for repo in upstream_repos ) - sync_command = f"uv --directory {devkit_repo.path} run platform workspace sync-cockpit-root --config workspace-cockpit.toml" + sync_command = f"uv --project {devkit_repo.path} run platform workspace sync-cockpit-root --config workspace-cockpit.toml" external_reference_lines = _render_markdown_bullets(manifest.docs_external_reference_lines) working_split_lines = _render_markdown_bullets(manifest.docs_working_split_lines) operational_note_lines = _render_markdown_bullets(manifest.docs_operational_note_lines) @@ -311,7 +311,7 @@ def _format_repo_map_line(repo: WorkspaceCockpitRepoDefinition) -> str: def _status_command(devkit_repo: WorkspaceCockpitRepoDefinition) -> str: - return f"uv --directory {devkit_repo.path} run platform workspace status-cockpit-root --config workspace-cockpit.toml" + return f"uv --project {devkit_repo.path} run platform workspace status-cockpit-root --config workspace-cockpit.toml" def _render_markdown_bullets(lines: tuple[str, ...]) -> str: @@ -325,6 +325,7 @@ def _render_plain_bullets(lines: tuple[str, ...]) -> str: def _default_agents_first_read_lines() -> tuple[str, ...]: return ( "Open [docs/README.md](docs/README.md) in this workspace root first.", + "If present, open [AGENTS.override.md](AGENTS.override.md) for local, non-secret operator details before touching infra, SSH, tunnels, or remote service configuration.", "Use [sources/devkit/AGENTS.md](sources/devkit/AGENTS.md) for the canonical shared operating guide.", "Use [sources/devkit/docs/README.md](sources/devkit/docs/README.md) for the canonical shared docs index.", "Use the tenant-specific `workspace.toml` manifests when you need to run current local runtime commands through `odoo-devkit`.", @@ -334,9 +335,9 @@ def _default_agents_first_read_lines() -> tuple[str, ...]: def _default_agents_ownership_lines() -> tuple[str, ...]: return ( "`odoo-devkit` owns shared DX/runtime/workspace behavior plus local runtime and explicit data workflows.", - "`harbor` owns remote release actions, deployment truth, release tuples, and promotion evidence.", + "`launchplane` owns remote release actions, deployment truth, release tuples, and promotion evidence.", "Stable remote lanes are `testing` and `prod`.", - "Harbor PR previews replace any durable shared `dev` lane.", + "Launchplane PR previews replace any durable shared `dev` lane.", ) @@ -359,9 +360,9 @@ def _default_docs_external_reference_lines() -> tuple[str, ...]: def _default_docs_working_split_lines() -> tuple[str, ...]: return ( "Use `odoo-devkit` for shared DX/runtime/workspace behavior and for local runtime plus explicit data workflows.", - "Use `harbor` for remote release actions, deployment truth, release tuples, and promotion evidence.", + "Use `launchplane` for remote release actions, deployment truth, release tuples, and promotion evidence.", "Stable remote lanes are `testing` and `prod`.", - "Harbor PR previews replace any durable shared `dev` lane.", + "Launchplane PR previews replace any durable shared `dev` lane.", ) @@ -375,9 +376,9 @@ def _default_session_prompt_rule_lines() -> tuple[str, ...]: return ( "Treat repos under sources/ as the primary system under construction.", "Use odoo-devkit for shared DX/runtime/workspace behavior and local/data workflows.", - "Use harbor for remote release actions, deployment truth, release tuples, and promotion evidence.", + "Use launchplane for remote release actions, deployment truth, release tuples, and promotion evidence.", "Stable remote lanes are testing and prod.", - "Harbor PR previews replace any durable shared dev lane.", + "Launchplane PR previews replace any durable shared dev lane.", "Do not bring odoo-ai into the normal workspace context unless the task is explicit archaeology.", "Keep tenant repos thin and tenant-specific; fix shared behavior in devkit.", "When `workspace-cockpit.toml`, the workspace root, and source repos disagree, treat the source repos as the source of truth, then regenerate the cockpit.", diff --git a/odoo_devkit/workspace_surface.py b/odoo_devkit/workspace_surface.py index 3ff050f..2d96052 100644 --- a/odoo_devkit/workspace_surface.py +++ b/odoo_devkit/workspace_surface.py @@ -103,18 +103,19 @@ def _render_workspace_agents( "This file is generated by `uv run platform workspace sync`. Treat the workspace root as the shared Every Code cockpit for this tenant while keeping hand-edited source-of-truth changes in the underlying repos.\n\n" "## Start Here\n\n" "- Open `docs/README.md` in this workspace root first. It routes to the shared devkit docs and the tenant-specific docs.\n" + "- If present, open `AGENTS.override.md` for local, non-secret operator details before touching infra, SSH, tunnels, or remote service configuration. Keep secrets in `.env`.\n" "- For the deeper shared operating guide, open `sources/devkit/AGENTS.md`.\n" "- For workspace command details, open `sources/devkit/docs/tooling/workspace-cli.md`.\n" "- Use `sources/tenant/` for the active tenant source tree. PyCharm should still open that tenant repo directly by default.\n" "- Use `sources/devkit/` for shared DX/runtime/tooling ownership. That repo is the canonical owner of the shared operating guide and docs used to generate this workspace surface.\n" - "- Treat `platform runtime` as the home for local runtime work plus explicit Dokploy-managed data workflows. Stable remote lanes are `testing` and `prod`; Harbor PR previews replace a durable `dev` lane; release actions such as ship/promote/gate stay in `harbor`.\n" + "- Treat `platform runtime` as the home for local runtime work plus explicit Dokploy-managed data workflows. Stable remote lanes are `testing` and `prod`; Launchplane PR previews replace a durable `dev` lane; release actions such as ship/promote/gate stay in `launchplane`.\n" f"{shared_addons_pointer}\n" - "- Treat `.generated/` and `state/` as managed output only. They are safe to regenerate and should not become a long-term home for hand-edited code or secrets.\n\n" + "- Treat `.generated/` as managed output only. Treat `state/`, when present, as legacy or disposable local runtime output; it should not become a long-term home for hand-edited code or secrets.\n\n" "## Source Of Truth Rules\n\n" f"- Tenant handwritten code lives in `sources/tenant/` and resolves to `{tenant_repo_path}`.\n" f"- Shared DX/docs/runtime guidance lives in `sources/devkit/` and resolves to `{devkit_repo_path}`.\n" f"{shared_addons_source_of_truth}\n" - "- Generated runtime files live under `.generated/`. Disposable logs, caches, and state live under `state/`.\n" + "- Generated runtime files live under `.generated/`. Legacy or disposable local runtime output may live under `state/` when an older workspace still has it.\n" f"- The tracked manifest that defines this workspace is `{manifest.manifest_path}`.\n\n" "## Workspace Commands\n\n" f"- Resync this workspace with `{tenant_workspace_sync_command}`.\n" @@ -131,7 +132,7 @@ def _render_workspace_agents( "- Shared devkit checkout: `sources/devkit/`\n" "- Shared addons checkout: `sources/shared-addons/` when declared\n" "- Generated runtime output: `.generated/`\n" - "- Disposable local state: `state/`\n" + "- Legacy or disposable local runtime output: `state/` when present\n" ) @@ -239,7 +240,7 @@ def _render_workspace_docs_index( "- PyCharm should keep opening the tenant repo directly so search/indexing stays focused on the client code.\n" f"- Preferred tenant-root sync command: `{tenant_workspace_sync_command}`.\n" f"- Preferred tenant-root status command: `{tenant_workspace_status_command}`.\n" - "- Treat `platform runtime` as the local-runtime and remote-data-workflow surface. Stable remote lanes are `testing` and `prod`; Harbor PR previews replace a durable `dev` lane; release actions for remote environments belong in `harbor`.\n" + "- Treat `platform runtime` as the local-runtime and remote-data-workflow surface. Stable remote lanes are `testing` and `prod`; Launchplane PR previews replace a durable `dev` lane; release actions for remote environments belong in `launchplane`.\n" "- When in doubt about ownership, fix the source repo under `sources/` instead of editing generated files in the workspace root.\n" ) @@ -276,9 +277,9 @@ def _render_workspace_session_prompt( "- Keep tenant code in sources/tenant.\n" "- Keep shared DX/runtime/workspace behavior in odoo-devkit.\n" "- Use platform runtime for local runtime and explicit data workflows.\n" - "- Use harbor for remote release actions.\n" + "- Use launchplane for remote release actions.\n" "- Stable remote lanes are testing and prod.\n" - "- Harbor PR previews replace any durable shared dev lane.\n" + "- Launchplane PR previews replace any durable shared dev lane.\n" "- When generated files disagree with source repos, fix the source repo or generator rather than hand-editing generated output.\n" "```\n" ) diff --git a/templates/tenant-overlay/AGENTS.md b/templates/tenant-overlay/AGENTS.md index 42dda8f..3199c9e 100644 --- a/templates/tenant-overlay/AGENTS.md +++ b/templates/tenant-overlay/AGENTS.md @@ -19,7 +19,7 @@ Treat this file as the thin tenant-specific overlay for a tenant repo. `odoo-devkit` repo while shared-addon source stays explicit in the manifest. - Convenience shell commands under `scripts/` for workspace sync/status from the tenant repo root. -- Remote release actions belong in `harbor`; keep this overlay +- Remote release actions belong in `launchplane`; keep this overlay focused on tenant-specific guidance and local/data-workflow entrypoints. ## Do Not Duplicate Here diff --git a/templates/tenant-overlay/docs/README.md b/templates/tenant-overlay/docs/README.md index cff1e28..13092de 100644 --- a/templates/tenant-overlay/docs/README.md +++ b/templates/tenant-overlay/docs/README.md @@ -8,9 +8,9 @@ This docs index is intentionally thin. - Use `sources/devkit/docs/README.md` for shared DX/runtime/bootstrap docs. - Use the generated tenant-root run configurations when you need to call the current runtime commands in the sibling `odoo-devkit` repo. -- Use `harbor` for remote release actions such as ship, promote, +- Use `launchplane` for remote release actions such as ship, promote, and gate execution. Stable remote lanes are `testing` and `prod`; PR - previews belong to Harbor preview workflows instead of a durable `dev` lane. + previews belong to Launchplane preview workflows instead of a durable `dev` lane. - Keep this tenant docs tree focused on tenant-owned domain workflows, architecture notes, and operational quirks. diff --git a/templates/workspace-cockpit/workspace-cockpit.toml b/templates/workspace-cockpit/workspace-cockpit.toml index b0e8435..8782426 100644 --- a/templates/workspace-cockpit/workspace-cockpit.toml +++ b/templates/workspace-cockpit/workspace-cockpit.toml @@ -4,15 +4,16 @@ plans_directory = "~/.codex/plans" [guidance.agents] first_reads = [ "Open [docs/README.md](docs/README.md) in this workspace root first.", + "If present, open [AGENTS.override.md](AGENTS.override.md) for local, non-secret operator details before touching infra, SSH, tunnels, or remote service configuration.", "Use [sources/devkit/AGENTS.md](sources/devkit/AGENTS.md) for the canonical shared operating guide.", "Use [sources/devkit/docs/README.md](sources/devkit/docs/README.md) for the canonical shared docs index.", "Use the tenant-specific `workspace.toml` manifests when you need to run current local runtime commands through `odoo-devkit`.", ] ownership = [ "`odoo-devkit` owns shared DX/runtime/workspace behavior plus local runtime and explicit data workflows.", - "`harbor` owns remote release actions, deployment truth, release tuples, and promotion evidence.", + "`launchplane` owns remote release actions, deployment truth, release tuples, and promotion evidence.", "Stable remote lanes are `testing` and `prod`.", - "Harbor PR previews replace any durable shared `dev` lane.", + "Launchplane PR previews replace any durable shared `dev` lane.", ] notes = [ "This cockpit root is regenerated from `workspace-cockpit.toml` through `odoo-devkit`; keep the repo map and root guidance in that config instead of hand-editing markdown entrypoints.", @@ -28,9 +29,9 @@ external_reference_boundary = [ ] working_split = [ "Use `odoo-devkit` for shared DX/runtime/workspace behavior and for local runtime plus explicit data workflows.", - "Use `harbor` for remote release actions, deployment truth, release tuples, and promotion evidence.", + "Use `launchplane` for remote release actions, deployment truth, release tuples, and promotion evidence.", "Stable remote lanes are `testing` and `prod`.", - "Harbor PR previews replace any durable shared `dev` lane.", + "Launchplane PR previews replace any durable shared `dev` lane.", ] operational_notes = [ "Historical plans remain available under `/Users/cbusillo/.codex/plans/` when you need rationale or prior sequencing.", @@ -40,9 +41,9 @@ operational_notes = [ working_rules = [ "Treat repos under sources/ as the primary system under construction.", "Use odoo-devkit for shared DX/runtime/workspace behavior and local/data workflows.", - "Use harbor for remote release actions, deployment truth, release tuples, and promotion evidence.", + "Use launchplane for remote release actions, deployment truth, release tuples, and promotion evidence.", "Stable remote lanes are testing and prod.", - "Harbor PR previews replace any durable shared dev lane.", + "Launchplane PR previews replace any durable shared dev lane.", "Do not bring odoo-ai into the normal workspace context unless the task is explicit archaeology.", "Keep tenant repos thin and tenant-specific; fix shared behavior in devkit.", "When `workspace-cockpit.toml`, the workspace root, and source repos disagree, treat the source repos as the source of truth, then regenerate the cockpit.", @@ -80,8 +81,8 @@ repo_name = "odoo-tenant-opw" group = "primary" role = "control_plane" label = "Control plane" -path = "sources/harbor" -repo_name = "harbor" +path = "sources/launchplane" +repo_name = "launchplane" [[repos]] group = "upstream_image" diff --git a/tests/test_scaffold.py b/tests/test_scaffold.py index 3cd7cc2..b745bde 100644 --- a/tests/test_scaffold.py +++ b/tests/test_scaffold.py @@ -137,6 +137,7 @@ def test_scaffold_writes_workspace_cockpit_manifest_and_generated_docs(self) -> self.assertIn("schema_version = 1", manifest_text) self.assertIn("workspace-cockpit.toml", agents_text) + self.assertIn("AGENTS.override.md", agents_text) self.assertIn("sources/devkit", agents_text) self.assertIn("sync-cockpit-root", docs_index_text) self.assertIn("status-cockpit-root", docs_index_text) @@ -202,12 +203,14 @@ def test_real_workspace_cockpit_template_links_back_to_devkit(self) -> None: self.assertIn("[guidance.session_prompt]", manifest_text) self.assertIn("sources/devkit/AGENTS.md", agents_text) self.assertIn("sources/devkit/docs/README.md", agents_text) + self.assertIn("AGENTS.override.md", agents_text) self.assertIn("Shared operating guide", docs_index_text) self.assertIn("Shared workspace CLI guide", docs_index_text) self.assertIn("workspace-cockpit.toml", agents_text) + self.assertIn("uv --project sources/devkit", agents_text) self.assertIn("status-cockpit-root", agents_text) self.assertIn("workspace root, and source repos", session_prompt_text) - self.assertIn("harbor for remote release actions", session_prompt_text) + self.assertIn("launchplane for remote release actions", session_prompt_text) class WorkspaceCockpitSyncTests(unittest.TestCase): @@ -265,7 +268,9 @@ def test_sync_workspace_cockpit_rerenders_existing_root(self) -> None: self.assertEqual(result.output_directory, output_directory) self.assertIn(output_directory / "AGENTS.md", result.written_paths) - self.assertIn("sources/shared-addons", (output_directory / "AGENTS.md").read_text(encoding="utf-8")) + agents_text = (output_directory / "AGENTS.md").read_text(encoding="utf-8") + self.assertIn("sources/shared-addons", agents_text) + self.assertIn("AGENTS.override.md", agents_text) self.assertIn("Public base image", (output_directory / "docs" / "README.md").read_text(encoding="utf-8")) self.assertIn("source repos as the source of truth", (output_directory / "docs" / "session-prompt.md").read_text(encoding="utf-8")) diff --git a/tests/test_workspace.py b/tests/test_workspace.py index 2a4e205..9be8d01 100644 --- a/tests/test_workspace.py +++ b/tests/test_workspace.py @@ -121,8 +121,8 @@ def test_sync_creates_workspace_lock_and_pycharm_outputs(self) -> None: self.assertIn(str(devkit_repo_path), workspace_agents_contents) self.assertIn(str((tenant_repo_path / "addons" / "shared").resolve()), workspace_agents_contents) self.assertIn("Stable remote lanes are `testing` and `prod`", workspace_agents_contents) - self.assertIn("Harbor PR previews replace a durable `dev` lane", workspace_agents_contents) - self.assertIn("release actions such as ship/promote/gate stay in `harbor`", workspace_agents_contents) + self.assertIn("Launchplane PR previews replace a durable `dev` lane", workspace_agents_contents) + self.assertIn("release actions such as ship/promote/gate stay in `launchplane`", workspace_agents_contents) workspace_docs_index_contents = result.workspace_docs_index_path.read_text(encoding="utf-8") self.assertIn("Workspace Docs", workspace_docs_index_contents) @@ -134,8 +134,8 @@ def test_sync_creates_workspace_lock_and_pycharm_outputs(self) -> None: self.assertIn("Tenant overlay guide", workspace_docs_index_contents) self.assertIn(str((tenant_repo_path / "addons" / "shared").resolve()), workspace_docs_index_contents) self.assertIn("Stable remote lanes are `testing` and `prod`", workspace_docs_index_contents) - self.assertIn("Harbor PR previews replace a durable `dev` lane", workspace_docs_index_contents) - self.assertIn("release actions for remote environments belong in `harbor`", workspace_docs_index_contents) + self.assertIn("Launchplane PR previews replace a durable `dev` lane", workspace_docs_index_contents) + self.assertIn("release actions for remote environments belong in `launchplane`", workspace_docs_index_contents) workspace_session_prompt_contents = result.workspace_session_prompt_path.read_text(encoding="utf-8") self.assertIn("Session Prompt Template", workspace_session_prompt_contents) @@ -144,7 +144,7 @@ def test_sync_creates_workspace_lock_and_pycharm_outputs(self) -> None: self.assertIn(str(devkit_repo_path), workspace_session_prompt_contents) self.assertIn("generated cockpit, not the source of truth", workspace_session_prompt_contents) self.assertIn("Stable remote lanes are testing and prod", workspace_session_prompt_contents) - self.assertIn("Harbor PR previews replace any durable shared dev lane", workspace_session_prompt_contents) + self.assertIn("Launchplane PR previews replace any durable shared dev lane", workspace_session_prompt_contents) self.assertEqual(len(result.run_configuration_paths), 2) first_run_configuration = result.run_configuration_paths[0].read_text(encoding="utf-8") @@ -573,9 +573,11 @@ def test_workspace_surface_prefers_tenant_root_scripts_when_present(self) -> Non self.assertIn("sources/tenant/scripts/workspace-sync", workspace_agents_contents) self.assertIn("sources/tenant/scripts/workspace-status", workspace_agents_contents) + self.assertIn("AGENTS.override.md", workspace_agents_contents) + self.assertIn("Legacy or disposable local runtime output", workspace_agents_contents) self.assertIn("sources/tenant/scripts/workspace-sync", workspace_docs_contents) self.assertIn("sources/tenant/scripts/workspace-status", workspace_docs_contents) - self.assertIn("harbor for remote release actions", workspace_session_prompt_contents) + self.assertIn("launchplane for remote release actions", workspace_session_prompt_contents) def test_cli_parser_accepts_workspace_run_remainder(self) -> None: parser = build_parser()