bigpowers 2.63.0 → 2.64.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 (40) hide show
  1. package/.pi/package.json +1 -1
  2. package/.pi/prompts/build-epic.md +2 -1
  3. package/.pi/prompts/develop-tdd.md +4 -0
  4. package/.pi/prompts/investigate-bug.md +5 -0
  5. package/.pi/prompts/plan-refactor.md +5 -0
  6. package/.pi/prompts/run-benchmark.md +67 -48
  7. package/.pi/prompts/security-review.md +4 -0
  8. package/.pi/prompts/setup-environment.md +15 -0
  9. package/.pi/prompts/wire-observability.md +4 -0
  10. package/.pi/skills/build-epic/SKILL.md +2 -1
  11. package/.pi/skills/develop-tdd/SKILL.md +4 -0
  12. package/.pi/skills/investigate-bug/SKILL.md +5 -0
  13. package/.pi/skills/plan-refactor/SKILL.md +5 -0
  14. package/.pi/skills/run-benchmark/SKILL.md +67 -48
  15. package/.pi/skills/security-review/SKILL.md +4 -0
  16. package/.pi/skills/setup-environment/SKILL.md +15 -0
  17. package/.pi/skills/wire-observability/SKILL.md +4 -0
  18. package/CHANGELOG.md +17 -0
  19. package/CLAUDE.md +4 -0
  20. package/CONVENTIONS.md +10 -0
  21. package/README.md +6 -0
  22. package/SKILL-INDEX.md +1 -1
  23. package/package.json +5 -2
  24. package/scripts/build-receipts.sh +107 -0
  25. package/scripts/build-skill-graph.sh +226 -0
  26. package/scripts/build-skill-index.sh +10 -20
  27. package/scripts/generate-skill-index.sh +2 -4
  28. package/scripts/record-cycle-time.sh +6 -4
  29. package/scripts/regenerate-lockfile.sh +10 -38
  30. package/scripts/sync-skills.sh +106 -79
  31. package/scripts/validate-okf.sh +28 -0
  32. package/skills/build-epic/SKILL.md +2 -1
  33. package/skills/develop-tdd/SKILL.md +4 -0
  34. package/skills/investigate-bug/SKILL.md +5 -0
  35. package/skills/plan-refactor/SKILL.md +5 -0
  36. package/skills/run-benchmark/SKILL.md +67 -49
  37. package/skills/security-review/SKILL.md +4 -0
  38. package/skills/setup-environment/SKILL.md +15 -0
  39. package/skills/wire-observability/SKILL.md +4 -0
  40. package/skills-lock.json +83 -83
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env bash
2
+ # story: e41s01
3
+ # build-receipts.sh — aggregate all quality evidence into specs/receipts.json.
4
+ set -euo pipefail
5
+ ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" || { echo "build-receipts: not inside a git repo" >&2; exit 1; }
6
+ OUTFILE="$ROOT/specs/receipts.json"
7
+ TIMESTAMP="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
8
+ emit_json() {
9
+ python3 -c "
10
+ import json, sys, os, subprocess, glob as gb
11
+ sections = {}
12
+
13
+ # compliance
14
+ comp = '$ROOT/specs/verifications/features/'
15
+ if os.path.isdir(comp):
16
+ try:
17
+ r = subprocess.run(['bash','scripts/audit-compliance.sh','specs/verifications/features/','--dry-run'],
18
+ cwd='$ROOT', capture_output=True, text=True, timeout=30)
19
+ score = None
20
+ for line in r.stdout.splitlines():
21
+ if 'SCORE:' in line and '%' in line:
22
+ p = line.split('SCORE:')[1].strip()
23
+ s = p.split('%')[0].strip()
24
+ try: score = int(s)
25
+ except ValueError: pass
26
+ break
27
+ if score is not None:
28
+ sections['compliance'] = {'value':score,'generated_at':'$TIMESTAMP','source':'measured','command':'bash scripts/audit-compliance.sh specs/verifications/features/'}
29
+ else:
30
+ sections['compliance'] = {'source':'absent','generated_at':'$TIMESTAMP','command':'bash scripts/audit-compliance.sh specs/verifications/features/'}
31
+ except Exception as e:
32
+ print(f'build-receipts: compliance error: {e}', file=sys.stderr)
33
+ sections['compliance'] = {'source':'absent','generated_at':'$TIMESTAMP','command':'bash scripts/audit-compliance.sh specs/verifications/features/'}
34
+ else:
35
+ sections['compliance'] = {'source':'absent','generated_at':'$TIMESTAMP','command':'bash scripts/audit-compliance.sh specs/verifications/features/'}
36
+
37
+ # golden
38
+ gd = '$ROOT/specs/benchmarks/reports/'
39
+ if os.path.isdir(gd):
40
+ reports = sorted(gb.glob(os.path.join(gd, 'GOLDEN-*.yaml')))
41
+ if reports:
42
+ try:
43
+ with open(reports[-1]) as f:
44
+ import yaml; data = yaml.safe_load(f)
45
+ sm = data.get('summary',{})
46
+ sections['golden_suite'] = {'value':{'pass_rate':sm.get('pass_rate'),'passed':sm.get('passed'),'total':sm.get('total'),'overall':sm.get('overall','unknown'),'timestamp':data.get('timestamp',''),'commit':data.get('git_commit','')},'generated_at':'$TIMESTAMP','source':'measured','command':'specs/benchmarks/reports/GOLDEN-*.yaml (latest)'}
47
+ except Exception as e:
48
+ print(f'build-receipts: golden error: {e}', file=sys.stderr)
49
+ sections['golden_suite'] = {'source':'absent','generated_at':'$TIMESTAMP','command':'specs/benchmarks/reports/GOLDEN-*.yaml'}
50
+ else:
51
+ sections['golden_suite'] = {'source':'absent','generated_at':'$TIMESTAMP','command':'specs/benchmarks/reports/GOLDEN-*.yaml'}
52
+ else:
53
+ sections['golden_suite'] = {'source':'absent','generated_at':'$TIMESTAMP','command':'specs/benchmarks/reports/GOLDEN-*.yaml'}
54
+
55
+ # metrics
56
+ md = '$ROOT/specs/metrics/'
57
+ if os.path.isdir(md):
58
+ mfs = sorted(gb.glob(os.path.join(md, '*.okf.md')))
59
+ sc = 0; st = {'measured':0,'estimated':0,'backfilled':0}; te = 0.0; tb = 0
60
+ for mf in mfs:
61
+ try:
62
+ with open(mf) as f: content = f.read()
63
+ parts = content.split('---')
64
+ if len(parts) < 3: continue
65
+ import yaml; fm = yaml.safe_load(parts[1])
66
+ if fm is None or fm.get('okf_kind') != 'story-metrics': continue
67
+ sc += 1; src = fm.get('source','absent')
68
+ if src in st: st[src] += 1
69
+ e = (fm.get('effort') or {}).get('effort_hours')
70
+ if e is not None:
71
+ try: te += float(e)
72
+ except: pass
73
+ b = fm.get('bcps')
74
+ if b is not None:
75
+ try: tb += int(b)
76
+ except: pass
77
+ except: pass
78
+ if sc > 0:
79
+ sections['metrics'] = {'value':{'story_count':sc,'total_effort_hours':round(te,1),'total_bcps':tb,'source_breakdown':st},'generated_at':'$TIMESTAMP','source':'measured','command':'specs/metrics/*.okf.md'}
80
+ else:
81
+ sections['metrics'] = {'source':'absent','generated_at':'$TIMESTAMP','command':'specs/metrics/*.okf.md'}
82
+ else:
83
+ sections['metrics'] = {'source':'absent','generated_at':'$TIMESTAMP','command':'specs/metrics/*.okf.md'}
84
+
85
+ # traceability
86
+ tf = '$ROOT/specs/traceability-matrix.json'
87
+ if os.path.isfile(tf):
88
+ try:
89
+ with open(tf) as f: matrix = json.load(f)
90
+ stories = matrix.get('stories',[])
91
+ ts = len(stories); ls = sum(1 for s in stories if s.get('link_count',0)>0); lc = sum(s.get('link_count',0) for s in stories)
92
+ sections['traceability'] = {'value':{'total_stories':ts,'linked_stories':ls,'total_links':lc,'coverage_pct':round(ls/ts*100,1) if ts>0 else 0,'generated_at':matrix.get('generated_at','')},'generated_at':'$TIMESTAMP','source':'measured','command':'specs/traceability-matrix.json (trace-matrix.py)'}
93
+ except Exception as e:
94
+ print(f'build-receipts: trace error: {e}', file=sys.stderr)
95
+ sections['traceability'] = {'source':'absent','generated_at':'$TIMESTAMP','command':'specs/traceability-matrix.json'}
96
+ else:
97
+ sections['traceability'] = {'source':'absent','generated_at':'$TIMESTAMP','command':'specs/traceability-matrix.json'}
98
+
99
+ with open('$OUTFILE','w') as f: json.dump(sections,f,indent=2)
100
+ print(f'build-receipts: wrote {len(sections)} sections to specs/receipts.json')
101
+ for key, sec in sections.items(): print(f' {key}: source={sec.get(\"source\",\"?\")}')
102
+ "
103
+ }
104
+ echo "build-receipts: aggregating quality evidence..."
105
+ emit_json
106
+ echo "build-receipts: done — $(date -u +%H:%M:%SZ)"
107
+ exit 0
@@ -0,0 +1,226 @@
1
+ #!/usr/bin/env bash
2
+ # story: e39s01
3
+ # Consumes bigpowers-mcp build_skill_graph MCP tool output (graph.jsonl)
4
+ # and emits specs/skill-graph.json + specs/skills-wiki/ OKF concepts.
5
+ #
6
+ # Supersedes standalone SKILL.md parsing approach (2026-07-04).
7
+ # Graph must be built first: run MCP tool `build_skill_graph` or
8
+ # `node -e "..."` to generate bigpowers-mcp/graph.jsonl.
9
+
10
+ set -euo pipefail
11
+
12
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
13
+ REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
14
+ GRAPH_IN="$REPO_ROOT/bigpowers-mcp/graph.jsonl"
15
+ GRAPH_OUT="$REPO_ROOT/specs/skill-graph.json"
16
+ WIKI_DIR="$REPO_ROOT/specs/skills-wiki/skills"
17
+
18
+ # --help
19
+ if [[ "${1:-}" == "--help" ]]; then
20
+ cat <<EOF
21
+ Usage: build-skill-graph.sh
22
+
23
+ Reads bigpowers-mcp/graph.jsonl (generated by e32 build_skill_graph MCP tool)
24
+ and produces:
25
+ specs/skill-graph.json — nodes + typed edges (depends_on, gates, references,
26
+ enforces, handoff_to)
27
+ specs/skills-wiki/ — one OKF concept markdown file per skill
28
+
29
+ Options:
30
+ --help Show this message
31
+ --json Write only specs/skill-graph.json (skip OKF wiki)
32
+ --wiki Write only specs/skills-wiki/ (skip JSON graph)
33
+
34
+ Requires: bigpowers-mcp/graph.jsonl (run build_skill_graph MCP tool first)
35
+ EOF
36
+ exit 0
37
+ fi
38
+
39
+ MODE="${1:-both}"
40
+ case "$MODE" in
41
+ --json) DO_JSON=1; DO_WIKI=0 ;;
42
+ --wiki) DO_JSON=0; DO_WIKI=1 ;;
43
+ both) DO_JSON=1; DO_WIKI=1 ;;
44
+ *) echo "Unknown option: $MODE" >&2; exit 1 ;;
45
+ esac
46
+
47
+ # Pre-flight: graph.jsonl must exist
48
+ if [[ ! -f "$GRAPH_IN" ]]; then
49
+ cat >&2 <<EOF
50
+ ERROR: $GRAPH_IN not found.
51
+
52
+ The skill graph has not been built yet. Run the bigpowers-mcp
53
+ build_skill_graph tool to generate it:
54
+
55
+ # Option A: via MCP inspector
56
+ npx @modelcontextprotocol/inspector node bigpowers-mcp/build/index.js
57
+ # Then call build_skill_graph
58
+
59
+ # Option B: via Node.js directly
60
+ node -e "
61
+ const {discoverSkills,readSkillFile}=require('./bigpowers-mcp/build/lib/skill-parser.js');
62
+ const {buildGraphFromSkills}=require('./bigpowers-mcp/build/lib/graph/builder.js');
63
+ const {saveGraph}=require('./bigpowers-mcp/build/lib/graph/store.js');
64
+ const skills=discoverSkills(process.cwd(),()=>'');
65
+ const parsed=skills.map(s=>readSkillFile(process.cwd(),s.name));
66
+ saveGraph('bigpowers-mcp/graph.jsonl', buildGraphFromSkills(parsed));
67
+ "
68
+ EOF
69
+ exit 1
70
+ fi
71
+
72
+ # Parse graph.jsonl into nodes + edges
73
+ echo "Reading $GRAPH_IN ..." >&2
74
+
75
+ NODES_JSON="$REPO_ROOT/specs/.tmp-nodes.json"
76
+ EDGES_JSON="$REPO_ROOT/specs/.tmp-edges.json"
77
+
78
+ python3 - "$GRAPH_IN" "$NODES_JSON" "$EDGES_JSON" <<'PYEOF'
79
+ import json, sys
80
+ from pathlib import Path
81
+
82
+ graph_in = Path(sys.argv[1])
83
+ nodes_out = Path(sys.argv[2])
84
+ edges_out = Path(sys.argv[3])
85
+
86
+ nodes = []
87
+ edges = []
88
+ entity_count = 0
89
+ relation_count = 0
90
+ parse_errors = 0
91
+
92
+ with open(graph_in) as f:
93
+ for lineno, line in enumerate(f, 1):
94
+ line = line.strip()
95
+ if not line:
96
+ continue
97
+ try:
98
+ obj = json.loads(line)
99
+ except json.JSONDecodeError as e:
100
+ print(f"WARNING: {graph_in}:{lineno}: invalid JSON — {e}", file=sys.stderr)
101
+ parse_errors += 1
102
+ continue
103
+
104
+ if obj.get("type") == "entity":
105
+ nodes.append({
106
+ "name": obj["name"],
107
+ "entityType": obj.get("entityType", "Skill"),
108
+ "observations": obj.get("observations", []),
109
+ })
110
+ entity_count += 1
111
+ elif obj.get("type") == "relation":
112
+ edges.append({
113
+ "from": obj["from"],
114
+ "to": obj["to"],
115
+ "relationType": obj.get("relationType", "unknown"),
116
+ })
117
+ relation_count += 1
118
+ else:
119
+ print(f"WARNING: {graph_in}:{lineno}: unknown type {obj.get('type')}", file=sys.stderr)
120
+
121
+ with open(nodes_out, "w") as f:
122
+ json.dump(nodes, f, indent=2)
123
+ with open(edges_out, "w") as f:
124
+ json.dump(edges, f, indent=2)
125
+
126
+ print(f" Entities: {entity_count}, Relations: {relation_count}", file=sys.stderr)
127
+ if parse_errors:
128
+ print(f" Parse errors: {parse_errors}", file=sys.stderr)
129
+ PYEOF
130
+
131
+ # Emit specs/skill-graph.json
132
+ if [[ "$DO_JSON" -eq 1 ]]; then
133
+ echo "Writing $GRAPH_OUT ..." >&2
134
+ python3 - "$NODES_JSON" "$EDGES_JSON" "$GRAPH_OUT" <<'PYEOF'
135
+ import json, sys
136
+
137
+ with open(sys.argv[1]) as f:
138
+ nodes = json.load(f)
139
+ with open(sys.argv[2]) as f:
140
+ edges = json.load(f)
141
+
142
+ graph = {"nodes": nodes, "edges": edges}
143
+ with open(sys.argv[3], "w") as f:
144
+ json.dump(graph, f, indent=2)
145
+
146
+ print(f" nodes: {len(nodes)}, edges: {len(edges)}", file=sys.stderr)
147
+ PYEOF
148
+ fi
149
+
150
+ # Emit specs/skills-wiki/ OKF concepts
151
+ if [[ "$DO_WIKI" -eq 1 ]]; then
152
+ echo "Writing $WIKI_DIR/ ..." >&2
153
+ rm -rf "$WIKI_DIR"
154
+ mkdir -p "$WIKI_DIR"
155
+
156
+ python3 - "$NODES_JSON" "$WIKI_DIR" <<'PYEOF'
157
+ import json, sys, re
158
+ from pathlib import Path
159
+
160
+ nodes_file = Path(sys.argv[1])
161
+ wiki_dir = Path(sys.argv[2])
162
+
163
+ with open(nodes_file) as f:
164
+ nodes = json.load(f)
165
+
166
+ count = 0
167
+ for node in nodes:
168
+ name = node["name"]
169
+ observations = node.get("observations", [])
170
+
171
+ model = ""
172
+ effort = ""
173
+ description = ""
174
+ for obs in observations:
175
+ if obs.startswith("model: "):
176
+ model = obs[7:]
177
+ elif obs.startswith("effort: "):
178
+ effort = obs[8:]
179
+ elif obs.startswith("description: "):
180
+ description = obs[13:]
181
+
182
+ # Derive phase from name convention (simplified)
183
+ phase = "utility"
184
+ critical_path = [
185
+ "survey-context", "elaborate-spec", "model-domain", "define-language",
186
+ "design-interface", "plan-work", "plan-release", "slice-tasks",
187
+ "kickoff-branch", "develop-tdd", "verify-work", "audit-code",
188
+ "commit-message", "release-branch", "security-review",
189
+ "assess-impact", "investigate-bug", "validate-fix",
190
+ ]
191
+ if name in critical_path:
192
+ phase = "critical-path"
193
+
194
+ safe_desc = description.replace('"', '\\"')
195
+ md = f"""---
196
+ type: Skill
197
+ name: {name}
198
+ phase: {phase}
199
+ model: {model}
200
+ effort: {effort}
201
+ description: "{safe_desc}"
202
+ ---
203
+
204
+ # {name}
205
+
206
+ **Phase:** {phase}
207
+ **Model:** {model}
208
+ **Effort:** {effort}
209
+
210
+ {description}
211
+
212
+ > Auto-generated by build-skill-graph.sh (e39s01) from bigpowers-mcp/graph.jsonl.
213
+ > Source of truth: skills/{name}/SKILL.md
214
+ """
215
+ out_path = wiki_dir / f"{name}.md"
216
+ out_path.write_text(md)
217
+ count += 1
218
+
219
+ print(f" concepts written: {count}", file=sys.stderr)
220
+ PYEOF
221
+ fi
222
+
223
+ # Cleanup
224
+ rm -f "$NODES_JSON" "$EDGES_JSON"
225
+
226
+ echo "Done." >&2
@@ -1,28 +1,18 @@
1
1
  #!/usr/bin/env bash
