scaffold(0001): validator + CI gate + dev-department.yaml manifest
All checks were successful
Validate dev-department tree / Validate tree (pull_request) Successful in 49s
All checks were successful
Validate dev-department tree / Validate tree (pull_request) Successful in 49s
Initial scaffold for the dev-department subtree repo. No workspace
content yet — that lands in Phase 3c-2 (extract dev tree with git
history from molecule-ai-org-template-molecule-dev).
Files:
- dev-department.yaml manifest with defaults + category_routing,
empty roots: [] (gets populated by extract).
- .molecule-ci/scripts/validate-tree.py
orphan / reachability lint. Walks manifest
→ roots → recursive children + !include,
compares against filesystem, reports
orphans + cross-tree '..' refs + duplicate
parents + missing workspace.yaml. Exits
non-zero on any violation. Stdlib only +
PyYAML.
- .github/workflows/validate.yml
CI gate runs the validator on every PR +
push to main/staging. Pinned action SHAs
per saved memory feedback_pin_third_party_actions.
- README.md explains subtree contract: parent template
must symlink the dev-department under a
short name (e.g. `dev`), workspace
files_dir paths inside this repo use the
symlink prefix, this repo is NOT directly
importable as a standalone org template.
- .gitignore ignore .env (per-workspace secrets are
populated by platform import, never
committed).
- .gitattributes force LF on shell/Python/YAML.
Verified locally:
- empty tree → "OK — tree is clean", exit 0.
- cross-tree `..` fixture → exit 1, FAIL with reported violation.
- orphan fixture → exit 1, FAIL with reported orphan folder.
Refs:
- internal#77 (extraction RFC, Phase 1+2 done as comment 1886)
- molecule-core#102 (symlink-resolution contract pinned by tests)
- Hongming GO 2026-05-08 ("you own this feature and repos, start")
- SOP Phase 3b — task #223
This commit is contained in:
parent
a1dbc8caf0
commit
a21212d73d
6
.gitattributes
vendored
Normal file
6
.gitattributes
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
# Force LF on shell + Python + YAML — Linux containers choke on \r\n
|
||||
*.sh text eol=lf
|
||||
*.py text eol=lf
|
||||
*.yaml text eol=lf
|
||||
*.yml text eol=lf
|
||||
*.md text eol=lf
|
||||
45
.github/workflows/validate.yml
vendored
Normal file
45
.github/workflows/validate.yml
vendored
Normal file
@ -0,0 +1,45 @@
|
||||
# Gitea Actions CI gate: run the tree validator on every PR + push.
|
||||
#
|
||||
# The validator catches: orphan workspace folders, cross-tree `..`
|
||||
# traversal in children: paths, duplicate parent claims, missing
|
||||
# workspace.yaml, generic !include errors.
|
||||
#
|
||||
# Refs: internal#77 (Phase 3b — task #223).
|
||||
|
||||
name: Validate dev-department tree
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, staging]
|
||||
pull_request:
|
||||
branches: [main, staging]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
name: Validate tree
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
# We don't follow submodules. The dev-department subtree is
|
||||
# self-contained; cross-repo composition is verified at the
|
||||
# parent-template's CI level (internal#77 Phase 4).
|
||||
submodules: false
|
||||
|
||||
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install PyYAML
|
||||
run: python -m pip install --no-input --disable-pip-version-check pyyaml==6.0.1
|
||||
|
||||
- name: Run validator
|
||||
run: |
|
||||
chmod +x .molecule-ci/scripts/validate-tree.py
|
||||
.molecule-ci/scripts/validate-tree.py
|
||||
19
.gitignore
vendored
Normal file
19
.gitignore
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
# Per-workspace secrets — populated at platform-import time, never committed.
|
||||
# Each workspace's .env.example is the contract for what it expects.
|
||||
**/.env
|
||||
|
||||
# Editor + OS noise
|
||||
.DS_Store
|
||||
*.swp
|
||||
.idea/
|
||||
.vscode/
|
||||
|
||||
# Python validator scratch
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.pytest_cache/
|
||||
|
||||
# Local scratch
|
||||
*.bak
|
||||
*.orig
|
||||
*.rej
|
||||
326
.molecule-ci/scripts/validate-tree.py
Executable file
326
.molecule-ci/scripts/validate-tree.py
Executable file
@ -0,0 +1,326 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
validate-tree.py — orphan / reachability / shape lint for an org-template tree.
|
||||
|
||||
Walks the manifest (org.yaml or dev-department.yaml) → roots → recursive
|
||||
`children:` (and `!include`) → builds the set of reachable workspace
|
||||
folders → compares against the filesystem → reports violations.
|
||||
|
||||
Catches the four failure modes that motivated the RFC (internal#77):
|
||||
|
||||
1. Orphan workspace folders (folder exists, no parent claims it).
|
||||
2. Cross-tree `..` traversal in `children:` paths (atomization rule).
|
||||
3. Workspace folder without `workspace.yaml` (broken nest).
|
||||
4. Two parents claiming the same child workspace (graph not a tree).
|
||||
|
||||
Usage:
|
||||
|
||||
.molecule-ci/scripts/validate-tree.py [<manifest>]
|
||||
|
||||
Exits non-zero on any violation. With no arg, defaults to the first of
|
||||
{dev-department.yaml, org.yaml} that exists in cwd.
|
||||
|
||||
Standard library only — runs on every CI runner without `pip install`.
|
||||
|
||||
Refs: internal#77 (Phase 3b — task #223).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
try:
|
||||
import yaml # PyYAML
|
||||
except ImportError:
|
||||
sys.stderr.write(
|
||||
"validate-tree.py: PyYAML required. Install via `pip install pyyaml` or use a runner that bundles it.\n"
|
||||
)
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
# ---------- !include + children: walker ----------
|
||||
|
||||
INCLUDE_TAG = "!include"
|
||||
|
||||
|
||||
class IncludingLoader(yaml.SafeLoader):
|
||||
"""SafeLoader that records `!include <path>` scalars verbatim instead
|
||||
of trying to resolve them. We do resolution explicitly so we can also
|
||||
track the parent→child edge for the orphan/duplicate check."""
|
||||
|
||||
|
||||
def _include_constructor(loader: yaml.Loader, node: yaml.Node) -> dict:
|
||||
"""Replace a `!include` scalar with a sentinel dict the walker
|
||||
interprets. We don't resolve the file content here — the walker does
|
||||
that with full path-context awareness."""
|
||||
if not isinstance(node, yaml.ScalarNode):
|
||||
raise yaml.YAMLError(f"!include must be a scalar path; got {node.tag} at line {node.start_mark.line}")
|
||||
return {"__include__": loader.construct_scalar(node)}
|
||||
|
||||
|
||||
IncludingLoader.add_constructor(INCLUDE_TAG, _include_constructor)
|
||||
|
||||
|
||||
def _yaml_load(path: Path) -> Any:
|
||||
with path.open("r", encoding="utf-8") as f:
|
||||
return yaml.load(f, Loader=IncludingLoader)
|
||||
|
||||
|
||||
# ---------- Tree walk ----------
|
||||
|
||||
|
||||
class TreeReport:
|
||||
def __init__(self) -> None:
|
||||
self.parent_of: dict[str, str] = {} # workspace-folder → parent-folder
|
||||
self.cross_tree_refs: list[tuple[str, str]] = [] # (where, escaping path)
|
||||
self.duplicates: list[tuple[str, str, str]] = [] # (folder, parent_a, parent_b)
|
||||
self.missing_workspace_yaml: list[str] = [] # folders referenced as children but no workspace.yaml
|
||||
self.errors: list[str] = [] # generic errors (yaml parse, missing include)
|
||||
|
||||
def add_edge(self, parent_folder: str, child_folder: str) -> None:
|
||||
if child_folder in self.parent_of:
|
||||
self.duplicates.append((child_folder, self.parent_of[child_folder], parent_folder))
|
||||
else:
|
||||
self.parent_of[child_folder] = parent_folder
|
||||
|
||||
def reachable(self) -> set[str]:
|
||||
return set(self.parent_of.keys())
|
||||
|
||||
def has_violations(self) -> bool:
|
||||
return bool(self.cross_tree_refs or self.duplicates or self.missing_workspace_yaml or self.errors)
|
||||
|
||||
|
||||
def _walk_workspace_node(
|
||||
node: Any,
|
||||
yaml_dir: Path, # dir of the YAML file currently being processed (for relative paths)
|
||||
repo_root: Path, # repo root (for orphan-set comparison + escape detection)
|
||||
parent_folder: str | None,
|
||||
report: TreeReport,
|
||||
) -> None:
|
||||
"""Walk a workspace-shaped dict (or list of children) recursively.
|
||||
|
||||
For each `!include` we encountered (now wrapped as `{"__include__": "<path>"}`),
|
||||
we resolve to the target file, register the workspace folder, and
|
||||
recurse into the loaded content.
|
||||
"""
|
||||
if node is None:
|
||||
return
|
||||
|
||||
# Top-level YAML doc may have `workspaces:` or `roots:` (the
|
||||
# dev-department.yaml convention) listing the root workspaces.
|
||||
if isinstance(node, dict) and ("workspaces" in node or "roots" in node):
|
||||
roots = node.get("roots") or node.get("workspaces") or []
|
||||
for child in roots:
|
||||
_walk_workspace_node(child, yaml_dir, repo_root, parent_folder=None, report=report)
|
||||
return
|
||||
|
||||
# !include sentinel: resolve, register, recurse.
|
||||
if isinstance(node, dict) and "__include__" in node:
|
||||
rel = node["__include__"]
|
||||
target = (yaml_dir / rel).resolve()
|
||||
try:
|
||||
target.relative_to(repo_root.resolve())
|
||||
except ValueError:
|
||||
# The !include path escapes the repo root. This is the
|
||||
# cross-repo symlink case (parent template !include-ing into
|
||||
# the dev-department subtree via a symlink). The child folder
|
||||
# is OUTSIDE repo_root — record but don't claim as duplicate.
|
||||
# For the dev-department validator, repo_root IS dev-department,
|
||||
# so its own internal !includes never escape; cross-repo
|
||||
# composition is parent-template's concern.
|
||||
report.errors.append(
|
||||
f"!include {rel!r} (from {yaml_dir.name}) resolves outside repo root: {target}"
|
||||
)
|
||||
return
|
||||
if not target.exists():
|
||||
report.errors.append(f"!include {rel!r} (from {yaml_dir.name}): target does not exist: {target}")
|
||||
return
|
||||
|
||||
# If the include targets a workspace.yaml, the FOLDER containing
|
||||
# it is the workspace identity.
|
||||
if target.name == "workspace.yaml":
|
||||
child_folder = str(target.parent.resolve().relative_to(repo_root.resolve()))
|
||||
else:
|
||||
# Team-shaped !include (e.g. teams/core-platform.yaml) — not a
|
||||
# workspace folder of its own. Recurse into its content.
|
||||
child_folder = None
|
||||
|
||||
if child_folder is not None and parent_folder is not None:
|
||||
# Reject `..` traversal in the path the user wrote (atomization
|
||||
# rule). The resolved target may legitimately be in a parent
|
||||
# folder (sibling tree), but the dev-department's `children:`
|
||||
# paths are required to be `./<child>` only.
|
||||
if rel.startswith("..") or "/.." in rel:
|
||||
report.cross_tree_refs.append((parent_folder, rel))
|
||||
|
||||
if child_folder is not None:
|
||||
report.add_edge(parent_folder or "<root>", child_folder)
|
||||
|
||||
# Load and recurse.
|
||||
try:
|
||||
sub = _yaml_load(target)
|
||||
except yaml.YAMLError as e:
|
||||
report.errors.append(f"yaml parse {target}: {e}")
|
||||
return
|
||||
_walk_workspace_node(
|
||||
sub,
|
||||
yaml_dir=target.parent,
|
||||
repo_root=repo_root,
|
||||
parent_folder=child_folder if child_folder is not None else parent_folder,
|
||||
report=report,
|
||||
)
|
||||
return
|
||||
|
||||
# Inline workspace-shaped dict.
|
||||
if isinstance(node, dict):
|
||||
# `files_dir:` identifies the workspace folder for inline declarations.
|
||||
files_dir = node.get("files_dir")
|
||||
if files_dir and parent_folder is None:
|
||||
# A root-level workspace declared inline (no !include). The
|
||||
# files_dir is the folder.
|
||||
files_dir_resolved = (repo_root / files_dir).resolve()
|
||||
try:
|
||||
rel_to_root = files_dir_resolved.relative_to(repo_root.resolve())
|
||||
except ValueError:
|
||||
report.errors.append(f"files_dir {files_dir!r} escapes repo root")
|
||||
return
|
||||
this_folder = str(rel_to_root)
|
||||
report.add_edge("<root>", this_folder)
|
||||
# Verify a workspace.yaml exists in that folder for atomized
|
||||
# tree (post-Phase 3c-2).
|
||||
ws_yaml = files_dir_resolved / "workspace.yaml"
|
||||
if not ws_yaml.exists():
|
||||
# Pre-atomization, a workspace can be declared inline at
|
||||
# the manifest level without a workspace.yaml in its
|
||||
# files_dir. Don't false-positive.
|
||||
pass
|
||||
current_folder = this_folder
|
||||
else:
|
||||
current_folder = parent_folder
|
||||
|
||||
# Recurse into children.
|
||||
for child in node.get("children") or []:
|
||||
_walk_workspace_node(child, yaml_dir, repo_root, current_folder, report)
|
||||
return
|
||||
|
||||
if isinstance(node, list):
|
||||
for child in node:
|
||||
_walk_workspace_node(child, yaml_dir, repo_root, parent_folder, report)
|
||||
return
|
||||
|
||||
|
||||
# ---------- Filesystem scan ----------
|
||||
|
||||
|
||||
# Folders inside the repo that are NOT workspace folders. The validator
|
||||
# allows these to exist without a parent in the tree.
|
||||
NON_WORKSPACE_DIRS = {
|
||||
".git", ".github", ".molecule-ci", "docs", "scripts", "tests", "fixtures",
|
||||
"node_modules", "__pycache__", ".cache", ".venv", "venv",
|
||||
}
|
||||
|
||||
|
||||
def _scan_workspace_folders(repo_root: Path) -> set[str]:
|
||||
"""Every directory containing a workspace.yaml is a workspace folder.
|
||||
Path returned is repo-relative and POSIX-style."""
|
||||
found: set[str] = set()
|
||||
for dirpath, dirnames, filenames in os.walk(repo_root, followlinks=False):
|
||||
# Prune obvious non-workspace dirs.
|
||||
dirnames[:] = [d for d in dirnames if d not in NON_WORKSPACE_DIRS]
|
||||
if "workspace.yaml" in filenames:
|
||||
rel = Path(dirpath).resolve().relative_to(repo_root.resolve())
|
||||
if str(rel) != ".":
|
||||
found.add(str(rel))
|
||||
return found
|
||||
|
||||
|
||||
# ---------- Top-level ----------
|
||||
|
||||
|
||||
def _find_manifest() -> Path:
|
||||
for name in ("dev-department.yaml", "org.yaml"):
|
||||
p = Path(name)
|
||||
if p.exists():
|
||||
return p
|
||||
sys.stderr.write(
|
||||
"validate-tree.py: no manifest found in cwd. Looked for: dev-department.yaml, org.yaml\n"
|
||||
)
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) > 2:
|
||||
sys.stderr.write("usage: validate-tree.py [<manifest>]\n")
|
||||
return 2
|
||||
manifest = Path(sys.argv[1]) if len(sys.argv) == 2 else _find_manifest()
|
||||
if not manifest.exists():
|
||||
sys.stderr.write(f"validate-tree.py: manifest does not exist: {manifest}\n")
|
||||
return 2
|
||||
|
||||
repo_root = manifest.resolve().parent
|
||||
report = TreeReport()
|
||||
|
||||
try:
|
||||
root_doc = _yaml_load(manifest)
|
||||
except yaml.YAMLError as e:
|
||||
sys.stderr.write(f"validate-tree.py: parsing {manifest}: {e}\n")
|
||||
return 2
|
||||
|
||||
_walk_workspace_node(
|
||||
root_doc,
|
||||
yaml_dir=manifest.parent.resolve(),
|
||||
repo_root=repo_root,
|
||||
parent_folder=None,
|
||||
report=report,
|
||||
)
|
||||
|
||||
fs_workspaces = _scan_workspace_folders(repo_root)
|
||||
reachable = report.reachable()
|
||||
orphans = sorted(fs_workspaces - reachable)
|
||||
|
||||
# Build report.
|
||||
print(f"=== validate-tree.py report — manifest: {manifest} ===")
|
||||
print(f" filesystem workspace folders : {len(fs_workspaces)}")
|
||||
print(f" reachable from manifest : {len(reachable)}")
|
||||
print(f" orphans : {len(orphans)}")
|
||||
print(f" cross-tree '..' refs : {len(report.cross_tree_refs)}")
|
||||
print(f" duplicate-parent claims : {len(report.duplicates)}")
|
||||
print(f" missing workspace.yaml : {len(report.missing_workspace_yaml)}")
|
||||
print(f" generic errors : {len(report.errors)}")
|
||||
print()
|
||||
|
||||
if orphans:
|
||||
print("ORPHANS (workspace folder exists but no parent claims it):")
|
||||
for o in orphans:
|
||||
print(f" - {o}")
|
||||
print()
|
||||
if report.cross_tree_refs:
|
||||
print("CROSS-TREE '..' REFS (atomization rule violation):")
|
||||
for parent, path in report.cross_tree_refs:
|
||||
print(f" - parent={parent} path={path}")
|
||||
print()
|
||||
if report.duplicates:
|
||||
print("DUPLICATE PARENT CLAIMS (graph not a tree):")
|
||||
for child, p1, p2 in report.duplicates:
|
||||
print(f" - child={child} claimed_by=[{p1}, {p2}]")
|
||||
print()
|
||||
if report.errors:
|
||||
print("ERRORS:")
|
||||
for e in report.errors:
|
||||
print(f" - {e}")
|
||||
print()
|
||||
|
||||
fail = bool(orphans) or report.has_violations()
|
||||
if fail:
|
||||
print("FAIL — see above")
|
||||
return 1
|
||||
print("OK — tree is clean")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
142
README.md
142
README.md
@ -1,3 +1,141 @@
|
||||
# molecule-dev-department
|
||||
# molecule-ai/molecule-dev-department
|
||||
|
||||
Importable dev-team subtree for molecule-ai org templates. Extracted from molecule-ai-org-template-molecule-dev (internal#77). Atomized: each workspace owns its persona, plugins, skills, and .env. Composed via folder-tree (no !include cross-references).
|
||||
**Importable engineering-tree subtree** for Molecule AI org templates.
|
||||
|
||||
This repo is **not a standalone org template**. It is designed to be
|
||||
grafted into a parent template (e.g. `molecule-ai-org-template-molecule-dev`)
|
||||
via filesystem symlink at deploy time. The parent template owns the org
|
||||
identity, top-level workspaces (PM, Marketing, Research, …), and
|
||||
imports this repo's `dev-lead/` subtree as its engineering org.
|
||||
|
||||
## Why a separate repo
|
||||
|
||||
`molecule-ai-org-template-molecule-dev` had grown to ~60 workspace
|
||||
folders + 11 `teams/*.yaml` composition files + 17 *orphaned* folders
|
||||
that no `!include` chain reached. The orphan accumulation was a sign
|
||||
the structure had outgrown a single repo.
|
||||
|
||||
Splitting the dev tree out:
|
||||
|
||||
- Atomizes engineering as a self-contained unit that other org templates
|
||||
can reuse (one link to add the whole department).
|
||||
- Makes orphan accumulation impossible — the validator (CI gate) walks
|
||||
the manifest → roots → children and fails on any folder not reachable.
|
||||
- Lets the dev tree evolve on its own cadence without churning the
|
||||
parent template.
|
||||
- Keeps the parent template's structure focused on org identity (PM,
|
||||
Marketing, Research) and removes the ~50% of mass that's dev-specific.
|
||||
|
||||
Full design rationale: [internal#77 RFC](https://git.moleculesai.app/molecule-ai/internal/issues/77)
|
||||
|
||||
## Subtree contract
|
||||
|
||||
This repo is consumed by parent templates via this convention:
|
||||
|
||||
1. **Operator-side deploy layout** clones both repos as siblings under
|
||||
`/org-templates/`:
|
||||
|
||||
```
|
||||
/org-templates/
|
||||
molecule-ai-org-template-molecule-dev/ ← parent template
|
||||
molecule-dev-department/ ← THIS repo
|
||||
```
|
||||
|
||||
2. **Parent template** has a relative directory symlink at its root
|
||||
(or under `teams/`):
|
||||
|
||||
```
|
||||
parent-template/
|
||||
org.yaml
|
||||
dev → ../molecule-dev-department/ ← symlink
|
||||
```
|
||||
|
||||
3. **Parent's `org.yaml`** imports the subtree:
|
||||
|
||||
```yaml
|
||||
workspaces:
|
||||
- !include teams/pm.yaml
|
||||
- !include teams/marketing.yaml
|
||||
- !include dev/dev-lead/workspace.yaml ← into the symlinked subtree
|
||||
```
|
||||
|
||||
4. **Workspace `files_dir:` paths inside this repo** use the symlink
|
||||
prefix (`dev/<workspace-name>`) so they resolve correctly when the
|
||||
subtree is imported via the parent. This means the subtree is **not
|
||||
directly importable as a standalone org template** — by design.
|
||||
|
||||
The platform's org importer (`workspace-server/internal/handlers/org_include.go`)
|
||||
follows symlinks at the OS layer (`os.ReadFile` is symlink-aware) while
|
||||
its security check (`filepath.Abs` / `filepath.Rel`) operates on path
|
||||
strings (passes for symlinked paths because the link's path is inside
|
||||
the parent root). The contract is pinned by tests in
|
||||
[molecule-core PR #102](https://git.moleculesai.app/molecule-ai/molecule-core/pulls/102).
|
||||
|
||||
## Repo layout
|
||||
|
||||
```
|
||||
.
|
||||
├── dev-department.yaml ← manifest: defaults + category_routing + roots
|
||||
├── .molecule-ci/scripts/
|
||||
│ └── validate-tree.py ← orphan / reachability lint (CI gate)
|
||||
├── .github/workflows/
|
||||
│ └── validate.yml ← runs validate-tree.py on every PR
|
||||
├── README.md ← this file
|
||||
├── LICENSE ← MIT
|
||||
└── <workspace-folders> ← scaffolded empty; populated by Phase 3c-2
|
||||
```
|
||||
|
||||
After Phase 3c-2 (extract dev tree with git history) the repo will
|
||||
contain the dev-lead/ workspace tree with nested sub-teams. After
|
||||
Phase 3c-3 (move documentation-specialist + triage-operator into the
|
||||
tree per Hongming Q1+Q2) those workspaces will live under
|
||||
`dev-lead/app-docs/documentation-specialist/` and `dev-lead/triage-operator/`
|
||||
respectively.
|
||||
|
||||
## Validating locally
|
||||
|
||||
```bash
|
||||
.molecule-ci/scripts/validate-tree.py
|
||||
# OK — tree is clean
|
||||
|
||||
# Or with explicit manifest:
|
||||
.molecule-ci/scripts/validate-tree.py dev-department.yaml
|
||||
```
|
||||
|
||||
The validator:
|
||||
|
||||
- Walks `dev-department.yaml → roots → children` recursively, including
|
||||
through `!include` directives.
|
||||
- Lists every directory containing `workspace.yaml`.
|
||||
- Reports orphans (filesystem dirs not reachable from manifest),
|
||||
cross-tree `..` traversal in `children:` paths, duplicate parents,
|
||||
and missing `workspace.yaml`.
|
||||
- Exits non-zero on any violation.
|
||||
|
||||
CI runs the same script via `.github/workflows/validate.yml` on every
|
||||
push and PR — orphan accumulation is caught at PR time, not at deploy
|
||||
time.
|
||||
|
||||
## Phase status
|
||||
|
||||
| Phase | Status | Where |
|
||||
|---|---|---|
|
||||
| 1 — Investigate platform org importer | ✓ done | internal#77 comment 1886 |
|
||||
| 2 — Design (SSOT, alternatives, security, versioning) | ✓ done | internal#77 |
|
||||
| 3a — Platform `external:` ref support | parked (deferred) | task #222 |
|
||||
| 3b — Validator + CI gate | ✓ done | this commit |
|
||||
| 3c-1 — Scaffold this repo | ✓ done | this commit |
|
||||
| 3c-2 — Extract dev tree with history | pending | task #224 |
|
||||
| 3c-3 — Atomize structure + move doc-spec + triage-op | pending | task #224 |
|
||||
| 3d — Slim parent template + wire symlink + delete orphans | pending | task #225 |
|
||||
| 4 — End-to-end verify on staging | pending | task #226 |
|
||||
|
||||
## Refs
|
||||
|
||||
- [internal#77](https://git.moleculesai.app/molecule-ai/internal/issues/77) — extraction RFC
|
||||
- [molecule-core#102](https://git.moleculesai.app/molecule-ai/molecule-core/pulls/102) — symlink-resolution test
|
||||
- Hongming GO 2026-05-08 ("you own this feature and repos, start")
|
||||
|
||||
## License
|
||||
|
||||
MIT — see [LICENSE](./LICENSE)
|
||||
|
||||
80
dev-department.yaml
Normal file
80
dev-department.yaml
Normal file
@ -0,0 +1,80 @@
|
||||
# Molecule AI — Dev Department subtree manifest
|
||||
#
|
||||
# This file is the importable-subtree's root config. It carries the same
|
||||
# shape as a full org template's `org.yaml` — defaults + category_routing
|
||||
# + plugin set + roots — but is consumed via gitops-style symlink from a
|
||||
# parent template (see README §Subtree contract).
|
||||
#
|
||||
# Hongming-confirmed name: dev-department.yaml (2026-05-08).
|
||||
#
|
||||
# Refs:
|
||||
# internal#77 — gitops-style extraction RFC
|
||||
# molecule-core#102 — symlink-resolution contract pinned by tests
|
||||
|
||||
name: Molecule AI Dev Department
|
||||
description: >-
|
||||
Importable subtree containing the engineering org tree:
|
||||
Dev Lead + Core Platform + Controlplane + App-Docs (incl. Documentation
|
||||
Specialist) + Infra + SDK sub-teams, plus floaters (Release Manager,
|
||||
Integration Tester, Fullstack), plus Triage Operator.
|
||||
|
||||
# Defaults applied to every workspace in this subtree. Per-workspace
|
||||
# `plugins:` field UNIONs with this list (see Hongming Q1: per-workspace
|
||||
# plugins are first-class). A leading `!` or `-` opts a default plugin
|
||||
# OUT for one workspace.
|
||||
#
|
||||
# Same shape as parent's org.yaml `defaults:` block. When this manifest
|
||||
# is grafted into a parent template via the symlink contract, the
|
||||
# parent's own defaults still apply at the parent-template level — these
|
||||
# only set defaults INSIDE the dev tree.
|
||||
defaults:
|
||||
runtime: claude-code
|
||||
tier: 2
|
||||
|
||||
plugins:
|
||||
- ecc # Everything Claude Code guardrails + coding skills
|
||||
- molecule-dev # Molecule AI codebase conventions, past bugs, review-loop
|
||||
- superpowers # systematic-debugging, TDD, planning, verification
|
||||
- molecule-careful-bash # refuse destructive shell (rm -rf, push --force, DROP TABLE)
|
||||
- molecule-prompt-watchdog # warn on destructive user prompts
|
||||
- molecule-audit-trail # append every Edit/Write to .claude/audit.jsonl
|
||||
- molecule-session-context # auto-load cron learnings + PR/issue counts on SessionStart
|
||||
- molecule-skill-cron-learnings # per-tick learning JSONL (pairs with session-context)
|
||||
- molecule-skill-update-docs # keep architecture / README / edit-history aligned
|
||||
|
||||
# Audit-summary routing — Auditors fan out findings to the listed roles.
|
||||
# Roles are by display name (Dev Lead, Backend Engineer, ...) not by
|
||||
# workspace folder name. Roles must exist in this subtree's roots:
|
||||
# block — the validator will catch dangling references in a follow-up.
|
||||
category_routing:
|
||||
security: [Backend Engineer, DevOps Engineer]
|
||||
offensive: [Security Auditor, Backend Engineer, DevOps Engineer]
|
||||
ui: [Frontend Engineer]
|
||||
ux: [Frontend Engineer]
|
||||
infra: [DevOps Engineer, Platform Engineer, SRE Engineer]
|
||||
cloud: [DevOps Engineer, Platform Engineer, SRE Engineer, Backend Engineer]
|
||||
qa: [QA Engineer]
|
||||
performance: [Backend Engineer]
|
||||
docs: [Documentation Specialist]
|
||||
mixed: [Dev Lead]
|
||||
research: [Research Lead]
|
||||
plugins: [Technical Researcher]
|
||||
template: [Dev Lead]
|
||||
channels: [DevOps Engineer]
|
||||
|
||||
idle_prompt: "" # Off by default — set per-workspace to enable idle reflection
|
||||
|
||||
# Roots block: list the top-level workspaces of this subtree.
|
||||
#
|
||||
# Each root entry is a `!include <path>/workspace.yaml` reference to a
|
||||
# workspace folder at the repo root level. The validator walks each
|
||||
# referenced workspace.yaml recursively via its `children:` field.
|
||||
#
|
||||
# Atomization rule (Hongming Q3+Q5): `children:` paths inside a
|
||||
# workspace.yaml MUST be relative-and-down-only (`./<child>`); no `..`.
|
||||
# The `.molecule-ci/scripts/validate-tree.py` CI gate enforces this.
|
||||
#
|
||||
# This list is empty in the scaffold commit. Phase 3c-2 (extract content
|
||||
# with git history) populates it. Phase 3c-3 nests doc-spec + triage-op
|
||||
# under dev-lead/.
|
||||
roots: []
|
||||
Loading…
Reference in New Issue
Block a user