diff --git a/org-templates/molecule-dev/org.yaml b/org-templates/molecule-dev/org.yaml index 61789ad6..344c7169 100644 --- a/org-templates/molecule-dev/org.yaml +++ b/org-templates/molecule-dev/org.yaml @@ -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 diff --git a/org-templates/molecule-dev/teams/dev.yaml b/org-templates/molecule-dev/teams/dev.yaml new file mode 100644 index 00000000..f681cd71 --- /dev/null +++ b/org-templates/molecule-dev/teams/dev.yaml @@ -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 diff --git a/org-templates/molecule-dev/teams/documentation-specialist.yaml b/org-templates/molecule-dev/teams/documentation-specialist.yaml new file mode 100644 index 00000000..5bbc3023 --- /dev/null +++ b/org-templates/molecule-dev/teams/documentation-specialist.yaml @@ -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 + diff --git a/org-templates/molecule-dev/teams/marketing.yaml b/org-templates/molecule-dev/teams/marketing.yaml new file mode 100644 index 00000000..6419665a --- /dev/null +++ b/org-templates/molecule-dev/teams/marketing.yaml @@ -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 diff --git a/org-templates/molecule-dev/teams/pm.yaml b/org-templates/molecule-dev/teams/pm.yaml new file mode 100644 index 00000000..14736263 --- /dev/null +++ b/org-templates/molecule-dev/teams/pm.yaml @@ -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 diff --git a/org-templates/molecule-dev/teams/research.yaml b/org-templates/molecule-dev/teams/research.yaml new file mode 100644 index 00000000..688dbfa3 --- /dev/null +++ b/org-templates/molecule-dev/teams/research.yaml @@ -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 diff --git a/org-templates/molecule-dev/teams/triage-operator.yaml b/org-templates/molecule-dev/teams/triage-operator.yaml new file mode 100644 index 00000000..b335effd --- /dev/null +++ b/org-templates/molecule-dev/teams/triage-operator.yaml @@ -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 diff --git a/platform/internal/handlers/org.go b/platform/internal/handlers/org.go index f5786b2e..63f55905 100644 --- a/platform/internal/handlers/org.go +++ b/platform/internal/handlers/org.go @@ -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 } diff --git a/platform/internal/handlers/org_include.go b/platform/internal/handlers/org_include.go new file mode 100644 index 00000000..98e8a33b --- /dev/null +++ b/platform/internal/handlers/org_include.go @@ -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 ` 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 ` 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 +} diff --git a/platform/internal/handlers/org_include_test.go b/platform/internal/handlers/org_include_test.go new file mode 100644 index 00000000..6b437699 --- /dev/null +++ b/platform/internal/handlers/org_include_test.go @@ -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) + } +}