bigpowers 2.79.1 → 2.81.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.
package/.pi/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bigpowers",
3
- "version": "2.79.1",
3
+ "version": "2.81.0",
4
4
  "description": "80 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"
package/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ # [2.81.0](https://github.com/danielvm-git/bigpowers/compare/v2.80.0...v2.81.0) (2026-07-23)
2
+
3
+
4
+ ### Features
5
+
6
+ * **e54s02:** advisory catalog drift-detection gate ([7761f75](https://github.com/danielvm-git/bigpowers/commit/7761f75ed2312d419f3e28f192385304e36e83f2))
7
+
8
+ # [2.80.0](https://github.com/danielvm-git/bigpowers/compare/v2.79.1...v2.80.0) (2026-07-23)
9
+
10
+
11
+ ### Features
12
+
13
+ * **e54s01:** freeze skill catalog baseline snapshot ([0df0fff](https://github.com/danielvm-git/bigpowers/commit/0df0fff86fc48794a2343ae4b2c421835bc5c58c))
14
+
1
15
  ## [2.79.1](https://github.com/danielvm-git/bigpowers/compare/v2.79.0...v2.79.1) (2026-07-23)
2
16
 
3
17
 
package/CLAUDE.md CHANGED
@@ -65,7 +65,7 @@ Stack: Markdown / Bash (documentation-based; skills integrate with Claude Code,
65
65
  | Compliance | `npm run compliance` |
66
66
  | Verification Gates | `bash scripts/run-verification-gates.sh` |
67
67
  | Traceability | `bash scripts/trace-stories.sh --strict` | grep for story tags (traceability check) |
68
- | Preflight | `npm run compliance && bash scripts/run-verification-gates.sh && bash scripts/sync-skills.sh && bash scripts/trace-stories.sh --strict` | Full local green stack before forward work |
68
+ | Preflight | `npm run compliance && bash scripts/run-verification-gates.sh && bash scripts/sync-skills.sh && bash scripts/trace-stories.sh --strict && bash scripts/check-catalog-drift.sh` | Full local green stack before forward work. Trailing step is advisory only (e54s02 catalog-freeze drift check) — it always exits 0 and can never break this chain. |
69
69
  | CI | `gh pr checks` | Remote CI green when a PR is open |
70
70
 
71
71
  ### Pre-Merge Checklist
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bigpowers",
3
- "version": "2.79.1",
3
+ "version": "2.81.0",
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": {
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env bash
2
+ # story: e54s02
3
+ # check-catalog-drift.sh — advisory (Confirm-gate) diff of the live skill
4
+ # catalog against e54s01's frozen baseline. ALWAYS exits 0: this warns during
5
+ # the freeze window, it never blocks Preflight (never add to GOLDEN_GATES).
6
+ set -euo pipefail
7
+
8
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
9
+ source "$SCRIPT_DIR/lib/skill-common.sh"
10
+ source "$SCRIPT_DIR/lib/python-env.sh"
11
+
12
+ resolve_repo_root
13
+ cd "$REPO_ROOT"
14
+
15
+ BASELINE="$(ls -1 "$REPO_ROOT"/specs/tech-architecture/CATALOG-BASELINE-*.yaml 2>/dev/null | sort | tail -1 || true)"
16
+
17
+ if [[ -z "$BASELINE" ]]; then
18
+ echo "no baseline found, skipping drift check"
19
+ exit 0
20
+ fi
21
+
22
+ "$PYTHON" - "$BASELINE" "$SKILLS_ROOT" <<'PY'
23
+ import sys, glob, os, re
24
+ import yaml
25
+
26
+ baseline_path, skills_root = sys.argv[1], sys.argv[2]
27
+ baseline = yaml.safe_load(open(baseline_path)) or {}
28
+ base_by_name = {s["name"]: s for s in baseline.get("skills") or []}
29
+
30
+ EXCEPTION_MARKER = "freeze-exception"
31
+
32
+ def has_exception(skill_md):
33
+ if not os.path.isfile(skill_md):
34
+ return False
35
+ with open(skill_md, encoding="utf-8") as f:
36
+ return EXCEPTION_MARKER in f.read()
37
+
38
+ def parse_live(skill_md):
39
+ text = open(skill_md, encoding="utf-8").read()
40
+ fm = re.search(r"^---\n(.*?)\n---", text, re.S | re.M)
41
+ fields = {}
42
+ if fm:
43
+ for line in fm.group(1).splitlines():
44
+ m = re.match(r"^(phase|effort|model):\s*(.+)$", line)
45
+ if m:
46
+ fields[m.group(1)] = m.group(2).strip()
47
+ return fields
48
+
49
+ live_by_name = {}
50
+ for skill_md in sorted(glob.glob(os.path.join(skills_root, "*", "SKILL.md"))):
51
+ name = os.path.basename(os.path.dirname(skill_md))
52
+ live_by_name[name] = {"path": skill_md, **parse_live(skill_md)}
53
+
54
+ warnings = []
55
+
56
+ for name, live in live_by_name.items():
57
+ if name not in base_by_name:
58
+ if not has_exception(live["path"]):
59
+ warnings.append(f"WARNING: addition — new skill '{name}' not in frozen baseline")
60
+ continue
61
+ base = base_by_name[name]
62
+ for field in ("phase", "effort", "model"):
63
+ base_val = base.get(field)
64
+ base_val = None if base_val in ("null", None) else str(base_val)
65
+ live_val = live.get(field)
66
+ if base_val != live_val:
67
+ if not has_exception(live["path"]):
68
+ warnings.append(
69
+ f"WARNING: structural change — '{name}' field '{field}' "
70
+ f"changed from {base_val!r} to {live_val!r}"
71
+ )
72
+
73
+ for name, base in base_by_name.items():
74
+ if name not in live_by_name:
75
+ warnings.append(f"WARNING: removal — skill '{name}' present in baseline but missing from live catalog")
76
+
77
+ for w in warnings:
78
+ print(w)
79
+ PY
80
+
81
+ exit 0
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env bash
2
+ # story: e54s01
3
+ # snapshot-catalog-baseline.sh — freeze the live skill catalog into a dated,
4
+ # immutable YAML baseline for e56's reclassification diff.
5
+ set -euo pipefail
6
+
7
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8
+ source "$SCRIPT_DIR/lib/skill-common.sh"
9
+ source "$SCRIPT_DIR/lib/python-env.sh"
10
+
11
+ resolve_repo_root
12
+ cd "$REPO_ROOT"
13
+
14
+ DRY_RUN=false
15
+ while [[ $# -gt 0 ]]; do
16
+ case "$1" in
17
+ --dry-run) DRY_RUN=true; shift ;;
18
+ -h|--help) echo "Usage: bash scripts/snapshot-catalog-baseline.sh [--dry-run]"; exit 0 ;;
19
+ *) echo "Unknown flag: $1" >&2; exit 2 ;;
20
+ esac
21
+ done
22
+
23
+ DATE_STR="$(date +%Y-%m-%d)"
24
+ COMMIT_HASH="$(git rev-parse HEAD 2>/dev/null || echo unknown)"
25
+ OUT_FILE="$REPO_ROOT/specs/tech-architecture/CATALOG-BASELINE-${DATE_STR}.yaml"
26
+
27
+ # unquote_source_desc — strip a source frontmatter's own surrounding quotes
28
+ # and unescape its inner \" , or re-quoting double-escapes the description.
29
+ unquote_source_desc() {
30
+ local desc="$1"
31
+ if [[ "$desc" == \"*\" ]]; then
32
+ desc="${desc#\"}"
33
+ desc="${desc%\"}"
34
+ desc="${desc//\\\"/\"}"
35
+ fi
36
+ echo "$desc"
37
+ }
38
+
39
+ # emit_skill_row — print one skill's YAML row to stdout; appends any missing-
40
+ # field warning line for that skill to $WARN_FILE (a plain file, since this
41
+ # function runs inside a command-substitution subshell and can't mutate the
42
+ # caller's shell variables).
43
+ emit_skill_row() {
44
+ local skill_md="$1" name phase model effort desc desc_escaped
45
+ name="$(basename "$(dirname "$skill_md")")"
46
+ parse_frontmatter_okf "$skill_md" || true
47
+
48
+ phase="$(awk '/^---/{f++} f==1 && /^phase:/{print; exit}' "$skill_md" | sed 's/^phase:[[:space:]]*//')"
49
+ model="${_PF_MODEL:-}"
50
+ effort="${_PF_EFFORT:-}"
51
+ desc="${_PF_DESCRIPTION:-}"
52
+
53
+ [[ -z "$phase" ]] && phase="null"
54
+ [[ -z "$model" ]] && { model="null"; echo " - \"${name}: missing model field\"" >> "$WARN_FILE"; }
55
+ [[ -z "$effort" ]] && { effort="null"; echo " - \"${name}: missing effort field\"" >> "$WARN_FILE"; }
56
+ [[ -z "$desc" ]] && desc="null"
57
+
58
+ desc="$(unquote_source_desc "$desc")"
59
+ desc_escaped="${desc//\"/\\\"}"
60
+
61
+ echo " - name: \"${name}\"\n path: \"skills/${name}/SKILL.md\"\n phase: ${phase}\n effort: ${effort}\n model: ${model}\n description: \"${desc_escaped}\"\n"
62
+ }
63
+
64
+ WARN_FILE="$(mktemp)"
65
+ trap 'rm -f "$WARN_FILE"' EXIT
66
+
67
+ ROWS=""
68
+ COUNT=0
69
+
70
+ for skill_md in "$SKILLS_ROOT"/*/SKILL.md; do
71
+ [[ -f "$skill_md" ]] || continue
72
+ ROWS="${ROWS}$(emit_skill_row "$skill_md")"
73
+ COUNT=$((COUNT + 1))
74
+ done
75
+
76
+ WARNINGS="$(cat "$WARN_FILE")"
77
+ WARN_COUNT="$(grep -c '^ -' "$WARN_FILE" || true)"
78
+
79
+ if $DRY_RUN; then
80
+ echo "${COUNT} skills scanned"
81
+ echo "${WARN_COUNT} warnings"
82
+ [[ -n "$WARNINGS" ]] && echo -e "warnings:\n${WARNINGS}"
83
+ exit 0
84
+ fi
85
+
86
+ mkdir -p "$REPO_ROOT/specs/tech-architecture"
87
+
88
+ {
89
+ echo "# CATALOG-BASELINE-${DATE_STR}.yaml"
90
+ echo "# frozen for e54-e59 migration; do not hand-edit"
91
+ echo "snapshot_date: \"${DATE_STR}\""
92
+ echo "commit_hash: \"${COMMIT_HASH}\""
93
+ echo "total_skill_count: ${COUNT}"
94
+ echo "warnings:"
95
+ if [[ -n "$WARNINGS" ]]; then
96
+ echo -e "$WARNINGS"
97
+ else
98
+ echo " []"
99
+ fi
100
+ echo "skills:"
101
+ echo -e "$ROWS"
102
+ } > "$OUT_FILE"
103
+
104
+ echo "Wrote ${OUT_FILE} (${COUNT} skills, ${WARN_COUNT} warnings)"
@@ -102,6 +102,48 @@ else
102
102
  tt_fail "expired tombstone was not flagged"
103
103
  fi
104
104
 
105
+ # --- Scenario: rejects path-traversal / invalid skill names (threat model CWE-22) ---
106
+ # tombstone-skill.sh exits non-zero on rejection, and this script runs under
107
+ # `set -o pipefail` — capture output via `|| true` first so the pipe's exit
108
+ # status doesn't mask the grep check below.
109
+ output=$(bash scripts/tombstone-skill.sh "../../etc/passwd" "target" 2>&1 || true)
110
+ if echo "$output" | grep -qi "not a valid skill name"; then
111
+ tt_pass "tombstone-skill.sh rejects a path-traversal old-name"
112
+ else
113
+ tt_fail "tombstone-skill.sh did not reject a path-traversal old-name"
114
+ fi
115
+
116
+ output=$(bash scripts/tombstone-skill.sh "$FIXTURE_OLD" "../../etc/passwd" 2>&1 || true)
117
+ if echo "$output" | grep -qi "not a valid skill name"; then
118
+ tt_pass "tombstone-skill.sh rejects a path-traversal new-name"
119
+ else
120
+ tt_fail "tombstone-skill.sh did not reject a path-traversal new-name"
121
+ fi
122
+
123
+ # --- Scenario: validate-tombstones.sh rejects an invalid name read back from the ledger ---
124
+ python3 -c "
125
+ import yaml
126
+ d = yaml.safe_load(open('$TOMBSTONES_FILE')) or {'tombstones': []}
127
+ d.setdefault('tombstones', []).append({
128
+ 'old_name': '../../etc/passwd',
129
+ 'new_name_or_merge_target': 'target',
130
+ 'created_at_version': '0.0.0',
131
+ })
132
+ yaml.dump(d, open('$TOMBSTONES_FILE', 'w'), default_flow_style=False)
133
+ "
134
+ output=$(bash scripts/validate-tombstones.sh 2>&1 | strip_ansi || true)
135
+ if echo "$output" | grep -qi "FAIL.*not a valid skill name"; then
136
+ tt_pass "validate-tombstones.sh rejects a path-traversal name read from the ledger"
137
+ else
138
+ tt_fail "validate-tombstones.sh did not reject a path-traversal name read from the ledger"
139
+ fi
140
+ python3 -c "
141
+ import yaml
142
+ d = yaml.safe_load(open('$TOMBSTONES_FILE')) or {'tombstones': []}
143
+ d['tombstones'] = [t for t in d.get('tombstones', []) if t.get('old_name') != '../../etc/passwd']
144
+ yaml.dump(d, open('$TOMBSTONES_FILE', 'w'), default_flow_style=False)
145
+ "
146
+
105
147
  echo "---"
106
148
  if [[ "$FAILURES" -eq 0 ]]; then
107
149
  echo -e "${GREEN}test-tombstone-mechanism: ALL PASS${NC}"