@pareto4/pp-core 1.7.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/bin/install.js +116 -0
  3. package/commands/pp-build-handoff.md +46 -0
  4. package/commands/pp-discovery-to-build.md +40 -0
  5. package/gemini-extension.json +1 -1
  6. package/package.json +2 -1
  7. package/pareto-skills/_shared/create-project-board.sh +195 -0
  8. package/pareto-skills/_shared/pp-claude-md-protocol.md +306 -0
  9. package/pareto-skills/_shared/sync-phase-status.sh +139 -0
  10. package/pareto-skills/pp-brand/SKILL.md +644 -0
  11. package/pareto-skills/pp-brand/assets/crew/ParetoAgent.inline.html +120 -0
  12. package/pareto-skills/pp-brand/assets/crew/ParetoAgent.tsx +239 -0
  13. package/pareto-skills/pp-brand/assets/crew/build.svg +11 -0
  14. package/pareto-skills/pp-brand/assets/crew/deploy.svg +11 -0
  15. package/pareto-skills/pp-brand/assets/crew/discovery.svg +11 -0
  16. package/pareto-skills/pp-brand/assets/crew/plan.svg +11 -0
  17. package/pareto-skills/pp-brand/assets/crew/run.svg +11 -0
  18. package/pareto-skills/pp-brand/assets/crew/validate.svg +11 -0
  19. package/pareto-skills/pp-brand/assets/pareto-brand-preview.html +547 -0
  20. package/pareto-skills/pp-obsidian-notes/SKILL.md +127 -0
  21. package/pareto-skills/pp-obsidian-notes/templates/CLAUDE.md +29 -0
  22. package/pareto-skills/pp-obsidian-notes/templates/client-context.md +116 -0
  23. package/pareto-skills/pp-obsidian-notes/templates/meeting-note.md +66 -0
  24. package/pareto-skills/pp-obsidian-notes/templates/pareto-context.md +102 -0
  25. package/pareto-skills/pp-obsidian-notes/templates/proposal.md +117 -0
  26. package/pareto-skills/pp-project-setup/SKILL.md +1364 -0
  27. package/pp-core/bin/lib/intel.cjs +1 -1
  28. package/pp-core/bin/lib/io.cjs +120 -0
  29. package/pp-core/bin/lib/profile-pipeline-command-router.cjs +1 -1
  30. package/pp-core/bin/lib/roadmap-parser.cjs +1 -1
  31. package/pp-core/bin/lib/runtime-artifact-conversion.cjs +2 -2
  32. package/pp-core/bin/lib/surface.cjs +1 -1
  33. package/pp-core/bin/lib/worktree-safety.cjs +2 -2
  34. package/pp-core/bin/pp-tools.cjs +23 -0
  35. package/pp-core/references/ui-brand.md +73 -11
  36. package/pp-core/templates/codebase/structure.md +5 -5
  37. package/pp-core/templates/spec.md +7 -7
  38. package/pp-core/workflows/add-backlog.md +1 -1
  39. package/pp-core/workflows/autonomous.md +4 -4
  40. package/pp-core/workflows/debug.md +1 -1
  41. package/pp-core/workflows/discovery-to-build.md +598 -0
  42. package/pp-core/workflows/execute-phase.md +9 -1
  43. package/pp-core/workflows/plan-phase.md +14 -12
  44. package/pp-core/workflows/pr-branch.md +5 -4
  45. package/pp-core/workflows/reapply-patches.md +1 -1
  46. package/pp-core/workflows/ship.md +1 -1
  47. package/pp-core/workflows/thread.md +1 -1
  48. package/pp-core/workflows/verify-work.md +6 -5
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pp-core",
3
3
  "displayName": "PP Core",
4
- "version": "1.7.0",
4
+ "version": "1.8.0",
5
5
  "description": "PP Core is a meta-prompting, context engineering, and spec-driven development system for AI coding agents.",
6
6
  "author": {
7
7
  "name": "pareto4",
package/bin/install.js CHANGED
@@ -7387,6 +7387,120 @@ function installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile) {
7387
7387
  const nestedGsdDirForCleanup = path.join(configDir, 'skills', 'gsd');
7388
7388
  _removeHermesBareStemDirs(nestedGsdDirForCleanup);
7389
7389
  }
