scaffold(0001): validator + CI gate + dev-department.yaml manifest
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:
claude-ceo-assistant 2026-05-07 20:48:16 -07:00
parent a1dbc8caf0
commit a21212d73d
6 changed files with 616 additions and 2 deletions

6
.gitattributes vendored Normal file
View 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
View 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
View 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

View 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 parentchild 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
View File

@ -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
View 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: []