Skip to content

Commit 53edaa8

Browse files
committed
feat(governance): add Crystal Projects v2 schema doc + issue import script
- governance/projects.md: canonical schema for Crystal Missions Project v2 (custom fields, 6 canonical views, field assignment rules, intake policy). - scripts/import-issues-to-project.py: import open issues from the 6 Crystal repos into the Project v2. - Self-pin .crystal-governance.yaml bumped to v1.1.0.
1 parent 9331ba2 commit 53edaa8

4 files changed

Lines changed: 232 additions & 5 deletions

File tree

.crystal-governance.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
schema: crystal-governance-pin/v1
2-
governance_version: v1.0.2
2+
governance_version: v1.1.0
33
source: Malakof/.github
44
# Self-pin: Malakof/.github references its own current version.
55
# Lets tooling (sync, governance-check) treat this repo like any other

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
Single source of truth for Crystal team GitHub conventions:
44
labels, issue/PR templates, naming, reusable workflows, agent skill.
55

6-
**Current version**: `v1.0.2` (see `.crystal-governance.yaml`).
6+
**Current version**: `v1.1.0` (see `.crystal-governance.yaml`).
77

88
## For humans
99

@@ -34,7 +34,7 @@ Add to the target repo:
3434
1. `.crystal-governance.yaml`:
3535
```yaml
3636
schema: crystal-governance-pin/v1
37-
governance_version: v1.0.2
37+
governance_version: v1.1.0
3838
source: Malakof/.github
3939
```
4040
@@ -44,7 +44,7 @@ Add to the target repo:
4444
on: [push, pull_request]
4545
jobs:
4646
check:
47-
uses: Malakof/.github/.github/workflows/governance-check.yml@v1.0.2
47+
uses: Malakof/.github/.github/workflows/governance-check.yml@v1.1.0
4848
```
4949

5050
3. `.github/workflows/enforce-conventions.yml`:
@@ -53,7 +53,7 @@ Add to the target repo:
5353
on: [pull_request]
5454
jobs:
5555
enforce:
56-
uses: Malakof/.github/.github/workflows/enforce-conventions.yml@v1.0.2
56+
uses: Malakof/.github/.github/workflows/enforce-conventions.yml@v1.1.0
5757
```
5858

5959
4. Run label sync:

