bigpowers 2.77.0 → 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.
Files changed (76) hide show
  1. package/.pi/package.json +1 -1
  2. package/.pi/prompts/audit-code.md +1 -1
  3. package/.pi/prompts/craft-skill.md +2 -2
  4. package/.pi/prompts/elaborate-spec.md +1 -1
  5. package/.pi/prompts/evolve-skill.md +1 -1
  6. package/.pi/prompts/grill-me.md +1 -1
  7. package/.pi/prompts/guard-git.md +1 -1
  8. package/.pi/prompts/investigate-bug.md +2 -2
  9. package/.pi/prompts/model-domain.md +3 -3
  10. package/.pi/prompts/plan-refactor.md +2 -2
  11. package/.pi/prompts/plan-release.md +2 -2
  12. package/.pi/prompts/using-bigpowers.md +1 -1
  13. package/.pi/prompts/write-document.md +2 -2
  14. package/.pi/skills/audit-code/SKILL.md +1 -1
  15. package/.pi/skills/change-request/SKILL.md +1 -1
  16. package/.pi/skills/commit-message/SKILL.md +3 -3
  17. package/.pi/skills/compose-workflow/SKILL.md +1 -1
  18. package/.pi/skills/craft-skill/SKILL.md +4 -4
  19. package/.pi/skills/deepen-architecture/SKILL.md +13 -13
  20. package/.pi/skills/deploy/SKILL.md +4 -4
  21. package/.pi/skills/develop-tdd/SKILL.md +5 -5
  22. package/.pi/skills/elaborate-spec/SKILL.md +1 -1
  23. package/.pi/skills/evolve-skill/SKILL.md +2 -2
  24. package/.pi/skills/extract-design/SKILL.md +1 -1
  25. package/.pi/skills/generate-allure-report/SKILL.md +1 -1
  26. package/.pi/skills/grill-me/SKILL.md +2 -2
  27. package/.pi/skills/grill-with-docs/SKILL.md +1 -1
  28. package/.pi/skills/guard-git/SKILL.md +5 -5
  29. package/.pi/skills/harden-vps/SKILL.md +1 -1
  30. package/.pi/skills/investigate-bug/SKILL.md +2 -2
  31. package/.pi/skills/migrate-spec/SKILL.md +13 -13
  32. package/.pi/skills/model-domain/SKILL.md +5 -5
  33. package/.pi/skills/orchestrate-project/SKILL.md +2 -2
  34. package/.pi/skills/organize-workspace/SKILL.md +1 -1
  35. package/.pi/skills/plan-refactor/SKILL.md +2 -2
  36. package/.pi/skills/plan-release/SKILL.md +2 -2
  37. package/.pi/skills/plan-work/SKILL.md +2 -2
  38. package/.pi/skills/publish-package/SKILL.md +8 -8
  39. package/.pi/skills/release-branch/SKILL.md +2 -2
  40. package/.pi/skills/research-first/SKILL.md +1 -1
  41. package/.pi/skills/run-evals/SKILL.md +1 -1
  42. package/.pi/skills/security-review/SKILL.md +3 -3
  43. package/.pi/skills/seed-conventions/SKILL.md +3 -3
  44. package/.pi/skills/smoke-test/SKILL.md +5 -5
  45. package/.pi/skills/stocktake-skills/SKILL.md +1 -1
  46. package/.pi/skills/using-bigpowers/SKILL.md +1 -1
  47. package/.pi/skills/validate-contracts/SKILL.md +6 -6
  48. package/.pi/skills/verify-work/SKILL.md +1 -1
  49. package/.pi/skills/wire-ci/SKILL.md +8 -8
  50. package/.pi/skills/write-document/SKILL.md +3 -3
  51. package/CHANGELOG.md +14 -0
  52. package/package.json +4 -3
  53. package/scripts/adapters/pi.sh +4 -1
  54. package/scripts/check-skill-links.py +78 -0
  55. package/scripts/generate-reference-tables.sh +2 -1
  56. package/scripts/lib/__pycache__/link_utils.cpython-312.pyc +0 -0
  57. package/scripts/lib/link_utils.py +49 -0
  58. package/scripts/lib/simple_yaml.py +64 -13
  59. package/scripts/lib/srp-engine.py +100 -4
  60. package/scripts/sync-skills.sh +1 -1
  61. package/scripts/test-setup-no-pyyaml.sh +83 -0
  62. package/scripts/validate-skill-yaml.py +12 -5
  63. package/skills/audit-code/SKILL.md +1 -1
  64. package/skills/craft-skill/SKILL.md +2 -2
  65. package/skills/elaborate-spec/SKILL.md +1 -1
  66. package/skills/evolve-skill/REFERENCE.md +1 -1
  67. package/skills/grill-me/REFERENCE.md +1 -1
  68. package/skills/guard-git/SKILL.md +1 -1
  69. package/skills/investigate-bug/SKILL.md +2 -2
  70. package/skills/model-domain/CONTEXT-FORMAT.md +3 -3
  71. package/skills/plan-refactor/SKILL.md +2 -2
  72. package/skills/plan-release/SKILL.md +2 -2
  73. package/skills/using-bigpowers/SKILL.md +1 -1
  74. package/skills/write-document/REFERENCE.md +2 -2
  75. package/skills-lock.json +8 -8
  76. package/tests/test_srp_engine.py +148 -0
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env python3
2
+ # check-skill-links.py — fail on machine-absolute paths or dangling relative
3
+ # links in skill docs. Scans skills/**/SKILL.md and .pi/skills/**/SKILL.md.
4
+ # Run: python3 scripts/check-skill-links.py (exit 1 with a report on failure)
5
+ #
6
+ # NOTE: .cursor/rules/ and .gemini/extensions/ are intentionally excluded.
7
+ # Those trees are auto-generated from SKILL.md sources via sync-skills.sh and
8
+ # are not published to npm, so dangling links there are a rendering gap, not a
9
+ # source defect. Enable once the sync pipeline gains link-rewriting support.
10
+ # See: https://github.com/danielvm-git/bigpowers/issues/78
11
+ import glob
12
+ import os
13
+ import sys
14
+
15
+ # Import shared patterns — single source of truth so the rewriter (srp-engine)
16
+ # and the gate always agree on what constitutes a "link".
17
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
18
+ from lib.link_utils import LINK_RE, EXTERNAL_RE, MACHINE_PATH_RE, strip_code_spans # noqa: E402
19
+
20
+
21
+ def resolve_repo_root() -> str:
22
+ here = os.path.dirname(os.path.abspath(__file__))
23
+ candidate = os.path.dirname(here) # scripts/ -> repo root
24
+ if os.path.isdir(os.path.join(candidate, "skills")):
25
+ return candidate
26
+ return os.getcwd()
27
+
28
+
29
+ def check_file_links(smd: str, root: str) -> list[str]:
30
+ """Return a list of problem strings for a single SKILL.md file."""
31
+ rel = os.path.relpath(smd, root)
32
+ with open(smd, encoding="utf-8") as f:
33
+ text = f.read()
34
+
35
+ problems = []
36
+
37
+ # Machine-absolute path check runs on raw text (line-by-line for location).
38
+ for i, line in enumerate(text.splitlines(), 1):
39
+ if MACHINE_PATH_RE.search(line):
40
+ problems.append(f"{rel}:{i}: machine-absolute path: {line.strip()[:100]}")
41
+
42
+ # Link check runs on shadow (code spans blanked) to avoid false positives
43
+ # on example paths shown inside fenced blocks or inline code.
44
+ shadow = strip_code_spans(text)
45
+ for m in LINK_RE.finditer(shadow):
46
+ target = m.group(2).split("#", 1)[0] # strip fragment
47
+ if not target or EXTERNAL_RE.match(target) or target.startswith(("#", "/")):
48
+ continue
49
+ resolved = os.path.normpath(os.path.join(os.path.dirname(smd), target))
50
+ if not os.path.exists(resolved):
51
+ problems.append(f"{rel}: dangling link -> {target}")
52
+
53
+ return problems
54
+
55
+
56
+ def check(root: str) -> list[str]:
57
+ problems = []
58
+ for tree in ("skills", os.path.join(".pi", "skills")):
59
+ pattern = os.path.join(root, tree, "*", "SKILL.md")
60
+ for smd in sorted(glob.glob(pattern)):
61
+ problems.extend(check_file_links(smd, root))
62
+ return problems
63
+
64
+
65
+ def main() -> int:
66
+ root = resolve_repo_root()
67
+ problems = check(root)
68
+ if problems:
69
+ print(f"check-skill-links: {len(problems)} problem(s):")
70
+ for p in problems:
71
+ print(f" {p}")
72
+ return 1
73
+ print("check-skill-links: OK — no machine paths or dangling links")
74
+ return 0
75
+
76
+
77
+ if __name__ == "__main__":
78
+ sys.exit(main())
@@ -32,10 +32,11 @@ BLOCK="<!-- AUTO-GENERATED-CATALOG: begin — do not edit manually; run scripts/
32
32
  for row in "${ROWS[@]}"; do
