Skip to content

Commit 9e50c1d

Browse files
authored
Plugin: make project manager skill configurable (#24)
1 parent ea1c417 commit 9e50c1d

File tree

5 files changed

+142
-46
lines changed

5 files changed

+142
-46
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"repo": "pwrdrvr/openclaw-codex-app-server",
3+
"projectOwner": "pwrdrvr",
4+
"projectNumber": 7,
5+
"projectUrl": "https://github.com/orgs/pwrdrvr/projects/7",
6+
"trackerPath": ".local/work-items.yaml",
7+
"issueDraftDir": ".local/issue-drafts",
8+
"localIdPrefix": "ocas-"
9+
}

.agents/skills/project-manager/SKILL.md

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
---
22
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`."
3+
description: "Manage GitHub issues and the GitHub Project board for the current 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 the repo's configured project board, or reconcile GitHub state with `.local/work-items.yaml`."
44
---
55

66
# Project Manager
77

8-
Use this skill for repo-specific project management on [OpenClaw Codex App Server Project](https://github.com/orgs/pwrdrvr/projects/7).
8+
Use this skill for repo-local project management.
99

1010
## Automation Preference
1111

@@ -19,14 +19,21 @@ Use this skill for repo-specific project management on [OpenClaw Codex App Serve
1919
- Treat `.local/work-items.yaml` as a derived repo-local cross-reference map that can be regenerated from the project board.
2020
- Put temporary issue writeups only in `.local/issue-drafts/`.
2121
- Do not create parallel scratch directories or alternate tracker files for the same purpose.
22-
23-
Current repo-specific locations:
24-
25-
- Canonical repo: `pwrdrvr/openclaw-codex-app-server`
26-
- Project board: `https://github.com/orgs/pwrdrvr/projects/7`
27-
- Local tracker: `.local/work-items.yaml`
28-
- Issue drafts: `.local/issue-drafts/`
29-
- Local id prefix: `ocas-`
22+
- Read repo-specific values from `.agents/project-manager.config.json` before taking action.
23+
24+
Expected config shape:
25+
26+
```json
27+
{
28+
"repo": "owner/repo",
29+
"projectOwner": "owner",
30+
"projectNumber": 7,
31+
"projectUrl": "https://github.com/orgs/owner/projects/7",
32+
"trackerPath": ".local/work-items.yaml",
33+
"issueDraftDir": ".local/issue-drafts",
34+
"localIdPrefix": "item-"
35+
}
36+
```
3037

3138
Refresh the derived tracker with:
3239

@@ -51,9 +58,10 @@ pnpm project:sync
5158
- Use `gh issue create`, `gh issue edit`, and `gh issue comment`.
5259
- Keep titles short and imperative, usually starting with `Plugin:`.
5360

54-
4. Add the issue or PR to project `7`.
61+
4. Add the issue or PR to the configured project board.
5562

56-
- Use `gh project item-add 7 --owner pwrdrvr --url <issue-or-pr-url>`.
63+
- Read the configured project number and owner from `.agents/project-manager.config.json`.
64+
- Use `gh project item-add <project-number> --owner <project-owner> --url <issue-or-pr-url>`.
5765
- For issues, set `Status`, `Priority`, `Size`, and `Workflow`.
5866
- For PRs, usually set `Status` and `Workflow`; `Priority` and `Size` are issue-planning fields unless there is a specific reason to set them on the PR item.
5967

@@ -91,17 +99,17 @@ Start by discovering current project field ids instead of assuming they never ch
9199

92100
```bash
93101
gh repo view --json nameWithOwner,url
94-
gh project view 7 --owner pwrdrvr --format json
95-
gh project field-list 7 --owner pwrdrvr --format json
102+
gh project view <project-number> --owner <project-owner> --format json
103+
gh project field-list <project-number> --owner <project-owner> --format json
96104
```
97105

98106
Typical flow:
99107