7390
+
7391
+ // Verbatim Pareto skill payload — MUST be the LAST step (post-prune ordering
7392
+ // is load-bearing, Pitfall 1). The skills-kind loop above runs _removeGsdEntries,
7393
+ // which unconditionally deletes EVERY pp-* skill dir; running the verbatim copy
7394
+ // here (delete-then-recopy) makes the payload self-healing across repeated
7395
+ // `npx` installs. Copies are byte-for-byte with NO content-rewrite pass (D-03).
7396
+ installVerbatimSkills(runtime, configDir, scope);
7397
+ }
7398
+
7399
+ /**
7400
+ * Locate the bundled `pareto-skills/` source directory.
7401
+ *
7402
+ * Walks up from `__dirname` (up to 6 ancestors) looking for the sentinel
7403
+ * `pareto-skills/pp-brand/SKILL.md`, mirroring the source-root walk-up the
7404
+ * installer already uses to anchor `commands/`. Returns the absolute path to
7405
+ * the `pareto-skills/` directory. Throws a clear error if it cannot be found.
7406
+ *
7407
+ * @returns {string} absolute path to the bundled `pareto-skills/` directory
7408
+ */
7409
+ function findParetoSkillsSrc() {
7410
+ let dir = __dirname;
7411
+ for (let i = 0; i < 6; i += 1) {
7412
+ const candidate = path.join(dir, 'pareto-skills');
7413
+ if (fs.existsSync(path.join(candidate, 'pp-brand', 'SKILL.md'))) return candidate;
7414
+ const parent = path.dirname(dir);
7415
+ if (parent === dir) break;
7416
+ dir = parent;
7417
+ }
7418
+ throw new Error(`findParetoSkillsSrc: could not locate pareto-skills/ from ${__dirname}`);
7419
+ }
7420
+
7421
+ /**
7422
+ * Place the verbatim Pareto skill payload into a runtime's resolved skills dir.
7423
+ *
7424
+ * This is the "second channel" for shipping skills: unlike command-derived
7425
+ * skills (generated from commands/pp-*.md via the converter), these three
7426
+ * `pp-*` skill dirs — plus a peer `_shared/` — are copied byte-for-byte with
7427
+ * NO content-rewrite pass (D-03/D-07). That keeps them out of docs/COMMANDS.md
7428
+ * and prevents the rewrite walker from corrupting templates or the protocol
7429
+ * file (Pitfall 3). The payload is binary-safe (Crew assets), so we use a plain
7430
+ * recursive fs.cpSync — never applyRuntimeContentRewritesInPlace,
7431
+ * stageSkillsForRuntimeAsSkills, or the skillsKind converter.
7432
+ *
7433
+ * The destination is resolved from the artifact layout's skills-kind
7434
+ * destSubpath (never a hardcoded `skills/`), so Hermes' nested `skills/gsd/`
7435
+ * base is honoured automatically. Runtimes with no skills kind (gemini has only
7436
+ * a commands kind; grok has no skills-layout descriptor) are skipped gracefully
7437
+ * with an early `return 0` — never a throw (Pitfall 5).
7438
+ *
7439
+ * The skill names are a FIXED, package-owned allowlist — never interpolated
7440
+ * input (V5 / T-16-03). No manifest entry is added, so `/pp-surface` pruning
7441
+ * leaves these non-manifest pp-* dirs untouched (Pitfall 2). `_shared/*.sh`
7442
+ * regain their exec bit after copy, because a content-level cpSync does not
7443
+ * reliably preserve the exec bit cross-platform.
7444
+ *
7445
+ * @param {string} runtime - target runtime id
7446
+ * @param {string} configDir - resolved runtime config directory
7447
+ * @param {string} scope - 'local' | 'global'
7448
+ * @returns {number} number of skill dirs placed (0 if the runtime has no skills kind)
7449
+ */
7450
+ function installVerbatimSkills(runtime, configDir, scope) {
7451
+ // Resolve the runtime's artifact layout. Two graceful-skip cases (Pitfall 5,
7452
+ // no throw): gemini's layout has only a commands kind (skillsKindEntry
7453
+ // undefined below); grok is not in the layout registry at all, so
7454
+ // resolveRuntimeArtifactLayout throws 'Unknown runtime' — treat that as
7455
+ // "no skills surface" and skip. In the production call path
7456
+ // installRuntimeArtifacts has already resolved the layout, but this function
7457
+ // is exported and called standalone by test harnesses (Plan 16-03), so the
7458
+ // guard lives here too.
7459
+ let layout;
7460
+ try {
7461
+ layout = resolveRuntimeArtifactLayout(runtime, configDir, scope);
7462
+ } catch {
7463
+ return 0;
7464
+ }
7465
+ const skillsKindEntry = layout.kinds.find((k) => k.kind === 'skills');
7466
+ // gemini (commands-only): no skills surface — skip, don't throw.
7467
+ if (!skillsKindEntry) return 0;
7468
+
7469
+ const src = findParetoSkillsSrc();
7470
+ // destSubpath already encodes Hermes' nested `skills/gsd` — never hardcode `skills/`.
7471
+ const skillsBase = path.join(configDir, skillsKindEntry.destSubpath);
7472
+ fs.mkdirSync(skillsBase, { recursive: true });
7473
+
7474
+ // FIXED, package-owned allowlist — never interpolated input (V5 / T-16-03).
7475
+ const BUNDLED = ['pp-brand', 'pp-project-setup', 'pp-obsidian-notes'];
7476
+ let placed = 0;
7477
+ for (const name of BUNDLED) {
7478
+ const skillDest = path.join(skillsBase, name);
7479
+ // Plain recursive, binary-safe copy — NO content-rewrite pass (D-03/Pitfall 3).
7480
+ fs.cpSync(path.join(src, name), skillDest, { recursive: true });
7481
+ if (verifyInstalled(skillDest, `skill ${name}`)) {
7482
+ placed += 1;
7483
+ }
7484
+ }
7485
+
7486
+ // Copy _shared/ as a PEER of the skill dirs so `../_shared/` resolves from any
7487
+ // skill body (D-04). Verbatim copy, no rewrite pass.
7488
+ const sharedDest = path.join(skillsBase, '_shared');
7489
+ fs.cpSync(path.join(src, '_shared'), sharedDest, { recursive: true });
7490
+ verifyInstalled(sharedDest, 'skill _shared');
7491
+
7492
+ // Restore the exec bit on bundled *.sh — cpSync content copy does not reliably
7493
+ // preserve it cross-platform (PATTERNS "No Analog Found").
7494
+ try {
7495
+ for (const entry of fs.readdirSync(sharedDest)) {
7496
+ if (entry.endsWith('.sh')) {
7497
+ fs.chmodSync(path.join(sharedDest, entry), 0o755);
7498
+ }
7499
+ }
7500
+ } catch { /* best-effort: non-fatal if _shared has no scripts */ }
7501
+
7502
+ console.log(` ${green}✓${reset} Placed ${placed} verbatim Pareto skill(s) + _shared into ${skillsKindEntry.destSubpath}`);
7503
+ return placed;
7390
7504
  }