33
33
  skill="${row%% *}"
34
34
  model="${row##* }"
35
+ skill_rel="${skill#$REPO_ROOT/}"
35
36
  # capitalise first letter for display
36
37
  model_cap="$(echo "${model:0:1}" | tr '[:lower:]' '[:upper:]')${model:1}"
37
38
  BLOCK+="
38
- | \`$skill\` | **$model_cap** |"
39
+ | \`$skill_rel\` | **$model_cap** |"
39
40
  done
40
41
  BLOCK+="
41
42
 
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env python3
2
+ """link_utils.py — shared Markdown link patterns used by srp-engine and check-skill-links.
3
+
4
+ Single source of truth for what constitutes a "link" so the rewriter and the
5
+ regression gate always agree on the definition.
6
+ """
7
+ import re
8
+
9
+ # Matches standard Markdown links — with or without an optional title attribute.
10
+ # [text](target)
11
+ # [text](target "title")
12
+ # Group 1: link text, Group 2: raw target (may include URL fragment)
13
+ LINK_RE = re.compile(
14
+ r'\[([^\]]*)\]\(' # [text](
15
+ r'([^)\s"]+)' # target (no spaces, no quotes)
16
+ r'(?:\s+"[^"]*")?' # optional title attribute: "title text"
17
+ r'\s*\)' # closing )
18
+ )
19
+
20
+ # Matches scheme-prefixed (non-relative) URLs that should never be rewritten
21
+ # or resolved as local file paths.
22
+ EXTERNAL_RE = re.compile(r'(?i)^(https?|ftp|file|mailto):')
23
+
24
+ # Detects machine-absolute paths that must not appear in skill docs.
25
+ # Covers macOS (/Users/), Linux (/home/), and Windows (C:\Users, D:\Users …).
26
+ MACHINE_PATH_RE = re.compile(r'file:///|/Users/|/home/|[A-Z]:\\Users')
27
+
28
+ # ---------------------------------------------------------------------------
29
+ # Markdown code-block stripping
30
+ # ---------------------------------------------------------------------------
31
+
32
+ # Fenced code block: ``` or ~~~ with optional language tag, any content, close fence.
33
+ _FENCED_BLOCK_RE = re.compile(r'(?m)^(`{3,}|~{3,})[^\n]*\n.*?\n\1\s*$', re.DOTALL)
34
+ # Inline code: `…` (single backtick, non-empty)
35
+ _INLINE_CODE_RE = re.compile(r'`[^`\n]+`')
36
+
37
+
38
+ def strip_code_spans(text: str) -> str:
39
+ """Return *text* with fenced blocks and inline code replaced by blank space.
40
+
41
+ Preserves original character positions so that any offset-based logic
42
+ remains valid; only the link-matching content is neutralised.
43
+ """
44
+ def _blank(m: re.Match) -> str:
45
+ return ' ' * len(m.group(0))
46
+
47
+ text = _FENCED_BLOCK_RE.sub(_blank, text)
48
+ text = _INLINE_CODE_RE.sub(_blank, text)
49
+ return text
@@ -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,11 +1,99 @@
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
+
10
+ # Ensure scripts/lib is on the path so sibling modules (link_utils, and the
11
+ # simple_yaml fallback below) resolve regardless of cwd.
12
+ _LIB_DIR = os.path.dirname(os.path.abspath(__file__))
13
+ if _LIB_DIR not in sys.path:
14
+ sys.path.insert(0, _LIB_DIR)
15
+
16
+ from link_utils import LINK_RE, EXTERNAL_RE, strip_code_spans # noqa: E402
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
+
28
+ def rewrite_links_for_pi(body, name):
29
+ """Repoint relative links so they resolve from .pi/skills/<name>/.
30
+
31
+ Sibling *.md content is inlined into the rendered SKILL.md body, but the
32
+ links to those files (and to repo-root docs) still point at paths relative
33
+ to the source dir, so they dangle in the rendered tree. Repoint them at
34
+ the shipped source tree (skills/, profiles/, repo-root files) so links
35
+ resolve in repo checkouts and npm installs alike. External, anchor, and
36
+ absolute links pass through untouched.
37
+
38
+ Code fences and inline code are skipped — links inside them are examples,
39
+ not navigation targets.
40
+ """
41
+ out_dir = os.path.join(".pi", "skills", name)
42
+ # Build a "shadow" copy with code spans blanked out so LINK_RE never
43
+ # matches inside ``` blocks or inline `code`.
44
+ shadow = strip_code_spans(body)
45
+
46
+ def repl(m):
47
+ # If this match falls inside a code span the shadow char is a space;
48
+ # the real body char at that position is not a '[' — so LINK_RE won't
49
+ # match in the shadow at all. The sub() runs on the shadow but we
50
+ # return the replacement for the real body using the captured groups.
51
+ text, raw_target = m.group(1), m.group(2).strip()
52
+ if EXTERNAL_RE.match(raw_target) or raw_target.startswith(('#', '/')):
53
+ return m.group(0)
54
+ # Split fragment before normpath to avoid mangling '#section/sub'.
55
+ if '#' in raw_target:
56
+ path_part, fragment = raw_target.split('#', 1)
57
+ frag_suffix = '#' + fragment
58
+ else:
59
+ path_part, frag_suffix = raw_target, ''
60
+ resolved = os.path.normpath(os.path.join("skills", name, path_part))
61
+ if resolved.startswith(".."):
62
+ return m.group(0) # escapes repo root; leave untouched
63
+ new_target = os.path.relpath(resolved, out_dir).replace(os.sep, '/') + frag_suffix
64
+ return f"[{text}]({new_target})"
65
+
66
+ return LINK_RE.sub(repl, shadow).replace(shadow, body) if shadow == body else _apply_repl_on_real(body, shadow, name, out_dir)
67
+
68
+
69
+ def _apply_repl_on_real(body, shadow, name, out_dir):
70
+ """Apply link rewrites to *body* using *shadow* to skip code spans.
71
+
72
+ Iterates LINK_RE matches on *shadow* (which has code spans blanked) and
73
+ rebuilds body with only the matched positions replaced.
74
+ """
75
+ result = []
76
+ last = 0
77
+ for m in LINK_RE.finditer(shadow):
78
+ result.append(body[last:m.start()])
79
+ text, raw_target = m.group(1), m.group(2).strip()
80
+ if EXTERNAL_RE.match(raw_target) or raw_target.startswith(('#', '/')):
81
+ result.append(body[m.start():m.end()])
82
+ else:
83
+ if '#' in raw_target:
84
+ path_part, fragment = raw_target.split('#', 1)
85
+ frag_suffix = '#' + fragment
86
+ else:
87
+ path_part, frag_suffix = raw_target, ''
88
+ resolved = os.path.normpath(os.path.join("skills", name, path_part))
89
+ if resolved.startswith(".."):
90
+ result.append(body[m.start():m.end()])
91
+ else:
92
+ new_target = os.path.relpath(resolved, out_dir).replace(os.sep, '/') + frag_suffix
93
+ result.append(f"[{text}]({new_target})")
94
+ last = m.end()
95
+ result.append(body[last:])
96
+ return ''.join(result)
9
97
 