2
- # build-skill-index.sh — lexical index for search-skills (zero embedding deps)
2
+ # story: e28s04
3
+ # build-skill-index.sh — lexical index for search-skills
3
4
  set -euo pipefail
4
-
5
- REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ source "$(dirname "${BASH_SOURCE[0]}")/lib/skill-common.sh"
6
6
  OUT="$REPO_ROOT/specs/SKILL-SEARCH-INDEX_LATEST.md"
7
7
  mkdir -p "$(dirname "$OUT")"
8
-
9
- {
10
- echo "# Skill Search Index (auto-generated)"
11
- echo ""
12
- echo "Regenerate: \`bash scripts/build-skill-index.sh\`"
13
- echo ""
14
- echo "| name | model | description |"
15
- echo "|------|-------|-------------|"
16
- for skill_dir in $(find "$REPO_ROOT" -maxdepth 1 -mindepth 1 -type d | sort); do
8
+ { echo "# Skill Search Index (auto-generated)"; echo ""; echo "Regenerate: \`bash scripts/build-skill-index.sh\`"; echo ""; echo "| name | model | description |"; echo "|------|-------|-------------|"
9
+ while IFS= read -r skill_dir; do
17
10
  skill_md="$skill_dir/SKILL.md"
18
- [[ -f "$skill_md" ]] || continue
19
- name=$(awk '/^---/{f++} f==1 && /^name:/{print; exit}' "$skill_md" | sed 's/^name:[[:space:]]*//')
20
- model=$(awk '/^---/{f++} f==1 && /^model:/{print; exit}' "$skill_md" | sed 's/^model:[[:space:]]*//')
21
- desc=$(awk '/^---/{f++} f==1 && /^description:/{sub(/^description:[[:space:]]*/,""); print; exit}' "$skill_md" \
22
- | tr -d '\n' | sed 's/|/\\|/g' | LC_ALL=C head -c 200)
11
+ parse_frontmatter "$skill_md" || continue
12
+ name="$_PF_NAME"; model="${_PF_MODEL:-sonnet}"
13
+ desc=$(echo "$_PF_DESCRIPTION" | sed 's/|/\\|/g' | LC_ALL=C head -c 200)
23
14
  [[ -z "$name" ]] && continue
24
- echo "| $name | ${model:-sonnet} | $desc |"
25
- done
15
+ echo "| $name | $model | $desc |"
16
+ done < <(iterate_skills)
26
17
  } > "$OUT"
27
-
28
18
  echo "build-skill-index: wrote $OUT"
@@ -5,12 +5,10 @@
5
5
  # DO NOT edit SKILL-INDEX.md manually — it is auto-generated.
6
6
  set -euo pipefail
7
7
 
8
- REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
8
+ source "$(dirname "${BASH_SOURCE[0]}")/lib/skill-common.sh"
9
+
9
10
  LOCKFILE="$REPO_ROOT/skills-lock.json"
10
11
  INDEX_FILE="$REPO_ROOT/SKILL-INDEX.md"
11
- # SKILLS_ROOT: use skills/ when it exists, fall back to repo root
12
- SKILLS_ROOT="$REPO_ROOT"
13
- [[ -d "$REPO_ROOT/skills" ]] && SKILLS_ROOT="$REPO_ROOT/skills"
14
12
 
15
13
  if [[ ! -f "$LOCKFILE" ]]; then
16
14
  echo "generate-skill-index: ERROR — skills-lock.json not found. Run regenerate-lockfile.sh first." >&2
@@ -37,7 +37,7 @@
37
37
  # Usage:
38
38
  # scripts/record-cycle-time.sh report [--range <git-range>] [--story-key <key>]
39
39
  # scripts/record-cycle-time.sh append --story <id> --bcps <n> \
40
- # [--range <git-range>] [--merged-at <epoch>] [--file <path>] [--story-key <key>]
40
+ # [--bcp-plus <n>] [--range <git-range>] [--merged-at <epoch>] [--file <path>] [--story-key <key>]
41
41
  #
42
42
  # `report` prints the per-story partition + an additivity self-check.
43
43
  # `append` recomputes the partition and appends one row for <id> to the ledger.
@@ -59,9 +59,7 @@ Usage: scripts/record-cycle-time.sh <command> [flags]
59
59
  Commands:
60
60
  report Compute per-story effort partition + additivity self-check.
61
61
  Flags: --range <git-range> --story-key <trailer-key>
62
- append Recompute partition and append one row for a story.
63
- Flags: --story <id> --bcps <n> [--range <git-range>]
64
- [--merged-at <epoch>] [--file <path>] [--story-key <trailer-key>]
62
+ append Recompute partition and append one row for a story.\n Flags: --story <id> --bcps <n> [--bcp-plus <n>] [--range <git-range>]\n [--merged-at <epoch>] [--file <path>] [--story-key <trailer-key>]
65
63
  self-test Run report, then exit 0 on additivity PASS; exit 1 on FAIL.
66
64
  Flags: --range <git-range> --story-key <trailer-key>
67
65
  help Show this message.
@@ -133,6 +131,7 @@ cmd_append() {
133
131
  case "$1" in
134
132
  --story) story="$2"; shift 2 ;;
135
133
  --bcps) bcps="$2"; shift 2 ;;