7391
7505
 
7392
7506
  /**
@@ -12309,6 +12423,8 @@ module.exports = {
12309
12423
  readGsdCommandNames,
12310
12424
  installRuntimeArtifacts,
12311
12425
  installOpencodeFamilySkills,
12426
+ installVerbatimSkills,
12427
+ findParetoSkillsSrc,
12312
12428
  uninstallRuntimeArtifacts,
12313
12429
  parseConfigDirFromArgs,
12314
12430
  cleanupLegacyGsdCc,
@@ -0,0 +1,46 @@
1
+ ---
2
+ name: pp:build-handoff
3
+ description: "[DEPRECATED] Alias for /pp-discovery-to-build — transition a completed Pareto Discovery Sprint into a pp-core-ready project. Use /pp-discovery-to-build; this alias will be removed in a future major."
4
+ argument-hint: "[--skip-validation] [--scaffold]"
5
+ allowed-tools:
6
+ - Read
7
+ - Write
8
+ - Edit
9
+ - Bash
10
+ - Glob
11
+ - Grep
12
+ - Agent
13
+ - AskUserQuestion
14
+ requires: [new-project]
15
+ ---
16
+
17
+ <runtime_note>
18
+ **Copilot (VS Code):** Use `vscode_askquestions` wherever this workflow calls `AskUserQuestion`. They are equivalent — `vscode_askquestions` is the VS Code Copilot implementation of the same interactive question API.
19
+ </runtime_note>
20
+
21
+ <deprecation_note>
22
+ **Deprecated:** `/pp-build-handoff` was renamed to `/pp-discovery-to-build` in v1.8.0. This
23
+ alias runs the identical workflow and is kept for back-compat with v1.7.x. Prefer
24
+ `/pp-discovery-to-build`; this alias will be removed in a future major release.
25
+ </deprecation_note>
26
+
27
+ <objective>
28
+ Bridge the gap between a completed Pareto Discovery Sprint and pp-core execution.
29
+
30
+ Validates all 13 Discovery sessions are complete, synthesizes their outputs into a
31
+ single structured handoff document, bootstraps the `.planning/` project state
32
+ (PROJECT.md, REQUIREMENTS.md, ROADMAP.md, STATE.md, config.json), and wires up
33
+ cross-references so pp-core agents can trace every decision back to the Discovery
34
+ session that produced it.
35
+
36
+ **After this command:** pp-core is initialized. Run `/pp-plan-phase 1` to start
37
+ execution, or `/pp-autonomous` to run all phases.
38
+ </objective>
39
+
40
+ <execution_context>
41
+ @~/.claude/pp-core/workflows/discovery-to-build.md
42
+ </execution_context>
43
+
44
+ <process>
45
+ Execute end-to-end.
46
+ </process>
@@ -0,0 +1,40 @@
1
+ ---
2
+ name: pp:discovery-to-build
3
+ description: Transition a completed Pareto Discovery Sprint into a pp-core-ready project — synthesize sessions into .planning/ and wire cross-references.
4
+ argument-hint: "[--skip-validation] [--scaffold]"
5
+ allowed-tools:
6
+ - Read
7
+ - Write
8
+ - Edit
9
+ - Bash
10
+ - Glob
11
+ - Grep
12
+ - Agent
13
+ - AskUserQuestion
14
+ requires: [new-project]
15
+ ---
16
+
17
+ <runtime_note>
18
+ **Copilot (VS Code):** Use `vscode_askquestions` wherever this workflow calls `AskUserQuestion`. They are equivalent — `vscode_askquestions` is the VS Code Copilot implementation of the same interactive question API.
19
+ </runtime_note>
20
+
21
+ <objective>
22
+ Bridge the gap between a completed Pareto Discovery Sprint and pp-core execution.
23
+
24
+ Validates all 13 Discovery sessions are complete, synthesizes their outputs into a
25
+ single structured handoff document, bootstraps the `.planning/` project state
26
+ (PROJECT.md, REQUIREMENTS.md, ROADMAP.md, STATE.md, config.json), and wires up
27
+ cross-references so pp-core agents can trace every decision back to the Discovery
28
+ session that produced it.
29
+
30
+ **After this command:** pp-core is initialized. Run `/pp-plan-phase 1` to start
31
+ execution, or `/pp-autonomous` to run all phases.
32
+ </objective>
33
+
34
+ <execution_context>
35
+ @~/.claude/pp-core/workflows/discovery-to-build.md
36
+ </execution_context>
37
+
38
+ <process>
39
+ Execute end-to-end.
40
+ </process>
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pp-core",
3
- "version": "1.7.0",
3
+ "version": "1.8.0",
4
4
  "description": "PP Core — a meta-prompting, context engineering, and spec-driven development system for AI coding agents. Loads pp-core's operating context into every Gemini CLI session.",
5
5
  "contextFileName": "GEMINI.md"
6
6
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pareto4/pp-core",
3
- "version": "1.7.0",
3
+ "version": "1.8.0",
4
4
  "description": "Pareto pp-core — The Pareto Project's AI-assisted engineering framework: meta-prompting, context engineering, and spec-driven development for AI coding agents.",
5
5
  "bin": {
6
6
  "pp-core": "bin/install.js",
@@ -13,6 +13,7 @@
13
13
  "pp-core",
14
14
  "assets",
15
15
  "agents",
16
+ "pareto-skills",
16
17
  ".claude-plugin",
17
18
  "gemini-extension.json",
18
19
  "GEMINI.md",
@@ -0,0 +1,195 @@
1
+ #!/usr/bin/env bash
2
+ # create-project-board.sh
3
+ #
4
+ # Canonical GitHub Project v2 board creation for Pareto skills.
5
+ # Creates (or updates) a project board for a given owner/repo with the
6
+ # standard Pareto status columns, custom fields, and label palette.
7
+ #
8
+ # Used by:
9
+ # - pareto-project-setup (initial scaffold)
10
+ # - pareto-build-handoff (Discovery → pp-core handoff)
11
+ # - pareto-push-issues (standalone requirements push)
12
+ #
13
+ # Single source of truth — edit here, not in the skills.
14
+ #
15
+ # Usage:
16
+ # source _shared/create-project-board.sh
17
+ # create_project_board "<owner>" "<project_title>" "<repo_slug>"
18
+ #
19
+ # Example:
20
+ # create_project_board "pareto-project" "my-app" "pareto-project/my-app"
21
+ #
22
+ # Environment variables honored:
23
+ # BOARD_OWNER_FALLBACK (default: "@me") — used if org create fails
24
+ #
25
+ # Returns (via stdout, space-separated): <board_number> <owner>
26
+ # Exits non-zero on fatal error.
27
+
28
+ set -euo pipefail
29
+
30
+ # -----------------------------------------------------------------------------
31
+ # Canonical column schema
32
+ # -----------------------------------------------------------------------------
33
+ # Any change here propagates to every skill that sources this file.
34
+ #
35
+ # Status flow:
36
+ # Backlog → Discuss → Planned → In Progress → Verification → PR Raised → Done
37
+ #
38
+ # Each column maps 1:1 to a pp-core workflow step:
39
+ # Discuss = /pp-discuss-phase (CONTEXT.md written)
40
+ # Planned = /pp-plan-phase (PLAN.md created)
41
+ # In Progress = /pp-execute-phase (branch active, executing)
42
+ # Verification = VERIFICATION.md written, tests passing
43
+ # PR Raised = PR created from phase branch, awaiting merge
44
+ # Done = PR merged, issue closed
45
+ #
46
+ PARETO_STATUS_OPTIONS='[
47
+ {name: "Backlog", color: GRAY, description: "Not yet started"},
48
+ {name: "Discuss", color: PINK, description: "Context gathering via /pp-discuss-phase"},
49
+ {name: "Planned", color: BLUE, description: "Plans created via /pp-plan-phase"},
50
+ {name: "In Progress", color: YELLOW, description: "Executing via /pp-execute-phase"},
51
+ {name: "Verification", color: ORANGE, description: "Verification passed — VERIFICATION.md written"},
52
+ {name: "PR Raised", color: PURPLE, description: "PR created from phase branch, awaiting merge"},
53
+ {name: "Done", color: GREEN, description: "PR merged, issue closed"}
54
+ ]'
55
+
56
+ # Label color palette for per-EPIC labels. Cycle through these in order.
57
+ PARETO_EPIC_COLORS=(
58
+ "0E8A16" "1AA393" "D93F0B" "B44EFF"
59
+ "4A9EFF" "EF4444" "F5C842" "8B5CF6"
60
+ )
61
+
62
+ # -----------------------------------------------------------------------------
63
+ # create_project_board <owner> <title> <owner/repo>
64
+ # -----------------------------------------------------------------------------
65
+ create_project_board() {
66
+ local OWNER="$1"
67
+ local TITLE="$2"
68
+ local REPO="$3"
69
+ local BOARD_NUM
70
+
71
+ # 1. Check for existing board matching title
72
+ BOARD_NUM=$(gh project list --owner "$OWNER" --format json 2>/dev/null \
73
+ | jq -r ".projects[] | select(.title==\"$TITLE\") | .number" || true)
74
+
75
+ if [ -z "$BOARD_NUM" ]; then
76
+ # 2. Try to create under the given owner
77
+ BOARD_NUM=$(gh project create --owner "$OWNER" --title "$TITLE" \
78
+ --format json 2>/dev/null | jq -r '.number' || true)
79
+
80
+ # 3. Fall back to personal projects if org creation failed
81
+ if [ -z "$BOARD_NUM" ]; then
82
+ OWNER="${BOARD_OWNER_FALLBACK:-@me}"
83
+ BOARD_NUM=$(gh project create --owner "$OWNER" --title "$TITLE" \
84
+ --format json | jq -r '.number')
85
+ fi
86
+
87
+ # 4. Link board to repo
88
+ gh project link "$BOARD_NUM" --owner "$OWNER" --repo "$REPO" >/dev/null
89
+ fi
90
+
91
+ # 5. Configure Status field with Pareto schema (idempotent — safe to re-run)
92
+ configure_status_field "$BOARD_NUM" "$OWNER"
93
+
94
+ # 6. Ensure custom fields exist (Priority, Sprint, Estimate, Phase)
95
+ ensure_custom_fields "$BOARD_NUM" "$OWNER"
96
+
97
+ echo "$BOARD_NUM $OWNER"
98
+ }
99
+
100
+ # -----------------------------------------------------------------------------
101
+ # configure_status_field <board_num> <owner>
102
+ # -----------------------------------------------------------------------------
103
+ configure_status_field() {
104
+ local BOARD_NUM="$1"
105
+ local OWNER="$2"
106
+ local STATUS_FIELD_ID
107
+
108
+ STATUS_FIELD_ID=$(gh project field-list "$BOARD_NUM" --owner "$OWNER" --format json \
109
+ --jq '.fields[] | select(.name=="Status") | .id')
110
+
111
+ if [ -z "$STATUS_FIELD_ID" ]; then
112
+ echo "ERROR: Status field not found on board $BOARD_NUM" >&2
113
+ return 1
114
+ fi
115
+
116
+ gh api graphql -f query="
117
+ mutation {
118
+ updateProjectV2Field(input: {
119
+ fieldId: \"$STATUS_FIELD_ID\"
120
+ singleSelectOptions: $PARETO_STATUS_OPTIONS
121
+ }) {
122
+ projectV2Field { ... on ProjectV2SingleSelectField { id name } }
123
+ }
124
+ }
125
+ " >/dev/null
126
+ }
127
+
128
+ # -----------------------------------------------------------------------------
129
+ # ensure_custom_fields <board_num> <owner>
130
+ # -----------------------------------------------------------------------------
131
+ ensure_custom_fields() {
132
+ local BOARD_NUM="$1"
133
+ local OWNER="$2"
134
+
135
+ # Priority (single-select)
136
+ gh project field-create "$BOARD_NUM" --owner "$OWNER" \
137
+ --name "Priority" --data-type "SINGLE_SELECT" \
138
+ --single-select-options "Must Have,Should Have,Could Have,Won't Have" \
139
+ 2>/dev/null || true
140
+
141
+ # Sprint (iteration — created manually in UI if needed)
142
+ gh project field-create "$BOARD_NUM" --owner "$OWNER" \
143
+ --name "Sprint" --data-type "ITERATION" 2>/dev/null || true
144
+
145
+ # Estimate (number)
146
+ gh project field-create "$BOARD_NUM" --owner "$OWNER" \
147
+ --name "Estimate" --data-type "NUMBER" 2>/dev/null || true
148
+
149
+ # Phase (text — pp-core phase number reference, e.g. "3" or "3.1")
150
+ gh project field-create "$BOARD_NUM" --owner "$OWNER" \
151
+ --name "Phase" --data-type "TEXT" 2>/dev/null || true
152
+ }
153
+
154
+ # -----------------------------------------------------------------------------
155
+ # ensure_core_labels <owner/repo>
156
+ # -----------------------------------------------------------------------------
157
+ # Idempotent — uses --force so existing labels get updated.
158
+ ensure_core_labels() {
159
+ local REPO="$1"
160
+
161
+ gh label create "epic" -c "3E4B9E" -d "Epic — high-level feature group" --force -R "$REPO"
162
+ gh label create "user-story" -c "1D76DB" -d "User story" --force -R "$REPO"
163
+ gh label create "must-have" -c "B60205" -d "Must Have — MVP required" --force -R "$REPO"
164
+ gh label create "should-have" -c "FBCA04" -d "Should Have — important" --force -R "$REPO"
165
+ gh label create "could-have" -c "0E8A16" -d "Could Have — nice to have" --force -R "$REPO"
166
+ gh label create "in-progress" -c "FBCA04" -d "Phase branch actively executing" --force -R "$REPO"
167
+ gh label create "verification" -c "D93F0B" -d "In pp-core verification + PR review" --force -R "$REPO"
168
+ }
169
+
170
+ # -----------------------------------------------------------------------------
171
+ # create_epic_label <owner/repo> <slug> <epic_id> <epic_title> <index>
172
+ # -----------------------------------------------------------------------------
173
+ # Color cycles through PARETO_EPIC_COLORS by index.
174
+ create_epic_label() {
175
+ local REPO="$1"
176
+ local SLUG="$2"
177
+ local EPIC_ID="$3"
178
+ local TITLE="$4"
179
+ local IDX="$5"
180
+ local COLOR="${PARETO_EPIC_COLORS[$((IDX % ${#PARETO_EPIC_COLORS[@]}))]}"
181
+
182
+ gh label create "epic:${SLUG}" -c "$COLOR" -d "${EPIC_ID}: ${TITLE}" --force -R "$REPO"
183
+ }
184
+
185
+ # -----------------------------------------------------------------------------
186
+ # If sourced, functions above are exported to the caller. If executed directly,
187
+ # run create_project_board with positional args.
188
+ # -----------------------------------------------------------------------------
189
+ if [ "${BASH_SOURCE[0]}" = "$0" ]; then
190
+ if [ $# -lt 3 ]; then
191
+ echo "Usage: $0 <owner> <title> <owner/repo>" >&2
192
+ exit 1
193
+ fi
194
+ create_project_board "$@"
195
+ fi