10
98
  def resolve_repo_root():
11
99
  lib_dir = os.path.dirname(os.path.abspath(__file__))
@@ -37,7 +125,7 @@ def parse_skill(skill_md_path):
37
125
  skill_body = '---'.join(parts[2:])
38
126
 
39
127
  try:
40
- frontmatter = yaml.safe_load(frontmatter_raw)
128
+ frontmatter = _load_yaml(frontmatter_raw)
41
129
  except Exception as e:
42
130
  print(f"Error parsing YAML frontmatter in {skill_md_path}: {e}", file=sys.stderr)
43
131
  sys.exit(1)
@@ -83,7 +171,7 @@ def get_active_adapters(repo_root):
83
171
  return ["cursor", "gemini", "pi"]
84
172
  try:
85
173
  with open(targets_file, 'r', encoding='utf-8') as f:
86
- data = yaml.safe_load(f)
174
+ data = _load_yaml(f.read())
87
175
  adapters = []
88
176
  for target in data.get('targets', []):
89
177
  if target.get('skill') is not None:
@@ -210,7 +298,11 @@ def main():
210
298
  continue
211
299
 
212
300
  for adapter in adapters:
213
- dispatch_to_adapter(skill_data, adapter, repo_root)
301
+ data = skill_data
302
+ if adapter == "pi":
303
+ data = dict(skill_data)
304
+ data["body_pi_skill"] = rewrite_links_for_pi(skill_data['body'], skill_data['name'])
305
+ dispatch_to_adapter(data, adapter, repo_root)
214
306
 
