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:
rabbitblood 2026-04-16 00:28:40 -07:00 committed by PM (Molecule AI)
parent 159197ed4a
commit 112c28d885
10 changed files with 945 additions and 567 deletions

View File

@ -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

View 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

View File

@ -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

View 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

View 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

View 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

View 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

View File

@ -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
}

View 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
}

View 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)
}
}