governance/projects.md

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# Crystal Projects v2 — canonical schema and views
2+
3+
The Crystal mission portfolio is materialised as a single GitHub Projects v2
4+
board: **Crystal Missions** at https://github.com/users/Malakof/projects/1.
5+
This document is the canonical reference for the project's custom fields,
6+
canonical views, and policies.
7+
8+
> **Status**: v1.0.2 — schema codified, views to be created via UI per
9+
> §2 below. Auto-sync from paperclip kernel will land in v1.1.0.
10+
11+
## 1. Custom fields
12+
13+
| Field | Type | Values |
14+
|---|---|---|
15+
| Title | text (default) | issue title |
16+
| Status | single-select (default) | Todo, In Progress, Done |
17+
| Mission code | text | `<REPO_PREFIX>-<TYPE>-<NUM>` (see [`README.md §3.1`](./README.md#31-mission-codes)) |
18+
| Priority | single-select | `p0`, `p1`, `p2`, `p3` |
19+
| Type | single-select | `epic`, `feature`, `bug`, `chore`, `docs`, `refactor`, `spike`, `test` |
20+
| Stream | single-select | `atlas`, `beacon`, `forge`, `compass` |
21+
| Runtime | single-select | `claude`, `codex`, `openhands` |
22+
| Release | text | `v1.0.0`, `v1.0-rc1`, `v2.0`, free form |
23+
| Repository | text (default) | auto from issue source |
24+
| Labels | text (default) | mirrored from the issue |
25+
| Linked pull requests | default | auto |
26+
| Milestone | default | auto |
27+
| Parent issue | default | auto |
28+
| Sub-issues progress | default | auto |
29+
30+
Fields named in the canonical taxonomy mirror the labels documented in
31+
[`labels.yaml`](./labels.yaml). When the paperclip kernel projection is
32+
enabled (v1.1.0+), values are written by the kernel and **never edited
33+
manually**.
34+
35+
## 2. Canonical views
36+
37+
These six views are the supported lenses on the project. They are created
38+
manually via the GitHub UI today; v1.1.0 will provide a script to apply
39+
them via GraphQL.
40+
41+
### View 1 — Operations Kanban (`Status`)
42+
43+
- **Layout**: Board
44+
- **Group by**: Status
45+
- **Sort**: Priority ascending (p0 first)
46+
- **Columns shown**: Title, Mission code, Priority, Type, Repository,
47+
Assignees
48+
- **Filter**: `is:open` AND (`Type` is not `epic`)
49+
- **Purpose**: daily ops view of in-flight work, excludes EPICs which
50+
are tracked separately (see View 4).
51+
52+
### View 2 — Priority × Stream (table)
53+
54+
- **Layout**: Table
55+
- **Group by**: Priority
56+
- **Sort**: Stream then Mission code
57+
- **Columns shown**: Title, Mission code, Type, Stream, Runtime, Status,
58+
Repository
59+
- **Filter**: `is:open`
60+
- **Purpose**: portfolio-wide capacity planning.
61+
62+
### View 3 — Release Roadmap
63+
64+
- **Layout**: Roadmap (or Table grouped by Release if Roadmap not available)
65+
- **Group by**: Release
66+
- **Filter**: `Release` is not empty
67+
- **Purpose**: cross-repo release readiness at a glance.
68+
69+
### View 4 — EPIC tree (Hierarchy)
70+
71+
- **Layout**: Table
72+
- **Filter**: `Type` is `epic` OR `Parent issue` is not empty
73+
- **Group by**: Parent issue (if available) else Repository
74+
- **Sort**: Priority
75+
- **Purpose**: track every EPIC and its native sub-issues progress.
76+
77+
### View 5 — Per Runtime (load balance)
78+
79+
- **Layout**: Board
80+
- **Group by**: Runtime
81+
- **Filter**: `is:open` AND `Status` is not `Done`
82+
- **Sort**: Priority then Mission code
83+
- **Purpose**: see what each runtime (Claude, Codex, OpenHands) is
84+
currently working on.
85+
86+
### View 6 — Triage queue
87+
88+
- **Layout**: Table
89+
- **Filter**: label `status:triage` OR labels are empty (no `priority:p*`
90+
AND no `type:*`)
91+
- **Sort**: created date descending
92+
- **Purpose**: catch issues that need product/impl triage.
93+
94+
## 3. Field assignment rules
95+
96+
| Source | Fields written | When |
97+
|---|---|---|
98+
| Issue creator (human) | Title, Type, Priority, Repository (auto) | At creation via ISSUE_TEMPLATE |
99+
| Paperclip kernel (v1.1.0+) | Mission code, Status, Stream, Runtime | On every mission stage transition |
100+
| Dark factory ingestion | Mission code (if not set), Type=epic on parents | At `epic-plan` ingestion |
101+
| GitHub auto | Linked pull requests, Sub-issues progress, Milestone, Parent issue | Continuously |
102+
103+
**Don't edit manually** what the kernel projects (Mission code, Stream,
104+
Runtime, Status when kernel-driven). Manual edits will be overwritten on
105+
the next projection.
106+
107+
## 4. Issue intake into the project
108+
109+
For new repos onboarded after v1.0.2, ensure they are listed in
110+
`scripts/import-issues-to-project.py:REPOS`. New issues created from the
111+
provided ISSUE_TEMPLATE forms are automatically eligible for the project
112+
once added (manually for now via `gh project item-add`, automatically in
113+
v1.1.0).
114+
115+
## 5. Coming in v1.1.0
116+
117+
- Kernel-side projection: paperclip writes to Mission code / Stream /
118+
Runtime / Status on every stage transition.
119+
- GraphQL view definitions: views above codified as JSON, applied via
120+
`scripts/setup-project-views.py`.
121+
- `Crystal status` field added to extend the default `Status` with the
122+
fine-grained `triage`, `ready`, `blocked`, `needs-human` states.
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
#!/usr/bin/env python3
2+
"""Import all open issues from Crystal repos into the Crystal Missions Project v2.
3+
4+
Usage:
5+
python scripts/import-issues-to-project.py [--state open|closed|all] [--dry-run]
6+
"""
7+
8+
from __future__ import annotations
9+
10+
import argparse
11+
import json
12+
import subprocess
13+
import sys
14+
import time
15+
16+
REPOS = [
17+
"Malakof/crystal-dark-factory-poc",
18+
"Malakof/crystal-dark-factory-target-lab",
19+
"Malakof/crystal-discord-bot",
20+
"Malakof/crystal-specs",
21+
"Malakof/crystal-assistant-ui-poc",
22+
"Malakof/crystal-company",
23+
]
24+
25+
PROJECT_NUMBER = 1
26+
PROJECT_OWNER = "Malakof"
27+
28+
29+
def gh_json(args: list[str]) -> object:
30+
result = subprocess.run(["gh"] + args, capture_output=True, text=True, check=False)
31+
if result.returncode != 0:
32+
return {"_error": result.stderr.strip()[:200]}
33+
if not result.stdout.strip():
34+
return None
35+
try:
36+
return json.loads(result.stdout)
37+
except json.JSONDecodeError:
38+
return {"_raw": result.stdout[:200]}
39+
40+
41+
def list_issues(repo: str, state: str) -> list[dict]:
42+
payload = gh_json([
43+
"issue", "list",
44+
"--repo", repo,
45+
"--state", state,
46+
"--json", "number,url,title",
47+
"--limit", "500",
48+
]) or []
49+
return payload if isinstance(payload, list) else []
50+
51+
52+
def add_to_project(url: str, dry_run: bool) -> str:
53+
if dry_run:
54+
return "would-add"
55+
for attempt in range(3):
56+
result = subprocess.run(
57+
["gh", "project", "item-add", str(PROJECT_NUMBER),
58+
"--owner", PROJECT_OWNER, "--url", url],
59+
capture_output=True, text=True, check=False,
60+
)
61+
if result.returncode == 0:
62+
return "added"
63+
err = result.stderr.strip()
64+
if "already in the project" in err.lower() or "exists" in err.lower():
65+
return "already-in-project"
66+
if "504" in err or "502" in err or "Timeout" in err:
67+
time.sleep(1.5 * (attempt + 1))
68+
continue
69+
return f"error:{err.splitlines()[0][:80]}"
70+
return "max-retries"
71+
72+
73+
def main() -> int:
74+
parser = argparse.ArgumentParser()
75+
parser.add_argument("--state", default="open", choices=["open", "closed", "all"])
76+
parser.add_argument("--dry-run", action="store_true")
77+
args = parser.parse_args()
78+
79+
summary: dict[str, dict[str, int]] = {}
80+
total_issues = 0
81+
82+
for repo in REPOS:
83+
issues = list_issues(repo, args.state)
84+
repo_summary = {"total": len(issues), "added": 0, "already": 0, "error": 0, "would": 0}
85+
for issue in issues:
86+
status = add_to_project(issue["url"], args.dry_run)
87+
if status == "added":
88+
repo_summary["added"] += 1
89+
elif status == "already-in-project":
90+
repo_summary["already"] += 1
91+
elif status == "would-add":
92+
repo_summary["would"] += 1
93+
else:
94+
repo_summary["error"] += 1
95+
print(f"[{repo} #{issue['number']}] {status}", file=sys.stderr)
96+
summary[repo] = repo_summary
97+
total_issues += repo_summary["total"]
98+
print(f" {repo}: {repo_summary}")
99+
100+
print(f"\nTotal: {total_issues} issues processed across {len(REPOS)} repos.")
101+
return 0
102+
103+
104+
if __name__ == "__main__":
105+
sys.exit(main())

0 commit comments

Comments
 (0)