215
307
  if okf_mode:
216
308
  render_okf_concept(skill_data, okf_wiki_skills)
@@ -235,6 +327,10 @@ def main():
235
327
 
236
328
  skill_data = parse_skill(skill_md_path)
237
329
 
330
+ if target == "pi":
331
+ skill_data = dict(skill_data)
332
+ skill_data["body_pi_skill"] = rewrite_links_for_pi(skill_data['body'], skill_data['name'])
333
+
238
334
  if dry_run or not target:
239
335
  print(json.dumps(skill_data, indent=2))
240
336
  return
@@ -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
 
@@ -31,7 +31,7 @@ Apply the full checklist to churn-ranked files in descending order. Files with z
31
31
  - Default: full checklist
32
32
  - --quick: Run only Supply Chain and Test Coverage. Use for changes under 50 LOC.
33
33
  - --gate: Non-interactive mode for automated CI gating (used by build-epic step 6). Exit with non-zero status code (`exit 1`) on ANY checklist failure; `exit 0` only if ALL items pass. Produces a compact pass/fail summary to stderr. On failure, list every ✗ item with reason.
34
- - --parallel: Run checklist sections in **isolated git worktrees** (e45s18) so concurrent checks cannot corrupt each other's working tree. See [REFERENCE.md](REFERENCE.md) or:
34
+ - --parallel: Run checklist sections in **isolated git worktrees** (e45s18) so concurrent checks cannot corrupt each other's working tree:
35
35
 
