bigpowers 2.58.7 → 2.58.9
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/.pi/prompts/using-bigpowers.md +3 -2
- package/.pi/skills/using-bigpowers/SKILL.md +3 -2
- package/CHANGELOG.md +14 -0
- package/CLAUDE.md +1 -1
- package/CONTRIBUTING.md +4 -2
- package/GEMINI.md +1 -1
- package/README.md +10 -11
- package/package.json +1 -1
- package/scripts/golden-g08-anti-vacuity.sh +55 -0
- package/scripts/golden-g09-yaml-roundtrip.sh +96 -0
- package/scripts/install.sh +1 -1
- package/scripts/lib/trace-stories.py +4 -1
- package/scripts/run-golden-suite.sh +3 -0
- package/scripts/validate-specs-yaml.sh +75 -15
- package/scripts/yaml-tools.py +28 -68
- package/skills/using-bigpowers/SKILL.md +3 -2
- package/skills-lock.json +1 -1
package/.pi/package.json
CHANGED
|
@@ -12,8 +12,9 @@ Welcome to **bigpowers** — a lifecycle of **70** agent skills for production-r
|
|
|
12
12
|
## Install
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
|
-
npx bigpowers
|
|
16
|
-
npm install -g bigpowers # global install
|
|
15
|
+
npx bigpowers setup # one-shot setup
|
|
16
|
+
npm install -g bigpowers # global install
|
|
17
|
+
bigpowers setup # sync skills + link to your tools
|
|
17
18
|
```
|
|
18
19
|
|
|
19
20
|
From source (contributors): `git clone` → `npm install` or `bash scripts/install.sh`.
|
|
@@ -14,8 +14,9 @@ Welcome to **bigpowers** — a lifecycle of **70** agent skills for production-r
|
|
|
14
14
|
## Install
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
|
-
npx bigpowers
|
|
18
|
-
npm install -g bigpowers # global install
|
|
17
|
+
npx bigpowers setup # one-shot setup
|
|
18
|
+
npm install -g bigpowers # global install
|
|
19
|
+
bigpowers setup # sync skills + link to your tools
|
|
19
20
|
```
|
|
20
21
|
|
|
21
22
|
From source (contributors): `git clone` → `npm install` or `bash scripts/install.sh`.
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
## [2.58.9](https://github.com/danielvm-git/bigpowers/compare/v2.58.8...v2.58.9) (2026-07-04)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **trace:** exclude backlog stories from strict P0 gate ([4341ea0](https://github.com/danielvm-git/bigpowers/commit/4341ea0360a74bf5955a25174827c19d5c996a94))
|
|
7
|
+
|
|
8
|
+
## [2.58.8](https://github.com/danielvm-git/bigpowers/compare/v2.58.7...v2.58.8) (2026-07-04)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **release-pipeline:** stop YAML round-trip corruption, recover specs ([db09bbf](https://github.com/danielvm-git/bigpowers/commit/db09bbf34ed10092bd874cb7ce46af9de3fce563))
|
|
14
|
+
|
|
1
15
|
## [2.58.7](https://github.com/danielvm-git/bigpowers/compare/v2.58.6...v2.58.7) (2026-07-03)
|
|
2
16
|
|
|
3
17
|
|
package/CLAUDE.md
CHANGED
|
@@ -13,7 +13,7 @@ Stack: Markdown / Bash (documentation-based; skills integrate with Claude Code,
|
|
|
13
13
|
|
|
14
14
|
| Action | Command |
|
|
15
15
|
|---------|---------|
|
|
16
|
-
| Install | `
|
|
16
|
+
| Install | `npm install -g bigpowers && bigpowers setup` |
|
|
17
17
|
| Run | `bash scripts/sync-skills.sh` |
|
|
18
18
|
| Test | N/A (documentation project) |
|
|
19
19
|
| Build | `bash scripts/install.sh` (from source) |
|
package/CONTRIBUTING.md
CHANGED
|
@@ -9,7 +9,7 @@ Be respectful, constructive, and patient. We're building tools for developers
|
|
|
9
9
|
## Before You Start
|
|
10
10
|
|
|
11
11
|
1. **Read the docs.** Start with [`README.md`](README.md), [`CLAUDE.md`](CLAUDE.md), and [`CONVENTIONS.md`](CONVENTIONS.md). These define the project's architecture, conventions, and engineering standards.
|
|
12
|
-
2. **Run bigpowers.** Install with `
|
|
12
|
+
2. **Run bigpowers.** Install with `npm install -g bigpowers && bigpowers setup` and walk through the experience as a user before contributing as a developer.
|
|
13
13
|
3. **Understand the lifecycle.** bigpowers follows a 6-phase methodology: Discover → Elaborate → Plan → Build → Verify → Release. Read [`docs/WORKFLOW-SOP-v2.md`](docs/WORKFLOW-SOP-v2.md) for the full SOP.
|
|
14
14
|
|
|
15
15
|
## How to Contribute
|
|
@@ -66,8 +66,10 @@ Documentation is a first-class contribution. You don't need to write code to hel
|
|
|
66
66
|
git clone https://github.com/danielvm-git/bigpowers.git
|
|
67
67
|
cd bigpowers
|
|
68
68
|
|
|
69
|
-
# Install dependencies
|
|
69
|
+
# Install dependencies
|
|
70
70
|
npm install
|
|
71
|
+
# Sync skills and set up symlinks
|
|
72
|
+
bash scripts/install.sh
|
|
71
73
|
|
|
72
74
|
# Or manually:
|
|
73
75
|
bash scripts/install.sh
|
package/GEMINI.md
CHANGED
|
@@ -13,7 +13,7 @@ Stack: Markdown / Bash (documentation-based; skills integrate with Claude Code,
|
|
|
13
13
|
|
|
14
14
|
| Action | Command |
|
|
15
15
|
|---------|---------|
|
|
16
|
-
| Install | `
|
|
16
|
+
| Install | `npm install -g bigpowers && bigpowers setup` |
|
|
17
17
|
| Run | `bash scripts/sync-skills.sh` |
|
|
18
18
|
| Test | N/A (documentation project) |
|
|
19
19
|
| Build | `bash scripts/install.sh` (from source) |
|
package/README.md
CHANGED
|
@@ -36,12 +36,12 @@ After installing, ask your agent to run the `using-bigpowers` skill — it is th
|
|
|
36
36
|
### npm (recommended)
|
|
37
37
|
|
|
38
38
|
```bash
|
|
39
|
-
#
|
|
40
|
-
npx bigpowers
|
|
41
|
-
|
|
42
|
-
# Or install globally and run the setup command anytime
|
|
39
|
+
# Global install (no lifecycle scripts — npm v10+ safe)
|
|
43
40
|
npm install -g bigpowers
|
|
44
|
-
bigpowers
|
|
41
|
+
bigpowers setup # runs sync + install, links skills to your tools
|
|
42
|
+
|
|
43
|
+
# Or one-shot with npx
|
|
44
|
+
npx bigpowers setup
|
|
45
45
|
```
|
|
46
46
|
|
|
47
47
|
Both commands sync skill artifacts and link them to Claude Code, Gemini CLI, and Cursor (see [Prerequisites](#-prerequisites)).
|
|
@@ -50,10 +50,8 @@ Both commands sync skill artifacts and link them to Claude Code, Gemini CLI, and
|
|
|
50
50
|
|
|
51
51
|
```bash
|
|
52
52
|
git clone https://github.com/danielvm-git/bigpowers.git && cd bigpowers
|
|
53
|
-
npm install
|
|
54
|
-
# or manually:
|
|
53
|
+
npm install
|
|
55
54
|
bash scripts/install.sh
|
|
56
|
-
npm run sync
|
|
57
55
|
```
|
|
58
56
|
|
|
59
57
|
---
|
|
@@ -253,7 +251,7 @@ Or add manually to `.claude/settings.json`:
|
|
|
253
251
|
|
|
254
252
|
```bash
|
|
255
253
|
npm update -g bigpowers
|
|
256
|
-
bigpowers
|
|
254
|
+
bigpowers update # re-sync and refresh symlinks
|
|
257
255
|
```
|
|
258
256
|
|
|
259
257
|
**git clone:**
|
|
@@ -261,6 +259,7 @@ bigpowers # re-sync and refresh symlinks
|
|
|
261
259
|
```bash
|
|
262
260
|
git pull
|
|
263
261
|
npm run sync
|
|
262
|
+
bash scripts/install.sh
|
|
264
263
|
```
|
|
265
264
|
|
|
266
265
|
*Install uses symlinks — re-running setup refreshes links without duplicating files.*
|
|
@@ -283,9 +282,9 @@ bash scripts/install.sh --uninstall
|
|
|
283
282
|
### Reinstall
|
|
284
283
|
|
|
285
284
|
```bash
|
|
286
|
-
npx bigpowers
|
|
285
|
+
npx bigpowers setup
|
|
287
286
|
# or, if installed globally:
|
|
288
|
-
bigpowers
|
|
287
|
+
bigpowers setup
|
|
289
288
|
```
|
|
290
289
|
|
|
291
290
|
---
|
package/package.json
CHANGED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# golden-g08-anti-vacuity.sh — validate-specs-yaml.sh Anti-Vacuity Self-Test (G-08)
|
|
3
|
+
#
|
|
4
|
+
# Proves that validate-specs-yaml.sh CAN detect structurally corrupt YAML
|
|
5
|
+
# (BUG-2026-07-03-validate-specs-no-real-parser bug class). It is fed a
|
|
6
|
+
# fixture where specs/verifications/fixtures/corrupt-yaml/release-plan.yaml
|
|
7
|
+
# is deliberately flattened/corrupt (matching the real yaml-roundtrip
|
|
8
|
+
# corruption pattern) while still containing every string the grep-only
|
|
9
|
+
# checks look for (^epics:, version:, ^release:). A validator that only
|
|
10
|
+
# greps for those strings passes vacuously; a validator that really parses
|
|
11
|
+
# the YAML must FAIL.
|
|
12
|
+
#
|
|
13
|
+
# Mirrors scripts/golden-g07-negative-path.sh's fixture + non-zero-exit
|
|
14
|
+
# pattern.
|
|
15
|
+
#
|
|
16
|
+
# Usage: bash scripts/golden-g08-anti-vacuity.sh
|
|
17
|
+
# Exit 0: validate-specs-yaml.sh correctly detected the corrupt fixture
|
|
18
|
+
# Exit 1: validate-specs-yaml.sh passed vacuously on the corrupt fixture
|
|
19
|
+
|
|
20
|
+
set -euo pipefail
|
|
21
|
+
|
|
22
|
+
RED='\033[0;31m'
|
|
23
|
+
GREEN='\033[0;32m'
|
|
24
|
+
NC='\033[0m'
|
|
25
|
+
|
|
26
|
+
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
27
|
+
FIXTURE_DIR="$REPO_ROOT/specs/verifications/fixtures/corrupt-yaml"
|
|
28
|
+
VALIDATOR="$REPO_ROOT/scripts/validate-specs-yaml.sh"
|
|
29
|
+
|
|
30
|
+
echo "=== G-08: Anti-Vacuity Self-Test (validate-specs-yaml.sh) ==="
|
|
31
|
+
echo "Fixture: $FIXTURE_DIR"
|
|
32
|
+
echo ""
|
|
33
|
+
|
|
34
|
+
if [[ ! -f "$FIXTURE_DIR/release-plan.yaml" ]]; then
|
|
35
|
+
echo "ERROR: fixture not found: $FIXTURE_DIR/release-plan.yaml"
|
|
36
|
+
exit 1
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
set +e
|
|
40
|
+
actual_output=$(bash "$VALIDATOR" "$FIXTURE_DIR" 2>&1)
|
|
41
|
+
actual_exit=$?
|
|
42
|
+
set -e
|
|
43
|
+
|
|
44
|
+
echo "$actual_output"
|
|
45
|
+
echo ""
|
|
46
|
+
|
|
47
|
+
if [[ $actual_exit -ne 0 ]]; then
|
|
48
|
+
echo -e "${GREEN}PASS${NC} validate-specs-yaml.sh correctly detected the corrupt fixture (exit $actual_exit)"
|
|
49
|
+
echo "G-08: PASS"
|
|
50
|
+
exit 0
|
|
51
|
+
else
|
|
52
|
+
echo -e "${RED}FAIL${NC} validate-specs-yaml.sh passed vacuously on a structurally corrupt fixture (exit 0)"
|
|
53
|
+
echo "G-08: FAIL — fail-open risk (same bug class as GAP-1 / BUG-2026-07-03-validate-specs-no-real-parser)"
|
|
54
|
+
exit 1
|
|
55
|
+
fi
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# golden-g09-yaml-roundtrip.sh — yaml-tools.py Lossless Round-Trip Self-Test (G-09)
|
|
3
|
+
#
|
|
4
|
+
# Proves yaml-tools.py's `set` command preserves structure it doesn't touch
|
|
5
|
+
# (BUG-2026-07-03-yaml-roundtrip-corruption bug class). The old hand-rolled
|
|
6
|
+
# parser/dumper had no concept of YAML lists or block scalars and flattened
|
|
7
|
+
# both on every write. This feeds `set` a fixture containing an epics[] list
|
|
8
|
+
# (nested dicts + a flow-style list) and a block scalar (`note: >`), changes
|
|
9
|
+
# one unrelated field, and asserts:
|
|
10
|
+
# 1. the file still parses as valid YAML afterward
|
|
11
|
+
# 2. the epics[] list still has the same length and nested keys
|
|
12
|
+
# 3. fields untouched by the `set` call keep their original values
|
|
13
|
+
# 4. the targeted field actually changed
|
|
14
|
+
#
|
|
15
|
+
# Usage: bash scripts/golden-g09-yaml-roundtrip.sh
|
|
16
|
+
# Exit 0: round trip is lossless
|
|
17
|
+
# Exit 1: round trip destroyed structure (corruption bug is back)
|
|
18
|
+
|
|
19
|
+
set -euo pipefail
|
|
20
|
+
|
|
21
|
+
RED='\033[0;31m'
|
|
22
|
+
GREEN='\033[0;32m'
|
|
23
|
+
NC='\033[0m'
|
|
24
|
+
|
|
25
|
+
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
26
|
+
FIXTURE="$REPO_ROOT/specs/verifications/fixtures/yaml-roundtrip/sample.yaml"
|
|
27
|
+
TOOL="$REPO_ROOT/scripts/yaml-tools.py"
|
|
28
|
+
|
|
29
|
+
echo "=== G-09: yaml-tools.py Lossless Round-Trip Self-Test ==="
|
|
30
|
+
|
|
31
|
+
if [[ ! -f "$FIXTURE" ]]; then
|
|
32
|
+
echo "ERROR: fixture not found: $FIXTURE"
|
|
33
|
+
exit 1
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
WORKDIR="$(mktemp -d)"
|
|
37
|
+
trap 'rm -rf "$WORKDIR"' EXIT
|
|
38
|
+
cp "$FIXTURE" "$WORKDIR/sample.yaml"
|
|
39
|
+
|
|
40
|
+
python3 "$TOOL" set "$WORKDIR/sample.yaml" release.version "9.9.9-test"
|
|
41
|
+
|
|
42
|
+
set +e
|
|
43
|
+
RESULT="$(python3 -c "
|
|
44
|
+
import sys, yaml
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
with open(sys.argv[1]) as f:
|
|
48
|
+
d = yaml.safe_load(f)
|
|
49
|
+
except yaml.YAMLError as e:
|
|
50
|
+
print('FAIL: file no longer parses as YAML:', str(e).splitlines()[0])
|
|
51
|
+
sys.exit(1)
|
|
52
|
+
|
|
53
|
+
errors = []
|
|
54
|
+
|
|
55
|
+
if d.get('release', {}).get('version') != '9.9.9-test':
|
|
56
|
+
errors.append('targeted field release.version was not updated')
|
|
57
|
+
|
|
58
|
+
epics = d.get('epics')
|
|
59
|
+
if not isinstance(epics, list) or len(epics) != 2:
|
|
60
|
+
errors.append(f'epics[] list destroyed: expected 2 items, got {epics!r}')
|
|
61
|
+
else:
|
|
62
|
+
if epics[0].get('id') != 'e19' or epics[0].get('wsjf') != 4.7:
|
|
63
|
+
errors.append('epics[0] fields not preserved')
|
|
64
|
+
if 'note' not in epics[0] or 'Delivered' not in epics[0].get('note', ''):
|
|
65
|
+
errors.append('epics[0].note (block scalar) not preserved')
|
|
66
|
+
if epics[1].get('id') != 'e40' or epics[1].get('depends_on') != ['e39', 'e28']:
|
|
67
|
+
errors.append('epics[1] fields (incl. flow-style list) not preserved')
|
|
68
|
+
|
|
69
|
+
if d.get('release', {}).get('codename') != 'Deepening':
|
|
70
|
+
errors.append('release.codename (untouched sibling field) not preserved')
|
|
71
|
+
|
|
72
|
+
if d.get('bigpowers_version') != '2.58.7':
|
|
73
|
+
errors.append('bigpowers_version (untouched top-level field) not preserved')
|
|
74
|
+
|
|
75
|
+
if errors:
|
|
76
|
+
for e in errors:
|
|
77
|
+
print('FAIL:', e)
|
|
78
|
+
sys.exit(1)
|
|
79
|
+
|
|
80
|
+
print('OK: round trip is lossless')
|
|
81
|
+
sys.exit(0)
|
|
82
|
+
" "$WORKDIR/sample.yaml")"
|
|
83
|
+
EXIT=$?
|
|
84
|
+
set -e
|
|
85
|
+
|
|
86
|
+
echo "$RESULT"
|
|
87
|
+
|
|
88
|
+
if [[ $EXIT -eq 0 ]]; then
|
|
89
|
+
echo -e "${GREEN}PASS${NC} yaml-tools.py round trip is lossless"
|
|
90
|
+
echo "G-09: PASS"
|
|
91
|
+
exit 0
|
|
92
|
+
else
|
|
93
|
+
echo -e "${RED}FAIL${NC} yaml-tools.py round trip destroyed structure"
|
|
94
|
+
echo "G-09: FAIL — same bug class as BUG-2026-07-03-yaml-roundtrip-corruption"
|
|
95
|
+
exit 1
|
|
96
|
+
fi
|
package/scripts/install.sh
CHANGED
|
@@ -366,7 +366,10 @@ if STRICT:
|
|
|
366
366
|
wsjf_sorted = sorted(set(s["wsjf"] for s in matrix_stories), reverse=True)
|
|
367
367
|
cutoff_idx = max(0, len(wsjf_sorted) // 4 - 1)
|
|
368
368
|
p0_threshold = wsjf_sorted[cutoff_idx] if cutoff_idx < len(wsjf_sorted) else 0
|
|
369
|
-
|
|
369
|
+
# Only flag non-backlog stories — backlog stories haven't been implemented yet
|
|
370
|
+
uncovered_p0 = [s["id"] for s in matrix_stories
|
|
371
|
+
if s["wsjf"] >= p0_threshold and len(s["links"]) == 0
|
|
372
|
+
and s["status"] != "backlog"]
|
|
370
373
|
if uncovered_p0:
|
|
371
374
|
print(f"trace-stories.sh: STRICT FAIL — P0 stories with 0% coverage: {', '.join(uncovered_p0)}", file=sys.stderr)
|
|
372
375
|
sys.exit(2)
|
|
@@ -32,6 +32,9 @@ GATES=(
|
|
|
32
32
|
"compliance:npm run compliance:false"
|
|
33
33
|
"g04-selftest:bash scripts/golden-g04-selftest.sh:true"
|
|
34
34
|
"g07-negative-path:bash scripts/golden-g07-negative-path.sh:false"
|
|
35
|
+
"g08-anti-vacuity:bash scripts/golden-g08-anti-vacuity.sh:false"
|
|
36
|
+
"g09-yaml-roundtrip:bash scripts/golden-g09-yaml-roundtrip.sh:false"
|
|
37
|
+
"specs-parse:bash scripts/validate-specs-yaml.sh:false"
|
|
35
38
|
)
|
|
36
39
|
|
|
37
40
|
GREEN='\033[0;32m'
|
|
@@ -1,38 +1,98 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# validate-specs-yaml.sh — required keys for state,
|
|
2
|
+
# validate-specs-yaml.sh — real-parse gate + required keys for state,
|
|
3
|
+
# release-plan, execution-status.
|
|
4
|
+
#
|
|
5
|
+
# BUG-2026-07-03-validate-specs-no-real-parser: this validator used to only
|
|
6
|
+
# grep for key strings, so a structurally flattened/corrupt YAML file that
|
|
7
|
+
# still contained those strings passed vacuously. It now really parses every
|
|
8
|
+
# specs/**/*.yaml with PyYAML first, and runs the required-key checks
|
|
9
|
+
# against the *parsed* objects, not against grep.
|
|
3
10
|
set -euo pipefail
|
|
4
11
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
5
12
|
SPECS="${1:-$REPO_ROOT/specs}"
|
|
6
13
|
|
|
7
14
|
err=0
|
|
8
|
-
|
|
9
|
-
|
|
15
|
+
|
|
16
|
+
# ── Phase 1: real parse pass — every specs/**/*.yaml must be valid YAML ──────
|
|
17
|
+
parse_report="$(python3 -c "
|
|
18
|
+
import sys, glob, os
|
|
19
|
+
import yaml
|
|
20
|
+
|
|
21
|
+
specs = sys.argv[1]
|
|
22
|
+
bad = []
|
|
23
|
+
for f in sorted(glob.glob(os.path.join(specs, '**', '*.yaml'), recursive=True)):
|
|
24
|
+
# Test fixtures (e.g. specs/verifications/fixtures/corrupt-yaml/) hold
|
|
25
|
+
# deliberately-broken YAML for the G-07/G-08 negative-path golden checks.
|
|
26
|
+
# Skip them when scanning a real specs/ tree — but still validate them
|
|
27
|
+
# when a fixture dir is itself the target (G-08 invokes this script
|
|
28
|
+
# with the fixture dir as $SPECS directly).
|
|
29
|
+
rel = os.path.relpath(f, specs)
|
|
30
|
+
if rel.startswith('verifications' + os.sep + 'fixtures' + os.sep):
|
|
31
|
+
continue
|
|
32
|
+
try:
|
|
33
|
+
yaml.safe_load(open(f))
|
|
34
|
+
except yaml.YAMLError as e:
|
|
35
|
+
bad.append((f, str(e).splitlines()[0] if str(e) else type(e).__name__))
|
|
36
|
+
|
|
37
|
+
for f, msg in bad:
|
|
38
|
+
print(f'PARSE ERROR: {f}: {msg}')
|
|
39
|
+
|
|
40
|
+
sys.exit(1 if bad else 0)
|
|
41
|
+
" "$SPECS")" || err=1
|
|
42
|
+
|
|
43
|
+
if [[ -n "$parse_report" ]]; then
|
|
44
|
+
echo "$parse_report"
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
if [[ "$err" -ne 0 ]]; then
|
|
48
|
+
echo "validate-specs-yaml: FAIL — one or more specs/**/*.yaml files are not valid YAML"
|
|
49
|
+
exit 1
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# ── Phase 2: required-key checks against the parsed objects, not grep ────────
|
|
53
|
+
need_key() {
|
|
54
|
+
local file="$1" py_check="$2" msg="$3"
|
|
10
55
|
if [[ ! -f "$file" ]]; then
|
|
11
56
|
echo "missing: $file"
|
|
12
57
|
err=1
|
|
13
58
|
return
|
|
14
59
|
fi
|
|
15
|
-
if !
|
|
60
|
+
if ! python3 -c "
|
|
61
|
+
import sys, yaml
|
|
62
|
+
d = yaml.safe_load(open(sys.argv[1])) or {}
|
|
63
|
+
sys.exit(0 if ($py_check) else 1)
|
|
64
|
+
" "$file" 2>/dev/null; then
|
|
16
65
|
echo "$file: $msg"
|
|
17
66
|
err=1
|
|
18
67
|
fi
|
|
19
68
|
}
|
|
20
69
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
70
|
+
need_key "$SPECS/state.yaml" "isinstance(d.get('active_flow'), str)" 'missing active_flow'
|
|
71
|
+
need_key "$SPECS/release-plan.yaml" "isinstance(d.get('release'), dict)" 'missing release block'
|
|
72
|
+
need_key "$SPECS/release-plan.yaml" "isinstance(d.get('release'), dict) and 'version' in d['release']" 'missing release.version'
|
|
73
|
+
need_key "$SPECS/release-plan.yaml" "isinstance(d.get('epics'), list) and len(d['epics']) > 0" 'missing epics list'
|
|
74
|
+
need_key "$SPECS/execution-status.yaml" "isinstance(d.get('development_status'), dict)" 'missing development_status'
|
|
26
75
|
|
|
76
|
+
# ── Phase 3: every epic's `file` reference must exist, read from the parsed object ──
|
|
27
77
|
if [[ -f "$SPECS/release-plan.yaml" ]]; then
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
78
|
+
missing_files="$(python3 -c "
|
|
79
|
+
import sys, os, yaml
|
|
80
|
+
specs = sys.argv[1]
|
|
81
|
+
d = yaml.safe_load(open(os.path.join(specs, 'release-plan.yaml'))) or {}
|
|
82
|
+
for epic in d.get('epics') or []:
|
|
83
|
+
if not isinstance(epic, dict):
|
|
84
|
+
continue
|
|
85
|
+
f = epic.get('file')
|
|
86
|
+
if f and not os.path.isfile(os.path.join(specs, f)):
|
|
87
|
+
print(os.path.join(specs, f))
|
|
88
|
+
" "$SPECS")"
|
|
89
|
+
if [[ -n "$missing_files" ]]; then
|
|
90
|
+
while IFS= read -r path; do
|
|
91
|
+
[[ -z "$path" ]] && continue
|
|
32
92
|
echo "release-plan: missing epic file $path"
|
|
33
93
|
err=1
|
|
34
|
-
|
|
35
|
-
|
|
94
|
+
done <<< "$missing_files"
|
|
95
|
+
fi
|
|
36
96
|
fi
|
|
37
97
|
|
|
38
98
|
if [[ "$err" -ne 0 ]]; then
|
package/scripts/yaml-tools.py
CHANGED
|
@@ -1,78 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
2
|
+
"""YAML helpers for bigpowers specs.
|
|
3
|
+
|
|
4
|
+
BUG-2026-07-03-yaml-roundtrip-corruption: this used to hand-roll a
|
|
5
|
+
"_parse_simple_yaml" reader with no concept of YAML lists or block
|
|
6
|
+
scalars, and re-serialize with a matching hand-rolled dumper. Any file
|
|
7
|
+
with a `- item` sequence (every epics[] list, every stories[] list) or
|
|
8
|
+
a block scalar (`note: >`) got silently flattened and destroyed on the
|
|
9
|
+
very first `set` call. It now uses PyYAML's safe_load/safe_dump for a
|
|
10
|
+
real, information-preserving round trip.
|
|
11
|
+
"""
|
|
3
12
|
from __future__ import annotations
|
|
4
13
|
|
|
5
|
-
import re
|
|
6
14
|
import sys
|
|
7
15
|
from pathlib import Path
|
|
8
16
|
from typing import Any
|
|
9
17
|
|
|
10
|
-
|
|
11
|
-
def _parse_simple_yaml(text: str) -> dict[str, Any]:
|
|
12
|
-
"""Parse flat and one-level-nested YAML (no lists of objects)."""
|
|
13
|
-
root: dict[str, Any] = {}
|
|
14
|
-
stack: list[tuple[int, dict[str, Any]]] = [(0, root)]
|
|
15
|
-
for raw in text.splitlines():
|
|
16
|
-
if not raw.strip() or raw.strip().startswith("#"):
|
|
17
|
-
continue
|
|
18
|
-
indent = len(raw) - len(raw.lstrip())
|
|
19
|
-
line = raw.strip()
|
|
20
|
-
if ":" not in line:
|
|
21
|
-
continue
|
|
22
|
-
key, _, val = line.partition(":")
|
|
23
|
-
key = key.strip()
|
|
24
|
-
val = val.strip()
|
|
25
|
-
while stack and indent < stack[-1][0]:
|
|
26
|
-
stack.pop()
|
|
27
|
-
cur = stack[-1][1]
|
|
28
|
-
if val == "":
|
|
29
|
-
nxt: dict[str, Any] = {}
|
|
30
|
-
cur[key] = nxt
|
|
31
|
-
stack.append((indent + 2, nxt))
|
|
32
|
-
else:
|
|
33
|
-
if val in ("true", "false"):
|
|
34
|
-
cur[key] = val == "true"
|
|
35
|
-
elif val.startswith('"') and val.endswith('"'):
|
|
36
|
-
cur[key] = val[1:-1]
|
|
37
|
-
elif val.startswith("'") and val.endswith("'"):
|
|
38
|
-
cur[key] = val[1:-1]
|
|
39
|
-
else:
|
|
40
|
-
try:
|
|
41
|
-
if "." in val:
|
|
42
|
-
cur[key] = float(val)
|
|
43
|
-
else:
|
|
44
|
-
cur[key] = int(val)
|
|
45
|
-
except ValueError:
|
|
46
|
-
cur[key] = val
|
|
47
|
-
return root
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def _dump_value(val: Any, indent: int) -> str:
|
|
51
|
-
sp = " " * indent
|
|
52
|
-
if isinstance(val, dict):
|
|
53
|
-
lines = []
|
|
54
|
-
for k, v in val.items():
|
|
55
|
-
if isinstance(v, dict):
|
|
56
|
-
lines.append(f"{sp}{k}:")
|
|
57
|
-
lines.append(_dump_value(v, indent + 2).rstrip())
|
|
58
|
-
elif isinstance(v, bool):
|
|
59
|
-
lines.append(f"{sp}{k}: {'true' if v else 'false'}")
|
|
60
|
-
elif isinstance(v, (int, float)):
|
|
61
|
-
lines.append(f"{sp}{k}: {v}")
|
|
62
|
-
else:
|
|
63
|
-
s = str(v).replace('"', '\\"')
|
|
64
|
-
lines.append(f'{sp}{k}: "{s}"')
|
|
65
|
-
return "\n".join(lines) + "\n"
|
|
66
|
-
return f"{sp}{val}\n"
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
def dump_yaml(data: dict[str, Any]) -> str:
|
|
70
|
-
return _dump_value(data, 0)
|
|
18
|
+
import yaml
|
|
71
19
|
|
|
72
20
|
|
|
73
21
|
def set_path(path: Path, dotted_key: str, value: str) -> None:
|
|
22
|
+
"""Surgically set one dotted-path key, preserving everything else in the file."""
|
|
74
23
|
text = path.read_text(encoding="utf-8") if path.exists() else ""
|
|
75
|
-
|
|
24
|
+
loaded: Any = yaml.safe_load(text) if text.strip() else {}
|
|
25
|
+
data: dict[str, Any] = loaded if isinstance(loaded, dict) else {}
|
|
26
|
+
|
|
76
27
|
parts = dotted_key.split(".")
|
|
77
28
|
cur: Any = data
|
|
78
29
|
for part in parts[:-1]:
|
|
@@ -80,6 +31,7 @@ def set_path(path: Path, dotted_key: str, value: str) -> None:
|
|
|
80
31
|
cur[part] = {}
|
|
81
32
|
cur = cur[part]
|
|
82
33
|
last = parts[-1]
|
|
34
|
+
|
|
83
35
|
if value.lower() in ("true", "false"):
|
|
84
36
|
cur[last] = value.lower() == "true"
|
|
85
37
|
else:
|
|
@@ -90,16 +42,24 @@ def set_path(path: Path, dotted_key: str, value: str) -> None:
|
|
|
90
42
|
cur[last] = float(value)
|
|
91
43
|
except ValueError:
|
|
92
44
|
cur[last] = value.strip('"').strip("'")
|
|
45
|
+
|
|
93
46
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
94
|
-
|
|
47
|
+
# sort_keys=False preserves author-intended ordering (e.g. epics[]
|
|
48
|
+
# sequencing by WSJF priority) instead of alphabetizing every key.
|
|
49
|
+
path.write_text(
|
|
50
|
+
yaml.safe_dump(data, sort_keys=False, allow_unicode=True, width=100),
|
|
51
|
+
encoding="utf-8",
|
|
52
|
+
)
|
|
95
53
|
|
|
96
54
|
|
|
97
55
|
def validate_file(path: Path, required_keys: list[str]) -> list[str]:
|
|
98
|
-
errors: list[str] = []
|
|
99
56
|
if not path.exists():
|
|
100
57
|
return [f"missing: {path}"]
|
|
101
|
-
|
|
102
|
-
|
|
58
|
+
try:
|
|
59
|
+
data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
|
60
|
+
except yaml.YAMLError as e:
|
|
61
|
+
return [f"{path}: PARSE ERROR: {str(e).splitlines()[0]}"]
|
|
62
|
+
errors: list[str] = []
|
|
103
63
|
for key in required_keys:
|
|
104
64
|
parts = key.split(".")
|
|
105
65
|
cur: Any = data
|
|
@@ -14,8 +14,9 @@ Welcome to **bigpowers** — a lifecycle of **70** agent skills for production-r
|
|
|
14
14
|
## Install
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
|
-
npx bigpowers
|
|
18
|
-
npm install -g bigpowers # global install
|
|
17
|
+
npx bigpowers setup # one-shot setup
|
|
18
|
+
npm install -g bigpowers # global install
|
|
19
|
+
bigpowers setup # sync skills + link to your tools
|
|
19
20
|
```
|
|
20
21
|
|
|
21
22
|
From source (contributors): `git clone` → `npm install` or `bash scripts/install.sh`.
|
package/skills-lock.json
CHANGED
|
@@ -328,7 +328,7 @@
|
|
|
328
328
|
},
|
|
329
329
|
"using-bigpowers": {
|
|
330
330
|
"description": "One-time bootstrap that introduces the bigpowers skills system, the PMBOK lifecycle arc, and tells you which skill to call first for your situation. Use when starting with bigpowers for the first time, when user asks \"where do I start?\", or when the skills system needs to be explained.",
|
|
331
|
-
"sha256": "
|
|
331
|
+
"sha256": "33f6d163b536e87d",
|
|
332
332
|
"path": "skills/using-bigpowers/SKILL.md"
|
|
333
333
|
},
|
|
334
334
|
"validate-contracts": {
|