Install
openclaw skills install pmoAI-native PMO (Project Management Office) Skill for OpenClaw. Acts as Glue Code — connects to GitHub Issues, Notion, and other tools, then summarizes, alerts, and surfaces cross-project risks. MUST activate when user asks about: portfolio status, project health, weekly report, risk check, new project intake, or cross-project dependencies. Triggers on explicit /pmo commands OR any message containing: 'portfolio', 'all projects', '周报', 'weekly report', 'risk check', 'project status', '项目状态', '风险', 'new project', 'add project', 'stale', 'blocked'. This skill does NOT store project data — it reads from external tools and caches only events + risks locally.
openclaw skills install pmoYou MUST invoke this skill when the message matches ANY of:
IF user_message matches regex: /(?:^|\s)(\/pmo|pmo\s+)(list|status|risks|check|sync|weekly|add|remove)/i
IF user_message contains any of: ["portfolio", "all projects", "总览", "项目总览"]
IF user_message contains any of: ["周报", "weekly report", "weekly status", "周度报告"]
IF user_message contains any of: ["risk check", "scan risks", "风险扫描", "check risks"]
IF user_message contains any of: ["project status", "项目状态", "XXX 怎么样了", "how is.*doing"]
IF user_message contains any of: ["new project", "add project", "start tracking", "跟踪新项目"]
IF user_message contains any of: ["stale", "dormant", "blocked", "没有更新", "卡住了"]
IF user_message contains any of: ["cross-project", "dependency", "依赖", "关联项目"]
IF user_message matches regex: /p-?\d{3}/i # project IDs like p-001
If none of the above match, do NOT activate this skill.
def classify_intent(user_message: str, has_attachment: bool = False) -> str:
"""
Returns one of: intake | remove | query_portfolio | query_project
| query_risks | weekly_report | detect_risks | sync | unknown
"""
msg = user_message.strip().lower()
# === INTENT: intake ===
if any(kw in msg for kw in ["new project", "add project", "start tracking",
"跟踪新项目", "想跟踪一个", "我要加一个项目"]):
return "intake"
# === INTENT: remove ===
if any(kw in msg for kw in ["remove project", "stop tracking", "delete project",
"删除项目", "不跟踪了", "移除项目"]):
return "remove"
# === INTENT: weekly_report ===
if any(kw in msg for kw in ["weekly", "周报", "周报生成", "weekly report",
"status report", "周度"]):
return "weekly_report"
# === INTENT: detect_risks ===
if any(kw in msg for kw in ["risk check", "scan", "风险扫描", "check risks",
"scan risks", "detect risks", "风险检测"]):
return "detect_risks"
# === INTENT: sync ===
if any(kw in msg for kw in ["sync", "refresh", "更新缓存", "re-sync"]):
return "sync"
# === INTENT: query_risks ===
if any(kw in msg for kw in ["risks", "风险", "problems", "blockers",
"有什么风险", "风险列表"]):
return "query_risks"
# === INTENT: query_portfolio ===
if any(kw in msg for kw in ["portfolio", "all projects", "总览", "项目总览",
"所有项目", "overview", "全局"]):
return "query_portfolio"
# === INTENT: query_project ===
# Must have a project reference + status keyword, but NOT portfolio/risk keywords
project_keywords = ["status", "怎么样了", "进度", "更新", "health",
"最近怎么样", "现在什么情况"]
if any(pk in msg for pk in project_keywords):
# Also check for project name/ID presence
if not any(kw in msg for kw in ["portfolio", "all projects", "总览", "risks"]):
return "query_project"
# === INTENT: unknown ===
return "unknown"
def route_intent(intent: str, user_message: str) -> str:
"""
Given an intent, return the action to take.
Each branch is an actual function name to call.
"""
if intent == "intake":
return "handle_intake"
elif intent == "remove":
return "handle_remove"
elif intent == "query_portfolio":
return "handle_portfolio"
elif intent == "query_project":
return "handle_project_status"
elif intent == "query_risks":
return "handle_risks"
elif intent == "weekly_report":
return "handle_weekly_report"
elif intent == "detect_risks":
return "handle_risk_detection"
elif intent == "sync":
return "handle_sync"
elif intent == "unknown":
return "handle_unknown"
else:
return "handle_unknown"
This Skill does NOT store your project data. Your project data lives in:
This Skill reads from those tools, analyzes with AI, and alerts proactively.
Local storage is minimal — only:
memory/PMO/
├── config.yaml # Tool connections (GitHub tokens, Notion IDs, etc.)
├── projects.yaml # Project registry (id, name, source_url, owner, tags)
├── events.jsonl # Append-only event log
├── risks.yaml # Cross-project risks (things no single tool captures)
└── cache/ # Temporary cache of external tool data (TTL 5min)
└── {project-id}.json
integrations:
github:
enabled: true
repos:
- owner: my-org
repo: erp-system
token_env: GH_TOKEN
notion:
enabled: false
databases: []
feishu:
enabled: true
space_id: "xxx"
alerts:
channels:
telegram: true
feishu: false
stale_threshold_days: 7
dormant_threshold_days: 14
milestone_reminder_days: 1
review_cycle: weekly
projects:
- id: p-001
name: "ERP System" # Fictional: enterprise resource planning
source: github
source_url: "https://github.com/my-org/erp-system/issues"
owner: alice
tags: [backend, priority-high]
methodology: agile
created_at: "2026-04-01T09:00:00"
- id: p-002
name: "E-commerce App" # Fictional: consumer-facing shopping app
source: github
source_url: "https://github.com/my-org/shop-app/issues"
owner: bob
tags: [frontend, priority-high]
methodology: scrum
created_at: "2026-04-01T09:00:00"
- id: p-003
name: "Mobile App iOS" # Fictional: native iOS shopping companion
source: notion
source_url: "https://notion.so/my-org/mobile-ios"
owner: carol
tags: [mobile, ios, planning]
methodology: kanban
created_at: "2026-04-03T09:00:00"
{"ts":"2026-04-06T10:00:00","event":"status_check","project_id":"p-001","source":"github","data":{"open_issues":5,"closed_this_week":2}}
{"ts":"2026-04-06T10:00:00","event":"risk_detected","project_id":"p-001","risk_id":"r-001","severity":"high","data":{"reason":"No update in 14 days"}}
{"ts":"2026-04-06T10:00:00","event":"report_generated","type":"weekly","actor":"pmo-skill","data":{"projects_reviewed":3,"risks_flagged":2}}
risks:
- id: r-001
title: "Vendor API delay may block ERP Phase 2" # Fictional
category: technical
severity: high
status: open
projects: ["p-001", "p-002"]
owner: alice
mitigation: "Get vendor API specs by April 15"
created_at: "2026-04-06T10:00:00"
- id: r-002
title: "iOS developer OOO Apr 10-20 — blocks Mobile App iOS" # Fictional
category: resource
severity: medium
status: open
projects: ["p-003"]
owner: carol
mitigation: "Bring in backup developer by Apr 8"
created_at: "2026-04-05T10:00:00"
User: "show me the portfolio overview"
Model: Calls handle_portfolio()
→ Read projects.yaml → Read all cache/*.json → Aggregate
→ Output: table of all projects with health indicators
User: "generate the weekly PMO report"
Model: Calls handle_weekly_report()
→ Read events.jsonl (last 7 days) + projects.yaml + risks.yaml
→ Output: formatted weekly report with activity, blockers, risks, next priorities
User: "我想跟踪一个新项目,叫电商App,是GitHub上的 my-org/shop"
Model: Calls handle_intake()
→ Extract: name="E-commerce App", source=github, repo="my-org/shop"
→ Append to projects.yaml (id: p-004)
→ Append intake event to events.jsonl
→ Output: "Project added. ID: p-004. Generate initial status report?"
User: "run a risk check across all projects"
Model: Calls handle_risk_detection()
→ For each project: check last_update, open issues age, milestone dates
→ Rules: stale=7d, dormant=14d, overdue=milestone past due
→ Compare cross-project dependencies for cascade risk
→ Output: new risks → write to risks.yaml; surface to user
User: "how is the ERP System doing?"
Model: Calls handle_project_status()
→ Find project by name "ERP System" → id p-001
→ Read cache/p-001.json (or fetch from GitHub if stale)
→ Output: project health, open/closed issues, last update, blockers
Explicit prohibitions — the skill should NEVER do these:
DO NOT create local copies of GitHub Issues, Notion entries, or task items. Always read from source; never duplicate data.
DO NOT become a data-entry tool. If user describes tasks verbally, ask them to update the source system (GitHub/Notion), then re-sync.
DO NOT store passwords, tokens, or secrets in plain text files. Use environment variables (e.g., GH_TOKEN in config.yaml → token_env).
DO NOT generate fake project data or fabricate status updates. If no real data exists, say "No data found for this project — have you pushed any updates to GitHub/Notion?"
DO NOT send alerts to channels not configured in config.yaml. Always check alerts.channels before sending.
DO NOT modify or close GitHub Issues / Notion entries. This skill is read-only for external tools. It only writes to local files (projects.yaml, events.jsonl, risks.yaml).
DO NOT respond to casual conversation that has nothing to do with project management (e.g., "how was your weekend?"). Return unknown intent and respond minimally.
DO NOT run risk detection on more than 20 projects in a single pass to avoid token/API exhaustion. Batch into groups of 10.
DO NOT store sensitive project details (customer names, revenue figures, internal politics) in risks.yaml or events.jsonl. Keep entries high-level.
DO NOT auto-close risks. Risks stay open until a human marks them resolved in the source system and re-syncs.
/pmo sync → Re-read all sources, update cache, detect risks/pmo check → Run risk detection, surface stale projectsWhen:
- Project stale (7+ days no update) → "Hey, {project} hasn't been updated in X days"
- Risk severity >= high → "⚠️ Risk alert: {risk title}"
- Milestone due today → "📅 Milestone due today: {milestone} on {project}"
- Cross-project dependency blocked → "🔗 Dependency chain alert: {A} blocked → {B} blocked"
/pmo check or /pmo sync| Command | Intent | Example |
|---|---|---|
/pmo list | query_portfolio | /pmo list |
/pmo status {project} | query_project | /pmo status erp |
/pmo risks | query_risks | /pmo risks |
/pmo check | detect_risks | /pmo check |
/pmo sync | sync | /pmo sync |
/pmo weekly | weekly_report | /pmo weekly |
/pmo add {name} | intake | /pmo add "New Project" |
/pmo remove {project} | remove | /pmo remove p-001 |
Reads: projects.yaml + cache/*.json + risks.yaml
Outputs: Portfolio overview table with health indicators per project.
Reads: events.jsonl (last 7 days) + projects.yaml + risks.yaml
Outputs: Formatted weekly report.
Reads: projects.yaml + cache/* + events.jsonl
Rules: stale=7d, dormant=14d, overdue milestone, cascade risk from dependencies.
Outputs: New risks → write to risks.yaml; display to user.
Reads: projects.yaml (find by name/ID) + cache/{id}.json (or fetch from source)
Outputs: Single project status with issues and blockers.
Writes: Append to projects.yaml; append event to events.jsonl.
Outputs: Project ID, initial confirmation, optional status report.
Updates: Mark archived in projects.yaml; append removal event to events.jsonl.
Does NOT delete historical events.
Reads: Re-read all integrations (GitHub, Notion), update all cache files. Outputs: Sync summary (N projects updated, M events logged).
If intent is unknown: ask one clarifying question, do NOT assume. Do NOT invoke full PMO logic for unrelated messages.
Script: scripts/github_fetch.py
The official GitHub integration uses github_fetch.py to read issues from repos configured in config.yaml.
Usage:
python scripts/github_fetch.py --owner my-org --repo my-project --token-env GH_TOKEN
Output format (JSON):
{
"project_name": "my-org/my-project",
"owner": "my-org",
"repo": "my-project",
"open_count": 5,
"closed_count": 12,
"total_count": 17,
"progress_pct": 70.6,
"last_updated": "2026-04-05T14:30:00Z",
"fetched_at": "2026-04-06T10:00:00Z",
"issues": [
{
"id": 42,
"title": "Fix authentication bug",
"state": "open",
"labels": ["bug", "priority-high"],
"url": "https://github.com/my-org/my-project/issues/42",
"created_at": "2026-04-01T09:00:00Z",
"updated_at": "2026-04-05T14:30:00Z",
"assignees": ["alice"]
}
]
}
In skill handlers, call it like:
import subprocess, json
def fetch_github_issues(owner: str, repo: str, token_env: str = "GH_TOKEN") -> dict:
result = subprocess.run(
["python", "scripts/github_fetch.py",
"--owner", owner, "--repo", repo,
"--token-env", token_env],
capture_output=True, text=True, cwd="/home/nekai/.openclaw/workspace/skills/pmo-true-skill"
)
return json.loads(result.stdout)
Constraints:
GH_TOKEN by default) — never hardcodedmemory/PMO/cache/{owner}-{repo}.json with 5-min TTLdef fetch_notion_database(database_id: str, token: str) -> dict:
"""
Reads from Notion Database API.
Returns: {entries, last_updated, status_distribution}
"""
scripts/github_fetch.py, updated config.yaml template with real integration examples, enhanced docs for GitHub glue code.