36
36
  ```bash
37
37
  bash scripts/lib/parallel-review-worktrees.sh audit-code
@@ -33,7 +33,7 @@ Move process detail into the SKILL.md body or REFERENCE.md — never into `descr
33
33
  - Any reference materials to include?
34
34
  - What specs/ output does it produce (if any)?
35
35
 
36
- 2. **Verify Principles** — Ensure the skill aligns with [PRINCIPLES.md](../docs/PRINCIPLES.md):
36
+ 2. **Verify Principles** — Ensure the skill aligns with [PRINCIPLES.md](https://github.com/danielvm-git/bigpowers/blob/main/docs/PRINCIPLES.md):
37
37
  - Is it atomic (verb-noun)?
38
38
  - Is it "deep" (simple interface, complex internal logic)?
39
39
  - Does it include Hard Gates?
@@ -46,7 +46,7 @@ Move process detail into the SKILL.md body or REFERENCE.md — never into `descr
46
46
 
47
47
  **Auto-skill from library README:** When user provides a library README or API docs URL, extract: triggers, HARD GATEs, verify commands, specs/ output — draft SKILL.md without inventing APIs not in the source.
48
48
 
49
- 4. Add `model:` frontmatter (`haiku` | `sonnet` | `opus`) per [model-profiles.md](../docs/references/model-profiles.md).
49
+ 4. Add `model:` frontmatter (`haiku` | `sonnet` | `opus`) per [model-profiles.md](https://github.com/danielvm-git/bigpowers/blob/main/docs/references/model-profiles.md).
50
50
 
51
51
  > **STREAM CONTINUITY** — When writing file content, output in continuous chunks of ~200 lines. Do not pause. Continue immediately until complete. If you need time, emit a placeholder comment rather than going silent.
52
52
 
@@ -65,7 +65,7 @@ Once the user has answered the main questions, probe for assumptions:
65
65
 
66
66
  ### 4. Synthesize and confirm
67
67
 
68
- Summarize your understanding in 3–5 bullet points aligned with [countable-story-format.md](file:///Users/danielvm/Developer/bigpowers/countable-story-format.md):
68
+ Summarize your understanding in 3–5 bullet points aligned with [countable-story-format.md](https://github.com/danielvm-git/bigpowers/blob/main/docs/countable-story-format.md):
69
69
  - The problem (feeds into §1 Business narrative)
70
70
  - The solution and main flow (feeds into §5)
71
71
  - The key constraints and alternative flows (feeds into §6)
@@ -9,4 +9,4 @@
9
9
  **Evidence:** path/to/benchmark-report.md
10
10
  ```
11
11
 
12
- Benchmark repo: `/Users/danielvm/Developer/bigpowers-benchmark/`
12
+ Benchmark repo: `bigpowers-benchmark` — resolve the local clone on this machine (e.g. `gh repo clone danielvm-git/bigpowers-benchmark` if absent); never assume a hard-coded absolute path.
@@ -43,7 +43,7 @@ Ask one question at a time. For each challenge, cite the specific URL and sectio
43
43
 
44
44
  When an assumption doesn't match the docs:
45
45
 
46
- > "Your plan uses `library.doThing(a, b)` but the [docs](URL) show the signature is `doThing(config: {a, b})` with a config object. This will fail at runtime."
46
+ > "Your plan uses `library.doThing(a, b)` but the [docs](https://example.com/api-reference) show the signature is `doThing(config: {a, b})` with a config object. This will fail at runtime."
47
47
 
48
48
  Document each discrepancy clearly.
49
49
 
@@ -22,7 +22,7 @@ Installs a shared hook that blocks destructive git operations and enforces workf
22
22
  ## Quick start
23
23
 
24
24
  1. **Scope**: ask project-only vs global (paths differ per product).
25
- 2. **Copy the hook bundle** from the root [hooks/](hooks/) directory to the client's hooks directory.
25
+ 2. **Write the hook bundle** from [REFERENCE.md](REFERENCE.md) into the client's hooks directory.
26
26
  3. **Run `chmod +x`** on `pre-tool-use.sh`.
27
27
  4. **Merge** the hook snippet from [REFERENCE.md](REFERENCE.md) into the right settings file — do not wipe unrelated keys.
28
28
  5. **Verify** with the tests in [REFERENCE.md](REFERENCE.md).
@@ -124,6 +124,6 @@ After writing the bug file, print a one-line summary of the root cause and sugge
124
124
 
125
125
  <!-- story: e35s10 -->
126
126
 
127
- - [Feathers' Seams & Characterization Tests](../../docs/references/feathers.md) — seam types, characterization tests, and the legacy code change algorithm
128
- - [Fowler's Code Smells](../../docs/references/fowler.md) — identifying structural problems by smell before investigating
127
+ - [Feathers' Seams & Characterization Tests](https://github.com/danielvm-git/bigpowers/blob/main/docs/references/feathers.md) — seam types, characterization tests, and the legacy code change algorithm
128
+ - [Fowler's Code Smells](https://github.com/danielvm-git/bigpowers/blob/main/docs/references/fowler.md) — identifying structural problems by smell before investigating
129
129
 
@@ -57,9 +57,9 @@ _Avoid_: Client, buyer, account
57
57
 
58
58
  ## Contexts
59
59
 
60
- - [Ordering](./src/ordering/CONTEXT.md) — receives and tracks customer orders
61
- - [Billing](./src/billing/CONTEXT.md) — generates invoices and processes payments
62
- - [Fulfillment](./src/fulfillment/CONTEXT.md) — manages warehouse picking and shipping
60
+ - Ordering — `src/ordering/CONTEXT.md` — receives and tracks customer orders
61
+ - Billing — `src/billing/CONTEXT.md` — generates invoices and processes payments
62
+ - Fulfillment — `src/fulfillment/CONTEXT.md` — manages warehouse picking and shipping
63
63
 
64
64
  ## Relationships
65
65
 
@@ -82,8 +82,8 @@ After writing `specs/REFACTOR_LATEST.md`, suggest running `kickoff-branch` next
82
82
 
83
83
  <!-- story: e35s10 -->
84
84
 
85
- - [Fowler's Refactoring Catalog](../../docs/references/fowler.md) — canonical refactoring vocabulary and code-smell taxonomy
86
- - [Beck's Tidy First?](../../docs/references/kent-beck.md) — structural change before behavioral change
85
+ - [Fowler's Refactoring Catalog](https://github.com/danielvm-git/bigpowers/blob/main/docs/references/fowler.md) — canonical refactoring vocabulary and code-smell taxonomy
86
+ - [Beck's Tidy First?](https://github.com/danielvm-git/bigpowers/blob/main/docs/references/kent-beck.md) — structural change before behavioral change
87
87
 
88
88
  ## Verify
89
89
 
@@ -17,7 +17,7 @@ Synthesize the conversation context into `specs/release-plan.yaml` (index) and s
17
17
  |------|---------|
18
18
  | `specs/release-plan.yaml` | `release.version`, semver bump hint, WSJF-ordered epic list with `id`, `capsule_dir`, `wsjf`, `bcps` — **no story status** |
19
19
  | `specs/epics/eNN-<slug>/epic.yaml` | Epic manifest: `id`, `title`, `wsjf`, `total_bcps`, `status`, `stories[]` list |
20
- | `specs/epics/eNN-<slug>/eNNsYY-<slug>.md` | Story spec in [countable-story-format.md](file:///Users/danielvm/Developer/bigpowers/countable-story-format.md) with 20 sections and Gherkin acceptance criteria |
20
+ | `specs/epics/eNN-<slug>/eNNsYY-<slug>.md` | Story spec in [countable-story-format.md](https://github.com/danielvm-git/bigpowers/blob/main/docs/countable-story-format.md) with 20 sections and Gherkin acceptance criteria |
21
21
  | `specs/epics/eNN-<slug>/eNNsYY-tasks.yaml` | Decoupled task checklist with `verify:` commands per task |
22
22
  | `specs/execution-status.yaml` | Flat key-value store for story status (`eNNsYY: todo`) |
23
23
 
@@ -102,7 +102,7 @@ stories:
102
102
  ```
103
103
  ### 6. Save story specs (countable-story-format .md)
104
104
 
105
- Each story becomes a standalone `.md` file following [countable-story-format.md](file:///Users/danielvm/Developer/bigpowers/countable-story-format.md). Minimum: maturity 3 (Countable) with all 20 sections present. Acceptance criteria in §17 use Gherkin scenarios.
105
+ Each story becomes a standalone `.md` file following [countable-story-format.md](https://github.com/danielvm-git/bigpowers/blob/main/docs/countable-story-format.md). Minimum: maturity 3 (Countable) with all 20 sections present. Acceptance criteria in §17 use Gherkin scenarios.
106
106
  ### 7. Save decoupled task files (`-tasks.yaml`)
107
107
 
108
108
  Each story has a decoupled `-tasks.yaml` with implementation steps: