Skip to content

Commit 4589fc8

Browse files
authored
Plugin: add project manager skill (#22)
1 parent 695b18f commit 4589fc8

File tree

6 files changed

+308
-1
lines changed

6 files changed

+308
-1
lines changed
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
---
2+
name: project-manager
3+
description: "Manage GitHub issues and the GitHub Project board for this repository, while keeping the local tracker in sync. Use when the user wants to capture freeform requirements as issues, flesh out issue descriptions from repo or upstream research, triage Priority/Size/Workflow/Status, add issues or PRs to project 7, or reconcile GitHub state with `.local/work-items.yaml`."
4+
---
5+
6+
# Project Manager
7+
8+
Use this skill for repo-specific project management on [OpenClaw Codex App Server Project](https://github.com/orgs/pwrdrvr/projects/7).
9+
10+
## Automation Preference
11+
12+
- Prefer Node scripts for repo-local automation.
13+
- If a script needs dependencies, add them as repo `devDependencies` and invoke them through `pnpm` or `node`.
14+
- Avoid Python for repo-local skill automation unless a Python-native library is clearly worth the extra runtime dependency.
15+
16+
## Canonical Locations
17+
18+
- Treat GitHub Issues and PRs as the public source of truth.
19+
- Treat `.local/work-items.yaml` as a derived repo-local cross-reference map that can be regenerated from the project board.
20+
- Put temporary issue writeups only in `.local/issue-drafts/`.
21+
- Do not create parallel scratch directories or alternate tracker files for the same purpose.
22+
23+
Current repo-specific locations:
24+
25+
- Project board: `https://github.com/orgs/pwrdrvr/projects/7`
26+
- Local tracker: `.local/work-items.yaml`
27+
- Issue drafts: `.local/issue-drafts/`
28+
- Local id prefix: `ocas-`
29+
30+
Refresh the derived tracker with:
31+
32+
```bash
33+
pnpm project:sync
34+
```
35+
36+
## Workflow
37+
38+
1. Explore before filing.
39+
40+
- Read local code, tests, docs, and upstream references before creating or expanding an issue.
41+
- Prefer issue bodies with concrete findings, source pointers, and proposed scope over vague placeholders.
42+
43+
2. Draft locally when the issue is non-trivial.
44+
45+
- Write or refresh the issue body in `.local/issue-drafts/<nn>-<slug>.md`.
46+
- Reuse that file for edits; do not fork the same issue into multiple local scratch notes.
47+
48+
3. Create or update the GitHub issue.
49+
50+
- Use `gh issue create`, `gh issue edit`, and `gh issue comment`.
51+
- Keep titles short and imperative, usually starting with `Plugin:`.
52+
53+
4. Add the issue or PR to project `7`.
54+
55+
- Use `gh project item-add 7 --owner pwrdrvr --url <issue-or-pr-url>`.
56+
- Set `Status`, `Priority`, `Size`, and `Workflow`.
57+
58+
5. Sync `.local/work-items.yaml`.
59+
60+
- Add or update the item entry with issue number, URLs, project item id, workflow, status, priority, size, and concise notes.
61+
- Update `last_synced_at` whenever the tracker changes.
62+
- Prefer pushing durable notes into GitHub issues or `.local/issue-drafts/`; the tracker should stay compact.
63+
64+
6. Reconcile if anything drifted.
65+
66+
- Use `gh issue list`, `gh project item-list`, and `gh project field-list` to confirm GitHub matches the local tracker.
67+
68+
## Field Conventions
69+
70+
- `Status`: `Inbox`, `Ready`, `In Progress`, `In Review`, `Done`
71+
- `Workflow`: `Plan`, `Review`, `Threads`, `Worktrees`, `Branches`
72+
73+
Triage heuristic for this repo:
74+
75+
- `P0`: quick wins that shrink the board fast, plus high-visibility completeness or pizazz work
76+
- `P1`: larger user-visible completeness work
77+
- `P2`: infrastructure, refactors, planning spikes, and corner-case cleanup unless they are very quick
78+
79+
Size heuristic:
80+
81+
- `XS` or `S`: obvious quick wins
82+
- `M`: bounded feature or bug fix with a few moving parts
83+
- `L`: visible feature touching multiple flows
84+
- `XL`: large architectural or cross-cutting work
85+
86+
## Command Pattern
87+
88+
Start by discovering current project field ids instead of assuming they never change:
89+
90+
```bash
91+
gh project field-list 7 --owner pwrdrvr --format json
92+
```
93+
94+
Typical flow:
95+
96+
```bash
97+
gh issue create --repo pwrdrvr/openclaw-codex-app-server --title "<title>" --body-file .local/issue-drafts/<file>.md
98+
gh project item-add 7 --owner pwrdrvr --url <issue-or-pr-url> --format json
99+
gh project item-edit --project-id <project-id> --id <item-id> --field-id <field-id> --single-select-option-id <option-id>
100+
gh project item-list 7 --owner pwrdrvr --format json
101+
```
102+
103+
Refresh the local tracker:
104+
105+
```bash
106+
pnpm project:sync
107+
```
108+
109+
## Tracker Shape
110+
111+
Each `.local/work-items.yaml` item should keep:
112+
113+
- `local_id`
114+
- `title`
115+
- `repo`
116+
- `source_note`
117+
- `github.issue_number`
118+
- `github.issue_url`
119+
- `github.project_number`
120+
- `github.project_url`
121+
- `github.project_item_id`
122+
- `state.issue_state`
123+
- `state.project_status`
124+
- `state.workflow`
125+
- `state.priority`
126+
- `state.size`
127+
- optional branch / PR fields
128+
- concise `notes`
129+
130+
Keep notes factual and short. Store raw findings and writeups in the issue draft file or GitHub issue, not as sprawling tracker prose.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
interface:
2+
display_name: "Project Manager"
3+
short_description: "Manage issues, board, and local tracker"
4+
default_prompt: "Use $project-manager to capture requirements into GitHub issues, keep project 7 in sync, and update the local .local tracker for this repo."
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
#!/usr/bin/env node
2+
3+
import { execFileSync } from "node:child_process";
4+
import fs from "node:fs";
5+
import path from "node:path";
6+
import YAML from "yaml";
7+
8+
const REPO = "pwrdrvr/openclaw-codex-app-server";
9+
const PROJECT_OWNER = "pwrdrvr";
10+
const PROJECT_NUMBER = 7;
11+
const PROJECT_URL = "https://github.com/orgs/pwrdrvr/projects/7";
12+
const TRACKER_PATH = path.resolve(".local/work-items.yaml");
13+
14+
function runGh(args) {
15+
return execFileSync("gh", args, {
16+
cwd: process.cwd(),
17+
encoding: "utf8",
18+
stdio: ["ignore", "pipe", "pipe"],
19+
});
20+
}
21+
22+
function loadExistingTracker() {
23+
if (!fs.existsSync(TRACKER_PATH)) {
24+
return { version: 1, last_synced_at: null, items: [] };
25+
}
26+
27+
const raw = fs.readFileSync(TRACKER_PATH, "utf8");
28+
const parsed = YAML.parse(raw) ?? {};
29+
return {
30+
version: parsed.version ?? 1,
31+
last_synced_at: parsed.last_synced_at ?? null,
32+
items: Array.isArray(parsed.items) ? parsed.items : [],
33+
};
34+
}
35+
36+
function normalizeNumber(value) {
37+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
38+
}
39+
40+
function nextLocalId(existingItems) {
41+
let max = 0;
42+
for (const item of existingItems) {
43+
const match =
44+
typeof item?.local_id === "string" ? item.local_id.match(/^ocas-(\d{4,})$/) : null;
45+
if (match) {
46+
max = Math.max(max, Number(match[1]));
47+
}
48+
}
49+
return `ocas-${String(max + 1).padStart(4, "0")}`;
50+
}
51+
52+
function mergeExistingItem(existingByIssue, issueNumber, fallbackLocalId) {
53+
const current = existingByIssue.get(issueNumber);
54+
if (current && typeof current === "object" && current !== null) {
55+
return structuredClone(current);
56+
}
57+
return {
58+
local_id: fallbackLocalId,
59+
title: "",
60+
repo: REPO,
61+
source_note: "",
62+
github: {},
63+
state: {},
64+
notes: [],
65+
};
66+
}
67+
68+
function main() {
69+
const existing = loadExistingTracker();
70+
const existingByIssue = new Map();
71+
for (const item of existing.items) {
72+
const issueNumber = normalizeNumber(item?.github?.issue_number);
73+
if (issueNumber > 0) {
74+
existingByIssue.set(issueNumber, item);
75+
}
76+
}
77+
78+
const issueList = JSON.parse(
79+
runGh(["issue", "list", "--repo", REPO, "--state", "all", "--limit", "500", "--json", "number,state,title,url"]),
80+
);
81+
const issueByNumber = new Map(issueList.map((issue) => [issue.number, issue]));
82+
83+
const projectData = JSON.parse(
84+
runGh(["project", "item-list", String(PROJECT_NUMBER), "--owner", PROJECT_OWNER, "--format", "json"]),
85+
);
86+
87+
const items = [];
88+
for (const item of projectData.items ?? []) {
89+
const content = item?.content ?? {};
90+
if (content.type !== "Issue") {
91+
continue;
92+
}
93+
if (content.repository !== REPO) {
94+
continue;
95+
}
96+
const issueNumber = normalizeNumber(content.number);
97+
if (issueNumber <= 0) {
98+
continue;
99+
}
100+
101+
const issue = issueByNumber.get(issueNumber);
102+
const merged = mergeExistingItem(existingByIssue, issueNumber, nextLocalId(existing.items));
103+
merged.title = content.title ?? issue?.title ?? merged.title ?? "";
104+
merged.repo = REPO;
105+
merged.source_note = typeof merged.source_note === "string" ? merged.source_note : "";
106+
if (typeof merged.raw_example !== "string") {
107+
delete merged.raw_example;
108+
}
109+
merged.github = {
110+
...(merged.github && typeof merged.github === "object" ? merged.github : {}),
111+
issue_number: issueNumber,
112+
issue_url: content.url ?? issue?.url ?? "",
113+
project_number: PROJECT_NUMBER,
114+
project_url: PROJECT_URL,
115+
project_item_id: item.id ?? "",
116+
};
117+
merged.state = {
118+
...(merged.state && typeof merged.state === "object" ? merged.state : {}),
119+
issue_state: issue?.state ?? "OPEN",
120+
project_status: item.status ?? "",
121+
workflow: item.workflow ?? "",
122+
priority: item.priority ?? "",
123+
size: item.size ?? "",
124+
branch: typeof merged.state?.branch === "string" ? merged.state.branch : "",
125+
pr_number: normalizeNumber(merged.state?.pr_number),
126+
pr_url: typeof merged.state?.pr_url === "string" ? merged.state.pr_url : "",
127+
};
128+
if (!Array.isArray(merged.notes)) {
129+
merged.notes = [];
130+
}
131+
items.push(merged);
132+
}
133+
134+
items.sort((a, b) => normalizeNumber(a.github?.issue_number) - normalizeNumber(b.github?.issue_number));
135+
136+
const usedIds = new Set();
137+
let counter = 1;
138+
for (const item of items) {
139+
if (typeof item.local_id !== "string" || usedIds.has(item.local_id)) {
140+
while (usedIds.has(`ocas-${String(counter).padStart(4, "0")}`)) {
141+
counter += 1;
142+
}
143+
item.local_id = `ocas-${String(counter).padStart(4, "0")}`;
144+
counter += 1;
145+
}
146+
usedIds.add(item.local_id);
147+
}
148+
149+
const output = {
150+
version: 1,
151+
last_synced_at: new Date().toISOString().replace(/\.\d{3}Z$/, "Z"),
152+
items,
153+
};
154+
155+
fs.mkdirSync(path.dirname(TRACKER_PATH), { recursive: true });
156+
fs.writeFileSync(TRACKER_PATH, YAML.stringify(output, { lineWidth: 0 }), "utf8");
157+
process.stdout.write(`Synced ${items.length} items to ${TRACKER_PATH}\n`);
158+
}
159+
160+
main();

AGENTS.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@
55

66
Design notes and upstream behavior captures live under [`docs/specs/`](./docs/specs). Before changing approval, trust, sandbox, file-edit, or media-handling behavior, review [`docs/specs/PERMISSIONS.md`](./docs/specs/PERMISSIONS.md) and [`docs/specs/MEDIA.md`](./docs/specs/MEDIA.md).
77

8+
## Project Management
9+
Use the repo-local [`project-manager`](./.agents/skills/project-manager/SKILL.md) skill for GitHub issue and project-board work in this repository.
10+
11+
- Project board: <https://github.com/orgs/pwrdrvr/projects/7>
12+
- Canonical local tracker: `.local/work-items.yaml` (derived; refresh with `pnpm project:sync`)
13+
- Canonical local issue drafts: `.local/issue-drafts/`
14+
- Do not create parallel scratch trackers or alternate temp directories for issue workups.
15+
816
## Build, Test, and Development Commands
917
Use `pnpm` for local work.
1018

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
]
1313
},
1414
"scripts": {
15+
"project:sync": "node ./.agents/skills/project-manager/scripts/sync-work-items.mjs",
1516
"test": "vitest run",
1617
"typecheck": "tsc --noEmit",
1718
"pack:smoke": "node ./scripts/pack-smoke.mjs",
@@ -27,6 +28,7 @@
2728
"devDependencies": {
2829
"@types/node": "^24.6.0",
2930
"typescript": "^5.9.2",
30-
"vitest": "^3.2.4"
31+
"vitest": "^3.2.4",
32+
"yaml": "^2.8.2"
3133
}
3234
}

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)