bigpowers 2.86.3 → 2.86.4

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.86.3",
3
+ "version": "2.86.4",
4
4
  "description": "80 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.86.4](https://github.com/danielvm-git/bigpowers/compare/v2.86.3...v2.86.4) (2026-07-26)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **specs:** reconcile planning SoT drift and gate it with G-12 ([d8553a7](https://github.com/danielvm-git/bigpowers/commit/d8553a748d4cdc81fa612564f84600a62c4789ce))
7
+
1
8
  ## [2.86.3](https://github.com/danielvm-git/bigpowers/compare/v2.86.2...v2.86.3) (2026-07-26)
2
9
 
3
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bigpowers",
3
- "version": "2.86.3",
3
+ "version": "2.86.4",
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": {
@@ -0,0 +1,232 @@
1
+ #!/usr/bin/env bash
2
+ # story: e51s04
3
+ # golden-g12-status-consistency.sh — Planning SoT Consistency Gate (G-12)
4
+ #
5
+ # CONVENTIONS.md § specs/ declares specs/execution-status.yaml the sole SoT for
6
+ # story state. Nothing enforced that. sync-status-from-epics.sh is a *seeder*:
7
+ # it preloads keys from execution-status.yaml itself and uses setdefault(), so
8
+ # an existing key keeps its old value and epic.yaml's stories[].status is never
9
+ # read. Story status written by build-epic step 8 into a capsule could therefore
10
+ # disagree with the SoT forever, undetected
11
+ # (BUG-2026-07-26-planning-sot-drift).
12
+ #
13
+ # This gate makes the two agree or fails.
14
+ #
15
+ # Authority model (CONVENTIONS.md § specs/): execution-status.yaml is the sole
16
+ # SoT for story state; a capsule's stories[].status is derived. Capsules under
17
+ # specs/epics/archive/ are frozen historical records — an archived shard that
18
+ # still reads 'todo' while the SoT reads 'done' is normal history, not drift, so
19
+ # archive/ is excluded. Status vocabulary is normalised before comparison
20
+ # ('passing' == 'done'; 'todo'/'planned'/'not-started' == 'backlog').
21
+ #
22
+ # Checks:
23
+ # 1. A LIVE capsule must not disagree with execution-status.yaml (both the
24
+ # stories: and development_status: sections). In practice this catches a
25
+ # capsule marked done by build-epic step 8 whose SoT was never updated.
26
+ # 2. Every capsule_dir in release-plan.yaml must resolve to a real directory.
27
+ # 3. A live capsule must not also exist under archive/ — two shards for one
28
+ # epic makes "which status is authoritative" undecidable.
29
+ #
30
+ # Usage: bash scripts/golden-g12-status-consistency.sh [--self-test]
31
+ # Exit 0: SoT is consistent
32
+ # Exit 1: drift detected (or, under --self-test, the gate failed to detect
33
+ # deliberately injected drift — a vacuous gate, G-08 bug class)
34
+
35
+ set -euo pipefail
36
+ source "$(dirname "${BASH_SOURCE[0]}")/lib/python-env.sh"
37
+ source "$(dirname "${BASH_SOURCE[0]}")/lib/skill-common.sh"
38
+ resolve_repo_root
39
+
40
+ RED='\033[0;31m'
41
+ GREEN='\033[0;32m'
42
+ NC='\033[0m'
43
+
44
+ SELF_TEST=0
45
+ [[ "${1:-}" == "--self-test" ]] && SELF_TEST=1
46
+
47
+ check_tree() {
48
+ # $1 = specs dir to check
49
+ $PYTHON - "$1" <<'PY'
50
+ import sys, re
51
+ from pathlib import Path
52
+ import yaml
53
+
54
+ specs = Path(sys.argv[1])
55
+ epics_dir = specs / "epics"
56
+ exec_path = specs / "execution-status.yaml"
57
+ rp_path = specs / "release-plan.yaml"
58
+
59
+ problems = []
60
+ archived_skipped = 0
61
+
62
+ # Status vocabulary normalisation — different skills have written different
63
+ # words for the same state over the repo's history.
64
+ ALIASES = {
65
+ "passing": "done",
66
+ "complete": "done",
67
+ "completed": "done",
68
+ "todo": "backlog",
69
+ "planned": "backlog",
70
+ "not-started": "backlog",
71
+ }
72
+
73
+
74
+ def norm(v):
75
+ return ALIASES.get(str(v).strip().lower(), str(v).strip().lower())
76
+
77
+
78
+ exec_data = yaml.safe_load(exec_path.read_text(encoding="utf-8")) or {}
79
+ dev = exec_data.get("development_status") or {}
80
+ stories = exec_data.get("stories") or {}
81
+
82
+ # --- Check 1: LIVE epic.yaml story status vs execution-status.yaml ---
83
+ for epic_file in sorted(epics_dir.rglob("epic.yaml")):
84
+ rel = epic_file.relative_to(specs)
85
+ if "archive" in epic_file.relative_to(epics_dir).parts:
86
+ archived_skipped += 1
87
+ continue
88
+
89
+ try:
90
+ ed = yaml.safe_load(epic_file.read_text(encoding="utf-8")) or {}
91
+ except Exception as exc:
92
+ problems.append(f"unparseable epic shard {rel}: {exc}")
93
+ continue
94
+
95
+ for story in ed.get("stories") or []:
96
+ if not isinstance(story, dict):
97
+ continue
98
+ sid = story.get("id")
99
+ declared = story.get("status")
100
+ if not sid or not declared:
101
+ continue
102
+ if not re.match(r"^e\d+s\d+$", str(sid)):
103
+ continue
104
+
105
+ sot_detail = stories.get(sid, {})
106
+ sot_detail = sot_detail.get("status") if isinstance(sot_detail, dict) else None
107
+ sot_summary = dev.get(sid)
108
+
109
+ if sot_detail is not None and norm(sot_detail) != norm(declared):
110
+ problems.append(
111
+ f"{sid}: {rel} says '{declared}' but execution-status.yaml "
112
+ f"stories.{sid}.status says '{sot_detail}'"
113
+ )
114
+ if sot_summary is not None and norm(sot_summary) != norm(declared):
115
+ problems.append(
116
+ f"{sid}: {rel} says '{declared}' but execution-status.yaml "
117
+ f"development_status.{sid} says '{sot_summary}'"
118
+ )
119
+
120
+ # --- Check 2: release-plan.yaml capsule_dir resolves ---
121
+ if rp_path.exists():
122
+ rp = yaml.safe_load(rp_path.read_text(encoding="utf-8")) or {}
123
+ for ep in rp.get("epics") or []:
124
+ if not isinstance(ep, dict):
125
+ continue
126
+ cd = ep.get("capsule_dir")
127
+ if cd and not (specs / cd).is_dir():
128
+ problems.append(
129
+ f"{ep.get('id')}: release-plan.yaml capsule_dir '{cd}' does not exist"
130
+ )
131
+
132
+ # --- Check 3: no epic has both a live and an archived capsule ---
133
+ archive_dir = epics_dir / "archive"
134
+ if archive_dir.is_dir():
135
+ live = {p.name for p in epics_dir.iterdir() if p.is_dir() and p.name != "archive"}
136
+ for p in archive_dir.iterdir():
137
+ if p.is_dir() and p.name in live:
138
+ problems.append(
139
+ f"{p.name}: capsule exists both live and under archive/ — "
140
+ f"authoritative status is undecidable"
141
+ )
142
+
143
+ if archived_skipped:
144
+ print(f" info: {archived_skipped} archived capsule(s) skipped (frozen history)")
145
+
146
+ if problems:
147
+ for p in problems:
148
+ print(f" DRIFT {p}")
149
+ print(f"\n{len(problems)} inconsistencies")
150
+ sys.exit(1)
151
+
152
+ print(" ok : epic shards, execution-status.yaml and release-plan.yaml agree")
153
+ sys.exit(0)
154
+ PY
155
+ }
156
+
157
+ if [[ "$SELF_TEST" -eq 1 ]]; then
158
+ echo "=== G-12 self-test: prove the gate can fail ==="
159
+ TMP=$(mktemp -d)
160
+ trap 'rm -rf "$TMP"' EXIT
161
+ # Copy only what check_tree reads — specs/ as a whole is ~28MB and the copy
162
+ # would dominate the gate's runtime.
163
+ mkdir -p "$TMP/specs"
164
+ cp -R "$REPO_ROOT/specs/epics" "$TMP/specs/epics"
165
+ cp "$REPO_ROOT/specs/execution-status.yaml" "$TMP/specs/"
166
+ cp "$REPO_ROOT/specs/release-plan.yaml" "$TMP/specs/"
167
+
168
+ # Inject drift into a LIVE capsule. Archived shards are excluded from the
169
+ # check, so mutating one would prove nothing — the gate would rightly stay
170
+ # silent and the self-test would report a false vacuity failure.
171
+ $PYTHON - "$TMP/specs" <<'PY'
172
+ import sys, re
173
+ from pathlib import Path
174
+ import yaml
175
+
176
+ specs = Path(sys.argv[1])
177
+ epics_dir = specs / "epics"
178
+ exec_path = specs / "execution-status.yaml"
179
+ data = yaml.safe_load(exec_path.read_text(encoding="utf-8")) or {}
180
+
181
+ for epic_file in sorted(epics_dir.rglob("epic.yaml")):
182
+ if "archive" in epic_file.relative_to(epics_dir).parts:
183
+ continue
184
+ ed = yaml.safe_load(epic_file.read_text(encoding="utf-8")) or {}
185
+ for story in ed.get("stories") or []:
186
+ if not isinstance(story, dict):
187
+ continue
188
+ sid, st = story.get("id"), story.get("status")
189
+ if sid and st and re.match(r"^e\d+s\d+$", str(sid)):
190
+ flipped = "backlog" if st != "backlog" else "done"
191
+ data.setdefault("stories", {}).setdefault(sid, {})["status"] = flipped
192
+ data.setdefault("development_status", {})[sid] = flipped
193
+ exec_path.write_text(
194
+ yaml.dump(data, default_flow_style=False, sort_keys=False),
195
+ encoding="utf-8",
196
+ )
197
+ print(f"injected: {sid} {st} -> {flipped} (live capsule)")
198
+ sys.exit(0)
199
+ print("ERROR: no live story with a status found to mutate", file=sys.stderr)
200
+ sys.exit(2)
201
+ PY
202
+
203
+ set +e
204
+ check_tree "$TMP/specs" >/dev/null 2>&1
205
+ injected_exit=$?
206
+ set -e
207
+
208
+ if [[ $injected_exit -ne 0 ]]; then
209
+ echo -e "${GREEN}PASS${NC} G-12 detected injected drift (exit $injected_exit)"
210
+ echo "G-12 self-test: PASS"
211
+ exit 0
212
+ fi
213
+ echo -e "${RED}FAIL${NC} G-12 passed on deliberately drifted specs — vacuous gate"
214
+ echo "G-12 self-test: FAIL — same bug class as G-08 / BUG-2026-07-26-planning-sot-drift"
215
+ exit 1
216
+ fi
217
+
218
+ echo "=== G-12: Planning SoT Consistency ==="
219
+ set +e
220
+ output=$(check_tree "$REPO_ROOT/specs" 2>&1)
221
+ actual_exit=$?
222
+ set -e
223
+ echo "$output"
224
+
225
+ if [[ $actual_exit -eq 0 ]]; then
226
+ echo -e "${GREEN}PASS${NC} planning SoT is consistent"
227
+ echo "G-12: PASS"
228
+ exit 0
229
+ fi
230
+ echo -e "${RED}FAIL${NC} planning SoT has drifted"
231
+ echo "G-12: FAIL — see BUG-2026-07-26-planning-sot-drift"
232
+ exit 1
@@ -14,6 +14,11 @@ GOLDEN_GATES=(
14
14
  "g09-yaml-roundtrip:bash scripts/golden-g09-yaml-roundtrip.sh:false"
15
15
  "g10-trace-anti-vacuity:bash scripts/golden-g10-trace-anti-vacuity.sh:false"
16
16
  "g11-gitignore-venv:bash scripts/golden-g11-gitignore-venv.sh:false"
17
+ # Planning SoT consistency (BUG-2026-07-26-planning-sot-drift): live epic
18
+ # shards, execution-status.yaml and release-plan.yaml must agree. Self-test
19
+ # first so a vacuous gate is caught before its verdict is trusted.
20
+ "g12-status-selftest:bash scripts/golden-g12-status-consistency.sh --self-test:false"
21
+ "g12-status-consistency:bash scripts/golden-g12-status-consistency.sh:false"
17
22
  "install-helpers:bash scripts/test-install-helpers.sh:false"
18
23
  "import-boundaries:bash scripts/check-import-boundaries.sh:false"
19
24
  "skill-catalog:bash scripts/validate-skill-catalog.sh:false"
@@ -1,5 +1,13 @@
1
1
  #!/usr/bin/env bash
2
2
  # sync-status-from-epics.sh — seed execution-status.yaml keys from epic shards
3
+ #
4
+ # SEEDER, NOT A RECONCILER. `keys` is preloaded from execution-status.yaml
5
+ # itself and applied with setdefault(), so an already-present key keeps its
6
+ # existing value; epic.yaml's stories[].status is never read. Running this does
7
+ # NOT pull a capsule's status into the SoT — it only adds missing keys and
8
+ # refreshes task counters. Drift between a capsule and the SoT is detected by
9
+ # scripts/golden-g12-status-consistency.sh (G-12), not corrected here.
10
+ # See specs/bugs/BUG-2026-07-26-planning-sot-drift.md.
3
11
  set -euo pipefail
4
12
  source "$(dirname "${BASH_SOURCE[0]}")/lib/python-env.sh"
5
13
  source "$(dirname "${BASH_SOURCE[0]}")/lib/skill-common.sh"