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 +1 -1
- package/CHANGELOG.md +14 -0
- package/CLAUDE.md +1 -1
- package/package.json +1 -1
- package/scripts/check-catalog-drift.sh +81 -0
- package/scripts/lib/__pycache__/link_utils.cpython-312.pyc +0 -0
- package/scripts/snapshot-catalog-baseline.sh +104 -0
- package/scripts/test-tombstone-mechanism.sh +42 -0
package/.pi/package.json
CHANGED
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
|
@@ -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
|
|
Binary file
|
|
@@ -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}"
|