100108
```bash
101-
gh issue create --repo pwrdrvr/openclaw-codex-app-server --title "<title>" --body-file .local/issue-drafts/<file>.md
102-
gh project item-add 7 --owner pwrdrvr --url <issue-or-pr-url> --format json
109+
gh issue create --repo <owner/repo> --title "<title>" --body-file .local/issue-drafts/<file>.md
110+
gh project item-add <project-number> --owner <project-owner> --url <issue-or-pr-url> --format json
103111
gh project item-edit --project-id <project-id> --id <item-id> --field-id <field-id> --single-select-option-id <option-id>
104-
gh project item-list 7 --owner pwrdrvr --format json
112+
gh project item-list <project-number> --owner <project-owner> --format json
105113
```
106114

107115
Refresh the local tracker:
@@ -112,10 +120,10 @@ pnpm project:sync
112120

113121
## Gotchas
114122

115-
- Verify the repo slug before issue commands. The canonical repo is `pwrdrvr/openclaw-codex-app-server`; older shorthand like `pwrdrvr/openclaw-app-server` is wrong and will make `gh issue ...` fail.
123+
- Verify the repo slug before issue commands. Treat `.agents/project-manager.config.json` as canonical when it is present.
116124
- `gh project item-edit` needs opaque ids for the project, item, field, and single-select option. Always discover them with `gh project view ...` and `gh project field-list ...` instead of assuming cached ids still match.
117125
- GitHub Projects custom views are not well-supported by `gh` or GraphQL mutations. Reading views works, but creating/editing/copying views is still better done in the web UI or browser automation. `gh project copy` does not carry over custom views.
118-
- `.local/work-items.yaml` is currently issue-only. Add PRs to project `7`, but do not expect `pnpm project:sync` to mirror PR items into the local tracker.
126+
- `.local/work-items.yaml` is currently issue-only. Add PRs to the project board, but do not expect `pnpm project:sync` to mirror PR items into the local tracker.
119127
- `.local/issue-drafts/<nn>-<slug>.md` filenames are local scratch ids, not GitHub issue numbers. Keep them stable enough to reuse, but do not try to force them to match the eventual GitHub issue number.
120128

121129
## Tracker Shape
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
interface:
22
display_name: "Project Manager"
33
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."
4+
default_prompt: "Use $project-manager to capture requirements into GitHub issues, keep the configured project board in sync, and update the local .local tracker for this repo."

.agents/skills/project-manager/scripts/sync-work-items.mjs

Lines changed: 104 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,9 @@ import fs from "node:fs";
55
import path from "node:path";
66
import YAML from "yaml";
77

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");
8+
const CONFIG_PATH = path.resolve(".agents/project-manager.config.json");
9+
const DEFAULT_TRACKER_PATH = ".local/work-items.yaml";
10+
const DEFAULT_LOCAL_ID_PREFIX = "item-";
1311

