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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bigpowers",
3
- "version": "2.77.1",
3
+ "version": "2.77.2",
4
4
  "description": "79 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,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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bigpowers",
3
- "version": "2.77.1",
3
+ "version": "2.77.2",
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": {
@@ -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 # skip block scalar continuation lines (| and >)
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
- # Skip block scalar continuation (| and > in YAML)
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
- else:
23
- skip_until_indent = None
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: v}
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][key] = val # store the marker
107
+ tgt = container[-1]
94
108
  elif isinstance(container, dict):
95
- container[key] = val
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 link_utils resolves regardless of cwd.
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 = yaml.safe_load(frontmatter_raw)
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 = yaml.safe_load(f)
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:
@@ -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
- except ImportError:
10
- print("ERROR: PyYAML required — pip install pyyaml")
11
- sys.exit(2)
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 = yaml.safe_load(frontmatter)
34
- except yaml.YAMLError as e:
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