134
+ --bcp-plus) bcp_plus="$2"; shift 2 ;;
136
135
  --range) range="$2"; shift 2 ;;
137
136
  --merged-at) merged_at="$2"; shift 2 ;;
138
137
  --file) file="$2"; shift 2 ;;
@@ -216,6 +215,9 @@ cmd_append() {
216
215
  printf -- 'id: %s\n' "$story"
217
216
  printf -- 'epic: %s\n' "${story%%s*}" # e40s03 → e40
218
217
  printf -- 'bcps: %s\n' "$bcps"
218
+ if [ -n "$bcp_plus" ]; then
219
+ printf -- 'bcp_plus: %s\n' "$bcp_plus"
220
+ fi
219
221
  printf -- 'commit_range: "%s..%s"\n' "${range_lo:-$range_hi}" "$range_hi"
220
222
  printf -- 'source: measured\n'
221
223
  printf -- 'generated_at: %s\n' "$generated_at"
@@ -1,49 +1,21 @@
1
1
  #!/usr/bin/env bash
2
+ # story: e28s04
2
3
  # regenerate-lockfile.sh — Scan all SKILL.md files and regenerate skills-lock.json
3
- # Output: Canonical catalog with name, description, sha256, path for every skill.
4
- # Run this after adding, removing, or updating any skill.
5
4
  set -euo pipefail
6
-
7
- REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ source "$(dirname "${BASH_SOURCE[0]}")/lib/skill-common.sh"
8
6
  LOCKFILE="$REPO_ROOT/skills-lock.json"
9
- # SKILLS_ROOT: use skills/ when it exists, fall back to repo root
10
- SKILLS_ROOT="$REPO_ROOT"
11
- [[ -d "$REPO_ROOT/skills" ]] && SKILLS_ROOT="$REPO_ROOT/skills"
12
-
13
- # Build JSON using jq from individual skill records
14
- # We pipe each skill as a JSON object, then jq -s merges them into an object keyed by name
15
7
  records=()
16
- for skill_dir in "$SKILLS_ROOT"/*/; do
8
+ while IFS= read -r skill_dir; do
17
9
  skill_md="$skill_dir/SKILL.md"
18
- [[ -f "$skill_md" ]] || continue
19
-
20
10
  name=$(basename "$skill_dir")
21
- # Extract description from YAML frontmatter (between first --- and second ---)
22
- description=$(awk '/^---/{f++; next} f==1 && /^description:/{p=1; sub(/^description:[[:space:]]*/,""); print; next} f==1 && p && /^[a-z]+:/{exit} f==1 && p{print}' "$skill_md" \
23
- | tr -d '\n' | sed -E 's/[[:space:]]+/ /g')
24
-
25
- # Compute SHA-256 of full SKILL.md content (first 16 hex chars for compact uniqueness)
26
- # Falls back to shasum for macOS where sha256sum is not installed by default
27
- if command -v sha256sum &>/dev/null; then
28
- sha256=$(sha256sum "$skill_md" | cut -c1-16)
29
- else
30
- sha256=$(shasum -a 256 "$skill_md" | cut -c1-16)
31
- fi
32
-
33
- # Relative path from repo root
11
+ parse_frontmatter "$skill_md" || continue
12
+ description="$_PF_DESCRIPTION"
13
+ if command -v sha256sum &>/dev/null; then sha256=$(sha256sum "$skill_md" | cut -c1-16)
14
+ else sha256=$(shasum -a 256 "$skill_md" | cut -c1-16); fi
34
15
  relpath="${skill_dir#$REPO_ROOT/}SKILL.md"
35
-
36
- # Emit JSON record for this skill
37
- records+=("$(jq -n --arg name "$name" \
38
- --arg desc "$description" \
39
- --arg sha "$sha256" \
40
- --arg path "$relpath" \
16
+ records+=("$(jq -n --arg name "$name" --arg desc "$description" --arg sha "$sha256" --arg path "$relpath" \
41
17
  '{($name): {description: $desc, sha256: $sha, path: $path}}')")
42
- done
43
-
44
- # Merge all records into a single "skills" object with version
45
- jq -n --argjson skills "$(printf '%s\n' "${records[@]}" | jq -s 'add')" \
46
- '{version: 1, skills: $skills}' > "$LOCKFILE"
47
-
18
+ done < <(iterate_skills)
19
+ jq -n --argjson skills "$(printf '%s\n' "${records[@]}" | jq -s 'add')" '{version: 1, skills: $skills}' > "$LOCKFILE"
48
20
  count=$(jq '.skills | length' "$LOCKFILE")
49
21
  echo "regenerate-lockfile: $count skills written to skills-lock.json"