bigpowers 2.77.1 → 2.77.2
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 +7 -0
- package/package.json +1 -1
- package/scripts/lib/__pycache__/link_utils.cpython-312.pyc +0 -0
- package/scripts/lib/simple_yaml.py +64 -13
- package/scripts/lib/srp-engine.py +15 -4
- package/scripts/sync-skills.sh +1 -1
- package/scripts/test-setup-no-pyyaml.sh +83 -0
- package/scripts/validate-skill-yaml.py +12 -5
package/.pi/package.json
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
## [2.77.2](https://github.com/danielvm-git/bigpowers/compare/v2.77.1...v2.77.2) (2026-07-19)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **scripts:** fall back to simple_yaml when PyYAML is absent during setup ([#81](https://github.com/danielvm-git/bigpowers/issues/81)) ([2608c64](https://github.com/danielvm-git/bigpowers/commit/2608c645e1143f1bbf96c3174341b107c672ae8e))
|
|
7
|
+
|
|
1
8
|
## [2.77.1](https://github.com/danielvm-git/bigpowers/compare/v2.77.0...v2.77.1) (2026-07-19)
|
|
2
9
|
|
|
3
10
|
|
package/package.json
CHANGED
|
Binary file
|
|
@@ -7,20 +7,30 @@ def parse_simple_yaml(text: str) -> dict:
|
|
|
7
7
|
root: dict = {}
|
|
8
8
|
# Each stack entry: (indent, container, parent_dict, key_in_parent)
|
|
9
9
|
stack: list = [(0, root, None, None)]
|
|
10
|
-
skip_until_indent = None #
|
|
10
|
+
skip_until_indent = None # indent of an active block scalar's key line
|
|
11
|
+
block: dict = {} # active block scalar: {'lines': [...], 'style': str, 'assign': fn}
|
|
12
|
+
|
|
13
|
+
def flush_block():
|
|
14
|
+
if block:
|
|
15
|
+
block['assign'](_fold_block(block['lines'], block['style']))
|
|
16
|
+
block.clear()
|
|
17
|
+
|
|
11
18
|
for i, raw in enumerate(text.splitlines()):
|
|
12
19
|
stripped = raw.strip()
|
|
13
|
-
if not stripped or stripped.startswith("#"):
|
|
14
|
-
# Reset block-scalar skip on blank lines
|
|
15
|
-
continue
|
|
16
20
|
indent = len(raw) - len(raw.lstrip())
|
|
17
|
-
|
|
18
|
-
#
|
|
21
|
+
|
|
22
|
+
# Inside a block scalar: capture continuation lines verbatim. Blank and
|
|
23
|
+
# '#'-prefixed lines are literal content here, not comments.
|
|
19
24
|
if skip_until_indent is not None:
|
|
20
|
-
if indent > skip_until_indent:
|
|
25
|
+
if not stripped or indent > skip_until_indent:
|
|
26
|
+
if block:
|
|
27
|
+
block['lines'].append(raw)
|
|
21
28
|
continue
|
|
22
|
-
|
|
23
|
-
|
|
29
|
+
flush_block()
|
|
30
|
+
skip_until_indent = None
|
|
31
|
+
|
|
32
|
+
if not stripped or stripped.startswith("#"):
|
|
33
|
+
continue
|
|
24
34
|
|
|
25
35
|
# Pop stack until we're at the right nesting level
|
|
26
36
|
# Don't pop list containers at same indent — sibling items stay in same list
|
|
@@ -68,8 +78,11 @@ def parse_simple_yaml(text: str) -> dict:
|
|
|
68
78
|
v = v.strip()
|
|
69
79
|
# Detect block scalars in list items too
|
|
70
80
|
if v in ("|", ">", "|-", ">-", "|+", ">+"):
|
|
71
|
-
item = {k:
|
|
81
|
+
item = {k: None}
|
|
72
82
|
container.append(item)
|
|
83
|
+
block.clear()
|
|
84
|
+
block.update(lines=[], style=v,
|
|
85
|
+
assign=lambda s, _i=item, _k=k: _i.__setitem__(_k, s))
|
|
73
86
|
skip_until_indent = indent
|
|
74
87
|
continue
|
|
75
88
|
item = {k: _yaml_scalar(v) if v else None}
|
|
@@ -87,12 +100,18 @@ def parse_simple_yaml(text: str) -> dict:
|
|
|
87
100
|
key = key.strip()
|
|
88
101
|
val = val.strip()
|
|
89
102
|
|
|
90
|
-
# Detect block scalars (|, >, |- etc.)
|
|
103
|
+
# Detect block scalars (|, >, |- etc.) and capture their folded content.
|
|
91
104
|
if val in ("|", ">", "|-", ">-", "|+", ">+"):
|
|
105
|
+
tgt = None
|
|
92
106
|
if isinstance(container, list) and container:
|
|
93
|
-
container[-1]
|
|
107
|
+
tgt = container[-1]
|
|
94
108
|
elif isinstance(container, dict):
|
|
95
|
-
|
|
109
|
+
tgt = container
|
|
110
|
+
if tgt is not None:
|
|
111
|
+
tgt[key] = None
|
|
112
|
+
block.clear()
|
|
113
|
+
block.update(lines=[], style=val,
|
|
114
|
+
assign=lambda s, _t=tgt, _k=key: _t.__setitem__(_k, s))
|
|
96
115
|
skip_until_indent = indent
|
|
97
116
|
continue
|
|
98
117
|
|
|
@@ -111,11 +130,43 @@ def parse_simple_yaml(text: str) -> dict:
|
|
|
111
130
|
stack.append((indent, nxt, container, key))
|
|
112
131
|
else:
|
|
113
132
|
container[key] = _yaml_scalar(val)
|
|
133
|
+
flush_block()
|
|
114
134
|
return root
|
|
115
135
|
|
|
116
136
|
|
|
137
|
+
def _fold_block(raw_lines, style):
|
|
138
|
+
"""Fold a YAML block scalar (| literal / > folded) to match PyYAML."""
|
|
139
|
+
# Strip the common leading indentation of the block's content lines.
|
|
140
|
+
indents = [len(l) - len(l.lstrip()) for l in raw_lines if l.strip()]
|
|
141
|
+
ci = min(indents) if indents else 0
|
|
142
|
+
lines = [l[ci:] if len(l) >= ci else l.strip() for l in raw_lines]
|
|
143
|
+
if style[0] == "|": # literal: keep line breaks
|
|
144
|
+
text = "\n".join(lines)
|
|
145
|
+
else: # folded: single break -> space, blank line -> newline
|
|
146
|
+
parts, prev_blank = [], True
|
|
147
|
+
for l in lines:
|
|
148
|
+
if l == "":
|
|
149
|
+
parts.append("\n")
|
|
150
|
+
prev_blank = True
|
|
151
|
+
else:
|
|
152
|
+
if parts and not prev_blank:
|
|
153
|
+
parts.append(" ")
|
|
154
|
+
parts.append(l)
|
|
155
|
+
prev_blank = False
|
|
156
|
+
text = "".join(parts)
|
|
157
|
+
text = text.rstrip("\n")
|
|
158
|
+
# Chomping indicator: '-' strip, default clip (single trailing newline).
|
|
159
|
+
return text if style.endswith("-") else text + "\n"
|
|
160
|
+
|
|
161
|
+
|
|
117
162
|
def _yaml_scalar(val: str):
|
|
118
163
|
"""Convert a YAML scalar string to Python type."""
|
|
164
|
+
stripped = val.strip()
|
|
165
|
+
# Double-quoted scalar: unescape YAML escapes (\" and \\) like PyYAML, and
|
|
166
|
+
# keep it a string (no bool/int coercion). Keeps the PyYAML-free fallback
|
|
167
|
+
# byte-identical to PyYAML for quoted descriptions.
|
|
168
|
+
if len(stripped) >= 2 and stripped[0] == '"' and stripped[-1] == '"':
|
|
169
|
+
return stripped[1:-1].replace('\\"', '"').replace("\\\\", "\\")
|
|
119
170
|
val = val.strip('"').strip("'")
|
|
120
171
|
if val in ("true", "false"):
|
|
121
172
|
return val == "true"
|
|
@@ -1,19 +1,30 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
# story: e48s15
|
|
3
|
+
# story: BUG-2026-07-18-setup-pyyaml-missing
|
|
3
4
|
import os
|
|
4
5
|
import sys
|
|
5
6
|
import json
|
|
6
7
|
import glob
|
|
7
8
|
import subprocess
|
|
8
|
-
import yaml
|
|
9
9
|
|
|
10
|
-
# Ensure scripts/lib is on the path so
|
|
10
|
+
# Ensure scripts/lib is on the path so sibling modules (link_utils, and the
|
|
11
|
+
# simple_yaml fallback below) resolve regardless of cwd.
|
|
11
12
|
_LIB_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
12
13
|
if _LIB_DIR not in sys.path:
|
|
13
14
|
sys.path.insert(0, _LIB_DIR)
|
|
14
15
|
|
|
15
16
|
from link_utils import LINK_RE, EXTERNAL_RE, strip_code_spans # noqa: E402
|
|
16
17
|
|
|
18
|
+
# End-user setup (bigpowers setup) may run on a python3 without PyYAML.
|
|
19
|
+
# Prefer PyYAML for exact parsing; fall back to the dependency-free
|
|
20
|
+
# simple_yaml.py (story e38s01) so setup never hard-crashes.
|
|
21
|
+
try:
|
|
22
|
+
import yaml
|
|
23
|
+
_load_yaml = yaml.safe_load
|
|
24
|
+
except ModuleNotFoundError:
|
|
25
|
+
from simple_yaml import parse_simple_yaml
|
|
26
|
+
_load_yaml = parse_simple_yaml
|
|
27
|
+
|
|
17
28
|
def rewrite_links_for_pi(body, name):
|
|
18
29
|
"""Repoint relative links so they resolve from .pi/skills/<name>/.
|
|
19
30
|
|
|
@@ -114,7 +125,7 @@ def parse_skill(skill_md_path):
|
|
|
114
125
|
skill_body = '---'.join(parts[2:])
|
|
115
126
|
|
|
116
127
|
try:
|
|
117
|
-
frontmatter =
|
|
128
|
+
frontmatter = _load_yaml(frontmatter_raw)
|
|
118
129
|
except Exception as e:
|
|
119
130
|
print(f"Error parsing YAML frontmatter in {skill_md_path}: {e}", file=sys.stderr)
|
|
120
131
|
sys.exit(1)
|
|
@@ -160,7 +171,7 @@ def get_active_adapters(repo_root):
|
|
|
160
171
|
return ["cursor", "gemini", "pi"]
|
|
161
172
|
try:
|
|
162
173
|
with open(targets_file, 'r', encoding='utf-8') as f:
|
|
163
|
-
data =
|
|
174
|
+
data = _load_yaml(f.read())
|
|
164
175
|
adapters = []
|
|
165
176
|
for target in data.get('targets', []):
|
|
166
177
|
if target.get('skill') is not None:
|
package/scripts/sync-skills.sh
CHANGED
|
@@ -74,7 +74,7 @@ if [[ -f "$TARGETS_FILE" ]] && command -v yq >/dev/null 2>&1; then
|
|
|
74
74
|
[[ -n "$adapter" && "$adapter" != "null" ]] && SKILL_ADAPTERS+=("$adapter")
|
|
75
75
|
done < <(yq -r '.targets[] | select(.skill != null) | .skill.adapter' "$TARGETS_FILE" | sort -u)
|
|
76
76
|
else
|
|
77
|
-
echo "sync-skills: WARN — targets.yaml missing; using legacy render list" >&2
|
|
77
|
+
echo "sync-skills: WARN — yq unavailable or targets.yaml missing; using legacy render list (cursor gemini pi)" >&2
|
|
78
78
|
SKILL_ADAPTERS=(cursor gemini pi)
|
|
79
79
|
fi
|
|
80
80
|
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test-setup-no-pyyaml.sh — recurrence guard for BUG-2026-07-18-setup-pyyaml-missing
|
|
3
|
+
#
|
|
4
|
+
# `bigpowers setup` must not crash when the resolved python3 lacks PyYAML.
|
|
5
|
+
# Two setup-path scripts hard-depended on PyYAML: srp-engine.py (import yaml)
|
|
6
|
+
# and validate-skill-yaml.py (sys.exit(2) on ImportError). Both now fall back
|
|
7
|
+
# to scripts/lib/simple_yaml.py. This guard asserts:
|
|
8
|
+
# 1. srp-engine.py parses a skill under a PyYAML-free interpreter.
|
|
9
|
+
# 2. validate-skill-yaml.py runs (does not exit 2) under the same.
|
|
10
|
+
# 3. simple_yaml's output equals PyYAML's for every skill's frontmatter
|
|
11
|
+
# (covers escaped quotes and folded/literal block scalars).
|
|
12
|
+
#
|
|
13
|
+
# Usage: bash scripts/test-setup-no-pyyaml.sh
|
|
14
|
+
set -euo pipefail
|
|
15
|
+
source "$(dirname "${BASH_SOURCE[0]}")/lib/skill-common.sh"
|
|
16
|
+
resolve_repo_root
|
|
17
|
+
cd "$REPO_ROOT"
|
|
18
|
+
|
|
19
|
+
GREEN='\033[0;32m'; RED='\033[0;31m'; NC='\033[0m'
|
|
20
|
+
FAILURES=0
|
|
21
|
+
pass() { echo -e "${GREEN}PASS${NC} $1"; }
|
|
22
|
+
fail() { echo -e "${RED}FAIL${NC} $1"; FAILURES=$((FAILURES + 1)); }
|
|
23
|
+
|
|
24
|
+
# Build a throwaway PyYAML-free interpreter — a fresh venv has no PyYAML,
|
|
25
|
+
# reproducing a stock macOS python3 without touching the caller's environment.
|
|
26
|
+
NOYAML_DIR="$(mktemp -d)"
|
|
27
|
+
trap 'rm -rf "$NOYAML_DIR"' EXIT
|
|
28
|
+
if ! python3 -m venv "$NOYAML_DIR/venv" >/dev/null 2>&1; then
|
|
29
|
+
echo "test-setup-no-pyyaml: SKIP — cannot create venv"; exit 0
|
|
30
|
+
fi
|
|
31
|
+
NOPY="$NOYAML_DIR/venv/bin/python"
|
|
32
|
+
if "$NOPY" -c "import yaml" >/dev/null 2>&1; then
|
|
33
|
+
echo "test-setup-no-pyyaml: SKIP — fresh venv unexpectedly has PyYAML"; exit 0
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
# 1. srp-engine.py must parse a skill's frontmatter without crashing.
|
|
37
|
+
if "$NOPY" scripts/lib/srp-engine.py skills/fix-bug/SKILL.md >/dev/null 2>&1; then
|
|
38
|
+
pass "srp-engine.py parses frontmatter without PyYAML"
|
|
39
|
+
else
|
|
40
|
+
fail "srp-engine.py crashed without PyYAML"
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
# 2. validate-skill-yaml.py must not bail with exit 2 (the old PyYAML-required path).
|
|
44
|
+
rc=0
|
|
45
|
+
"$NOPY" scripts/validate-skill-yaml.py >/dev/null 2>&1 || rc=$?
|
|
46
|
+
if [[ "$rc" -ne 2 ]]; then
|
|
47
|
+
pass "validate-skill-yaml.py runs without PyYAML (exit $rc)"
|
|
48
|
+
else
|
|
49
|
+
fail "validate-skill-yaml.py bailed with exit 2 (PyYAML required)"
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# 3. Fallback fidelity: simple_yaml must equal PyYAML for every skill's
|
|
53
|
+
# frontmatter. Runs under python3 (CI has PyYAML); skips if PyYAML absent.
|
|
54
|
+
if python3 - <<'PY'; then
|
|
55
|
+
import glob, sys
|
|
56
|
+
sys.path.insert(0, "scripts/lib")
|
|
57
|
+
from simple_yaml import parse_simple_yaml
|
|
58
|
+
try:
|
|
59
|
+
import yaml
|
|
60
|
+
except ImportError:
|
|
61
|
+
print("skip: PyYAML not available for comparison"); sys.exit(0)
|
|
62
|
+
bad = []
|
|
63
|
+
for p in sorted(glob.glob("skills/*/SKILL.md")):
|
|
64
|
+
fm = open(p, encoding="utf-8").read().split("---")[1]
|
|
65
|
+
a = parse_simple_yaml(fm)
|
|
66
|
+
b = yaml.safe_load(fm)
|
|
67
|
+
for k in ("name", "description", "effort", "model"):
|
|
68
|
+
if a.get(k) != b.get(k):
|
|
69
|
+
bad.append(f"{p}:{k}")
|
|
70
|
+
if bad:
|
|
71
|
+
print("mismatch: " + ", ".join(bad)); sys.exit(1)
|
|
72
|
+
sys.exit(0)
|
|
73
|
+
PY
|
|
74
|
+
pass "simple_yaml matches PyYAML for all skill frontmatter"
|
|
75
|
+
else
|
|
76
|
+
fail "simple_yaml diverged from PyYAML for some skill frontmatter"
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
if [[ "$FAILURES" -gt 0 ]]; then
|
|
80
|
+
echo "test-setup-no-pyyaml: $FAILURES failure(s)" >&2
|
|
81
|
+
exit 1
|
|
82
|
+
fi
|
|
83
|
+
echo "test-setup-no-pyyaml: PASS"
|
|
@@ -4,11 +4,18 @@ import glob
|
|
|
4
4
|
import sys
|
|
5
5
|
import os
|
|
6
6
|
|
|
7
|
+
# Prefer PyYAML; fall back to the dependency-free scripts/lib/simple_yaml.py
|
|
8
|
+
# (story e38s01) so `bigpowers setup` works on a python3 without PyYAML.
|
|
9
|
+
# story: BUG-2026-07-18-setup-pyyaml-missing
|
|
10
|
+
_LIB = os.path.join(os.path.dirname(os.path.abspath(__file__)), "lib")
|
|
7
11
|
try:
|
|
8
12
|
import yaml
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
sys.
|
|
13
|
+
_load_yaml = yaml.safe_load
|
|
14
|
+
except ModuleNotFoundError:
|
|
15
|
+
if _LIB not in sys.path:
|
|
16
|
+
sys.path.insert(0, _LIB)
|
|
17
|
+
from simple_yaml import parse_simple_yaml
|
|
18
|
+
_load_yaml = parse_simple_yaml
|
|
12
19
|
|
|
13
20
|
SKILLS_DIR = ".pi/skills"
|
|
14
21
|
files = sorted(glob.glob(f"{SKILLS_DIR}/*/SKILL.md"))
|
|
@@ -30,8 +37,8 @@ for path in files:
|
|
|
30
37
|
frontmatter = parts[1]
|
|
31
38
|
|
|
32
39
|
try:
|
|
33
|
-
data =
|
|
34
|
-
except
|
|
40
|
+
data = _load_yaml(frontmatter)
|
|
41
|
+
except Exception as e:
|
|
35
42
|
parse_failures.append((skill_name, str(e).split("\n")[0]))
|
|
36
43
|
continue
|
|
37
44
|
|