bigpowers 2.73.3 → 2.73.4

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.
package/.pi/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bigpowers",
3
- "version": "2.73.3",
3
+ "version": "2.73.4",
4
4
  "description": "75 skills — 73 agent skills synthesizing 17 years of software engineering discipline into a prescriptive methodology for solo developers",
5
5
  "keywords": [
6
6
  "pi-package"
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: "Deterministic traceability quality gate — reads coverage matrix + blind-spot data, applies decision rules with oracle confidence downgrade, emits PASS/CONCERNS/FAIL/WAIVED verdict. Use before release-branch to gate merges on traceability." # story: e38s06
2
+ description: "Deterministic traceability quality gate — reads coverage matrix + blind-spot data, applies decision rules with oracle confidence downgrade, emits PASS/CONCERNS/FAIL/WAIVED verdict. Use before release-branch to gate merges on traceability."
3
3
  ---
4
4
 
5
5
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: "Agent-maintained OKF wiki — INGEST source docs, LINT for issues, QUERY across concept pages. Run as part of build-epic Step 8 and verify-work Phase 3." # story: e39s07
2
+ description: "Agent-maintained OKF wiki — INGEST source docs, LINT for issues, QUERY across concept pages. Run as part of build-epic Step 8 and verify-work Phase 3."
3
3
  ---
4
4
 
5
5
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: > AI-powered security analysis of code changes — traces data flow, detects injection, auth bypass, secrets exposure, and unsafe deserialization across files. Use when reviewing pending changes, before release-branch, during verify-work Phase 5, during build-epic Step 0 threat modeling, or when the user says "security review" or "scan for vulns".
2
+ description: >
3
3
  ---
4
4
 
5
5
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: gate-trace
3
- description: "\"Deterministic traceability quality gate — reads coverage matrix + blind-spot data, applies decision rules with oracle confidence downgrade, emits PASS/CONCERNS/FAIL/WAIVED verdict. Use before release-branch to gate merges on traceability.\" # story: e38s06"
3
+ description: "\"Deterministic traceability quality gate — reads coverage matrix + blind-spot data, applies decision rules with oracle confidence downgrade, emits PASS/CONCERNS/FAIL/WAIVED verdict. Use before release-branch to gate merges on traceability.\""
4
4
  model: haiku
5
5
  ---
6
6
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: maintain-wiki
3
- description: "\"Agent-maintained OKF wiki — INGEST source docs, LINT for issues, QUERY across concept pages. Run as part of build-epic Step 8 and verify-work Phase 3.\" # story: e39s07"
3
+ description: "\"Agent-maintained OKF wiki — INGEST source docs, LINT for issues, QUERY across concept pages. Run as part of build-epic Step 8 and verify-work Phase 3.\""
4
4
  model: haiku
5
5
  ---
6
6
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: security-review
3
- description: "> AI-powered security analysis of code changes — traces data flow, detects injection, auth bypass, secrets exposure, and unsafe deserialization across files. Use when reviewing pending changes, before release-branch, during verify-work Phase 5, during build-epic Step 0 threat modeling, or when the user says \"security review\" or \"scan for vulns\"."
3
+ description: ">"
4
4
  model: sonnet
5
5
  ---
6
6
 
package/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ ## [2.73.4](https://github.com/danielvm-git/bigpowers/compare/v2.73.3...v2.73.4) (2026-07-06)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **sync:** split sync-skills.sh into lib modules under 300-line cap ([#59](https://github.com/danielvm-git/bigpowers/issues/59)) ([eed704f](https://github.com/danielvm-git/bigpowers/commit/eed704fe87a9f89dec211bbc6c6695afee884e52))
7
+
1
8
  ## [2.73.3](https://github.com/danielvm-git/bigpowers/compare/v2.73.2...v2.73.3) (2026-07-06)
2
9
 
3
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bigpowers",
3
- "version": "2.73.3",
3
+ "version": "2.73.4",
4
4
  "description": "73 agent skills synthesizing 17 years of software engineering discipline into a prescriptive methodology for solo developers",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -91,6 +91,27 @@ parse_frontmatter() {
91
91
  export SKILL_NAME SKILL_MODEL SKILL_DESC _PF_NAME _PF_MODEL _PF_DESCRIPTION
92
92
  }
93
93
 
94
+ # parse_frontmatter_okf — like parse_frontmatter but also extracts effort field
95
+ parse_frontmatter_okf() {
96
+ local file="$1"
97
+ _PF_EFFORT=""
98
+
99
+ if [[ ! -f "$file" ]] || ! grep -q '^---$' "$file"; then
100
+ return 1
101
+ fi
102
+
103
+ _PF_NAME=$(awk '/^---/{f++} f==1 && /^name:/{print; exit}' "$file" | sed 's/^name:[[:space:]]*//')
104
+ _PF_MODEL=$(awk '/^---/{f++} f==1 && /^model:/{print; exit}' "$file" | sed 's/^model:[[:space:]]*//')
105
+ _PF_EFFORT=$(awk '/^---/{f++} f==1 && /^effort:/{print; exit}' "$file" | sed 's/^effort:[[:space:]]*//')
106
+ _PF_DESCRIPTION=$(awk '/^---/{f++; next} f==1 && /^description:/{p=1; sub(/^description:[[:space:]]*/,""); print; next} f==1 && p && /^[a-z]+:/{exit} f==1 && p{print}' "$file" \
107
+ | tr -d '\n' \
108
+ | sed -E 's/[[:space:]]+/ /g')
109
+
110
+ [[ -z "$_PF_NAME" ]] && return 1
111
+ export _PF_NAME _PF_MODEL _PF_EFFORT _PF_DESCRIPTION
112
+ return 0
113
+ }
114
+
94
115
  # iterate_skills — list skill directories containing SKILL.md, stable sorted order
95
116
  iterate_skills() {
96
117
  resolve_repo_root 2>/dev/null || true
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env bash
2
+ # story: e13s03 e28s03
3
+ # Post-sync pipeline for sync-skills.sh — manifests, lockfiles, guards, OKF wikis.
4
+ # Args: $1=skill_count $2=opencode_count (optional, default 0)
5
+
6
+ if [ -n "${SYNC_POST_LOADED:-}" ]; then
7
+ return 0
8
+ fi
9
+ SYNC_POST_LOADED=1
10
+
11
+ sync_post_run() {
12
+ local skill_count="${1:-0}"
13
+ local opencode_count="${2:-0}"
14
+ local pkg_version pkg_desc readme readme_tmp trace_mdc manifest ext_ver pkg_ver validate_script
15
+
16
+ pkg_version=$(jq -r '.version' "$REPO_ROOT/package.json")
17
+ pkg_desc=$(jq -r '.description' "$REPO_ROOT/package.json")
18
+
19
+ jq -n --arg name "bigpowers" \
20
+ --arg version "$pkg_version" \
21
+ --arg desc "${skill_count} skills — ${pkg_desc}" \
22
+ '{name: $name, version: $version, description: $desc}' > "$GEMINI_MANIFEST"
23
+
24
+ jq -n --arg version "$pkg_version" \
25
+ --arg desc "${skill_count} skills — ${pkg_desc}" \
26
+ '{
27
+ "name": "bigpowers",
28
+ "version": $version,
29
+ "description": $desc,
30
+ "keywords": ["pi-package"],
31
+ "pi": {
32
+ "skills": ["./skills"],
33
+ "prompts": ["./prompts"]
34
+ }
35
+ }' > "$PI_PACKAGE_JSON"
36
+
37
+ {
38
+ echo "{"
39
+ echo " \"\$schema\": \"https://opencode.ai/config.json\","
40
+ echo " \"instructions\": [\"CLAUDE.md\", \"CONVENTIONS.md\"]"
41
+ echo "}"
42
+ } > "$REPO_ROOT/opencode.json"
43
+
44
+ if [[ -x "$REPO_ROOT/scripts/regenerate-lockfile.sh" ]]; then
45
+ bash "$REPO_ROOT/scripts/regenerate-lockfile.sh" || {
46
+ echo "sync-skills: FAIL — lockfile regeneration failed" >&2
47
+ exit 1
48
+ }
49
+ fi
50
+
51
+ if [[ -x "$REPO_ROOT/scripts/generate-skill-index.sh" ]]; then
52
+ bash "$REPO_ROOT/scripts/generate-skill-index.sh" || {
53
+ echo "sync-skills: FAIL — SKILL-INDEX.md generation failed" >&2
54
+ exit 1
55
+ }
56
+ fi
57
+
58
+ if [[ -x "$REPO_ROOT/scripts/build-skill-index.sh" ]]; then
59
+ bash "$REPO_ROOT/scripts/build-skill-index.sh" || true
60
+ fi
61
+
62
+ local CURSOR_KEEP="context7.mdc" mdc mdc_base
63
+ for mdc in "$CURSOR_RULES"/*.mdc; do
64
+ [[ -e "$mdc" ]] || continue
65
+ mdc_base=$(basename "$mdc")
66
+ case " $CURSOR_KEEP " in *" $mdc_base "*) continue ;; esac
67
+ if [[ ! -f "$SKILLS_ROOT/${mdc_base%.mdc}/SKILL.md" ]]; then
68
+ rm "$mdc"
69
+ echo " → pruned orphan cursor rule: $mdc_base"
70
+ fi
71
+ done
72
+
73
+ readme="$REPO_ROOT/README.md"
74
+ if [[ -f "$readme" ]] && grep -q 'badge/skills-' "$readme"; then
75
+ readme_tmp=$(mktemp)
76
+ sed -E "s|(badge/skills-)[0-9]+(-brightgreen)|\1${skill_count}\2|" "$readme" > "$readme_tmp"
77
+ mv "$readme_tmp" "$readme"
78
+ fi
79
+
80
+ echo "sync-skills: $skill_count skills synced"
81
+ echo " → .cursor/rules/ ($skill_count .mdc files)"
82
+ echo " → .gemini/extensions/bigpowers/skills/ (Agent Skills)"
83
+ echo " → .gemini/extensions/bigpowers/commands/ (Slash Commands)"
84
+ echo " → .gemini/extensions/bigpowers/gemini-extension.json"
85
+ echo " → .pi/skills/ ($skill_count skill dirs — pi Agent Skills)"
86
+ echo " → .pi/prompts/ ($skill_count prompt templates — pi slash commands)"
87
+ echo " → .pi/package.json (pi package manifest)"
88
+ echo " → skills-lock.json (catalog with SHA-256 hashes)"
89
+ echo " → SKILL-INDEX.md (auto-generated skill reference)"
90
+ echo " → opencode.json (CLAUDE.md + CONVENTIONS.md instructions)"
91
+ [[ -n "${OPN_TARGET:-}" ]] && echo " → bigpowers-opencode: $opencode_count skills"
92
+
93
+ sync_post_regression_guards
94
+
95
+ if [[ -d "$REPO_ROOT/.git" ]]; then
96
+ bash "$REPO_ROOT/scripts/generate-reference-tables.sh"
97
+ fi
98
+
99
+ echo ""
100
+ echo "Generating OKF wikis..."
101
+ bash "$REPO_ROOT/scripts/generate-epics-wiki.sh"
102
+ bash "$REPO_ROOT/scripts/generate-adr-wiki.sh"
103
+ echo "sync-skills: OKF wikis regenerated"
104
+ }
105
+
106
+ sync_post_regression_guards() {
107
+ local trace_mdc manifest ext_ver pkg_ver validate_script
108
+
109
+ trace_mdc="$REPO_ROOT/.cursor/rules/trace-requirement.mdc"
110
+ if [[ -f "$trace_mdc" ]] && ! grep -q 'release-plan.yaml + epic' "$trace_mdc"; then
111
+ echo "sync-skills: FAIL — '+' missing from trace-requirement; use sed -E for whitespace collapse" >&2
112
+ exit 1
113
+ fi
114
+
115
+ manifest="$REPO_ROOT/.gemini/extensions/bigpowers/gemini-extension.json"
116
+ if [[ -f "$manifest" ]]; then
117
+ ext_ver=$(jq -r '.version // empty' "$manifest")
118
+ pkg_ver=$(jq -r '.version // empty' "$REPO_ROOT/package.json")
119
+ if [[ -n "$ext_ver" && -n "$pkg_ver" && "$ext_ver" != "$pkg_ver" ]]; then
120
+ echo "sync-skills: FAIL — gemini-extension.json version ($ext_ver) != package.json ($pkg_ver)" >&2
121
+ exit 1
122
+ fi
123
+ fi
124
+
125
+ validate_script="$REPO_ROOT/scripts/validate-skill-yaml.py"
126
+ if [[ -f "$validate_script" ]] && command -v $PYTHON &>/dev/null; then
127
+ if ! $PYTHON "$validate_script" > /dev/null 2>&1; then
128
+ echo "sync-skills: FAIL — YAML frontmatter validation failed" >&2
129
+ $PYTHON "$validate_script" >&2
130
+ exit 1
131
+ fi
132
+ fi
133
+
134
+ if grep -rn '^declare -A\|^[[:space:]]*declare -A' \
135
+ scripts/sync-skills.sh scripts/generate-skill-index.sh \
136
+ scripts/regenerate-lockfile.sh scripts/build-skill-index.sh 2>/dev/null; then
137
+ echo "sync-skills: FAIL — bash 4+ features detected in install-chain scripts (not macOS-compatible)" >&2
138
+ exit 1
139
+ fi
140
+ }
141
+
142
+ sync_opencode_copy() {
143
+ local opencode_count=0 skill_dir skill_md skill_name
144
+ SYNC_OPENCODE_COUNT=0
145
+
146
+ [[ -z "${OPN_TARGET:-}" || ! -d "$OPN_TARGET" ]] && return 0
147
+
148
+ echo ""
149
+ echo "Syncing skills to opencode repo: $OPN_TARGET"
150
+ OPN_SKILLS="$OPN_TARGET/skills"
151
+ mkdir -p "$OPN_SKILLS"
152
+ for skill_dir in "$SKILLS_ROOT"/*/; do
153
+ skill_md="$skill_dir/SKILL.md"
154
+ [[ -f "$skill_md" ]] || continue
155
+ skill_name=$(basename "$skill_dir")
156
+ mkdir -p "$OPN_SKILLS/$skill_name"
157
+ cp "$skill_md" "$OPN_SKILLS/$skill_name/SKILL.md"
158
+ opencode_count=$((opencode_count + 1))
159
+ done
160
+ echo " → $opencode_count skills copied to $OPN_SKILLS/"
161
+ SYNC_OPENCODE_COUNT=$opencode_count
162
+ }
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env bash
2
+ # story: e13s03 e39s04
3
+ # Render helpers for sync-skills.sh — build_body + per-target render_* functions.
4
+ # Source after skill-common.sh; expects CURSOR_RULES, GEMINI_*, PI_*, OKF_WIKI_* globals.
5
+
6
+ if [ -n "${SYNC_RENDER_LOADED:-}" ]; then
7
+ return 0
8
+ fi
9
+ SYNC_RENDER_LOADED=1
10
+
11
+ build_body() {
12
+ local skill_md="$1"
13
+ local skill_dir="$2"
14
+ local body extra_md
15
+
16
+ body=$(awk '/^---/{f++; next} f>=2{print}' "$skill_md")
17
+ for extra_md in $(find "$skill_dir" -maxdepth 1 -name "*.md" ! -name "SKILL.md" | LC_ALL=C sort); do
18
+ body="$body"$'\n\n'"---"$'\n\n'"$(cat "$extra_md")"
19
+ done
20
+ echo "$body" | grep -v 'disable-model-invocation'
21
+ }
22
+
23
+ render_cursor() {
24
+ local cursor_file="$CURSOR_RULES/$IR_NAME.mdc"
25
+ {
26
+ echo "---"
27
+ echo "description: \"$IR_DESC_ESCAPED\""
28
+ echo "alwaysApply: false"
29
+ echo "---"
30
+ echo ""
31
+ echo "$IR_BODY"
32
+ } > "$cursor_file"
33
+ }
34
+
35
+ render_gemini_skill() {
36
+ mkdir -p "$GEMINI_SKILLS/$IR_NAME"
37
+ {
38
+ echo "---"
39
+ echo "name: $IR_NAME"
40
+ echo "description: \"$IR_DESC_ESCAPED\""
41
+ echo "---"
42
+ echo ""
43
+ echo "$IR_BODY"
44
+ } > "$GEMINI_SKILLS/$IR_NAME/SKILL.md"
45
+ }
46
+
47
+ render_gemini_command() {
48
+ mkdir -p "$GEMINI_COMMANDS/prompts"
49
+ local prompt_file="commands/prompts/$IR_NAME.md"
50
+ echo "$IR_BODY" > "$GEMINI_EXT_DIR/$prompt_file"
51
+ {
52
+ echo "description = \"$IR_DESC_ESCAPED\""
53
+ echo "prompt = \"@{$prompt_file}\""
54
+ } > "$GEMINI_COMMANDS/$IR_NAME.toml"
55
+ }
56
+
57
+ render_pi_skill() {
58
+ mkdir -p "$PI_SKILLS/$IR_NAME"
59
+ {
60
+ echo "---"
61
+ echo "name: $IR_NAME"
62
+ echo "description: \"$IR_DESC_ESCAPED\""
63
+ [[ -n "$IR_MODEL" ]] && echo "model: $IR_MODEL"
64
+ echo "---"
65
+ echo ""
66
+ echo "$IR_BODY"
67
+ } > "$PI_SKILLS/$IR_NAME/SKILL.md"
68
+ }
69
+
70
+ render_pi_prompt() {
71
+ {
72
+ echo "---"
73
+ echo "description: $IR_DESCRIPTION"
74
+ echo "---"
75
+ echo ""
76
+ echo "$IR_BODY"
77
+ } > "$PI_PROMPTS/$IR_NAME.md"
78
+ }
79
+
80
+ render_okf_concept() {
81
+ mkdir -p "$OKF_WIKI_SKILLS"
82
+ local okf_file="$OKF_WIKI_SKILLS/$IR_NAME.md"
83
+ local phase="utility" title desc_escaped short_desc effort_val="${_PF_EFFORT:-standard}"
84
+
85
+ case "$IR_NAME" in
86
+ survey-context|elaborate-spec|model-domain|define-language|\
87
+ design-interface|plan-work|plan-release|slice-tasks|\
88
+ kickoff-branch|develop-tdd|verify-work|audit-code|\
89
+ commit-message|release-branch|security-review|\
90
+ assess-impact|investigate-bug|validate-fix|dispatch-agents|\
91
+ request-review|respond-review|hook-commits|guard-git|\
92
+ wire-observability|wire-ci|smoke-test|deploy|enforce-first)
93
+ phase="critical-path" ;;
94
+ esac
95
+
96
+ title=$(echo "$IR_NAME" | sed -E 's/-/ /g; s/\b(.)/\U\1/g')
97
+ desc_escaped=$(echo "$IR_DESCRIPTION" | sed 's/\\/\\\\/g; s/"/\\"/g')
98
+ short_desc="$IR_DESCRIPTION"
99
+ [[ ${#short_desc} -gt 200 ]] && short_desc="${short_desc:0:197}..."
100
+
101
+ {
102
+ echo "---"
103
+ echo "okf_kind: concept"
104
+ echo "type: Skill"
105
+ echo "id: $IR_NAME"
106
+ echo "title: \"$title\""
107
+ echo "name: $IR_NAME"
108
+ echo "category: skills"
109
+ [[ -n "$IR_MODEL" ]] && echo "model: $IR_MODEL"
110
+ echo "effort: $effort_val"
111
+ echo "phase: $phase"
112
+ echo "description: \"$desc_escaped\""
113
+ echo "references: []"
114
+ echo "---"
115
+ echo ""
116
+ echo "# $title"
117
+ echo ""
118
+ echo "**Phase:** $phase"
119
+ [[ -n "$IR_MODEL" ]] && echo "**Model:** $IR_MODEL"
120
+ echo "**Effort:** $effort_val"
121
+ echo ""
122
+ echo "$IR_DESCRIPTION"
123
+ echo ""
124
+ echo "> Auto-generated by sync-skills.sh --okf (e39s04) from $SKILLS_ROOT/$IR_NAME/SKILL.md"
125
+ } > "$okf_file"
126
+ }
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env python3
2
+ # story: e39s04
3
+ """Add cross-references from skill-graph.json into OKF concept files."""
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ import re
8
+ import sys
9
+ from pathlib import Path
10
+
11
+
12
+ def main() -> int:
13
+ graph_json = Path(sys.argv[1])
14
+ wiki_skills = Path(sys.argv[2])
15
+
16
+ with open(graph_json, encoding="utf-8") as f:
17
+ graph = json.load(f)
18
+
19
+ edges = graph.get("edges", [])
20
+ nodes = {n.get("id") or n.get("name") for n in graph.get("nodes", [])}
21
+ nodes.discard(None)
22
+
23
+ refs: dict[str, list[dict[str, str]]] = {}
24
+ for edge in edges:
25
+ src = edge.get("from") or edge.get("source")
26
+ tgt = edge.get("to") or edge.get("target")
27
+ rel = edge.get("relationType") or edge.get("relation") or "references"
28
+ if src in nodes and tgt in nodes:
29
+ refs.setdefault(src, []).append({"concept": tgt, "type": rel})
30
+
31
+ skill_names = {f.stem for f in wiki_skills.glob("*.md")}
32
+ count_updated = 0
33
+
34
+ for path in wiki_skills.glob("*.md"):
35
+ name = path.stem
36
+ content = path.read_text(encoding="utf-8")
37
+ concept_refs = list(refs.get(name, []))
38
+
39
+ desc_match = re.search(r'description:\s*"(.*?)"', content, re.DOTALL)
40
+ if desc_match:
41
+ desc = desc_match.group(1)
42
+ for skill_name in skill_names:
43
+ if (
44
+ skill_name != name
45
+ and skill_name in desc
46
+ and not any(r["concept"] == skill_name for r in concept_refs)
47
+ ):
48
+ concept_refs.append({"concept": skill_name, "type": "references"})
49
+
50
+ seen: set[tuple[str, str]] = set()
51
+ unique_refs: list[dict[str, str]] = []
52
+ for ref in concept_refs:
53
+ key = (ref["concept"], ref["type"])
54
+ if key not in seen:
55
+ seen.add(key)
56
+ unique_refs.append(ref)
57
+
58
+ if unique_refs:
59
+ ref_lines = []
60
+ for ref in unique_refs:
61
+ ref_lines.append(f' - concept: {ref["concept"]}')
62
+ ref_lines.append(f' type: {ref["type"]}')
63
+ ref_block = "\n".join(ref_lines)
64
+ content = re.sub(r"references:\s*\[\]", f"references:\n{ref_block}", content)
65
+ else:
66
+ content = re.sub(
67
+ r"references:\s*\[\]",
68
+ "references:\n - concept: skills-wiki\n type: belongs_to",
69
+ content,
70
+ )
71
+ count_updated += 1
72
+ path.write_text(content, encoding="utf-8")
73
+
74
+ print(f" concepts with cross-references: {count_updated}", file=sys.stderr)
75
+ return 0
76
+
77
+
78
+ if __name__ == "__main__":
79
+ raise SystemExit(main())
@@ -10,7 +10,6 @@
10
10
  set -euo pipefail
11
11
  source "$(dirname "${BASH_SOURCE[0]}")/lib/python-env.sh"
12
12
 
13
- # ── Parse CLI flags (must happen before source to allow early --help) ─
14
13
  OKF_MODE=0
15
14
  OPN_TARGET=""
16
15
 
@@ -31,7 +30,10 @@ for arg in "$@"; do
31
30
  esac
32
31
  done
33
32
 
34
- source "$(dirname "${BASH_SOURCE[0]}")/lib/skill-common.sh"
33
+ _LIB_DIR="$(dirname "${BASH_SOURCE[0]}")/lib"
34
+ source "$_LIB_DIR/skill-common.sh"
35
+ source "$_LIB_DIR/sync-render.sh"
36
+ source "$_LIB_DIR/sync-post.sh"
35
37
  resolve_repo_root
36
38
 
37
39
  CURSOR_RULES="$REPO_ROOT/.cursor/rules"
@@ -42,220 +44,14 @@ GEMINI_MANIFEST="$GEMINI_EXT_DIR/gemini-extension.json"
42
44
  PI_SKILLS="$REPO_ROOT/.pi/skills"
43
45
  PI_PROMPTS="$REPO_ROOT/.pi/prompts"
44
46
  PI_PACKAGE_JSON="$REPO_ROOT/.pi/package.json"
45
-
46
- mkdir -p "$CURSOR_RULES" "$GEMINI_SKILLS" "$GEMINI_COMMANDS" "$PI_SKILLS" "$PI_PROMPTS"
47
-
48
- # Clear old artifacts to ensure a clean sync
49
- rm -rf "${GEMINI_SKILLS:?}"/*
50
- rm -rf "${GEMINI_COMMANDS:?}"/*
51
- rm -rf "${PI_SKILLS:?}"/*
52
- rm -rf "${PI_PROMPTS:?}"/*
53
-
54
- # ── IR: shared intermediate representation ──────────────────────────
55
- # These globals are set by the main loop per skill and consumed by render functions.
56
- IR_NAME=""
57
- IR_MODEL=""
58
- IR_DESCRIPTION=""
59
- IR_DESC_ESCAPED=""
60
- IR_BODY=""
61
-
62
- # ── build_body ──────────────────────────────────────────────────────
63
- # Builds the concatenated content: SKILL.md body + all other *.md files.
64
- # Args: $1 = path to SKILL.md, $2 = skill directory
65
- build_body() {
66
- local skill_md="$1"
67
- local skill_dir="$2"
68
- local body
69
-
70
- # Strip frontmatter from SKILL.md (content after second ---)
71
- body=$(awk '/^---/{f++; next} f>=2{print}' "$skill_md")
72
-
73
- # Append extra *.md files alphabetically
74
- local extra_md
75
- for extra_md in $(find "$skill_dir" -maxdepth 1 -name "*.md" ! -name "SKILL.md" | LC_ALL=C sort); do
76
- body="$body"$'\n\n'"---"$'\n\n'"$(cat "$extra_md")"
77
- done
78
-
79
- # Strip disable-model-invocation lines
80
- echo "$body" | grep -v 'disable-model-invocation'
81
- }
82
-
83
- # ── Render target: Cursor ───────────────────────────────────────────
84
- # Writes .cursor/rules/<name>.mdc with YAML frontmatter + body.
85
- render_cursor() {
86
- local cursor_file="$CURSOR_RULES/$IR_NAME.mdc"
87
- {
88
- echo "---"
89
- echo "description: \"$IR_DESC_ESCAPED\""
90
- echo "alwaysApply: false"
91
- echo "---"
92
- echo ""
93
- echo "$IR_BODY"
94
- } > "$cursor_file"
95
- }
96
-
97
- # ── Render target: Gemini Agent Skill ───────────────────────────────
98
- # Writes .gemini/extensions/bigpowers/skills/<name>/SKILL.md
99
- render_gemini_skill() {
100
- mkdir -p "$GEMINI_SKILLS/$IR_NAME"
101
- {
102
- echo "---"
103
- echo "name: $IR_NAME"
104
- echo "description: \"$IR_DESC_ESCAPED\""
105
- echo "---"
106
- echo ""
107
- echo "$IR_BODY"
108
- } > "$GEMINI_SKILLS/$IR_NAME/SKILL.md"
109
- }
110
-
111
- # ── Render target: Gemini Slash Command ─────────────────────────────
112
- # Writes .gemini/extensions/bigpowers/commands/<name>.toml + prompt file
113
- render_gemini_command() {
114
- mkdir -p "$GEMINI_COMMANDS/prompts"
115
- local prompt_file="commands/prompts/$IR_NAME.md"
116
- echo "$IR_BODY" > "$GEMINI_EXT_DIR/$prompt_file"
117
- {
118
- echo "description = \"$IR_DESC_ESCAPED\""
119
- echo "prompt = \"@{$prompt_file}\""
120
- } > "$GEMINI_COMMANDS/$IR_NAME.toml"
121
- }
122
-
123
- # ── Render target: pi Agent Skill ───────────────────────────────────
124
- # Writes .pi/skills/<name>/SKILL.md
125
- render_pi_skill() {
126
- mkdir -p "$PI_SKILLS/$IR_NAME"
127
- {
128
- echo "---"
129
- echo "name: $IR_NAME"
130
- echo "description: \"$IR_DESC_ESCAPED\""
131
- [[ -n "$IR_MODEL" ]] && echo "model: $IR_MODEL"
132
- echo "---"
133
- echo ""
134
- echo "$IR_BODY"
135
- } > "$PI_SKILLS/$IR_NAME/SKILL.md"
136
- }
137
-
138
- # ── Render target: pi Prompt Template ───────────────────────────────
139
- # Writes .pi/prompts/<name>.md
140
- render_pi_prompt() {
141
- {
142
- echo "---"
143
- echo "description: $IR_DESCRIPTION"
144
- echo "---"
145
- echo ""
146
- echo "$IR_BODY"
147
- } > "$PI_PROMPTS/$IR_NAME.md"
148
- }
149
-
150
- # ── Render target: OKF Concept (e39s04) ──────────────────────────────
151
- # Writes specs/skills-wiki/skills/<name>.md with OKF concept frontmatter.
152
- # Globals used: IR_NAME, IR_MODEL, IR_DESCRIPTION, IR_DESC_ESCAPED, IR_BODY
153
- # Also reads _PF_EFFORT from the extended parse_frontmatter.
154
47
  OKF_WIKI_SKILLS="$REPO_ROOT/specs/skills-wiki/skills"
155
48
  OKF_WIKI_DIR="$REPO_ROOT/specs/skills-wiki"
156
49
 
157
- render_okf_concept() {
158
- mkdir -p "$OKF_WIKI_SKILLS"
159
- local okf_file="$OKF_WIKI_SKILLS/$IR_NAME.md"
160
-
161
- # Determine phase: critical-path skills per the bigpowers core loop
162
- local phase="utility"
163
- case "$IR_NAME" in
164
- survey-context|elaborate-spec|model-domain|define-language|\
165
- design-interface|plan-work|plan-release|slice-tasks|\
166
- kickoff-branch|develop-tdd|verify-work|audit-code|\
167
- commit-message|release-branch|security-review|\
168
- assess-impact|investigate-bug|validate-fix|dispatch-agents|\
169
- request-review|respond-review|hook-commits|guard-git|\
170
- wire-observability|wire-ci|smoke-test|deploy|enforce-first)
171
- phase="critical-path" ;;
172
- esac
173
-
174
- # Build title from name: kebab-case → Title Case
175
- local title
176
- title=$(echo "$IR_NAME" | sed -E 's/-/ /g; s/\b(.)/\U\1/g')
177
-
178
- # Escape description for YAML double-quoted string
179
- local desc_escaped
180
- desc_escaped=$(echo "$IR_DESCRIPTION" | sed 's/\\/\\\\/g; s/"/\\"/g')
181
-
182
- # Truncate description to avoid overly long lines (OKF convention)
183
- local short_desc="$IR_DESCRIPTION"
184
- if [[ ${#short_desc} -gt 200 ]]; then
185
- short_desc="${short_desc:0:197}..."
186
- fi
187
-
188
- local effort_val="${_PF_EFFORT:-standard}"
189
-
190
- {
191
- echo "---"
192
- echo "okf_kind: concept"
193
- echo "type: Skill"
194
- echo "id: $IR_NAME"
195
- echo "title: \"$title\""
196
- echo "name: $IR_NAME"
197
- echo "category: skills"
198
- [[ -n "$IR_MODEL" ]] && echo "model: $IR_MODEL"
199
- echo "effort: $effort_val"
200
- echo "phase: $phase"
201
- echo "description: \"$desc_escaped\""
202
- echo "references: []"
203
- echo "---"
204
- echo ""
205
- echo "# $title"
206
- echo ""
207
- echo "**Phase:** $phase"
208
- [[ -n "$IR_MODEL" ]] && echo "**Model:** $IR_MODEL"
209
- echo "**Effort:** $effort_val"
210
- echo ""
211
- echo "$IR_DESCRIPTION"
212
- echo ""
213
- echo "> Auto-generated by sync-skills.sh --okf (e39s04) from $SKILLS_ROOT/$IR_NAME/SKILL.md"
214
- } > "$okf_file"
215
- }
216
-
217
- # ── Parse extended frontmatter for OKF (includes effort) ────────────
218
- # Wrapper for library parse_frontmatter — sets _PF_* compat vars
219
- parse_frontmatter() {
220
- # Delegate to library which now sets _PF_* vars
221
- if command -v skill_common_parse >/dev/null 2>&1 || [ -n "${SKILL_COMMON_LOADED:-}" ]; then
222
- source "$(dirname "${BASH_SOURCE[0]}")/lib/skill-common.sh" 2>/dev/null
223
- fi
224
- # Call library version (defined in skill-common.sh, sets SKILL_NAME and _PF_*)
225
- # But since we have the same name, we need the inline version
226
- _PF_NAME=$(awk '/^---/{f++} f==1 && /^name:/{print; exit}' "$1" | sed 's/^name:[[:space:]]*//')
227
- _PF_MODEL=$(awk '/^---/{f++} f==1 && /^model:/{print; exit}' "$1" | sed 's/^model:[[:space:]]*//')
228
- _PF_DESCRIPTION=$(awk '/^---/{f++; next} f==1 && /^description:/{p=1; sub(/^description:[[:space:]]*/,""); print; next} f==1 && p && /^[a-z]+:/{exit} f==1 && p{print}' "$1" | tr '\n' ' ' | sed 's/[[:space:]]*$//')
229
- [[ -z "$_PF_NAME" ]] && return 1
230
- return 0
231
- }
232
-
233
- # Extended parse_frontmatter for OKF mode (adds _PF_EFFORT)
234
- parse_frontmatter_okf() {
235
- local file="$1"
236
- _PF_EFFORT=""
237
-
238
- if [[ ! -f "$file" ]]; then
239
- return 1
240
- fi
241
-
242
- if ! grep -q '^---$' "$file"; then
243
- return 1
244
- fi
245
-
246
- _PF_NAME=$(awk '/^---/{f++} f==1 && /^name:/{print; exit}' "$file" | sed 's/^name:[[:space:]]*//')
247
- _PF_MODEL=$(awk '/^---/{f++} f==1 && /^model:/{print; exit}' "$file" | sed 's/^model:[[:space:]]*//')
248
- _PF_EFFORT=$(awk '/^---/{f++} f==1 && /^effort:/{print; exit}' "$file" | sed 's/^effort:[[:space:]]*//')
249
- _PF_DESCRIPTION=$(awk '/^---/{f++; next} f==1 && /^description:/{p=1; sub(/^description:[[:space:]]*/,""); print; next} f==1 && p && /^[a-z]+:/{exit} f==1 && p{print}' "$file" \
250
- | tr -d '\n' \
251
- | sed -E 's/[[:space:]]+/ /g')
50
+ mkdir -p "$CURSOR_RULES" "$GEMINI_SKILLS" "$GEMINI_COMMANDS" "$PI_SKILLS" "$PI_PROMPTS"
51
+ rm -rf "${GEMINI_SKILLS:?}"/* "${GEMINI_COMMANDS:?}"/* "${PI_SKILLS:?}"/* "${PI_PROMPTS:?}"/*
252
52
 
253
- [[ -z "$_PF_NAME" ]] && return 1
254
- return 0
255
- }
53
+ IR_NAME="" IR_MODEL="" IR_DESCRIPTION="" IR_DESC_ESCAPED="" IR_BODY=""
256
54
 
257
- # ── Target registry (e37s07) ────────────────────────────────────────
258
- # Load skill adapters from scripts/targets.yaml — no hardcoded target list.
259
55
  TARGETS_FILE="$REPO_ROOT/scripts/targets.yaml"
260
56
  SKILL_ADAPTERS=()
261
57
  if [[ -f "$TARGETS_FILE" ]] && command -v yq >/dev/null 2>&1; then
@@ -269,8 +65,6 @@ else
269
65
  fi
270
66
 
271
67
  skill_count=0
272
-
273
- # ── Main loop: Parse → IR → Render per skill ────────────────────────
274
68
  while IFS= read -r skill_dir; do
275
69
  skill_md="$skill_dir/SKILL.md"
276
70
 
@@ -283,16 +77,12 @@ while IFS= read -r skill_dir; do
283
77
  IR_NAME="$_PF_NAME"
284
78
  IR_MODEL="$_PF_MODEL"
285
79
  IR_DESCRIPTION="$_PF_DESCRIPTION"
286
-
287
- # Escape double quotes and backslashes for safe double-quoted YAML output
288
80
  IR_DESC_ESCAPED=$(echo "$IR_DESCRIPTION" | sed 's/\\/\\\\/g; s/\"/\\"/g')
289
-
290
81
  [[ -z "$IR_NAME" ]] && continue
291
82
 
292
83
  IR_BODY=$(build_body "$skill_md" "$skill_dir")
293
84
  SKILL_MD_PATH="$skill_md"
294
85
 
295
- # Dispatch to each skill adapter from registry (e37s07)
296
86
  for adapter_name in "${SKILL_ADAPTERS[@]}"; do
297
87
  adapter_script="$REPO_ROOT/scripts/adapters/${adapter_name}.sh"
298
88
  # shellcheck source=/dev/null
@@ -300,408 +90,18 @@ while IFS= read -r skill_dir; do
300
90
  render_skill
301
91
  done
302
92
 
303
- if [[ "$OKF_MODE" -eq 1 ]]; then
304
- render_okf_concept
305
- fi
306
-
93
+ [[ "$OKF_MODE" -eq 1 ]] && render_okf_concept
307
94
  skill_count=$((skill_count + 1))
308
95
  done < <(iterate_skills)
309
96
 
310
- # ── Post-loop: manifest, opencode, regeneration, guards, README badge ──
311
-
312
- # Assemble final gemini-extension.json
313
- pkg_version=$(jq -r '.version' "$REPO_ROOT/package.json")
314
- pkg_desc=$(jq -r '.description' "$REPO_ROOT/package.json")
315
-
316
- jq -n --arg name "bigpowers" \
317
- --arg version "$pkg_version" \
318
- --arg desc "${skill_count} skills — ${pkg_desc}" \
319
- '{name: $name, version: $version, description: $desc}' > "$GEMINI_MANIFEST"
320
-
321
- # Write pi package config: .pi/package.json
322
- jq -n --arg version "$pkg_version" \
323
- --arg desc "${skill_count} skills — ${pkg_desc}" \
324
- '{
325
- "name": "bigpowers",
326
- "version": $version,
327
- "description": $desc,
328
- "keywords": ["pi-package"],
329
- "pi": {
330
- "skills": ["./skills"],
331
- "prompts": ["./prompts"]
332
- }
333
- }' > "$PI_PACKAGE_JSON"
334
-
335
- # Write OpenCode configuration: opencode.json
336
- {
337
- echo "{"
338
- echo " \"\$schema\": \"https://opencode.ai/config.json\","
339
- echo " \"instructions\": [\"CLAUDE.md\", \"CONVENTIONS.md\"]"
340
- echo "}"
341
- } > "$REPO_ROOT/opencode.json"
342
-
343
- # Sync to bigpowers-opencode repo (if --opencode path is provided)
344
- if [[ -n "$OPN_TARGET" ]] && [[ -d "$OPN_TARGET" ]]; then
345
- echo ""
346
- echo "Syncing skills to opencode repo: $OPN_TARGET"
347
- OPN_SKILLS="$OPN_TARGET/skills"
348
- mkdir -p "$OPN_SKILLS"
349
- opencode_count=0
350
- for skill_dir in "$SKILLS_ROOT"/*/; do
351
- skill_md="$skill_dir/SKILL.md"
352
- [[ -f "$skill_md" ]] || continue
353
- skill_name=$(basename "$skill_dir")
354
- mkdir -p "$OPN_SKILLS/$skill_name"
355
- cp "$skill_md" "$OPN_SKILLS/$skill_name/SKILL.md"
356
- opencode_count=$((opencode_count + 1))
357
- done
358
- echo " → $opencode_count skills copied to $OPN_SKILLS/"
359
- fi
360
-
361
- # Regenerate skills-lock.json catalog
362
- if [[ -x "$REPO_ROOT/scripts/regenerate-lockfile.sh" ]]; then
363
- bash "$REPO_ROOT/scripts/regenerate-lockfile.sh" || { echo "sync-skills: FAIL — lockfile regeneration failed" >&2; exit 1; }
364
- fi
365
-
366
- # Regenerate SKILL-INDEX.md from lockfile + frontmatter
367
- if [[ -x "$REPO_ROOT/scripts/generate-skill-index.sh" ]]; then
368
- bash "$REPO_ROOT/scripts/generate-skill-index.sh" || { echo "sync-skills: FAIL — SKILL-INDEX.md generation failed" >&2; exit 1; }
369
- fi
370
-
371
- # Regenerate lexical skill index for search-skills
372
- if [[ -x "$REPO_ROOT/scripts/build-skill-index.sh" ]]; then
373
- bash "$REPO_ROOT/scripts/build-skill-index.sh" || true
374
- fi
375
-
376
- # Prune orphan cursor rules
377
- CURSOR_KEEP="context7.mdc"
378
- for mdc in "$CURSOR_RULES"/*.mdc; do
379
- [[ -e "$mdc" ]] || continue
380
- mdc_base=$(basename "$mdc")
381
- case " $CURSOR_KEEP " in *" $mdc_base "*) continue ;; esac
382
- if [[ ! -f "$SKILLS_ROOT/${mdc_base%.mdc}/SKILL.md" ]]; then
383
- rm "$mdc"
384
- echo " → pruned orphan cursor rule: $mdc_base"
385
- fi
386
- done
387
-
388
- # Stamp the README skill-count badge
389
- readme="$REPO_ROOT/README.md"
390
- if [[ -f "$readme" ]] && grep -q 'badge/skills-' "$readme"; then
391
- readme_tmp=$(mktemp)
392
- sed -E "s|(badge/skills-)[0-9]+(-brightgreen)|\1${skill_count}\2|" "$readme" > "$readme_tmp"
393
- mv "$readme_tmp" "$readme"
394
- fi
395
-
396
- echo "sync-skills: $skill_count skills synced"
397
- echo " → .cursor/rules/ ($skill_count .mdc files)"
398
- echo " → .gemini/extensions/bigpowers/skills/ (Agent Skills)"
399
- echo " → .gemini/extensions/bigpowers/commands/ (Slash Commands)"
400
- echo " → .gemini/extensions/bigpowers/gemini-extension.json"
401
- echo " → .pi/skills/ ($skill_count skill dirs — pi Agent Skills)"
402
- echo " → .pi/prompts/ ($skill_count prompt templates — pi slash commands)"
403
- echo " → .pi/package.json (pi package manifest)"
404
- echo " → skills-lock.json (catalog with SHA-256 hashes)"
405
- echo " → SKILL-INDEX.md (auto-generated skill reference)"
406
- echo " → opencode.json (CLAUDE.md + CONVENTIONS.md instructions)"
407
- [[ -n "$OPN_TARGET" ]] && echo " → bigpowers-opencode: $opencode_count skills"
97
+ opencode_count=0
98
+ [[ -n "$OPN_TARGET" ]] && sync_opencode_copy && opencode_count="${SYNC_OPENCODE_COUNT:-0}"
408
99
 
409
- # ── OKF post-sync pipeline (e39s04) ──────────────────────────────────
410
100
  if [[ "$OKF_MODE" -eq 1 ]]; then
411
101
  echo ""
412
102
  echo "--- OKF skills-wiki post-sync ---"
413
-
414
- # Step 2: Run build-skill-graph.sh to get cross-references in skill-graph.json
415
- GRAPH_JSON="$REPO_ROOT/specs/skill-graph.json"
416
- if [[ -x "$REPO_ROOT/scripts/build-skill-graph.sh" ]]; then
417
- echo "Running build-skill-graph.sh for cross-references..."
418
- bash "$REPO_ROOT/scripts/build-skill-graph.sh" --json || {
419
- echo "sync-skills: WARNING — build-skill-graph.sh --json failed; cross-references will be empty" >&2
420
- }
421
- else
422
- echo "sync-skills: WARNING — build-skill-graph.sh not found; skipping cross-references" >&2
423
- fi
424
-
425
- # Step 2b: Add cross-references from skill-graph.json edges into OKF concepts
426
- if [[ -f "$GRAPH_JSON" ]]; then
427
- echo "Adding cross-references from skill-graph.json..."
428
- $PYTHON - "$GRAPH_JSON" "$OKF_WIKI_SKILLS" <<'PYEOF'
429
- import json, sys, re
430
- from pathlib import Path
431
-
432
- graph_json = Path(sys.argv[1])
433
- wiki_skills = Path(sys.argv[2])
434
-
435
- # Load graph edges
436
- with open(graph_json) as f:
437
- graph = json.load(f)
438
-
439
- edges = graph.get("edges", [])
440
- nodes = {n["name"] for n in graph.get("nodes", [])}
441
-
442
- # Build adjacency: skill_name -> list of {concept, type} refs
443
- refs = {}
444
- for e in edges:
445
- src, tgt, rel = e["from"], e["to"], e.get("relationType", "references")
446
- # Only include edges where both ends are known skills
447
- if src in nodes and tgt in nodes:
448
- refs.setdefault(src, []).append({"concept": tgt, "type": rel})
449
-
450
- # Also detect intra-description references: scan description for skill names
451
- skill_names = set()
452
- for f in wiki_skills.glob("*.md"):
453
- skill_names.add(f.stem)
454
-
455
- # Update each concept file with references
456
- count_updated = 0
457
- for f in wiki_skills.glob("*.md"):
458
- name = f.stem
459
- content = f.read_text()
460
-
461
- # Collect references for this skill
462
- concept_refs = refs.get(name, [])
463
-
464
- # Also scan description for mentions of other skill names
465
- desc_match = re.search(r'description:\s*"(.*?)"', content, re.DOTALL)
466
- if desc_match:
467
- desc = desc_match.group(1)
468
- for sn in skill_names:
469
- if sn != name and sn in desc and not any(r["concept"] == sn for r in concept_refs):
470
- concept_refs.append({"concept": sn, "type": "references"})
471
-
472
- # Deduplicate
473
- seen = set()
474
- unique_refs = []
475
- for r in concept_refs:
476
- key = (r["concept"], r["type"])
477
- if key not in seen:
478
- seen.add(key)
479
- unique_refs.append(r)
480
-
481
- # Build YAML references block
482
- if unique_refs:
483
- ref_lines = []
484
- for r in unique_refs:
485
- ref_lines.append(f' - concept: {r["concept"]}')
486
- ref_lines.append(f' type: {r["type"]}')
487
- ref_block = "\n".join(ref_lines)
488
- content = re.sub(r'references:\s*\[\]', f'references:\n{ref_block}', content)
489
- count_updated += 1
490
- else:
491
- # No edges and no description mentions → self-reference as fallback
492
- content = re.sub(
493
- r'references:\s*\[\]',
494
- f'references:\n - concept: skills-wiki\n type: belongs_to',
495
- content
496
- )
497
- count_updated += 1
498
-
499
- f.write_text(content)
500
-
501
- print(f" concepts with cross-references: {count_updated}", file=sys.stderr)
502
- PYEOF
503
- else
504
- echo "sync-skills: WARNING — $GRAPH_JSON not found; adding fallback references" >&2
505
- # Add fallback references to all concepts
506
- for f in "$OKF_WIKI_SKILLS"/*.md; do
507
- [[ -f "$f" ]] || continue
508
- if command -v $PYTHON &>/dev/null; then
509
- $PYTHON -c "
510
- import re, sys
511
- content = open('$f').read()
512
- content = re.sub(r'references:\s*\[\]', 'references:\n - concept: skills-wiki\n type: belongs_to', content)
513
- open('$f', 'w').write(content)
514
- "
515
- fi
516
- done
517
- fi
518
-
519
- # Step 3: Generate specs/skills-wiki/index.md with progressive disclosure
520
- echo "Generating index.md..."
521
- OKF_INDEX="$OKF_WIKI_DIR/index.md"
522
- $PYTHON - "$OKF_WIKI_SKILLS" "$OKF_WIKI_DIR" "$OKF_INDEX" <<'PYEOF'
523
- import json, sys, re
524
- from pathlib import Path
525
- from collections import defaultdict
526
-
527
- wiki_skills = Path(sys.argv[1])
528
- wiki_dir = Path(sys.argv[2])
529
- index_path = Path(sys.argv[3])
530
-
531
- # Collect all concepts
532
- concepts = []
533
- for f in sorted(wiki_skills.glob("*.md")):
534
- content = f.read_text()
535
- fm_match = re.search(r'^---\n(.*?)\n---', content, re.DOTALL)
536
- if not fm_match:
537
- continue
538
- fm_text = fm_match.group(1)
539
- # Simple YAML extraction
540
- name = ""
541
- title = ""
542
- phase = "utility"
543
- model = ""
544
- effort = ""
545
- description = ""
546
- for line in fm_text.split('\n'):
547
- line = line.strip()
548
- if line.startswith('name:'):
549
- name = line.split(':', 1)[1].strip()
550
- elif line.startswith('title:'):
551
- title = line.split(':', 1)[1].strip().strip('"')
552
- elif line.startswith('phase:'):
553
- phase = line.split(':', 1)[1].strip()
554
- elif line.startswith('model:'):
555
- model = line.split(':', 1)[1].strip()
556
- elif line.startswith('effort:'):
557
- effort = line.split(':', 1)[1].strip()
558
- elif line.startswith('description:'):
559
- description = line.split(':', 1)[1].strip().strip('"')
560
- if not name:
561
- continue
562
- concepts.append({
563
- "name": name,
564
- "title": title or name.replace('-', ' ').title(),
565
- "phase": phase,
566
- "model": model,
567
- "effort": effort,
568
- "description": description[:120] + "..." if len(description) > 120 else description,
569
- })
570
-
571
- # Group by phase
572
- phases = defaultdict(list)
573
- for c in concepts:
574
- phases[c["phase"]].append(c)
575
-
576
- # Count
577
- total = len(concepts)
578
- cp_count = len(phases.get("critical-path", []))
579
- util_count = len(phases.get("utility", []))
580
-
581
- # Generate index.md
582
- with open(index_path, "w") as out:
583
- out.write(f"""---
584
- okf_kind: concept
585
- type: Index
586
- id: skills-wiki
587
- title: "BigPowers Skills Wiki"
588
- category: skills
589
- description: "Progressive-disclosure index of {total} bigpowers skills ({cp_count} critical-path, {util_count} utility) — auto-generated by sync-skills.sh --okf (e39s04)."
590
- references:
591
- - concept: bigpowers
592
- type: belongs_to
593
- ---
594
-
595
- # BigPowers Skills Wiki
596
-
597
- **{total} skills** ({cp_count} critical-path, {util_count} utility)
598
-
599
- > Auto-generated by sync-skills.sh --okf (e39s04).
600
- > Source of truth: skills/*/SKILL.md files.
601
-
602
- ## Critical-Path Skills ({cp_count})
603
-
604
- These skills form the bigpowers core development loop — used on every feature.
605
-
606
- | Skill | Model | Effort | Description |
607
- |-------|-------|--------|-------------|
608
- """)
609
-
610
- for c in phases.get("critical-path", []):
611
- out.write(f"| [{c['title']}](skills/{c['name']}.md) | {c['model']} | {c['effort']} | {c['description']} |\n")
612
-
613
- out.write(f"""
614
- ## Utility Skills ({util_count})
615
-
616
- Supporting skills for specialised workflows.
617
-
618
- | Skill | Model | Effort | Description |
619
- |-------|-------|--------|-------------|
620
- """)
621
-
622
- for c in phases.get("utility", []):
623
- out.write(f"| [{c['title']}](skills/{c['name']}.md) | {c['model']} | {c['effort']} | {c['description']} |\n")
624
-
625
- out.write(f"""
626
- ## Quick Reference
627
-
628
- - **Total skills:** {total}
629
- - **Critical-path:** {cp_count}
630
- - **Utility:** {util_count}
631
- - **Format:** OKF concept bundles (okf_kind: concept)
632
- - **Validation:** `bash scripts/validate-okf.sh --dir specs/skills-wiki/skills`
633
- """)
634
-
635
- print(f" index.md written: {total} skills indexed", file=sys.stderr)
636
- PYEOF
637
-
638
- # Step 4: Validate OKF conformance via validate-okf.sh
639
- echo ""
640
- echo "Validating OKF conformance..."
641
- VALIDATE_SCRIPT="$REPO_ROOT/scripts/validate-okf.sh"
642
- if [[ -x "$VALIDATE_SCRIPT" ]]; then
643
- if bash "$VALIDATE_SCRIPT" --dir "$OKF_WIKI_SKILLS" 2>&1; then
644
- echo "sync-skills: OKF validation PASSED"
645
- else
646
- OKF_EXIT=$?
647
- echo "sync-skills: OKF validation returned exit code $OKF_EXIT (non-zero — check output above)" >&2
648
- # Don't hard-fail on validation warnings; the bundle is still generated
649
- fi
650
- else
651
- echo "sync-skills: WARNING — validate-okf.sh not found; skipping validation" >&2
652
- fi
653
-
654
- OKF_CONCEPT_COUNT=$(find "$OKF_WIKI_SKILLS" -name "*.md" 2>/dev/null | wc -l | tr -d ' ')
655
- echo ""
656
- echo "sync-skills (--okf): $OKF_CONCEPT_COUNT OKF concepts generated"
657
- echo " → specs/skills-wiki/skills/ ($OKF_CONCEPT_COUNT .md files)"
658
- echo " → specs/skills-wiki/index.md (progressive-disclosure index)"
659
- fi
660
-
661
- # Regression guard (BUG-2026-06-02T164500)
662
- trace_mdc="$REPO_ROOT/.cursor/rules/trace-requirement.mdc"
663
- if [[ -f "$trace_mdc" ]] && ! grep -q 'release-plan.yaml + epic' "$trace_mdc"; then
664
- echo "sync-skills: FAIL — '+' missing from trace-requirement; use sed -E for whitespace collapse" >&2
665
- exit 1
666
- fi
667
- manifest="$REPO_ROOT/.gemini/extensions/bigpowers/gemini-extension.json"
668
- if [[ -f "$manifest" ]]; then
669
- ext_ver=$(jq -r '.version // empty' "$manifest")
670
- pkg_ver=$(jq -r '.version // empty' "$REPO_ROOT/package.json")
671
- if [[ -z "$ext_ver" || -z "$pkg_ver" ]]; then
672
- : # skip version compare when either field is missing
673
- elif [[ "$ext_ver" != "$pkg_ver" ]]; then
674
- echo "sync-skills: FAIL — gemini-extension.json version ($ext_ver) != package.json ($pkg_ver)" >&2
675
- exit 1
676
- fi
677
- fi
678
-
679
- # Regression guard (BUG-2026-06-18T100000): validate generated YAML frontmatter
680
- validate_script="$REPO_ROOT/scripts/validate-skill-yaml.py"
681
- if [[ -f "$validate_script" ]] && command -v $PYTHON &>/dev/null; then
682
- if ! $PYTHON "$validate_script" > /dev/null 2>&1; then
683
- echo "sync-skills: FAIL — YAML frontmatter validation failed" >&2
684
- $PYTHON "$validate_script" >&2
685
- exit 1
686
- fi
687
- fi
688
-
689
- # Regression guard (BUG-2026-07-02T103911): no bash 4+ features
690
- if grep -rn '^declare -A\|^[[:space:]]*declare -A' scripts/sync-skills.sh scripts/generate-skill-index.sh scripts/regenerate-lockfile.sh scripts/build-skill-index.sh 2>/dev/null; then
691
- echo "sync-skills: FAIL — bash 4+ features detected in install-chain scripts (not macOS-compatible)" >&2
692
- exit 1
103
+ bash "$REPO_ROOT/scripts/okf-post-sync.sh"
693
104
  fi
694
105
 
695
- # Regenerate derived reference tables
696
- if [[ -d "$REPO_ROOT/.git" ]]; then
697
- bash "$REPO_ROOT/scripts/generate-reference-tables.sh"
698
- fi
699
-
700
- # ── OKF wiki generation (e45s01) ──────────────────────────────────────
701
- echo ""
702
- echo "Generating OKF wikis..."
703
- bash "$REPO_ROOT/scripts/generate-epics-wiki.sh"
704
- bash "$REPO_ROOT/scripts/generate-adr-wiki.sh"
705
- echo "sync-skills: OKF wikis regenerated"
706
-
106
+ sync_post_run "$skill_count" "$opencode_count"
707
107
  exit 0