feat(org-templates): Phase 3 — !include directive + split org.yaml into team files
Part 3 of 4 in the scalability refactor. Adds YAML `!include` support to the org importer and splits molecule-dev/org.yaml (676 lines post- Phase 2) into 6 team / role files; top-level org.yaml drops to 114 lines of pure scaffolding. ## Platform changes New `platform/internal/handlers/org_include.go`: - `resolveYAMLIncludes(data, baseDir)` — pre-processes a YAML document, expanding any scalar tagged `!include <path>` with the parsed content of the referenced file. - Path resolution via `resolveInsideRoot` so a crafted `!include ../../etc/passwd` can't escape the org template directory (same defense the existing `files_dir` copy uses). - Nested includes supported: each included file carries its own search root (its directory), so `teams/pm.yaml` with `!include research.yaml` resolves to `teams/research.yaml` — matching the convention of C-include / Sass @import / most package systems. - Cycle detection via visited-set keyed on absolute path; belt-and- braces `maxIncludeDepth = 16` cap in case symlinks or path normalization defeats the set. - Inline-template mode (POST /org/import with raw JSON body, no `dir`) errors cleanly when a file ref is used — can't resolve without a base. Wired into both `ListTemplates` (so /org/templates shows an accurate workspace count after the split) and `Import` (expansion happens before unmarshal into OrgTemplate). ## Template changes molecule-dev/org.yaml now contains only: - name + description - defaults (runtime, plugins, category_routing, initial_prompt text) - `workspaces: [!include teams/pm.yaml, !include teams/marketing.yaml]` New files: - `teams/pm.yaml` — PM top-level, children are !include refs - `teams/research.yaml` — Research Lead + Market Analyst + Technical Researcher + Competitive Intelligence (inline children) - `teams/dev.yaml` — Dev Lead + FE/BE/DevOps/Security/QA/UIUX (inline) - `teams/marketing.yaml` — Marketing Lead + DevRel/PMM/Content/ Community/SEO/Social (inline) - `teams/documentation-specialist.yaml` — leaf - `teams/triage-operator.yaml` — leaf ## File-size impact | State | org.yaml lines | total config size | |---|---:|---:| | Before (main) | 1801 | 108 KB | | After Phase 1 (#389) | 1687 | 101 KB | | After Phase 2 (#390) | 676 | 35 KB | | After this PR | **114** | **4 KB** (org.yaml only) | With the 6 team files (total ~570 lines of structural yaml), every file is now under 230 lines and individually readable without scrolling past a single team's boundaries. ## Tests `platform/internal/handlers/org_include_test.go` — 9 cases: - Flat include (single file, single workspace) - Nested include (file → file → file) - Traversal rejection (`../secret.yaml`, `../../secret.yaml`) - Cycle detection (a↔b) - Empty path error - Missing file error - Inline-template error (baseDir empty) - No-op when YAML has no includes (safety: we always run the preprocessor) - **Integration**: load the real `org-templates/molecule-dev/org.yaml`, resolve includes, unmarshal into OrgTemplate, verify PM + Marketing Lead are top-level and PM has ≥4 children after expansion. All 9 pass + existing `TestResolvePromptRef` + `TestOrgYAML` suites stay green. ## Ownership implication Each team file can now be owned + reviewed independently. When the marketing team adds a 7th role, the diff is in `teams/marketing.yaml` alone — no merge conflicts against PM or research changes in the same review window. Same for the eventual engineer team, security team, etc. ## What's next - **Phase 4 (queued):** per-workspace atomization. Each role gets `<role>/workspace.yaml`; team files shrink to a list of !include refs. Terminal step in the scalability arc — at that point adding a new role is one new file under `org-templates/molecule-dev/<role>/` plus one line in the team's manifest. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f075c49af1
commit
24a882ccc9
@ -110,567 +110,5 @@ defaults:
|
||||
6. You are now ready. Wait for tasks from your parent — do not initiate contact.
|
||||
|
||||
workspaces:
|
||||
- name: PM
|
||||
role: Project Manager — coordinates Research and Dev teams
|
||||
tier: 3
|
||||
model: opus
|
||||
files_dir: pm
|
||||
workspace_dir: ${WORKSPACE_DIR}
|
||||
canvas: {x: 400, y: 50}
|
||||
# PM-specific: /triage (PR triage) and /retro (weekly retrospective).
|
||||
plugins: [molecule-workflow-triage, molecule-workflow-retro]
|
||||
# Auto-link Telegram so the user can talk to PM directly from Telegram.
|
||||
# Bot token + chat ID come from pm/.env (TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID).
|
||||
channels:
|
||||
- type: telegram
|
||||
config:
|
||||
bot_token: ${TELEGRAM_BOT_TOKEN}
|
||||
chat_id: ${TELEGRAM_CHAT_ID}
|
||||
enabled: true
|
||||
schedules:
|
||||
- name: Orchestrator pulse
|
||||
cron_expr: "1,6,11,16,21,26,31,36,41,46,51,56 * * * *"
|
||||
enabled: true
|
||||
prompt_file: schedules/orchestrator-pulse.md
|
||||
children:
|
||||
- name: Research Lead
|
||||
role: Market analysis and technical research
|
||||
files_dir: research-lead
|
||||
canvas: {x: 200, y: 250}
|
||||
# Research roles add browser-automation for live web scraping
|
||||
# (product pages, GitHub trending, docs).
|
||||
plugins: [browser-automation]
|
||||
# #383: notify on high-value async output (eco-watch summaries,
|
||||
# competitive intelligence findings) via Telegram so they're not
|
||||
# invisible until the user manually checks memory/canvas.
|
||||
channels:
|
||||
- type: telegram
|
||||
config:
|
||||
bot_token: ${TELEGRAM_BOT_TOKEN}
|
||||
chat_id: ${TELEGRAM_CHAT_ID}
|
||||
enabled: true
|
||||
schedules:
|
||||
- name: Orchestrator pulse
|
||||
cron_expr: "4,9,14,19,24,29,34,39,44,49,54,59 * * * *"
|
||||
enabled: true
|
||||
prompt_file: schedules/orchestrator-pulse.md
|
||||
- name: Hourly ecosystem watch
|
||||
cron_expr: "8,38 * * * *"
|
||||
enabled: true
|
||||
prompt_file: schedules/hourly-ecosystem-watch.md
|
||||
children:
|
||||
- name: Market Analyst
|
||||
role: Market sizing, trends, user research
|
||||
files_dir: market-analyst
|
||||
plugins: [browser-automation]
|
||||
# Idle-loop rollout wave 2 (#216 → #285 → #304 validated on Technical
|
||||
# Researcher 2026-04-16 02:40 UTC). Market Analyst gets the same
|
||||
# reflection-on-completion pattern tuned for market research work.
|
||||
idle_interval_seconds: 600
|
||||
idle_prompt_file: idle-prompt.md
|
||||
- name: Technical Researcher
|
||||
role: AI frameworks and protocol evaluation
|
||||
files_dir: technical-researcher
|
||||
plugins: [browser-automation]
|
||||
# Idle-loop pilot (#205) — Technical Researcher is the first workspace
|
||||
# to opt in to the reflection-on-completion pattern. Measure
|
||||
# activity_logs delta over 24h, then roll to the rest of the research
|
||||
# team if it produces useful backlog-pull dispatches.
|
||||
idle_interval_seconds: 600
|
||||
schedules:
|
||||
- name: Hourly plugin curation
|
||||
cron_expr: "22 * * * *"
|
||||
enabled: true
|
||||
prompt_file: schedules/hourly-plugin-curation.md
|
||||
idle_prompt_file: idle-prompt.md
|
||||
- name: Competitive Intelligence
|
||||
role: Competitor tracking and feature comparison
|
||||
files_dir: competitive-intelligence
|
||||
plugins: [browser-automation]
|
||||
# Idle-loop rollout wave 2 (sibling to Market Analyst).
|
||||
idle_interval_seconds: 600
|
||||
idle_prompt_file: idle-prompt.md
|
||||
initial_prompt_file: initial-prompt.md
|
||||
- name: Dev Lead
|
||||
role: Engineering planning and team coordination
|
||||
tier: 3
|
||||
model: opus
|
||||
files_dir: dev-lead
|
||||
# Dev Lead enforces PR quality gates (see gate 2a in
|
||||
# .claude/skills/triage/SKILL.md) and reviews engineering output
|
||||
# before handoff to PM. The code-review skill surfaces the
|
||||
# 16-criteria rubric — without it Dev Lead falls back to ad-hoc
|
||||
# review prompts. Issue #133.
|
||||
plugins: [molecule-skill-code-review, molecule-skill-llm-judge]
|
||||
canvas: {x: 650, y: 250}
|
||||
# #383: notify on critical engineering decisions, PR blocks, and
|
||||
# cross-team blockers via Telegram — completes the leadership tier
|
||||
# (PM + Dev Lead + Research Lead + DevOps + Security all on Telegram).
|
||||
channels:
|
||||
- type: telegram
|
||||
config:
|
||||
bot_token: ${TELEGRAM_BOT_TOKEN}
|
||||
chat_id: ${TELEGRAM_CHAT_ID}
|
||||
enabled: true
|
||||
schedules:
|
||||
- name: Orchestrator pulse
|
||||
cron_expr: "2,7,12,17,22,27,32,37,42,47,52,57 * * * *"
|
||||
enabled: true
|
||||
prompt_file: schedules/orchestrator-pulse.md
|
||||
- name: Hourly template fitness audit
|
||||
cron_expr: "15,45 * * * *"
|
||||
enabled: true
|
||||
prompt_file: schedules/hourly-template-fitness-audit.md
|
||||
children:
|
||||
- name: Frontend Engineer
|
||||
role: >-
|
||||
Owns the Next.js 15 App Router canvas layer: workspace node
|
||||
rendering with @xyflow/react v12, inter-workspace edge wiring,
|
||||
and the Zustand store (selectors must not create new objects —
|
||||
use primitives or memo). Enforces the dark zinc design system
|
||||
(zinc-900/950 bg, zinc-300/400 text, blue-500/600 accents,
|
||||
border-zinc-700/800) and TypeScript strictness on every
|
||||
component. Adds 'use client' to any .tsx that uses hooks; gates
|
||||
every commit with npm run build passing clean. Escalates to
|
||||
Backend Engineer for API shape questions — never guesses.
|
||||
"Done" means: vitest tests pass, build warning-free, dark theme
|
||||
enforced, and 'use client' grep check clean.
|
||||
tier: 3
|
||||
model: opus
|
||||
files_dir: frontend-engineer
|
||||
# #280: self-review rubric before raising a PR. Dev Lead uses
|
||||
# the same rubric, so catching issues here cuts the review loop.
|
||||
# #310: molecule-skill-llm-judge — gate own PR against issue body
|
||||
# before requesting review ("shipped the wrong thing" early catch).
|
||||
plugins: [molecule-skill-code-review, molecule-skill-llm-judge]
|
||||
idle_interval_seconds: 600
|
||||
initial_prompt_file: initial-prompt.md
|
||||
idle_prompt_file: idle-prompt.md
|
||||
- name: Backend Engineer
|
||||
role: >-
|
||||
Owns the Go/Gin platform layer: REST handlers, WebSocket hub,
|
||||
workspace provisioner, and A2A proxy. Manages Postgres schema,
|
||||
migrations, and parameterized query safety; Redis pub/sub,
|
||||
heartbeat TTLs, and per-workspace key cleanup. Enforces access
|
||||
control on every endpoint and structured error handling across
|
||||
all platform/ code. Primary reviewer for any platform-layer PR.
|
||||
tier: 3
|
||||
model: opus
|
||||
files_dir: backend-engineer
|
||||
# #266: HITL gate — Backend Engineer's scope includes destructive
|
||||
# DB migrations + runtime config changes; the @requires_approval
|
||||
# decorator stops an unattended agent from shipping a prod
|
||||
# schema mutation without a human click. UNION with defaults.
|
||||
# #280: molecule-skill-code-review — self-review rubric before
|
||||
# raising a PR (same rubric Dev Lead applies in review).
|
||||
# #303: molecule-security-scan — CVE gate at dev time, not
|
||||
# just at Security Auditor's 12h cron. Catches supply-chain
|
||||
# deps + secret patterns before they reach PR review.
|
||||
# #310: molecule-skill-llm-judge — self-gate before PR review.
|
||||
# #322: molecule-compliance — OA-03 excessive-agency cap; Backend
|
||||
# Engineer is the highest tool-call-volume role (platform PRs,
|
||||
# migrations, API changes) so a hard cap is a concrete guard
|
||||
# against runaway loops during large refactors.
|
||||
plugins: [molecule-hitl, molecule-skill-code-review, molecule-security-scan, molecule-skill-llm-judge, molecule-compliance]
|
||||
idle_interval_seconds: 600
|
||||
initial_prompt_file: initial-prompt.md
|
||||
idle_prompt_file: idle-prompt.md
|
||||
- name: DevOps Engineer
|
||||
role: >-
|
||||
Owns the container build pipeline: Dockerfiles for all six
|
||||
runtime images (langgraph, claude-code, openclaw, crewai,
|
||||
autogen, deepagents), docker-compose.infra.yml for the local
|
||||
dev stack, and build-all.sh hygiene. Manages GitHub Actions
|
||||
CI (platform-build, canvas-build, python-lint,
|
||||
mcp-server-build), coverage thresholds, and secrets hygiene
|
||||
in the pipeline. Keeps infra/scripts/setup.sh and nuke.sh
|
||||
in sync whenever migrations or services change. Escalates to
|
||||
Backend Engineer for schema/runtime-config changes and to
|
||||
Frontend Engineer for canvas build failures. "Done" means:
|
||||
all CI jobs green, all images buildable from a clean checkout,
|
||||
no *.log or .env files leaked into image layers.
|
||||
tier: 3
|
||||
model: opus
|
||||
files_dir: devops-engineer
|
||||
# #266: HITL gate — DevOps Engineer's scope covers fly deploys,
|
||||
# registry pushes, CI pipeline mutations. Any of these going
|
||||
# wrong affects every tenant; @requires_approval before
|
||||
# destructive infra ops is the point.
|
||||
# #280: molecule-skill-code-review — self-review rubric for
|
||||
# Dockerfiles, CI workflows, infra scripts before PR.
|
||||
# #322: molecule-freeze-scope — lock edits to infra/** during
|
||||
# risky operations (CI migrations, fly secret rotations, image
|
||||
# rebuilds). Plugin was an orphan for 3 weekly audits; DevOps
|
||||
# is the natural home.
|
||||
plugins: [molecule-hitl, molecule-skill-code-review, molecule-freeze-scope]
|
||||
# #247: notify on build-break — DevOps routes CI failures + infra
|
||||
# alerts via Telegram so they're not invisible until morning review.
|
||||
channels:
|
||||
- type: telegram
|
||||
config:
|
||||
bot_token: ${TELEGRAM_BOT_TOKEN}
|
||||
chat_id: ${TELEGRAM_CHAT_ID}
|
||||
enabled: true
|
||||
idle_interval_seconds: 600
|
||||
schedules:
|
||||
- name: Hourly channel expansion survey
|
||||
cron_expr: "47 * * * *"
|
||||
enabled: true
|
||||
prompt_file: schedules/hourly-channel-expansion-survey.md
|
||||
initial_prompt_file: initial-prompt.md
|
||||
idle_prompt_file: idle-prompt.md
|
||||
- name: Security Auditor
|
||||
role: >-
|
||||
Owns security posture across the full stack: Go/Gin handlers
|
||||
(SQL injection, path traversal, command injection, missing access
|
||||
control), Python workspace-template (RCE via subprocess, secrets
|
||||
in env/logs), Canvas (XSS in user-rendered content), and
|
||||
infrastructure (Docker socket exposure, secrets in images).
|
||||
Runs SAST via `gosec ./...` on every PR-touching Go file and
|
||||
`bandit -r .` on Python. Performs DAST checks against the running
|
||||
platform (`POST /workspaces/:id/a2a` CanCommunicate bypass
|
||||
attempts, CORS header validation, rate-limit enforcement).
|
||||
Escalates to Dev Lead immediately for: any SQL injection or RCE
|
||||
vector, leaked secrets in committed code, missing auth on a new
|
||||
endpoint. Files weekly summary to memory key
|
||||
`security-audit-latest`. Definition of done: every changed file
|
||||
reviewed, gosec/bandit clean (or false-positives annotated),
|
||||
no open critical findings without a linked issue.
|
||||
tier: 3
|
||||
model: opus
|
||||
files_dir: security-auditor
|
||||
# Security Auditor adds security-critical skills on top of defaults:
|
||||
# - molecule-skill-code-review: multi-criteria review for security-relevant PRs
|
||||
# - molecule-skill-cross-vendor-review: adversarial second opinion via non-Claude model
|
||||
# (use ONLY for noteworthy PRs — auth, billing, data)
|
||||
# - molecule-skill-llm-judge: cheap gate that catches "wrong thing shipped"
|
||||
# - molecule-security-scan (#275): supply-chain CVE gate via Snyk/pip-audit; wraps
|
||||
# builtin_tools/security_scan.py — gosec/bandit/etc
|
||||
# - molecule-hitl (#266): @requires_approval before filing critical issues
|
||||
# so false-positives don't spam the tracker
|
||||
# - molecule-compliance (#322): OWASP Top 10 for Agentic Applications — active
|
||||
# enforcement on Security Auditor's own tool calls
|
||||
# - molecule-audit (#322): immutable JSON-Lines audit log (EU AI Act Art 12/13/17)
|
||||
# — Security Auditor owns the report generation path
|
||||
plugins:
|
||||
- molecule-skill-code-review
|
||||
- molecule-skill-cross-vendor-review
|
||||
- molecule-skill-llm-judge
|
||||
- molecule-security-scan
|
||||
- molecule-hitl
|
||||
- molecule-compliance
|
||||
- molecule-audit
|
||||
# #246: notify on critical findings — Security Auditor pushes HIGH+
|
||||
# severity alerts via Telegram so they're not invisible until next
|
||||
# manual memory check.
|
||||
channels:
|
||||
- type: telegram
|
||||
config:
|
||||
bot_token: ${TELEGRAM_BOT_TOKEN}
|
||||
chat_id: ${TELEGRAM_CHAT_ID}
|
||||
enabled: true
|
||||
schedules:
|
||||
- name: Security audit (every 12h)
|
||||
cron_expr: "7 6,18 * * *"
|
||||
enabled: true
|
||||
prompt_file: schedules/security-audit-every-12h.md
|
||||
initial_prompt_file: initial-prompt.md
|
||||
- name: QA Engineer
|
||||
role: Testing, quality assurance, test automation
|
||||
tier: 3
|
||||
model: opus
|
||||
files_dir: qa-engineer
|
||||
# QA reviews test coverage + runs llm-judge on whether test
|
||||
# deliverables actually match acceptance criteria. Issue #133.
|
||||
# #322: molecule-compliance — OA-01 prompt-injection detection
|
||||
# (in detect mode, not block) catches adversarial test payloads
|
||||
# before they slip into production. OA-03 excessive-agency caps
|
||||
# prevent runaway test loops.
|
||||
plugins: [molecule-skill-code-review, molecule-skill-llm-judge, molecule-compliance]
|
||||
schedules:
|
||||
- name: Code quality audit (every 12h)
|
||||
cron_expr: "0 6,18 * * *"
|
||||
enabled: true
|
||||
prompt_file: schedules/code-quality-audit-every-12h.md
|
||||
initial_prompt_file: initial-prompt.md
|
||||
- name: UIUX Designer
|
||||
role: User flow design, visual design review, interaction patterns, accessibility
|
||||
tier: 3
|
||||
model: opus
|
||||
files_dir: uiux-designer
|
||||
# browser-automation for live canvas screenshots via Puppeteer
|
||||
# (Chrome CDP path; recipe in the cron prompt below).
|
||||
plugins: [browser-automation]
|
||||
schedules:
|
||||
- name: Hourly UI/UX audit with live screenshots
|
||||
# #306: was "5,20,35,50 * * * *" (every 15 min — 96
|
||||
# ticks/day × 8 screenshots × vision = runaway cost).
|
||||
# Hourly matches the schedule name and is sufficient
|
||||
# because the canvas UI only changes on deploys.
|
||||
cron_expr: "5 * * * *"
|
||||
enabled: true
|
||||
|
||||
prompt_file: schedules/hourly-ui-ux-audit-with-live-screenshots.md
|
||||
initial_prompt_file: initial-prompt.md
|
||||
initial_prompt_file: initial-prompt.md
|
||||
- name: Documentation Specialist
|
||||
role: >-
|
||||
Owns end-to-end documentation across THREE Molecule AI repos:
|
||||
(1) the platform monorepo (public, Molecule-AI/molecule-monorepo) —
|
||||
internal architecture, READMEs, edit-history, public API references;
|
||||
(2) the docs site (public, Molecule-AI/docs) — Fumadocs + Next.js 15,
|
||||
deployed to doc.moleculesai.app, customer-facing;
|
||||
(3) the SaaS controlplane (PRIVATE, Molecule-AI/molecule-controlplane) —
|
||||
Go service that provisions tenants on Fly Machines, with the strict
|
||||
rule that private implementation details NEVER leak into the public
|
||||
docs site. Documents controlplane changes only in its own internal
|
||||
README and the platform monorepo's docs/saas/ section (which itself
|
||||
is gated). Public docs only describe the SaaS PRODUCT (signup, billing,
|
||||
tenant lifecycle, multi-tenant data isolation guarantees) — not the
|
||||
provisioner's internals.
|
||||
Watches PRs landing on all three repos and opens corresponding docs
|
||||
PRs whenever a public API changes, a new template/plugin/channel
|
||||
lands, a user-facing concept evolves, or an ecosystem-watch entry
|
||||
needs publishing. Holds the line on terminology consistency — every
|
||||
concept has exactly one canonical name across all three repos.
|
||||
Definition of done: every public surface has accurate, current,
|
||||
example-rich documentation; every merged PR that touches a public
|
||||
surface has a paired docs PR open within one cron tick; every stub
|
||||
page on the docs site eventually gets backfilled; controlplane
|
||||
internal docs stay current; nothing private leaks to public.
|
||||
tier: 3
|
||||
model: opus
|
||||
files_dir: documentation-specialist
|
||||
canvas: {x: 900, y: 250}
|
||||
# Documentation Specialist needs browser-automation to crawl the live
|
||||
# docs site (visual regressions, broken links, dead anchors) plus
|
||||
# update-docs skill (already in defaults) for cross-repo docs sync.
|
||||
plugins: [browser-automation]
|
||||
# Phase 1 scalability: prompts externalized to sibling .md files.
|
||||
# See documentation-specialist/{initial-prompt.md, schedules/*.md}.
|
||||
# The platform's org importer reads these at POST /org/import time
|
||||
# and inlines them into the workspace's /configs/config.yaml and
|
||||
# workspace_schedules rows. Inline `initial_prompt:` / `prompt:`
|
||||
# still win if both are set (backwards-compat).
|
||||
initial_prompt_file: initial-prompt.md
|
||||
schedules:
|
||||
- name: Daily docs sync — backfill stubs and pair recent platform PRs
|
||||
cron_expr: "0 9 * * *"
|
||||
prompt_file: schedules/daily-docs-sync.md
|
||||
enabled: true
|
||||
- name: Weekly terminology + freshness audit
|
||||
cron_expr: "0 11 * * 1"
|
||||
prompt_file: schedules/weekly-terminology-audit.md
|
||||
enabled: true
|
||||
|
||||
- name: Triage Operator
|
||||
role: >-
|
||||
Owns the hourly PR + issue triage cycle across
|
||||
Molecule-AI/molecule-monorepo and Molecule-AI/molecule-controlplane.
|
||||
Runs a 7-gate verification on every open PR (CI, build, tests,
|
||||
security, design, line-review, Playwright-if-canvas), merges the
|
||||
ones that pass verified-merge rules, holds auth/billing/schema PRs
|
||||
for CEO approval, picks up at most 2 issues per tick through gates
|
||||
I-1..I-6, and appends one line per tick to cron-learnings.jsonl
|
||||
with a concrete next_action. Reports to PM for noteworthy
|
||||
escalations; never bypasses hierarchy. NOT an engineer — never
|
||||
writes logic, never touches design decisions. Mechanical fixes on
|
||||
other people's branches are OK (`fix(gate-N): ...`). The full
|
||||
philosophy + playbook + SKILL definition lives in
|
||||
/workspace/repo/org-templates/molecule-dev/triage-operator/.
|
||||
Read those four files AND
|
||||
~/.claude/projects/-Users-hongming-Documents-GitHub-molecule-monorepo/memory/cron-learnings.jsonl
|
||||
at the start of every tick before taking any action.
|
||||
tier: 3
|
||||
model: opus
|
||||
files_dir: triage-operator
|
||||
canvas: {x: 1150, y: 250}
|
||||
# #370-aligned: Triage Operator is a standing-rules-first role. The
|
||||
# plugin stack below is what the prior operator identified as the
|
||||
# minimum set to run the triage cycle correctly:
|
||||
# - molecule-careful-bash — REFUSE/WARN/ALLOW guards for the
|
||||
# destructive bash ops this role
|
||||
# will regularly encounter
|
||||
# - molecule-session-context — auto-injects recent cron-learnings
|
||||
# + open PR/issue counts at session
|
||||
# start (avoids stale-state ticks)
|
||||
# - molecule-skill-cron-learnings — defines the JSONL append format
|
||||
# - molecule-skill-code-review — 16-criterion per-PR review (Gate 6)
|
||||
# - molecule-skill-cross-vendor-review — second-model review for
|
||||
# noteworthy PRs (auth/billing/
|
||||
# data-deletion/migration)
|
||||
# - molecule-skill-llm-judge — draft-PR ready-or-not gate on
|
||||
# issue pickup (>=4 marks ready)
|
||||
# - molecule-skill-update-docs — post-merge docs sync workflow
|
||||
# - molecule-hitl — @requires_approval gate before
|
||||
# any destructive cross-repo op
|
||||
plugins:
|
||||
- molecule-careful-bash
|
||||
- molecule-session-context
|
||||
- molecule-skill-cron-learnings
|
||||
- molecule-skill-code-review
|
||||
- molecule-skill-cross-vendor-review
|
||||
- molecule-skill-llm-judge
|
||||
- molecule-skill-update-docs
|
||||
- molecule-hitl
|
||||
schedules:
|
||||
- name: Hourly triage
|
||||
cron_expr: "17 * * * *"
|
||||
enabled: true
|
||||
|
||||
# ============================================================
|
||||
# Marketing team (2026-04-16). Peer sub-tree of PM under CEO.
|
||||
# Marketing Lead = CMO-equivalent; runs a 5-min orchestrator
|
||||
# pulse mirroring Dev Lead. Workers (content, community, SEO,
|
||||
# social) run idle-loop backlog-pull; high-judgment roles
|
||||
# (DevRel, PMM) run hourly evolution crons plus idle loops.
|
||||
# Cross-functional: DevRel → Backend/Frontend for code demos,
|
||||
# PMM → Competitive Intelligence for eco-watch diffs. All A2A
|
||||
# summaries route via category_routing to the matching role.
|
||||
# ============================================================
|
||||
prompt_file: schedules/hourly-triage.md
|
||||
initial_prompt_file: initial-prompt.md
|
||||
initial_prompt_file: initial-prompt.md
|
||||
- name: Marketing Lead
|
||||
role: >-
|
||||
CMO-equivalent. Owns marketing strategy, narrative, and
|
||||
launch calendar for Molecule AI. Coordinates DevRel, PMM,
|
||||
Content, Community, SEO, and Social. Escalates cross-team
|
||||
resource asks to CEO + PM. Every campaign traces back to
|
||||
a positioning decision from PMM and a measurable goal
|
||||
(signups, organic rank, brand-search volume). Orchestrates
|
||||
on a 5-minute pulse like Dev Lead — dispatches work,
|
||||
reviews drafts, unblocks dependencies.
|
||||
tier: 3
|
||||
model: opus
|
||||
files_dir: marketing-lead
|
||||
canvas: {x: 1150, y: 50}
|
||||
plugins: [molecule-skill-code-review, molecule-skill-llm-judge]
|
||||
schedules:
|
||||
- name: Orchestrator pulse
|
||||
cron_expr: "4,9,14,19,24,29,34,39,44,49,54,59 * * * *"
|
||||
enabled: true
|
||||
prompt_file: schedules/orchestrator-pulse.md
|
||||
children:
|
||||
- name: DevRel Engineer
|
||||
role: >-
|
||||
Developer-facing voice of Molecule AI. Owns the code
|
||||
samples, runnable tutorials, and talk-track that turn
|
||||
"I've heard of this" into "I can run it". Partners with
|
||||
Content Marketer for blog narratives and with PMM for
|
||||
positioning. Never ships a tutorial that doesn't run
|
||||
green against the current main. On every feat: PR merge,
|
||||
produces a 20-line demo within 24 hours.
|
||||
tier: 3
|
||||
model: opus
|
||||
files_dir: devrel-engineer
|
||||
canvas: {x: 1000, y: 250}
|
||||
plugins: [molecule-skill-code-review, molecule-skill-llm-judge]
|
||||
idle_interval_seconds: 600
|
||||
schedules:
|
||||
- name: Hourly sample-coverage audit
|
||||
cron_expr: "18 * * * *"
|
||||
enabled: true
|
||||
prompt_file: schedules/hourly-sample-coverage-audit.md
|
||||
initial_prompt_file: initial-prompt.md
|
||||
idle_prompt_file: idle-prompt.md
|
||||
- name: Product Marketing Manager
|
||||
role: >-
|
||||
Owns positioning, messaging, and competitive framing.
|
||||
Every piece of copy from marketing roots back to a
|
||||
PMM positioning decision. Maintains docs/marketing/
|
||||
positioning.md + competitors.md as single-source-of-
|
||||
truth. For every feat: PR merge, writes the launch
|
||||
brief within 24 hours. Pulls competitor diffs from
|
||||
ecosystem-watch.md hourly.
|
||||
tier: 3
|
||||
model: opus
|
||||
files_dir: product-marketing-manager
|
||||
canvas: {x: 1150, y: 250}
|
||||
plugins: [molecule-skill-code-review, molecule-skill-llm-judge]
|
||||
idle_interval_seconds: 600
|
||||
schedules:
|
||||
- name: Hourly competitor diff
|
||||
cron_expr: "33 * * * *"
|
||||
enabled: true
|
||||
prompt_file: schedules/hourly-competitor-diff.md
|
||||
initial_prompt_file: initial-prompt.md
|
||||
idle_prompt_file: idle-prompt.md
|
||||
- name: Content Marketer
|
||||
role: >-
|
||||
Writes the blog posts, tutorials, launch write-ups,
|
||||
and case studies that drive organic traffic and
|
||||
credibility. Partners with DevRel on technical
|
||||
narratives and SEO Analyst on keyword briefs. Never
|
||||
invents benchmarks — only quotes merged PR measurements
|
||||
or labels a number as design intent.
|
||||
tier: 2
|
||||
files_dir: content-marketer
|
||||
canvas: {x: 1300, y: 250}
|
||||
plugins: [molecule-skill-llm-judge]
|
||||
idle_interval_seconds: 600
|
||||
schedules:
|
||||
- name: Hourly topic queue refresh
|
||||
cron_expr: "41 * * * *"
|
||||
enabled: true
|
||||
prompt_file: schedules/hourly-topic-queue-refresh.md
|
||||
initial_prompt_file: initial-prompt.md
|
||||
idle_prompt_file: idle-prompt.md
|
||||
- name: Community Manager
|
||||
role: >-
|
||||
Voice-of-the-user. Triages every inbound question
|
||||
(GH Discussions, Discord, Slack), routes technical
|
||||
ones to DevRel, feature requests to PM, vulnerability
|
||||
reports to Security Auditor. Owns response-time SLAs
|
||||
and user-feedback capture.
|
||||
tier: 2
|
||||
files_dir: community-manager
|
||||
canvas: {x: 1150, y: 400}
|
||||
plugins: []
|
||||
idle_interval_seconds: 600
|
||||
schedules:
|
||||
- name: Hourly unanswered sweep
|
||||
cron_expr: "12 * * * *"
|
||||
enabled: true
|
||||
prompt_file: schedules/hourly-unanswered-sweep.md
|
||||
initial_prompt_file: initial-prompt.md
|
||||
idle_prompt_file: idle-prompt.md
|
||||
- name: SEO Growth Analyst
|
||||
role: >-
|
||||
Owns organic search visibility and funnel conversion.
|
||||
Metrics: keyword rank, search impressions, CTR, time-
|
||||
on-page, signup conversion. Writes SEO briefs for every
|
||||
Content post; audits Lighthouse + Core Web Vitals daily;
|
||||
proposes A/B tests for weakest funnel step.
|
||||
tier: 2
|
||||
files_dir: seo-growth-analyst
|
||||
canvas: {x: 1000, y: 400}
|
||||
plugins: [browser-automation]
|
||||
idle_interval_seconds: 600
|
||||
schedules:
|
||||
- name: Daily Lighthouse + keyword audit
|
||||
cron_expr: "23 8 * * *"
|
||||
enabled: true
|
||||
prompt_file: schedules/daily-lighthouse-keyword-audit.md
|
||||
initial_prompt_file: initial-prompt.md
|
||||
idle_prompt_file: idle-prompt.md
|
||||
- name: Social Media Brand
|
||||
role: >-
|
||||
Owns Molecule AI's voice on X + LinkedIn and the visual
|
||||
identity across marketing surfaces. 1-2 X posts + 3-5
|
||||
replies/day; LinkedIn 2-3 posts/week. Maintains brand
|
||||
guidelines (zinc dark, blue accents, system-mono code).
|
||||
Every launch gets a 3-post thread within 24h.
|
||||
tier: 2
|
||||
files_dir: social-media-brand
|
||||
canvas: {x: 1300, y: 400}
|
||||
plugins: []
|
||||
idle_interval_seconds: 600
|
||||
schedules:
|
||||
- name: Hourly mention monitor
|
||||
cron_expr: "27 * * * *"
|
||||
enabled: true
|
||||
prompt_file: schedules/hourly-mention-monitor.md
|
||||
initial_prompt_file: initial-prompt.md
|
||||
idle_prompt_file: idle-prompt.md
|
||||
initial_prompt_file: initial-prompt.md
|
||||
- !include teams/pm.yaml
|
||||
- !include teams/marketing.yaml
|
||||
|
||||
222
org-templates/molecule-dev/teams/dev.yaml
Normal file
222
org-templates/molecule-dev/teams/dev.yaml
Normal file
@ -0,0 +1,222 @@
|
||||
name: Dev Lead
|
||||
role: Engineering planning and team coordination
|
||||
tier: 3
|
||||
model: opus
|
||||
files_dir: dev-lead
|
||||
# Dev Lead enforces PR quality gates (see gate 2a in
|
||||
# .claude/skills/triage/SKILL.md) and reviews engineering output
|
||||
# before handoff to PM. The code-review skill surfaces the
|
||||
# 16-criteria rubric — without it Dev Lead falls back to ad-hoc
|
||||
# review prompts. Issue #133.
|
||||
plugins: [molecule-skill-code-review, molecule-skill-llm-judge]
|
||||
canvas: {x: 650, y: 250}
|
||||
# #383: notify on critical engineering decisions, PR blocks, and
|
||||
# cross-team blockers via Telegram — completes the leadership tier
|
||||
# (PM + Dev Lead + Research Lead + DevOps + Security all on Telegram).
|
||||
channels:
|
||||
- type: telegram
|
||||
config:
|
||||
bot_token: ${TELEGRAM_BOT_TOKEN}
|
||||
chat_id: ${TELEGRAM_CHAT_ID}
|
||||
enabled: true
|
||||
schedules:
|
||||
- name: Orchestrator pulse
|
||||
cron_expr: "2,7,12,17,22,27,32,37,42,47,52,57 * * * *"
|
||||
enabled: true
|
||||
prompt_file: schedules/orchestrator-pulse.md
|
||||
- name: Hourly template fitness audit
|
||||
cron_expr: "15,45 * * * *"
|
||||
enabled: true
|
||||
prompt_file: schedules/hourly-template-fitness-audit.md
|
||||
children:
|
||||
- name: Frontend Engineer
|
||||
role: >-
|
||||
Owns the Next.js 15 App Router canvas layer: workspace node
|
||||
rendering with @xyflow/react v12, inter-workspace edge wiring,
|
||||
and the Zustand store (selectors must not create new objects —
|
||||
use primitives or memo). Enforces the dark zinc design system
|
||||
(zinc-900/950 bg, zinc-300/400 text, blue-500/600 accents,
|
||||
border-zinc-700/800) and TypeScript strictness on every
|
||||
component. Adds 'use client' to any .tsx that uses hooks; gates
|
||||
every commit with npm run build passing clean. Escalates to
|
||||
Backend Engineer for API shape questions — never guesses.
|
||||
"Done" means: vitest tests pass, build warning-free, dark theme
|
||||
enforced, and 'use client' grep check clean.
|
||||
tier: 3
|
||||
model: opus
|
||||
files_dir: frontend-engineer
|
||||
# #280: self-review rubric before raising a PR. Dev Lead uses
|
||||
# the same rubric, so catching issues here cuts the review loop.
|
||||
# #310: molecule-skill-llm-judge — gate own PR against issue body
|
||||
# before requesting review ("shipped the wrong thing" early catch).
|
||||
plugins: [molecule-skill-code-review, molecule-skill-llm-judge]
|
||||
idle_interval_seconds: 600
|
||||
initial_prompt_file: initial-prompt.md
|
||||
idle_prompt_file: idle-prompt.md
|
||||
- name: Backend Engineer
|
||||
role: >-
|
||||
Owns the Go/Gin platform layer: REST handlers, WebSocket hub,
|
||||
workspace provisioner, and A2A proxy. Manages Postgres schema,
|
||||
migrations, and parameterized query safety; Redis pub/sub,
|
||||
heartbeat TTLs, and per-workspace key cleanup. Enforces access
|
||||
control on every endpoint and structured error handling across
|
||||
all platform/ code. Primary reviewer for any platform-layer PR.
|
||||
tier: 3
|
||||
model: opus
|
||||
files_dir: backend-engineer
|
||||
# #266: HITL gate — Backend Engineer's scope includes destructive
|
||||
# DB migrations + runtime config changes; the @requires_approval
|
||||
# decorator stops an unattended agent from shipping a prod
|
||||
# schema mutation without a human click. UNION with defaults.
|
||||
# #280: molecule-skill-code-review — self-review rubric before
|
||||
# raising a PR (same rubric Dev Lead applies in review).
|
||||
# #303: molecule-security-scan — CVE gate at dev time, not
|
||||
# just at Security Auditor's 12h cron. Catches supply-chain
|
||||
# deps + secret patterns before they reach PR review.
|
||||
# #310: molecule-skill-llm-judge — self-gate before PR review.
|
||||
# #322: molecule-compliance — OA-03 excessive-agency cap; Backend
|
||||
# Engineer is the highest tool-call-volume role (platform PRs,
|
||||
# migrations, API changes) so a hard cap is a concrete guard
|
||||
# against runaway loops during large refactors.
|
||||
plugins: [molecule-hitl, molecule-skill-code-review, molecule-security-scan, molecule-skill-llm-judge, molecule-compliance]
|
||||
idle_interval_seconds: 600
|
||||
initial_prompt_file: initial-prompt.md
|
||||
idle_prompt_file: idle-prompt.md
|
||||
- name: DevOps Engineer
|
||||
role: >-
|
||||
Owns the container build pipeline: Dockerfiles for all six
|
||||
runtime images (langgraph, claude-code, openclaw, crewai,
|
||||
autogen, deepagents), docker-compose.infra.yml for the local
|
||||
dev stack, and build-all.sh hygiene. Manages GitHub Actions
|
||||
CI (platform-build, canvas-build, python-lint,
|
||||
mcp-server-build), coverage thresholds, and secrets hygiene
|
||||
in the pipeline. Keeps infra/scripts/setup.sh and nuke.sh
|
||||
in sync whenever migrations or services change. Escalates to
|
||||
Backend Engineer for schema/runtime-config changes and to
|
||||
Frontend Engineer for canvas build failures. "Done" means:
|
||||
all CI jobs green, all images buildable from a clean checkout,
|
||||
no *.log or .env files leaked into image layers.
|
||||
tier: 3
|
||||
model: opus
|
||||
files_dir: devops-engineer
|
||||
# #266: HITL gate — DevOps Engineer's scope covers fly deploys,
|
||||
# registry pushes, CI pipeline mutations. Any of these going
|
||||
# wrong affects every tenant; @requires_approval before
|
||||
# destructive infra ops is the point.
|
||||
# #280: molecule-skill-code-review — self-review rubric for
|
||||
# Dockerfiles, CI workflows, infra scripts before PR.
|
||||
# #322: molecule-freeze-scope — lock edits to infra/** during
|
||||
# risky operations (CI migrations, fly secret rotations, image
|
||||
# rebuilds). Plugin was an orphan for 3 weekly audits; DevOps
|
||||
# is the natural home.
|
||||
plugins: [molecule-hitl, molecule-skill-code-review, molecule-freeze-scope]
|
||||
# #247: notify on build-break — DevOps routes CI failures + infra
|
||||
# alerts via Telegram so they're not invisible until morning review.
|
||||
channels:
|
||||
- type: telegram
|
||||
config:
|
||||
bot_token: ${TELEGRAM_BOT_TOKEN}
|
||||
chat_id: ${TELEGRAM_CHAT_ID}
|
||||
enabled: true
|
||||
idle_interval_seconds: 600
|
||||
schedules:
|
||||
- name: Hourly channel expansion survey
|
||||
cron_expr: "47 * * * *"
|
||||
enabled: true
|
||||
prompt_file: schedules/hourly-channel-expansion-survey.md
|
||||
initial_prompt_file: initial-prompt.md
|
||||
idle_prompt_file: idle-prompt.md
|
||||
- name: Security Auditor
|
||||
role: >-
|
||||
Owns security posture across the full stack: Go/Gin handlers
|
||||
(SQL injection, path traversal, command injection, missing access
|
||||
control), Python workspace-template (RCE via subprocess, secrets
|
||||
in env/logs), Canvas (XSS in user-rendered content), and
|
||||
infrastructure (Docker socket exposure, secrets in images).
|
||||
Runs SAST via `gosec ./...` on every PR-touching Go file and
|
||||
`bandit -r .` on Python. Performs DAST checks against the running
|
||||
platform (`POST /workspaces/:id/a2a` CanCommunicate bypass
|
||||
attempts, CORS header validation, rate-limit enforcement).
|
||||
Escalates to Dev Lead immediately for: any SQL injection or RCE
|
||||
vector, leaked secrets in committed code, missing auth on a new
|
||||
endpoint. Files weekly summary to memory key
|
||||
`security-audit-latest`. Definition of done: every changed file
|
||||
reviewed, gosec/bandit clean (or false-positives annotated),
|
||||
no open critical findings without a linked issue.
|
||||
tier: 3
|
||||
model: opus
|
||||
files_dir: security-auditor
|
||||
# Security Auditor adds security-critical skills on top of defaults:
|
||||
# - molecule-skill-code-review: multi-criteria review for security-relevant PRs
|
||||
# - molecule-skill-cross-vendor-review: adversarial second opinion via non-Claude model
|
||||
# (use ONLY for noteworthy PRs — auth, billing, data)
|
||||
# - molecule-skill-llm-judge: cheap gate that catches "wrong thing shipped"
|
||||
# - molecule-security-scan (#275): supply-chain CVE gate via Snyk/pip-audit; wraps
|
||||
# builtin_tools/security_scan.py — gosec/bandit/etc
|
||||
# - molecule-hitl (#266): @requires_approval before filing critical issues
|
||||
# so false-positives don't spam the tracker
|
||||
# - molecule-compliance (#322): OWASP Top 10 for Agentic Applications — active
|
||||
# enforcement on Security Auditor's own tool calls
|
||||
# - molecule-audit (#322): immutable JSON-Lines audit log (EU AI Act Art 12/13/17)
|
||||
# — Security Auditor owns the report generation path
|
||||
plugins:
|
||||
- molecule-skill-code-review
|
||||
- molecule-skill-cross-vendor-review
|
||||
- molecule-skill-llm-judge
|
||||
- molecule-security-scan
|
||||
- molecule-hitl
|
||||
- molecule-compliance
|
||||
- molecule-audit
|
||||
# #246: notify on critical findings — Security Auditor pushes HIGH+
|
||||
# severity alerts via Telegram so they're not invisible until next
|
||||
# manual memory check.
|
||||
channels:
|
||||
- type: telegram
|
||||
config:
|
||||
bot_token: ${TELEGRAM_BOT_TOKEN}
|
||||
chat_id: ${TELEGRAM_CHAT_ID}
|
||||
enabled: true
|
||||
schedules:
|
||||
- name: Security audit (every 12h)
|
||||
cron_expr: "7 6,18 * * *"
|
||||
enabled: true
|
||||
prompt_file: schedules/security-audit-every-12h.md
|
||||
initial_prompt_file: initial-prompt.md
|
||||
- name: QA Engineer
|
||||
role: Testing, quality assurance, test automation
|
||||
tier: 3
|
||||
model: opus
|
||||
files_dir: qa-engineer
|
||||
# QA reviews test coverage + runs llm-judge on whether test
|
||||
# deliverables actually match acceptance criteria. Issue #133.
|
||||
# #322: molecule-compliance — OA-01 prompt-injection detection
|
||||
# (in detect mode, not block) catches adversarial test payloads
|
||||
# before they slip into production. OA-03 excessive-agency caps
|
||||
# prevent runaway test loops.
|
||||
plugins: [molecule-skill-code-review, molecule-skill-llm-judge, molecule-compliance]
|
||||
schedules:
|
||||
- name: Code quality audit (every 12h)
|
||||
cron_expr: "0 6,18 * * *"
|
||||
enabled: true
|
||||
prompt_file: schedules/code-quality-audit-every-12h.md
|
||||
initial_prompt_file: initial-prompt.md
|
||||
- name: UIUX Designer
|
||||
role: User flow design, visual design review, interaction patterns, accessibility
|
||||
tier: 3
|
||||
model: opus
|
||||
files_dir: uiux-designer
|
||||
# browser-automation for live canvas screenshots via Puppeteer
|
||||
# (Chrome CDP path; recipe in the cron prompt below).
|
||||
plugins: [browser-automation]
|
||||
schedules:
|
||||
- name: Hourly UI/UX audit with live screenshots
|
||||
# #306: was "5,20,35,50 * * * *" (every 15 min — 96
|
||||
# ticks/day × 8 screenshots × vision = runaway cost).
|
||||
# Hourly matches the schedule name and is sufficient
|
||||
# because the canvas UI only changes on deploys.
|
||||
cron_expr: "5 * * * *"
|
||||
enabled: true
|
||||
|
||||
prompt_file: schedules/hourly-ui-ux-audit-with-live-screenshots.md
|
||||
initial_prompt_file: initial-prompt.md
|
||||
initial_prompt_file: initial-prompt.md
|
||||
@ -0,0 +1,50 @@
|
||||
name: Documentation Specialist
|
||||
role: >-
|
||||
Owns end-to-end documentation across THREE Molecule AI repos:
|
||||
(1) the platform monorepo (public, Molecule-AI/molecule-monorepo) —
|
||||
internal architecture, READMEs, edit-history, public API references;
|
||||
(2) the docs site (public, Molecule-AI/docs) — Fumadocs + Next.js 15,
|
||||
deployed to doc.moleculesai.app, customer-facing;
|
||||
(3) the SaaS controlplane (PRIVATE, Molecule-AI/molecule-controlplane) —
|
||||
Go service that provisions tenants on Fly Machines, with the strict
|
||||
rule that private implementation details NEVER leak into the public
|
||||
docs site. Documents controlplane changes only in its own internal
|
||||
README and the platform monorepo's docs/saas/ section (which itself
|
||||
is gated). Public docs only describe the SaaS PRODUCT (signup, billing,
|
||||
tenant lifecycle, multi-tenant data isolation guarantees) — not the
|
||||
provisioner's internals.
|
||||
Watches PRs landing on all three repos and opens corresponding docs
|
||||
PRs whenever a public API changes, a new template/plugin/channel
|
||||
lands, a user-facing concept evolves, or an ecosystem-watch entry
|
||||
needs publishing. Holds the line on terminology consistency — every
|
||||
concept has exactly one canonical name across all three repos.
|
||||
Definition of done: every public surface has accurate, current,
|
||||
example-rich documentation; every merged PR that touches a public
|
||||
surface has a paired docs PR open within one cron tick; every stub
|
||||
page on the docs site eventually gets backfilled; controlplane
|
||||
internal docs stay current; nothing private leaks to public.
|
||||
tier: 3
|
||||
model: opus
|
||||
files_dir: documentation-specialist
|
||||
canvas: {x: 900, y: 250}
|
||||
# Documentation Specialist needs browser-automation to crawl the live
|
||||
# docs site (visual regressions, broken links, dead anchors) plus
|
||||
# update-docs skill (already in defaults) for cross-repo docs sync.
|
||||
plugins: [browser-automation]
|
||||
# Phase 1 scalability: prompts externalized to sibling .md files.
|
||||
# See documentation-specialist/{initial-prompt.md, schedules/*.md}.
|
||||
# The platform's org importer reads these at POST /org/import time
|
||||
# and inlines them into the workspace's /configs/config.yaml and
|
||||
# workspace_schedules rows. Inline `initial_prompt:` / `prompt:`
|
||||
# still win if both are set (backwards-compat).
|
||||
initial_prompt_file: initial-prompt.md
|
||||
schedules:
|
||||
- name: Daily docs sync — backfill stubs and pair recent platform PRs
|
||||
cron_expr: "0 9 * * *"
|
||||
prompt_file: schedules/daily-docs-sync.md
|
||||
enabled: true
|
||||
- name: Weekly terminology + freshness audit
|
||||
cron_expr: "0 11 * * 1"
|
||||
prompt_file: schedules/weekly-terminology-audit.md
|
||||
enabled: true
|
||||
|
||||
143
org-templates/molecule-dev/teams/marketing.yaml
Normal file
143
org-templates/molecule-dev/teams/marketing.yaml
Normal file
@ -0,0 +1,143 @@
|
||||
name: Marketing Lead
|
||||
role: >-
|
||||
CMO-equivalent. Owns marketing strategy, narrative, and
|
||||
launch calendar for Molecule AI. Coordinates DevRel, PMM,
|
||||
Content, Community, SEO, and Social. Escalates cross-team
|
||||
resource asks to CEO + PM. Every campaign traces back to
|
||||
a positioning decision from PMM and a measurable goal
|
||||
(signups, organic rank, brand-search volume). Orchestrates
|
||||
on a 5-minute pulse like Dev Lead — dispatches work,
|
||||
reviews drafts, unblocks dependencies.
|
||||
tier: 3
|
||||
model: opus
|
||||
files_dir: marketing-lead
|
||||
canvas: {x: 1150, y: 50}
|
||||
plugins: [molecule-skill-code-review, molecule-skill-llm-judge]
|
||||
schedules:
|
||||
- name: Orchestrator pulse
|
||||
cron_expr: "4,9,14,19,24,29,34,39,44,49,54,59 * * * *"
|
||||
enabled: true
|
||||
prompt_file: schedules/orchestrator-pulse.md
|
||||
children:
|
||||
- name: DevRel Engineer
|
||||
role: >-
|
||||
Developer-facing voice of Molecule AI. Owns the code
|
||||
samples, runnable tutorials, and talk-track that turn
|
||||
"I've heard of this" into "I can run it". Partners with
|
||||
Content Marketer for blog narratives and with PMM for
|
||||
positioning. Never ships a tutorial that doesn't run
|
||||
green against the current main. On every feat: PR merge,
|
||||
produces a 20-line demo within 24 hours.
|
||||
tier: 3
|
||||
model: opus
|
||||
files_dir: devrel-engineer
|
||||
canvas: {x: 1000, y: 250}
|
||||
plugins: [molecule-skill-code-review, molecule-skill-llm-judge]
|
||||
idle_interval_seconds: 600
|
||||
schedules:
|
||||
- name: Hourly sample-coverage audit
|
||||
cron_expr: "18 * * * *"
|
||||
enabled: true
|
||||
prompt_file: schedules/hourly-sample-coverage-audit.md
|
||||
initial_prompt_file: initial-prompt.md
|
||||
idle_prompt_file: idle-prompt.md
|
||||
- name: Product Marketing Manager
|
||||
role: >-
|
||||
Owns positioning, messaging, and competitive framing.
|
||||
Every piece of copy from marketing roots back to a
|
||||
PMM positioning decision. Maintains docs/marketing/
|
||||
positioning.md + competitors.md as single-source-of-
|
||||
truth. For every feat: PR merge, writes the launch
|
||||
brief within 24 hours. Pulls competitor diffs from
|
||||
ecosystem-watch.md hourly.
|
||||
tier: 3
|
||||
model: opus
|
||||
files_dir: product-marketing-manager
|
||||
canvas: {x: 1150, y: 250}
|
||||
plugins: [molecule-skill-code-review, molecule-skill-llm-judge]
|
||||
idle_interval_seconds: 600
|
||||
schedules:
|
||||
- name: Hourly competitor diff
|
||||
cron_expr: "33 * * * *"
|
||||
enabled: true
|
||||
prompt_file: schedules/hourly-competitor-diff.md
|
||||
initial_prompt_file: initial-prompt.md
|
||||
idle_prompt_file: idle-prompt.md
|
||||
- name: Content Marketer
|
||||
role: >-
|
||||
Writes the blog posts, tutorials, launch write-ups,
|
||||
and case studies that drive organic traffic and
|
||||
credibility. Partners with DevRel on technical
|
||||
narratives and SEO Analyst on keyword briefs. Never
|
||||
invents benchmarks — only quotes merged PR measurements
|
||||
or labels a number as design intent.
|
||||
tier: 2
|
||||
files_dir: content-marketer
|
||||
canvas: {x: 1300, y: 250}
|
||||
plugins: [molecule-skill-llm-judge]
|
||||
idle_interval_seconds: 600
|
||||
schedules:
|
||||
- name: Hourly topic queue refresh
|
||||
cron_expr: "41 * * * *"
|
||||
enabled: true
|
||||
prompt_file: schedules/hourly-topic-queue-refresh.md
|
||||
initial_prompt_file: initial-prompt.md
|
||||
idle_prompt_file: idle-prompt.md
|
||||
- name: Community Manager
|
||||
role: >-
|
||||
Voice-of-the-user. Triages every inbound question
|
||||
(GH Discussions, Discord, Slack), routes technical
|
||||
ones to DevRel, feature requests to PM, vulnerability
|
||||
reports to Security Auditor. Owns response-time SLAs
|
||||
and user-feedback capture.
|
||||
tier: 2
|
||||
files_dir: community-manager
|
||||
canvas: {x: 1150, y: 400}
|
||||
plugins: []
|
||||
idle_interval_seconds: 600
|
||||
schedules:
|
||||
- name: Hourly unanswered sweep
|
||||
cron_expr: "12 * * * *"
|
||||
enabled: true
|
||||
prompt_file: schedules/hourly-unanswered-sweep.md
|
||||
initial_prompt_file: initial-prompt.md
|
||||
idle_prompt_file: idle-prompt.md
|
||||
- name: SEO Growth Analyst
|
||||
role: >-
|
||||
Owns organic search visibility and funnel conversion.
|
||||
Metrics: keyword rank, search impressions, CTR, time-
|
||||
on-page, signup conversion. Writes SEO briefs for every
|
||||
Content post; audits Lighthouse + Core Web Vitals daily;
|
||||
proposes A/B tests for weakest funnel step.
|
||||
tier: 2
|
||||
files_dir: seo-growth-analyst
|
||||
canvas: {x: 1000, y: 400}
|
||||
plugins: [browser-automation]
|
||||
idle_interval_seconds: 600
|
||||
schedules:
|
||||
- name: Daily Lighthouse + keyword audit
|
||||
cron_expr: "23 8 * * *"
|
||||
enabled: true
|
||||
prompt_file: schedules/daily-lighthouse-keyword-audit.md
|
||||
initial_prompt_file: initial-prompt.md
|
||||
idle_prompt_file: idle-prompt.md
|
||||
- name: Social Media Brand
|
||||
role: >-
|
||||
Owns Molecule AI's voice on X + LinkedIn and the visual
|
||||
identity across marketing surfaces. 1-2 X posts + 3-5
|
||||
replies/day; LinkedIn 2-3 posts/week. Maintains brand
|
||||
guidelines (zinc dark, blue accents, system-mono code).
|
||||
Every launch gets a 3-post thread within 24h.
|
||||
tier: 2
|
||||
files_dir: social-media-brand
|
||||
canvas: {x: 1300, y: 400}
|
||||
plugins: []
|
||||
idle_interval_seconds: 600
|
||||
schedules:
|
||||
- name: Hourly mention monitor
|
||||
cron_expr: "27 * * * *"
|
||||
enabled: true
|
||||
prompt_file: schedules/hourly-mention-monitor.md
|
||||
initial_prompt_file: initial-prompt.md
|
||||
idle_prompt_file: idle-prompt.md
|
||||
initial_prompt_file: initial-prompt.md
|
||||
28
org-templates/molecule-dev/teams/pm.yaml
Normal file
28
org-templates/molecule-dev/teams/pm.yaml
Normal file
@ -0,0 +1,28 @@
|
||||
name: PM
|
||||
role: Project Manager — coordinates Research and Dev teams
|
||||
tier: 3
|
||||
model: opus
|
||||
files_dir: pm
|
||||
workspace_dir: ${WORKSPACE_DIR}
|
||||
canvas: {x: 400, y: 50}
|
||||
# PM-specific: /triage (PR triage) and /retro (weekly retrospective).
|
||||
plugins: [molecule-workflow-triage, molecule-workflow-retro]
|
||||
# Auto-link Telegram so the user can talk to PM directly from Telegram.
|
||||
# Bot token + chat ID come from pm/.env (TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID).
|
||||
channels:
|
||||
- type: telegram
|
||||
config:
|
||||
bot_token: ${TELEGRAM_BOT_TOKEN}
|
||||
chat_id: ${TELEGRAM_CHAT_ID}
|
||||
enabled: true
|
||||
schedules:
|
||||
- name: Orchestrator pulse
|
||||
cron_expr: "1,6,11,16,21,26,31,36,41,46,51,56 * * * *"
|
||||
enabled: true
|
||||
prompt_file: schedules/orchestrator-pulse.md
|
||||
children:
|
||||
- !include research.yaml
|
||||
- !include dev.yaml
|
||||
- !include documentation-specialist.yaml
|
||||
- !include triage-operator.yaml
|
||||
initial_prompt_file: initial-prompt.md
|
||||
58
org-templates/molecule-dev/teams/research.yaml
Normal file
58
org-templates/molecule-dev/teams/research.yaml
Normal file
@ -0,0 +1,58 @@
|
||||
name: Research Lead
|
||||
role: Market analysis and technical research
|
||||
files_dir: research-lead
|
||||
canvas: {x: 200, y: 250}
|
||||
# Research roles add browser-automation for live web scraping
|
||||
# (product pages, GitHub trending, docs).
|
||||
plugins: [browser-automation]
|
||||
# #383: notify on high-value async output (eco-watch summaries,
|
||||
# competitive intelligence findings) via Telegram so they're not
|
||||
# invisible until the user manually checks memory/canvas.
|
||||
channels:
|
||||
- type: telegram
|
||||
config:
|
||||
bot_token: ${TELEGRAM_BOT_TOKEN}
|
||||
chat_id: ${TELEGRAM_CHAT_ID}
|
||||
enabled: true
|
||||
schedules:
|
||||
- name: Orchestrator pulse
|
||||
cron_expr: "4,9,14,19,24,29,34,39,44,49,54,59 * * * *"
|
||||
enabled: true
|
||||
prompt_file: schedules/orchestrator-pulse.md
|
||||
- name: Hourly ecosystem watch
|
||||
cron_expr: "8,38 * * * *"
|
||||
enabled: true
|
||||
prompt_file: schedules/hourly-ecosystem-watch.md
|
||||
children:
|
||||
- name: Market Analyst
|
||||
role: Market sizing, trends, user research
|
||||
files_dir: market-analyst
|
||||
plugins: [browser-automation]
|
||||
# Idle-loop rollout wave 2 (#216 → #285 → #304 validated on Technical
|
||||
# Researcher 2026-04-16 02:40 UTC). Market Analyst gets the same
|
||||
# reflection-on-completion pattern tuned for market research work.
|
||||
idle_interval_seconds: 600
|
||||
idle_prompt_file: idle-prompt.md
|
||||
- name: Technical Researcher
|
||||
role: AI frameworks and protocol evaluation
|
||||
files_dir: technical-researcher
|
||||
plugins: [browser-automation]
|
||||
# Idle-loop pilot (#205) — Technical Researcher is the first workspace
|
||||
# to opt in to the reflection-on-completion pattern. Measure
|
||||
# activity_logs delta over 24h, then roll to the rest of the research
|
||||
# team if it produces useful backlog-pull dispatches.
|
||||
idle_interval_seconds: 600
|
||||
schedules:
|
||||
- name: Hourly plugin curation
|
||||
cron_expr: "22 * * * *"
|
||||
enabled: true
|
||||
prompt_file: schedules/hourly-plugin-curation.md
|
||||
idle_prompt_file: idle-prompt.md
|
||||
- name: Competitive Intelligence
|
||||
role: Competitor tracking and feature comparison
|
||||
files_dir: competitive-intelligence
|
||||
plugins: [browser-automation]
|
||||
# Idle-loop rollout wave 2 (sibling to Market Analyst).
|
||||
idle_interval_seconds: 600
|
||||
idle_prompt_file: idle-prompt.md
|
||||
initial_prompt_file: initial-prompt.md
|
||||
67
org-templates/molecule-dev/teams/triage-operator.yaml
Normal file
67
org-templates/molecule-dev/teams/triage-operator.yaml
Normal file
@ -0,0 +1,67 @@
|
||||
name: Triage Operator
|
||||
role: >-
|
||||
Owns the hourly PR + issue triage cycle across
|
||||
Molecule-AI/molecule-monorepo and Molecule-AI/molecule-controlplane.
|
||||
Runs a 7-gate verification on every open PR (CI, build, tests,
|
||||
security, design, line-review, Playwright-if-canvas), merges the
|
||||
ones that pass verified-merge rules, holds auth/billing/schema PRs
|
||||
for CEO approval, picks up at most 2 issues per tick through gates
|
||||
I-1..I-6, and appends one line per tick to cron-learnings.jsonl
|
||||
with a concrete next_action. Reports to PM for noteworthy
|
||||
escalations; never bypasses hierarchy. NOT an engineer — never
|
||||
writes logic, never touches design decisions. Mechanical fixes on
|
||||
other people's branches are OK (`fix(gate-N): ...`). The full
|
||||
philosophy + playbook + SKILL definition lives in
|
||||
/workspace/repo/org-templates/molecule-dev/triage-operator/.
|
||||
Read those four files AND
|
||||
~/.claude/projects/-Users-hongming-Documents-GitHub-molecule-monorepo/memory/cron-learnings.jsonl
|
||||
at the start of every tick before taking any action.
|
||||
tier: 3
|
||||
model: opus
|
||||
files_dir: triage-operator
|
||||
canvas: {x: 1150, y: 250}
|
||||
# #370-aligned: Triage Operator is a standing-rules-first role. The
|
||||
# plugin stack below is what the prior operator identified as the
|
||||
# minimum set to run the triage cycle correctly:
|
||||
# - molecule-careful-bash — REFUSE/WARN/ALLOW guards for the
|
||||
# destructive bash ops this role
|
||||
# will regularly encounter
|
||||
# - molecule-session-context — auto-injects recent cron-learnings
|
||||
# + open PR/issue counts at session
|
||||
# start (avoids stale-state ticks)
|
||||
# - molecule-skill-cron-learnings — defines the JSONL append format
|
||||
# - molecule-skill-code-review — 16-criterion per-PR review (Gate 6)
|
||||
# - molecule-skill-cross-vendor-review — second-model review for
|
||||
# noteworthy PRs (auth/billing/
|
||||
# data-deletion/migration)
|
||||
# - molecule-skill-llm-judge — draft-PR ready-or-not gate on
|
||||
# issue pickup (>=4 marks ready)
|
||||
# - molecule-skill-update-docs — post-merge docs sync workflow
|
||||
# - molecule-hitl — @requires_approval gate before
|
||||
# any destructive cross-repo op
|
||||
plugins:
|
||||
- molecule-careful-bash
|
||||
- molecule-session-context
|
||||
- molecule-skill-cron-learnings
|
||||
- molecule-skill-code-review
|
||||
- molecule-skill-cross-vendor-review
|
||||
- molecule-skill-llm-judge
|
||||
- molecule-skill-update-docs
|
||||
- molecule-hitl
|
||||
schedules:
|
||||
- name: Hourly triage
|
||||
cron_expr: "17 * * * *"
|
||||
enabled: true
|
||||
|
||||
# ============================================================
|
||||
# Marketing team (2026-04-16). Peer sub-tree of PM under CEO.
|
||||
# Marketing Lead = CMO-equivalent; runs a 5-min orchestrator
|
||||
# pulse mirroring Dev Lead. Workers (content, community, SEO,
|
||||
# social) run idle-loop backlog-pull; high-judgment roles
|
||||
# (DevRel, PMM) run hourly evolution crons plus idle loops.
|
||||
# Cross-functional: DevRel → Backend/Frontend for code demos,
|
||||
# PMM → Competitive Intelligence for eco-watch diffs. All A2A
|
||||
# summaries route via category_routing to the matching role.
|
||||
# ============================================================
|
||||
prompt_file: schedules/hourly-triage.md
|
||||
initial_prompt_file: initial-prompt.md
|
||||
@ -196,16 +196,23 @@ func (h *OrgHandler) ListTemplates(c *gin.Context) {
|
||||
continue
|
||||
}
|
||||
// Look for org.yaml inside the directory
|
||||
orgFile := filepath.Join(h.orgDir, e.Name(), "org.yaml")
|
||||
templateDir := filepath.Join(h.orgDir, e.Name())
|
||||
orgFile := filepath.Join(templateDir, "org.yaml")
|
||||
data, err := os.ReadFile(orgFile)
|
||||
if err != nil {
|
||||
// Try org.yml
|
||||
orgFile = filepath.Join(h.orgDir, e.Name(), "org.yml")
|
||||
orgFile = filepath.Join(templateDir, "org.yml")
|
||||
data, err = os.ReadFile(orgFile)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
// Expand !include directives before unmarshal so templates that
|
||||
// split across team/role files still report an accurate workspace
|
||||
// count on the /org/templates listing.
|
||||
if expanded, err := resolveYAMLIncludes(data, templateDir); err == nil {
|
||||
data = expanded
|
||||
}
|
||||
var tmpl OrgTemplate
|
||||
if err := yaml.Unmarshal(data, &tmpl); err != nil {
|
||||
continue
|
||||
@ -253,7 +260,15 @@ func (h *OrgHandler) Import(c *gin.Context) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("org template not found: %s", body.Dir)})
|
||||
return
|
||||
}
|
||||
if err := yaml.Unmarshal(data, &tmpl); err != nil {
|
||||
// Expand !include directives before unmarshal. Splits org.yaml
|
||||
// into per-team or per-role files; Phase 3 of the scalability
|
||||
// refactor. Fails loudly on missing / cyclic / escaping includes.
|
||||
expanded, err := resolveYAMLIncludes(data, orgBaseDir)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("!include expansion failed: %v", err)})
|
||||
return
|
||||
}
|
||||
if err := yaml.Unmarshal(expanded, &tmpl); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("invalid YAML: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
133
platform/internal/handlers/org_include.go
Normal file
133
platform/internal/handlers/org_include.go
Normal file
@ -0,0 +1,133 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// maxIncludeDepth caps !include recursion to prevent runaway chains or
|
||||
// cycles that slip past the visited-set check (e.g. relative paths that
|
||||
// normalize differently on different OSes). A depth of 16 easily covers
|
||||
// any realistic team/role hierarchy.
|
||||
const maxIncludeDepth = 16
|
||||
|
||||
// resolveYAMLIncludes expands `!include <path>` directives in a YAML
|
||||
// document. Used by POST /org/import + GET /org/templates to support
|
||||
// splitting a single large org.yaml into per-team or per-role files.
|
||||
//
|
||||
// Semantics:
|
||||
// - A scalar node tagged `!include` with a string value is replaced by
|
||||
// the parsed content of the referenced file.
|
||||
// - Paths are resolved relative to `baseDir` and must stay inside it
|
||||
// (same traversal defense as resolveInsideRoot).
|
||||
// - Includes may be nested (a team file can !include a role file).
|
||||
// Cycles are detected via a visited set keyed on absolute path;
|
||||
// `maxIncludeDepth` caps total recursion depth as a belt-and-braces
|
||||
// check.
|
||||
// - Missing files return an error — fail loud during import, not at
|
||||
// runtime.
|
||||
//
|
||||
// Returns the expanded YAML as bytes (to keep the caller's existing
|
||||
// `yaml.Unmarshal(data, ...)` flow unchanged).
|
||||
func resolveYAMLIncludes(data []byte, baseDir string) ([]byte, error) {
|
||||
var root yaml.Node
|
||||
if err := yaml.Unmarshal(data, &root); err != nil {
|
||||
return nil, fmt.Errorf("parse yaml: %w", err)
|
||||
}
|
||||
|
||||
visited := map[string]bool{}
|
||||
if err := expandNode(&root, baseDir, visited, 0); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out, err := yaml.Marshal(&root)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal expanded yaml: %w", err)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// expandNode walks the yaml.Node tree in-place and replaces any
|
||||
// `!include`-tagged scalar with the parsed content of the referenced
|
||||
// file. Compound nodes (document / mapping / sequence) recurse; alias
|
||||
// nodes are left alone (the yaml parser already resolves them pre-tag).
|
||||
func expandNode(n *yaml.Node, baseDir string, visited map[string]bool, depth int) error {
|
||||
if n == nil {
|
||||
return nil
|
||||
}
|
||||
if depth > maxIncludeDepth {
|
||||
return fmt.Errorf("!include: max depth %d exceeded (possible cycle)", maxIncludeDepth)
|
||||
}
|
||||
|
||||
if n.Kind == yaml.ScalarNode && n.Tag == "!include" {
|
||||
return resolveIncludeScalar(n, baseDir, visited, depth)
|
||||
}
|
||||
|
||||
for _, child := range n.Content {
|
||||
if err := expandNode(child, baseDir, visited, depth); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// resolveIncludeScalar replaces an `!include <path>` scalar with the
|
||||
// parsed content of the referenced file. The replacement happens by
|
||||
// mutating *n to take on the included file's root kind/content/tag.
|
||||
func resolveIncludeScalar(n *yaml.Node, baseDir string, visited map[string]bool, depth int) error {
|
||||
rel := n.Value
|
||||
if rel == "" {
|
||||
return fmt.Errorf("!include at line %d: empty path", n.Line)
|
||||
}
|
||||
if baseDir == "" {
|
||||
return fmt.Errorf("!include %q at line %d requires a dir-based org template (no baseDir in inline-template mode)", rel, n.Line)
|
||||
}
|
||||
abs, err := resolveInsideRoot(baseDir, rel)
|
||||
if err != nil {
|
||||
return fmt.Errorf("!include %q at line %d: %w", rel, n.Line, err)
|
||||
}
|
||||
if visited[abs] {
|
||||
return fmt.Errorf("!include cycle detected at %q (line %d)", rel, n.Line)
|
||||
}
|
||||
data, err := os.ReadFile(abs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("!include %q at line %d: %w", rel, n.Line, err)
|
||||
}
|
||||
|
||||
var sub yaml.Node
|
||||
if err := yaml.Unmarshal(data, &sub); err != nil {
|
||||
return fmt.Errorf("!include %q: parse: %w", rel, err)
|
||||
}
|
||||
// yaml.Unmarshal of a full file yields a DocumentNode wrapping the
|
||||
// actual root. Peel one layer so the includer sees the real content.
|
||||
root := &sub
|
||||
if root.Kind == yaml.DocumentNode && len(root.Content) == 1 {
|
||||
root = root.Content[0]
|
||||
}
|
||||
|
||||
// Mark visited for the whole descent through this file, then recurse
|
||||
// so nested !includes inside the included file resolve too. Each file
|
||||
// gets its own baseDir (the directory containing it) so paths like
|
||||
// `!include role-a/initial.yaml` inside `teams/dev.yaml` resolve
|
||||
// relative to the team file's directory.
|
||||
visited[abs] = true
|
||||
defer delete(visited, abs)
|
||||
|
||||
subDir := filepath.Dir(abs)
|
||||
if err := expandNode(root, subDir, visited, depth+1); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Replace the !include scalar with the resolved content in-place.
|
||||
*n = *root
|
||||
// Clear the !include tag (root's Tag is whatever kind it actually is —
|
||||
// !!map / !!seq / !!str — after unmarshal, which is correct).
|
||||
// If somehow root.Tag is still !include (shouldn't happen), drop it.
|
||||
if n.Tag == "!include" {
|
||||
n.Tag = ""
|
||||
}
|
||||
return nil
|
||||
}
|
||||
224
platform/internal/handlers/org_include_test.go
Normal file
224
platform/internal/handlers/org_include_test.go
Normal file
@ -0,0 +1,224 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// resolveYAMLIncludes is the preprocessor Phase 3 uses to split org.yaml
|
||||
// into per-team / per-role files. These tests cover the happy path,
|
||||
// nested includes, path traversal defense, cycle detection, depth cap,
|
||||
// and the inline-template (no baseDir) error.
|
||||
|
||||
func TestResolveYAMLIncludes_FlatInclude(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
// Write a team file with a single workspace.
|
||||
team := filepath.Join(tmp, "team.yaml")
|
||||
if err := os.WriteFile(team, []byte("name: Role A\nrole: Worker\ntier: 2\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
src := []byte(`name: Test Org
|
||||
workspaces:
|
||||
- !include team.yaml
|
||||
`)
|
||||
out, err := resolveYAMLIncludes(src, tmp)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
// Parse result and verify workspace name landed in place.
|
||||
var tmpl struct {
|
||||
Name string `yaml:"name"`
|
||||
Workspaces []OrgWorkspace `yaml:"workspaces"`
|
||||
}
|
||||
if err := yaml.Unmarshal(out, &tmpl); err != nil {
|
||||
t.Fatalf("re-parse failed: %v\n---\n%s", err, out)
|
||||
}
|
||||
if len(tmpl.Workspaces) != 1 {
|
||||
t.Fatalf("expected 1 workspace, got %d", len(tmpl.Workspaces))
|
||||
}
|
||||
if tmpl.Workspaces[0].Name != "Role A" {
|
||||
t.Errorf("workspace name: got %q, want %q", tmpl.Workspaces[0].Name, "Role A")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveYAMLIncludes_Nested(t *testing.T) {
|
||||
// team.yaml includes leaf.yaml. Prove nested resolution works + that
|
||||
// relative paths inside the included file resolve against THAT file's
|
||||
// dir, not the top-level org dir.
|
||||
tmp := t.TempDir()
|
||||
subDir := filepath.Join(tmp, "teams")
|
||||
if err := os.MkdirAll(subDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
leaf := filepath.Join(subDir, "leaf.yaml")
|
||||
if err := os.WriteFile(leaf, []byte("name: Leaf\ntier: 1\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
team := filepath.Join(subDir, "team.yaml")
|
||||
if err := os.WriteFile(team, []byte("name: Parent\ntier: 3\nchildren:\n - !include leaf.yaml\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
src := []byte(`name: Test
|
||||
workspaces:
|
||||
- !include teams/team.yaml
|
||||
`)
|
||||
out, err := resolveYAMLIncludes(src, tmp)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
var tmpl OrgTemplate
|
||||
if err := yaml.Unmarshal(out, &tmpl); err != nil {
|
||||
t.Fatalf("re-parse failed: %v\n---\n%s", err, out)
|
||||
}
|
||||
if len(tmpl.Workspaces) != 1 || tmpl.Workspaces[0].Name != "Parent" {
|
||||
t.Fatalf("workspaces[0]: %+v", tmpl.Workspaces)
|
||||
}
|
||||
if len(tmpl.Workspaces[0].Children) != 1 || tmpl.Workspaces[0].Children[0].Name != "Leaf" {
|
||||
t.Fatalf("children: %+v", tmpl.Workspaces[0].Children)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveYAMLIncludes_RejectsTraversal(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
// Write a file outside tmp that the include would exfiltrate.
|
||||
outside := filepath.Join(filepath.Dir(tmp), "secret.yaml")
|
||||
if err := os.WriteFile(outside, []byte("name: Leak\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Remove(outside)
|
||||
|
||||
cases := []string{"../secret.yaml", "../../secret.yaml"}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc, func(t *testing.T) {
|
||||
src := []byte("workspaces:\n - !include " + tc + "\n")
|
||||
_, err := resolveYAMLIncludes(src, tmp)
|
||||
if err == nil {
|
||||
t.Errorf("expected error for traversal %q", tc)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveYAMLIncludes_CycleDetected(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
a := filepath.Join(tmp, "a.yaml")
|
||||
b := filepath.Join(tmp, "b.yaml")
|
||||
if err := os.WriteFile(a, []byte("name: A\nchildren:\n - !include b.yaml\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(b, []byte("name: B\nchildren:\n - !include a.yaml\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
src := []byte("workspaces:\n - !include a.yaml\n")
|
||||
_, err := resolveYAMLIncludes(src, tmp)
|
||||
if err == nil {
|
||||
t.Fatal("expected cycle error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "cycle") && !strings.Contains(err.Error(), "depth") {
|
||||
t.Errorf("error should mention cycle or depth; got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveYAMLIncludes_EmptyPathErrors(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
src := []byte("workspaces:\n - !include \"\"\n")
|
||||
_, err := resolveYAMLIncludes(src, tmp)
|
||||
if err == nil {
|
||||
t.Error("expected error for empty !include path")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveYAMLIncludes_MissingFileErrors(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
src := []byte("workspaces:\n - !include nonexistent.yaml\n")
|
||||
_, err := resolveYAMLIncludes(src, tmp)
|
||||
if err == nil {
|
||||
t.Error("expected error for missing file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveYAMLIncludes_InlineTemplateErrors(t *testing.T) {
|
||||
src := []byte("workspaces:\n - !include team.yaml\n")
|
||||
_, err := resolveYAMLIncludes(src, "")
|
||||
if err == nil {
|
||||
t.Error("expected error when baseDir empty and !include used")
|
||||
}
|
||||
}
|
||||
|
||||
// Integration check: after Phase 3 split, the real molecule-dev/org.yaml
|
||||
// resolves cleanly via !include and unmarshal into OrgTemplate produces
|
||||
// the full workspace tree. Guards against split regressions landing on
|
||||
// main before they can be caught by a deploy.
|
||||
func TestResolveYAMLIncludes_RealMoleculeDev(t *testing.T) {
|
||||
// Locate the monorepo root from the test file location.
|
||||
// Test runs in platform/internal/handlers/; org template is at
|
||||
// ../../../org-templates/molecule-dev/org.yaml.
|
||||
here, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("getwd: %v", err)
|
||||
}
|
||||
orgDir := filepath.Clean(filepath.Join(here, "..", "..", "..", "org-templates", "molecule-dev"))
|
||||
orgFile := filepath.Join(orgDir, "org.yaml")
|
||||
data, err := os.ReadFile(orgFile)
|
||||
if err != nil {
|
||||
t.Skipf("molecule-dev/org.yaml not found (skipping integration test): %v", err)
|
||||
}
|
||||
expanded, err := resolveYAMLIncludes(data, orgDir)
|
||||
if err != nil {
|
||||
t.Fatalf("resolveYAMLIncludes on real org.yaml: %v", err)
|
||||
}
|
||||
var tmpl OrgTemplate
|
||||
if err := yaml.Unmarshal(expanded, &tmpl); err != nil {
|
||||
t.Fatalf("unmarshal expanded yaml: %v", err)
|
||||
}
|
||||
// Sanity: should have PM + Marketing Lead at top, and PM should have
|
||||
// at least Research Lead, Dev Lead, Documentation Specialist, Triage
|
||||
// Operator as children (the Phase 3 split targets).
|
||||
if len(tmpl.Workspaces) < 2 {
|
||||
t.Fatalf("expected ≥2 top-level workspaces, got %d", len(tmpl.Workspaces))
|
||||
}
|
||||
names := map[string]bool{}
|
||||
for _, w := range tmpl.Workspaces {
|
||||
names[w.Name] = true
|
||||
}
|
||||
for _, want := range []string{"PM", "Marketing Lead"} {
|
||||
if !names[want] {
|
||||
t.Errorf("expected top-level workspace %q, not found", want)
|
||||
}
|
||||
}
|
||||
var pm *OrgWorkspace
|
||||
for i := range tmpl.Workspaces {
|
||||
if tmpl.Workspaces[i].Name == "PM" {
|
||||
pm = &tmpl.Workspaces[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if pm == nil || len(pm.Children) < 4 {
|
||||
t.Errorf("PM should have ≥4 children after include resolution, got %d", len(pm.Children))
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveYAMLIncludes_NoIncludesIsNoop(t *testing.T) {
|
||||
// Ensure the preprocessor is a safe no-op for templates that don't
|
||||
// use !include — critical since we always run it on POST /org/import.
|
||||
tmp := t.TempDir()
|
||||
src := []byte(`name: Simple
|
||||
workspaces:
|
||||
- name: Only
|
||||
tier: 2
|
||||
`)
|
||||
out, err := resolveYAMLIncludes(src, tmp)
|
||||
if err != nil {
|
||||
t.Fatalf("no-op should not error, got %v", err)
|
||||
}
|
||||
var orig, expanded OrgTemplate
|
||||
_ = yaml.Unmarshal(src, &orig)
|
||||
_ = yaml.Unmarshal(out, &expanded)
|
||||
if orig.Name != expanded.Name || len(orig.Workspaces) != len(expanded.Workspaces) {
|
||||
t.Errorf("no-op changed semantics; orig=%+v expanded=%+v", orig, expanded)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user