1412
function runGh(args) {
1513
return execFileSync("gh", args, {
@@ -19,12 +17,65 @@ function runGh(args) {
1917
});
2018
}
2119

22-
function loadExistingTracker() {
23-
if (!fs.existsSync(TRACKER_PATH)) {
20+
function readJson(filePath) {
21+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
22+
}
23+
24+
function resolveRepoFromGh() {
25+
const repo = JSON.parse(runGh(["repo", "view", "--json", "nameWithOwner"]));
26+
const nameWithOwner = repo?.nameWithOwner?.trim();
27+
if (!nameWithOwner) {
28+
throw new Error("Could not determine repo from gh repo view.");
29+
}
30+
return nameWithOwner;
31+
}
32+
33+
function loadConfig() {
34+
if (!fs.existsSync(CONFIG_PATH)) {
35+
throw new Error(`Missing project manager config at ${CONFIG_PATH}`);
36+
}
37+
const raw = readJson(CONFIG_PATH);
38+
const repo =
39+
typeof raw.repo === "string" && raw.repo.trim() ? raw.repo.trim() : resolveRepoFromGh();
40+
const projectOwner =
41+
typeof raw.projectOwner === "string" && raw.projectOwner.trim()
42+
? raw.projectOwner.trim()
43+
: repo.split("/")[0];
44+
const projectNumber =
45+
typeof raw.projectNumber === "number" && Number.isFinite(raw.projectNumber)
46+
? raw.projectNumber
47+
: 0;
48+
if (projectNumber <= 0) {
49+
throw new Error(`Invalid projectNumber in ${CONFIG_PATH}`);
50+
}
51+
const trackerPath =
52+
typeof raw.trackerPath === "string" && raw.trackerPath.trim()
53+
? path.resolve(raw.trackerPath.trim())
54+
: path.resolve(DEFAULT_TRACKER_PATH);
55+
const projectUrl =
56+
typeof raw.projectUrl === "string" && raw.projectUrl.trim()
57+
? raw.projectUrl.trim()
58+
: `https://github.com/orgs/${projectOwner}/projects/${projectNumber}`;
59+
const localIdPrefix =
60+
typeof raw.localIdPrefix === "string" && raw.localIdPrefix.trim()
61+
? raw.localIdPrefix.trim()
62+
: DEFAULT_LOCAL_ID_PREFIX;
63+
return {
64+
repo,
65+
projectOwner,
66+
projectNumber,
67+
projectUrl,
68+
trackerPath,
69+
localIdPrefix,
70+
};
71+
}
72+
73+
function loadExistingTracker(trackerPath) {
74+
if (!fs.existsSync(trackerPath)) {
2475
return { version: 1, last_synced_at: null, items: [] };
2576
}
2677

27-
const raw = fs.readFileSync(TRACKER_PATH, "utf8");
78+
const raw = fs.readFileSync(trackerPath, "utf8");
2879
const parsed = YAML.parse(raw) ?? {};
2980
return {
3081
version: parsed.version ?? 1,
@@ -37,27 +88,29 @@ function normalizeNumber(value) {
3788
return typeof value === "number" && Number.isFinite(value) ? value : 0;
3889
}
3990

40-
function nextLocalId(existingItems) {
91+
function nextLocalId(existingItems, localIdPrefix) {
4192
let max = 0;
4293
for (const item of existingItems) {
4394
const match =
44-
typeof item?.local_id === "string" ? item.local_id.match(/^ocas-(\d{4,})$/) : null;
95+
typeof item?.local_id === "string"
96+
? item.local_id.match(new RegExp(`^${localIdPrefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(\\d{4,})$`))
97+
: null;
4598
if (match) {
4699
max = Math.max(max, Number(match[1]));
47100
}
48101
}
49-
return `ocas-${String(max + 1).padStart(4, "0")}`;
102+
return `${localIdPrefix}${String(max + 1).padStart(4, "0")}`;
50103
}
51104

52-
function mergeExistingItem(existingByIssue, issueNumber, fallbackLocalId) {
105+
function mergeExistingItem(existingByIssue, issueNumber, fallbackLocalId, repo) {
53106
const current = existingByIssue.get(issueNumber);
54107
if (current && typeof current === "object" && current !== null) {
55108
return structuredClone(current);
56109
}
57110
return {
58111
local_id: fallbackLocalId,
59112
title: "",
60-
repo: REPO,
113+
repo,
61114
source_note: "",
62115
github: {},
63116
state: {},
@@ -66,7 +119,8 @@ function mergeExistingItem(existingByIssue, issueNumber, fallbackLocalId) {
66119
}
67120

68121
function main() {
69-
const existing = loadExistingTracker();
122+
const config = loadConfig();
123+
const existing = loadExistingTracker(config.trackerPath);
70124
const existingByIssue = new Map();
71125
for (const item of existing.items) {
72126
const issueNumber = normalizeNumber(item?.github?.issue_number);
@@ -76,12 +130,31 @@ function main() {
76130
}
77131

78132
const issueList = JSON.parse(
79-
runGh(["issue", "list", "--repo", REPO, "--state", "all", "--limit", "500", "--json", "number,state,title,url"]),
133+
runGh([
134+
"issue",
135+
"list",
136+
"--repo",
137+
config.repo,
138+
"--state",
139+
"all",
140+
"--limit",
141+
"500",
142+
"--json",
143+
"number,state,title,url",
144+
]),
80145
);
81146
const issueByNumber = new Map(issueList.map((issue) => [issue.number, issue]));
82147

83148
const projectData = JSON.parse(
84-
runGh(["project", "item-list", String(PROJECT_NUMBER), "--owner", PROJECT_OWNER, "--format", "json"]),
149+
runGh([
150+
"project",
151+
"item-list",
152+
String(config.projectNumber),
153+
"--owner",
154+
config.projectOwner,
155+
"--format",
156+
"json",
157+
]),
85158
);
86159

87160
const items = [];
@@ -90,7 +163,7 @@ function main() {
90163
if (content.type !== "Issue") {
91164
continue;
92165
}
93-
if (content.repository !== REPO) {
166+
if (content.repository !== config.repo) {
94167
continue;
95168
}
96169
const issueNumber = normalizeNumber(content.number);
@@ -99,9 +172,14 @@ function main() {
99172
}
100173

101174
const issue = issueByNumber.get(issueNumber);
102-
const merged = mergeExistingItem(existingByIssue, issueNumber, nextLocalId(existing.items));
175+
const merged = mergeExistingItem(
176+
existingByIssue,
177+
issueNumber,
178+
nextLocalId(existing.items, config.localIdPrefix),
179+
config.repo,
180+
);
103181
merged.title = content.title ?? issue?.title ?? merged.title ?? "";
104-
merged.repo = REPO;
182+
merged.repo = config.repo;
105183
merged.source_note = typeof merged.source_note === "string" ? merged.source_note : "";
106184
if (typeof merged.raw_example !== "string") {
107185
delete merged.raw_example;
@@ -110,8 +188,8 @@ function main() {
110188
...(merged.github && typeof merged.github === "object" ? merged.github : {}),
111189
issue_number: issueNumber,
112190
issue_url: content.url ?? issue?.url ?? "",
113-
project_number: PROJECT_NUMBER,
114-
project_url: PROJECT_URL,
191+
project_number: config.projectNumber,
192+
project_url: config.projectUrl,
115193
project_item_id: item.id ?? "",
116194
};
117195
merged.state = {
@@ -137,10 +215,10 @@ function main() {
137215
let counter = 1;
138216
for (const item of items) {
139217
if (typeof item.local_id !== "string" || usedIds.has(item.local_id)) {
140-
while (usedIds.has(`ocas-${String(counter).padStart(4, "0")}`)) {
218+
while (usedIds.has(`${config.localIdPrefix}${String(counter).padStart(4, "0")}`)) {
141219
counter += 1;
142220
}
143-
item.local_id = `ocas-${String(counter).padStart(4, "0")}`;
221+
item.local_id = `${config.localIdPrefix}${String(counter).padStart(4, "0")}`;
144222
counter += 1;
145223
}
146224
usedIds.add(item.local_id);
@@ -152,9 +230,9 @@ function main() {
152230
items,
153231
};
154232

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`);
233+
fs.mkdirSync(path.dirname(config.trackerPath), { recursive: true });
234+
fs.writeFileSync(config.trackerPath, YAML.stringify(output, { lineWidth: 0 }), "utf8");
235+
process.stdout.write(`Synced ${items.length} items to ${config.trackerPath}\n`);
158236
}
159237

160238
main();

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Design notes and upstream behavior captures live under [`docs/specs/`](./docs/sp
88
## Project Management
99
Use the repo-local [`project-manager`](./.agents/skills/project-manager/SKILL.md) skill for GitHub issue and project-board work in this repository.
1010

11+
- Repo/project config: `.agents/project-manager.config.json`
1112
- Project board: <https://github.com/orgs/pwrdrvr/projects/7>
1213
- Canonical local tracker: `.local/work-items.yaml` (derived; refresh with `pnpm project:sync`)
1314
- Canonical local issue drafts: `.local/issue-drafts/`

0 commit comments

Comments
 (0)