bigpowers 2.78.1 → 2.79.1
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 +14 -0
- package/CONVENTIONS.md +33 -0
- package/package.json +1 -1
- package/scripts/lib/__pycache__/link_utils.cpython-312.pyc +0 -0
- package/scripts/lib/completeness-critic.sh +18 -5
- package/scripts/test-completeness-critic-scope.sh +99 -0
- package/scripts/test-generate-epics-wiki-zero-stories.sh +64 -0
- package/scripts/test-golden-g11-worktree-venv.sh +63 -0
- package/scripts/test-tombstone-mechanism.sh +111 -0
- package/scripts/tombstone-skill.sh +100 -0
- package/scripts/validate-tombstones.sh +82 -0
package/.pi/package.json
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
## [2.79.1](https://github.com/danielvm-git/bigpowers/compare/v2.79.0...v2.79.1) (2026-07-23)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **e53:** close migration-readiness gaps — code-injection, expiry semantics, fail-open gate ([4042c62](https://github.com/danielvm-git/bigpowers/commit/4042c62f5ccac9790596f81aac5aaebd1235c763))
|
|
7
|
+
|
|
8
|
+
# [2.79.0](https://github.com/danielvm-git/bigpowers/compare/v2.78.1...v2.79.0) (2026-07-23)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* **scripts:** implement tombstone-skill.sh and validate-tombstones.sh ([c81a451](https://github.com/danielvm-git/bigpowers/commit/c81a451598d8338188fb9e18b4486e20e6c177f8))
|
|
14
|
+
|
|
1
15
|
## [2.78.1](https://github.com/danielvm-git/bigpowers/compare/v2.78.0...v2.78.1) (2026-07-23)
|
|
2
16
|
|
|
3
17
|
|
package/CONVENTIONS.md
CHANGED
|
@@ -353,6 +353,39 @@ name must return < 5 results across the repo.
|
|
|
353
353
|
|
|
354
354
|
Any new exception requires an entry in this table before the skill is published.
|
|
355
355
|
|
|
356
|
+
## Tombstone Aliases (e53s02)
|
|
357
|
+
|
|
358
|
+
Every skill rename or merge ships a tombstone alias for one release, so consumers
|
|
359
|
+
referencing the old name don't break immediately.
|
|
360
|
+
|
|
361
|
+
**Mechanism:**
|
|
362
|
+
|
|
363
|
+
1. Run `bash scripts/tombstone-skill.sh <old-name> <new-name-or-merge-target>` when
|
|
364
|
+
renaming or merging a skill. It replaces `skills/<old-name>/SKILL.md` with a stub
|
|
365
|
+
pointing at the new name/location, preserves the old skill's `# story: eNNsNN` tags
|
|
366
|
+
for traceability, and registers the mapping in `specs/tombstones.yaml` (old name,
|
|
367
|
+
new name/target, creation timestamp, and the `created_at_version` at creation time).
|
|
368
|
+
2. Run `bash scripts/validate-tombstones.sh` to confirm every registered tombstone's
|
|
369
|
+
stub still resolves, and to flag any tombstone whose `created_at_version` is in a
|
|
370
|
+
different minor/major release train (major.minor) than the current version — that
|
|
371
|
+
stub has served its one-release transition window and should be removed. Patch-only
|
|
372
|
+
bumps within the same minor version don't count as a new release: this repo ships
|
|
373
|
+
via semantic-release on every merge to main, so comparing full semver strings would
|
|
374
|
+
expire a tombstone within minutes of creation instead of giving consumers a real
|
|
375
|
+
migration window.
|
|
376
|
+
3. **Required alongside the first real use of `tombstone-skill.sh`:** update
|
|
377
|
+
`docs/references/model-profiles.md`'s skill-count annotations. A tombstone stub is
|
|
378
|
+
still a `skills/*/SKILL.md` file — it increments the live skill count the same as
|
|
379
|
+
any other skill, and the count doc must reflect that.
|
|
380
|
+
|
|
381
|
+
**Rejected names:** both the old and new name must be a bare kebab-case skill name
|
|
382
|
+
(`^[a-z][a-z0-9-]*$`) — `tombstone-skill.sh` refuses anything else before touching the
|
|
383
|
+
filesystem (path segments, absolute paths, etc.).
|
|
384
|
+
|
|
385
|
+
This story (e53s02) builds the mechanism only; it creates no real tombstone stub
|
|
386
|
+
itself, so the live skill count doesn't move yet. The first real use is expected in
|
|
387
|
+
e56 (skill reclassification/merges).
|
|
388
|
+
|
|
356
389
|
## File-Size Exceptions
|
|
357
390
|
|
|
358
391
|
The 300-line file cap exists to ensure files fit in a single agent context
|
package/package.json
CHANGED
|
Binary file
|
|
@@ -33,12 +33,25 @@ if [[ -f specs/traceability-matrix.json ]]; then
|
|
|
33
33
|
# not-yet-built siblings (which have no code tags by definition until their
|
|
34
34
|
# own turn) and not unrelated epics (which may be plan-worked-to-build-ready
|
|
35
35
|
# but not yet kicked off).
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
# `|| true` guards the whole pipe under `set -o pipefail`: a state.yaml
|
|
37
|
+
# with no active_story line makes grep exit 1, which would otherwise kill
|
|
38
|
+
# this script outright (no WARNING, no summary line) instead of falling
|
|
39
|
+
# through to the empty-ACTIVE_STORY branch below.
|
|
40
|
+
ACTIVE_STORY="$(grep '^active_story:' specs/state.yaml 2>/dev/null | awk '{print $2}' || true)"
|
|
41
|
+
if [[ -z "$ACTIVE_STORY" ]]; then
|
|
42
|
+
classify WARNING "specs/state.yaml has no active_story set — zero-tag check skipped, not a pass"
|
|
43
|
+
else
|
|
44
|
+
STORY_EXISTS="$(jq --arg s "$ACTIVE_STORY" \
|
|
45
|
+
'[.stories[]? | select(.id == $s)] | length' \
|
|
40
46
|
specs/traceability-matrix.json 2>/dev/null || echo 0)"
|
|
41
|
-
|
|
47
|
+
if [[ "$STORY_EXISTS" -eq 0 ]]; then
|
|
48
|
+
classify WARNING "active_story $ACTIVE_STORY not found in specs/traceability-matrix.json — zero-tag check skipped, not a pass"
|
|
49
|
+
else
|
|
50
|
+
UNDONE="$(jq --arg s "$ACTIVE_STORY" \
|
|
51
|
+
'[.stories[]? | select(.id == $s and .status != "done" and ((.links // []) | map(select(.method == "explicit_tag")) | length) == 0)] | length' \
|
|
52
|
+
specs/traceability-matrix.json 2>/dev/null || echo 0)"
|
|
53
|
+
(( UNDONE > 0 )) && classify BLOCKER "active story $ACTIVE_STORY has zero explicit story-tag links"
|
|
54
|
+
fi
|
|
42
55
|
fi
|
|
43
56
|
COVERAGE="$(jq -r '.summary.coverage_percent // .coverage_percent // empty' specs/traceability-matrix.json 2>/dev/null)"
|
|
44
57
|
if [[ -n "$COVERAGE" && "$COVERAGE" != "null" ]]; then
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# story: e53s01
|
|
3
|
+
# test-completeness-critic-scope.sh — regression test for
|
|
4
|
+
# BUG-2026-07-23-completeness-critic-code-tags-schema-drift: the zero-tag
|
|
5
|
+
# check must scope to the active story only (not siblings/other epics), and
|
|
6
|
+
# must WARN — not silently no-op — when active_story is unset or unresolved.
|
|
7
|
+
#
|
|
8
|
+
# Usage: bash scripts/test-completeness-critic-scope.sh
|
|
9
|
+
|
|
10
|
+
set -euo pipefail
|
|
11
|
+
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
12
|
+
cd "$REPO_ROOT"
|
|
13
|
+
|
|
14
|
+
RED='\033[0;31m'
|
|
15
|
+
GREEN='\033[0;32m'
|
|
16
|
+
NC='\033[0m'
|
|
17
|
+
|
|
18
|
+
FAILURES=0
|
|
19
|
+
tt_pass() { echo -e "${GREEN}PASS${NC} $1"; }
|
|
20
|
+
tt_fail() { echo -e "${RED}FAIL${NC} $1"; FAILURES=$((FAILURES + 1)); }
|
|
21
|
+
|
|
22
|
+
STATE_FILE="specs/state.yaml"
|
|
23
|
+
MATRIX_FILE="specs/traceability-matrix.json"
|
|
24
|
+
STATE_BACKUP=$(mktemp)
|
|
25
|
+
MATRIX_BACKUP=$(mktemp)
|
|
26
|
+
cp "$STATE_FILE" "$STATE_BACKUP"
|
|
27
|
+
cp "$MATRIX_FILE" "$MATRIX_BACKUP"
|
|
28
|
+
|
|
29
|
+
cleanup() {
|
|
30
|
+
cp "$STATE_BACKUP" "$STATE_FILE"
|
|
31
|
+
cp "$MATRIX_BACKUP" "$MATRIX_FILE"
|
|
32
|
+
rm -f "$STATE_BACKUP" "$MATRIX_BACKUP"
|
|
33
|
+
}
|
|
34
|
+
trap cleanup EXIT
|
|
35
|
+
|
|
36
|
+
run_critic() {
|
|
37
|
+
bash scripts/lib/completeness-critic.sh 2>&1 || true
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# Fixture matrix: one untagged sibling story (e00s02) that must NOT block,
|
|
41
|
+
# and the active story (e00s01) which is under test.
|
|
42
|
+
write_matrix() {
|
|
43
|
+
local active_links="$1"
|
|
44
|
+
cat > "$MATRIX_FILE" <<FIXTUREEOF
|
|
45
|
+
{
|
|
46
|
+
"stories": [
|
|
47
|
+
{"id": "e00s01", "status": "in_progress", "links": ${active_links}},
|
|
48
|
+
{"id": "e00s02", "status": "todo", "links": []}
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
FIXTUREEOF
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# --- Scenario: active_story unset → WARNING, not silent pass ---
|
|
55
|
+
sed -i.bak '/^active_story:/d' "$STATE_FILE" && rm -f "${STATE_FILE}.bak"
|
|
56
|
+
write_matrix '[]'
|
|
57
|
+
output=$(run_critic)
|
|
58
|
+
if echo "$output" | grep -q "\[WARNING\].*no active_story set"; then
|
|
59
|
+
tt_pass "unset active_story produces an explicit WARNING"
|
|
60
|
+
else
|
|
61
|
+
tt_fail "unset active_story did not produce the expected WARNING"
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
# --- Scenario: active_story set but absent from the matrix → WARNING ---
|
|
65
|
+
printf 'active_story: e99s99\n' >> "$STATE_FILE"
|
|
66
|
+
output=$(run_critic)
|
|
67
|
+
if echo "$output" | grep -q "\[WARNING\].*e99s99 not found"; then
|
|
68
|
+
tt_pass "unresolved active_story produces an explicit WARNING"
|
|
69
|
+
else
|
|
70
|
+
tt_fail "unresolved active_story did not produce the expected WARNING"
|
|
71
|
+
fi
|
|
72
|
+
sed -i.bak '/^active_story:/d' "$STATE_FILE" && rm -f "${STATE_FILE}.bak"
|
|
73
|
+
|
|
74
|
+
# --- Scenario: active story has zero explicit_tag links → BLOCKER, scoped to it ---
|
|
75
|
+
printf 'active_story: e00s01\n' >> "$STATE_FILE"
|
|
76
|
+
write_matrix '[]'
|
|
77
|
+
output=$(run_critic)
|
|
78
|
+
if echo "$output" | grep -q "\[BLOCKER\] active story e00s01 has zero explicit story-tag links"; then
|
|
79
|
+
tt_pass "active story with zero tags is correctly flagged as BLOCKER"
|
|
80
|
+
else
|
|
81
|
+
tt_fail "active story with zero tags was not flagged as BLOCKER"
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
# --- Scenario: active story IS tagged → no BLOCKER, even though sibling e00s02 is untagged ---
|
|
85
|
+
write_matrix '[{"method": "explicit_tag", "file": "scripts/fixture.sh", "line": 1, "confidence": 1.0}]'
|
|
86
|
+
output=$(run_critic)
|
|
87
|
+
if echo "$output" | grep -q "\[BLOCKER\] active story e00s01"; then
|
|
88
|
+
tt_fail "tagged active story incorrectly flagged as BLOCKER"
|
|
89
|
+
else
|
|
90
|
+
tt_pass "tagged active story is not flagged, and untagged sibling e00s02 does not leak into the check"
|
|
91
|
+
fi
|
|
92
|
+
|
|
93
|
+
echo "---"
|
|
94
|
+
if [[ "$FAILURES" -eq 0 ]]; then
|
|
95
|
+
echo -e "${GREEN}test-completeness-critic-scope: ALL PASS${NC}"
|
|
96
|
+
else
|
|
97
|
+
echo -e "${RED}test-completeness-critic-scope: $FAILURES failure(s)${NC}"
|
|
98
|
+
fi
|
|
99
|
+
exit "$FAILURES"
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# story: e53s01
|
|
3
|
+
# test-generate-epics-wiki-zero-stories.sh — regression test for
|
|
4
|
+
# BUG-2026-07-23-epics-wiki-empty-references: generate-epics-wiki.sh must
|
|
5
|
+
# skip epics with zero sliced stories rather than emit an invalid OKF bundle
|
|
6
|
+
# with an empty references[].
|
|
7
|
+
#
|
|
8
|
+
# Usage: bash scripts/test-generate-epics-wiki-zero-stories.sh
|
|
9
|
+
|
|
10
|
+
set -euo pipefail
|
|
11
|
+
source "$(dirname "${BASH_SOURCE[0]}")/lib/skill-common.sh"
|
|
12
|
+
resolve_repo_root
|
|
13
|
+
cd "$REPO_ROOT"
|
|
14
|
+
|
|
15
|
+
RED='\033[0;31m'
|
|
16
|
+
GREEN='\033[0;32m'
|
|
17
|
+
NC='\033[0m'
|
|
18
|
+
|
|
19
|
+
FAILURES=0
|
|
20
|
+
tt_pass() { echo -e "${GREEN}PASS${NC} $1"; }
|
|
21
|
+
tt_fail() { echo -e "${RED}FAIL${NC} $1"; FAILURES=$((FAILURES + 1)); }
|
|
22
|
+
|
|
23
|
+
FIXTURE_ID="e00"
|
|
24
|
+
FIXTURE_DIR="specs/epics/e00-zero-story-fixture"
|
|
25
|
+
FIXTURE_BUNDLE="specs/epics-wiki/${FIXTURE_ID}.okf.md"
|
|
26
|
+
|
|
27
|
+
cleanup() {
|
|
28
|
+
rm -rf "$FIXTURE_DIR"
|
|
29
|
+
rm -f "$FIXTURE_BUNDLE"
|
|
30
|
+
}
|
|
31
|
+
trap cleanup EXIT
|
|
32
|
+
|
|
33
|
+
mkdir -p "$FIXTURE_DIR"
|
|
34
|
+
cat > "${FIXTURE_DIR}/epic.yaml" <<'FIXTUREEOF'
|
|
35
|
+
id: e00
|
|
36
|
+
title: "Zero-story fixture epic"
|
|
37
|
+
wsjf: 0
|
|
38
|
+
bcps: 0
|
|
39
|
+
capsule_dir: epics/e00-zero-story-fixture
|
|
40
|
+
mode: capsule
|
|
41
|
+
status: todo
|
|
42
|
+
depends_on: []
|
|
43
|
+
stories: []
|
|
44
|
+
FIXTUREEOF
|
|
45
|
+
|
|
46
|
+
# A scoped-but-unsliced epic has a `stories:` key with nothing under it —
|
|
47
|
+
# grep -c '^\s*- id:' correctly returns 0 for this shape.
|
|
48
|
+
[[ -f "$FIXTURE_BUNDLE" ]] && rm -f "$FIXTURE_BUNDLE"
|
|
49
|
+
|
|
50
|
+
bash scripts/generate-epics-wiki.sh > /dev/null
|
|
51
|
+
|
|
52
|
+
if [[ ! -f "$FIXTURE_BUNDLE" ]]; then
|
|
53
|
+
tt_pass "generate-epics-wiki.sh skips a zero-story epic (no bundle emitted)"
|
|
54
|
+
else
|
|
55
|
+
tt_fail "generate-epics-wiki.sh emitted a bundle for a zero-story epic: $FIXTURE_BUNDLE"
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
echo "---"
|
|
59
|
+
if [[ "$FAILURES" -eq 0 ]]; then
|
|
60
|
+
echo -e "${GREEN}test-generate-epics-wiki-zero-stories: ALL PASS${NC}"
|
|
61
|
+
else
|
|
62
|
+
echo -e "${RED}test-generate-epics-wiki-zero-stories: $FAILURES failure(s)${NC}"
|
|
63
|
+
fi
|
|
64
|
+
exit "$FAILURES"
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# story: e53s01
|
|
3
|
+
# test-golden-g11-worktree-venv.sh — regression test for
|
|
4
|
+
# BUG-2026-07-23-golden-g11-worktree-venv: golden-g11-gitignore-venv.sh must
|
|
5
|
+
# PASS in a fresh worktree where .venv does not exist yet on disk (before
|
|
6
|
+
# setup-environment has run), not just when a real .venv is already present.
|
|
7
|
+
#
|
|
8
|
+
# Usage: bash scripts/test-golden-g11-worktree-venv.sh
|
|
9
|
+
|
|
10
|
+
set -euo pipefail
|
|
11
|
+
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
12
|
+
cd "$REPO_ROOT"
|
|
13
|
+
|
|
14
|
+
RED='\033[0;31m'
|
|
15
|
+
GREEN='\033[0;32m'
|
|
16
|
+
NC='\033[0m'
|
|
17
|
+
|
|
18
|
+
FAILURES=0
|
|
19
|
+
tt_pass() { echo -e "${GREEN}PASS${NC} $1"; }
|
|
20
|
+
tt_fail() { echo -e "${RED}FAIL${NC} $1"; FAILURES=$((FAILURES + 1)); }
|
|
21
|
+
strip_ansi() { sed -E 's/\x1b\[[0-9;]*m//g'; }
|
|
22
|
+
|
|
23
|
+
VENV_MOVED=0
|
|
24
|
+
if [[ -e .venv ]]; then
|
|
25
|
+
mv .venv .venv.test-golden-g11-backup
|
|
26
|
+
VENV_MOVED=1
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
cleanup() {
|
|
30
|
+
rm -rf .venv
|
|
31
|
+
if [[ "$VENV_MOVED" -eq 1 ]]; then
|
|
32
|
+
mv .venv.test-golden-g11-backup .venv
|
|
33
|
+
fi
|
|
34
|
+
}
|
|
35
|
+
trap cleanup EXIT
|
|
36
|
+
|
|
37
|
+
# --- Scenario: .venv absent (fresh worktree) → gate still PASSes ---
|
|
38
|
+
if [[ -e .venv ]]; then
|
|
39
|
+
tt_fail "test setup failed to remove .venv before running the gate"
|
|
40
|
+
else
|
|
41
|
+
output=$(bash scripts/golden-g11-gitignore-venv.sh 2>&1 | strip_ansi) && exit_code=0 || exit_code=$?
|
|
42
|
+
if [[ "$exit_code" -eq 0 ]] && echo "$output" | grep -q "PASS git check-ignore .venv"; then
|
|
43
|
+
tt_pass "gate PASSes when .venv does not exist on disk"
|
|
44
|
+
else
|
|
45
|
+
tt_fail "gate did not PASS with .venv absent (exit=$exit_code)"
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
# The gate must clean up the .venv it created for the check — a fresh
|
|
49
|
+
# worktree shouldn't be left with a stray directory after a gate run.
|
|
50
|
+
if [[ ! -e .venv ]]; then
|
|
51
|
+
tt_pass "gate removes the .venv it created for the check"
|
|
52
|
+
else
|
|
53
|
+
tt_fail "gate left a stray .venv behind after the check"
|
|
54
|
+
fi
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
echo "---"
|
|
58
|
+
if [[ "$FAILURES" -eq 0 ]]; then
|
|
59
|
+
echo -e "${GREEN}test-golden-g11-worktree-venv: ALL PASS${NC}"
|
|
60
|
+
else
|
|
61
|
+
echo -e "${RED}test-golden-g11-worktree-venv: $FAILURES failure(s)${NC}"
|
|
62
|
+
fi
|
|
63
|
+
exit "$FAILURES"
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# story: e53s02
|
|
3
|
+
# test-tombstone-mechanism.sh — exercises tombstone-skill.sh and
|
|
4
|
+
# validate-tombstones.sh end-to-end against throwaway fixtures.
|
|
5
|
+
#
|
|
6
|
+
# Usage: bash scripts/test-tombstone-mechanism.sh
|
|
7
|
+
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
source "$(dirname "${BASH_SOURCE[0]}")/lib/skill-common.sh"
|
|
10
|
+
resolve_repo_root
|
|
11
|
+
cd "$REPO_ROOT"
|
|
12
|
+
|
|
13
|
+
RED='\033[0;31m'
|
|
14
|
+
GREEN='\033[0;32m'
|
|
15
|
+
NC='\033[0m'
|
|
16
|
+
|
|
17
|
+
FAILURES=0
|
|
18
|
+
tt_pass() { echo -e "${GREEN}PASS${NC} $1"; }
|
|
19
|
+
tt_fail() { echo -e "${RED}FAIL${NC} $1"; FAILURES=$((FAILURES + 1)); }
|
|
20
|
+
strip_ansi() { sed -E 's/\x1b\[[0-9;]*m//g'; }
|
|
21
|
+
|
|
22
|
+
FIXTURE_OLD="test-tombstone-fixture"
|
|
23
|
+
FIXTURE_NEW="test-tombstone-target"
|
|
24
|
+
FIXTURE_DIR="skills/${FIXTURE_OLD}"
|
|
25
|
+
TOMBSTONES_FILE="specs/tombstones.yaml"
|
|
26
|
+
TOMBSTONES_BACKUP=""
|
|
27
|
+
|
|
28
|
+
cleanup() {
|
|
29
|
+
rm -rf "$FIXTURE_DIR" "skills/${FIXTURE_NEW}"
|
|
30
|
+
if [[ -n "$TOMBSTONES_BACKUP" ]]; then
|
|
31
|
+
mv "$TOMBSTONES_BACKUP" "$TOMBSTONES_FILE"
|
|
32
|
+
else
|
|
33
|
+
rm -f "$TOMBSTONES_FILE"
|
|
34
|
+
fi
|
|
35
|
+
}
|
|
36
|
+
trap cleanup EXIT
|
|
37
|
+
|
|
38
|
+
if [[ -f "$TOMBSTONES_FILE" ]]; then
|
|
39
|
+
TOMBSTONES_BACKUP=$(mktemp)
|
|
40
|
+
cp "$TOMBSTONES_FILE" "$TOMBSTONES_BACKUP"
|
|
41
|
+
rm -f "$TOMBSTONES_FILE"
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
# --- Scenario: zero tombstones registered (6a) ---
|
|
45
|
+
if bash scripts/validate-tombstones.sh 2>&1 | grep -qi "no tombstones"; then
|
|
46
|
+
tt_pass "validate-tombstones.sh reports clean when zero tombstones registered"
|
|
47
|
+
else
|
|
48
|
+
tt_fail "validate-tombstones.sh did not report 'no tombstones' with an empty ledger"
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
# --- Scenario: tombstone-skill.sh generates a stub ---
|
|
52
|
+
mkdir -p "$FIXTURE_DIR"
|
|
53
|
+
cat > "${FIXTURE_DIR}/SKILL.md" <<'FIXTUREEOF'
|
|
54
|
+
# story: e00s00
|
|
55
|
+
---
|
|
56
|
+
name: test-tombstone-fixture
|
|
57
|
+
description: throwaway fixture for test-tombstone-mechanism.sh
|
|
58
|
+
---
|
|
59
|
+
Fixture body.
|
|
60
|
+
FIXTUREEOF
|
|
61
|
+
|
|
62
|
+
bash scripts/tombstone-skill.sh "$FIXTURE_OLD" "$FIXTURE_NEW"
|
|
63
|
+
|
|
64
|
+
if grep -q "TOMBSTONE" "${FIXTURE_DIR}/SKILL.md"; then
|
|
65
|
+
tt_pass "tombstone-skill.sh replaced SKILL.md with a stub"
|
|
66
|
+
else
|
|
67
|
+
tt_fail "SKILL.md was not replaced with a tombstone stub"
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
if grep -q "story: e00s00" "${FIXTURE_DIR}/SKILL.md"; then
|
|
71
|
+
tt_pass "tombstone-skill.sh preserved the old story tag"
|
|
72
|
+
else
|
|
73
|
+
tt_fail "story tag was not preserved in the stub"
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
if [[ -f "$TOMBSTONES_FILE" ]] && grep -q "$FIXTURE_OLD" "$TOMBSTONES_FILE"; then
|
|
77
|
+
tt_pass "tombstone-skill.sh registered the mapping in ${TOMBSTONES_FILE}"
|
|
78
|
+
else
|
|
79
|
+
tt_fail "mapping not registered in ${TOMBSTONES_FILE}"
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
# --- Scenario: fresh tombstone resolves cleanly ---
|
|
83
|
+
if bash scripts/validate-tombstones.sh 2>&1 | strip_ansi | grep -q "OK: ${FIXTURE_OLD}"; then
|
|
84
|
+
tt_pass "validate-tombstones.sh confirms the fresh stub resolves"
|
|
85
|
+
else
|
|
86
|
+
tt_fail "validate-tombstones.sh did not confirm the fresh stub"
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
# --- Scenario: expired tombstone flagged (6b) ---
|
|
90
|
+
python3 -c "
|
|
91
|
+
import yaml
|
|
92
|
+
d = yaml.safe_load(open('$TOMBSTONES_FILE')) or {'tombstones': []}
|
|
93
|
+
for t in d['tombstones']:
|
|
94
|
+
if t['old_name'] == '$FIXTURE_OLD':
|
|
95
|
+
t['created_at_version'] = '0.0.0'
|
|
96
|
+
yaml.dump(d, open('$TOMBSTONES_FILE', 'w'), default_flow_style=False)
|
|
97
|
+
"
|
|
98
|
+
|
|
99
|
+
if bash scripts/validate-tombstones.sh 2>&1 | strip_ansi | grep -qi "EXPIRED: ${FIXTURE_OLD}"; then
|
|
100
|
+
tt_pass "validate-tombstones.sh flags a tombstone whose expiry window has passed"
|
|
101
|
+
else
|
|
102
|
+
tt_fail "expired tombstone was not flagged"
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
echo "---"
|
|
106
|
+
if [[ "$FAILURES" -eq 0 ]]; then
|
|
107
|
+
echo -e "${GREEN}test-tombstone-mechanism: ALL PASS${NC}"
|
|
108
|
+
else
|
|
109
|
+
echo -e "${RED}test-tombstone-mechanism: $FAILURES failure(s)${NC}"
|
|
110
|
+
fi
|
|
111
|
+
exit "$FAILURES"
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# story: e53s02
|
|
3
|
+
# tombstone-skill.sh — replace a renamed/merged skill's SKILL.md with a stub,
|
|
4
|
+
# preserving its story tags and registering the mapping in specs/tombstones.yaml
|
|
5
|
+
# for a one-release expiry window.
|
|
6
|
+
#
|
|
7
|
+
# Usage: bash scripts/tombstone-skill.sh <old-name> <new-name-or-merge-target>
|
|
8
|
+
|
|
9
|
+
set -euo pipefail
|
|
10
|
+
source "$(dirname "${BASH_SOURCE[0]}")/lib/skill-common.sh"
|
|
11
|
+
resolve_repo_root
|
|
12
|
+
cd "$REPO_ROOT"
|
|
13
|
+
|
|
14
|
+
usage() {
|
|
15
|
+
cat <<'EOF'
|
|
16
|
+
Usage: tombstone-skill.sh <old-name> <new-name-or-merge-target>
|
|
17
|
+
|
|
18
|
+
Replaces skills/<old-name>/SKILL.md with a stub pointing at <new-name>,
|
|
19
|
+
preserves the old name's story tags, and registers the mapping in
|
|
20
|
+
specs/tombstones.yaml with a one-release expiry window.
|
|
21
|
+
EOF
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
|
|
25
|
+
usage
|
|
26
|
+
exit 0
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
OLD_NAME="${1:-}"
|
|
30
|
+
NEW_NAME="${2:-}"
|
|
31
|
+
NAME_PATTERN='^[a-z][a-z0-9-]*$'
|
|
32
|
+
|
|
33
|
+
if [[ -z "$OLD_NAME" || -z "$NEW_NAME" ]]; then
|
|
34
|
+
usage >&2
|
|
35
|
+
exit 1
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
# Threat-model mitigation (specs/security/epics/e53/THREAT_MODEL.md): reject
|
|
39
|
+
# anything that isn't a bare kebab-case skill name before any path use.
|
|
40
|
+
for name in "$OLD_NAME" "$NEW_NAME"; do
|
|
41
|
+
if [[ ! "$name" =~ $NAME_PATTERN ]]; then
|
|
42
|
+
echo "ERROR: '$name' is not a valid skill name (must match $NAME_PATTERN)" >&2
|
|
43
|
+
exit 1
|
|
44
|
+
fi
|
|
45
|
+
done
|
|
46
|
+
|
|
47
|
+
OLD_SKILL_MD="skills/${OLD_NAME}/SKILL.md"
|
|
48
|
+
if [[ ! -f "$OLD_SKILL_MD" ]]; then
|
|
49
|
+
echo "ERROR: $OLD_SKILL_MD does not exist" >&2
|
|
50
|
+
exit 1
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
STORY_TAGS=$(grep -E '^#\s*story:' "$OLD_SKILL_MD" || true)
|
|
54
|
+
CURRENT_VERSION=$(jq -r '.version' package.json 2>/dev/null)
|
|
55
|
+
CREATED_AT=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
56
|
+
|
|
57
|
+
cat > "$OLD_SKILL_MD" <<STUBEOF
|
|
58
|
+
---
|
|
59
|
+
name: ${OLD_NAME}
|
|
60
|
+
description: "TOMBSTONE — renamed/merged to ${NEW_NAME}. This stub resolves for one release then is removed."
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
# ${OLD_NAME} (tombstoned)
|
|
64
|
+
|
|
65
|
+
This skill has been renamed or merged into \`${NEW_NAME}\`. Use \`${NEW_NAME}\` instead.
|
|
66
|
+
|
|
67
|
+
This stub exists for one release as a transition aid, then is removed once
|
|
68
|
+
\`scripts/validate-tombstones.sh\` flags it as expired.
|
|
69
|
+
|
|
70
|
+
${STORY_TAGS}
|
|
71
|
+
STUBEOF
|
|
72
|
+
|
|
73
|
+
TOMBSTONES_FILE="specs/tombstones.yaml"
|
|
74
|
+
[[ -f "$TOMBSTONES_FILE" ]] || echo "tombstones: []" > "$TOMBSTONES_FILE"
|
|
75
|
+
|
|
76
|
+
# Values flow through the environment, never interpolated into the python
|
|
77
|
+
# source string — string-interpolating shell variables into `python3 -c`
|
|
78
|
+
# turns any unvalidated field (e.g. CURRENT_VERSION, sourced from
|
|
79
|
+
# package.json) into a code-injection vector.
|
|
80
|
+
TOMBSTONE_FILE="$TOMBSTONES_FILE" \
|
|
81
|
+
TOMBSTONE_OLD_NAME="$OLD_NAME" \
|
|
82
|
+
TOMBSTONE_NEW_NAME="$NEW_NAME" \
|
|
83
|
+
TOMBSTONE_CREATED_AT="$CREATED_AT" \
|
|
84
|
+
TOMBSTONE_VERSION="$CURRENT_VERSION" \
|
|
85
|
+
python3 -c "
|
|
86
|
+
import os
|
|
87
|
+
import yaml
|
|
88
|
+
path = os.environ['TOMBSTONE_FILE']
|
|
89
|
+
d = yaml.safe_load(open(path)) or {'tombstones': []}
|
|
90
|
+
d.setdefault('tombstones', []).append({
|
|
91
|
+
'old_name': os.environ['TOMBSTONE_OLD_NAME'],
|
|
92
|
+
'new_name_or_merge_target': os.environ['TOMBSTONE_NEW_NAME'],
|
|
93
|
+
'created_at': os.environ['TOMBSTONE_CREATED_AT'],
|
|
94
|
+
'created_at_version': os.environ['TOMBSTONE_VERSION'],
|
|
95
|
+
})
|
|
96
|
+
yaml.dump(d, open(path, 'w'), default_flow_style=False)
|
|
97
|
+
"
|
|
98
|
+
|
|
99
|
+
echo "Tombstoned: ${OLD_NAME} -> ${NEW_NAME}"
|
|
100
|
+
echo "Registered in ${TOMBSTONES_FILE}"
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# story: e53s02
|
|
3
|
+
# validate-tombstones.sh — confirm each registered tombstone's stub still
|
|
4
|
+
# resolves, and flag any whose one-release expiry window has passed.
|
|
5
|
+
#
|
|
6
|
+
# Usage: bash scripts/validate-tombstones.sh
|
|
7
|
+
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
source "$(dirname "${BASH_SOURCE[0]}")/lib/skill-common.sh"
|
|
10
|
+
resolve_repo_root
|
|
11
|
+
cd "$REPO_ROOT"
|
|
12
|
+
|
|
13
|
+
RED='\033[0;31m'
|
|
14
|
+
GREEN='\033[0;32m'
|
|
15
|
+
YELLOW='\033[1;33m'
|
|
16
|
+
NC='\033[0m'
|
|
17
|
+
|
|
18
|
+
TOMBSTONES_FILE="specs/tombstones.yaml"
|
|
19
|
+
NAME_PATTERN='^[a-z][a-z0-9-]*$'
|
|
20
|
+
FAIL=0
|
|
21
|
+
|
|
22
|
+
if [[ ! -f "$TOMBSTONES_FILE" ]]; then
|
|
23
|
+
echo -e "${GREEN}OK${NC}: no tombstones registered"
|
|
24
|
+
exit 0
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
CURRENT_VERSION=$(jq -r '.version' package.json 2>/dev/null)
|
|
28
|
+
|
|
29
|
+
# A "release" for tombstone-expiry purposes is a minor/major version bump.
|
|
30
|
+
# This repo ships via semantic-release on every merge to main (multiple
|
|
31
|
+
# patch releases per day) — comparing full semver strings would expire
|
|
32
|
+
# every tombstone within minutes instead of giving consumers the one-release
|
|
33
|
+
# migration window CONVENTIONS.md promises.
|
|
34
|
+
release_train() { echo "$1" | cut -d. -f1-2; }
|
|
35
|
+
|
|
36
|
+
ROWS=$(python3 -c "
|
|
37
|
+
import yaml
|
|
38
|
+
d = yaml.safe_load(open('$TOMBSTONES_FILE')) or {'tombstones': []}
|
|
39
|
+
for t in d.get('tombstones', []) or []:
|
|
40
|
+
print(f\"{t['old_name']}\t{t['new_name_or_merge_target']}\t{t.get('created_at_version', '')}\")
|
|
41
|
+
")
|
|
42
|
+
|
|
43
|
+
if [[ -z "$ROWS" ]]; then
|
|
44
|
+
echo -e "${GREEN}OK${NC}: no tombstones registered"
|
|
45
|
+
exit 0
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
while IFS=$'\t' read -r old_name new_name created_version; do
|
|
49
|
+
[[ -z "$old_name" ]] && continue
|
|
50
|
+
|
|
51
|
+
# Threat-model mitigation (specs/security/epics/e53/THREAT_MODEL.md): apply
|
|
52
|
+
# the same kebab-case validation tombstone-skill.sh applies before writing
|
|
53
|
+
# entries, since these names come back out of specs/tombstones.yaml here
|
|
54
|
+
# and are used to construct a filesystem path.
|
|
55
|
+
if [[ ! "$old_name" =~ $NAME_PATTERN || ! "$new_name" =~ $NAME_PATTERN ]]; then
|
|
56
|
+
echo -e "${RED}FAIL${NC}: '${old_name}' -> '${new_name}' — not a valid skill name (must match ${NAME_PATTERN})"
|
|
57
|
+
FAIL=1
|
|
58
|
+
continue
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
stub="skills/${old_name}/SKILL.md"
|
|
62
|
+
|
|
63
|
+
if [[ ! -f "$stub" ]]; then
|
|
64
|
+
echo -e "${RED}FAIL${NC}: ${old_name} -> ${new_name} — stub missing at ${stub}"
|
|
65
|
+
FAIL=1
|
|
66
|
+
continue
|
|
67
|
+
fi
|
|
68
|
+
if ! grep -q "TOMBSTONE" "$stub"; then
|
|
69
|
+
echo -e "${RED}FAIL${NC}: ${old_name} — stub exists but is not a tombstone"
|
|
70
|
+
FAIL=1
|
|
71
|
+
continue
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
if [[ -n "$created_version" && "$(release_train "$created_version")" != "$(release_train "$CURRENT_VERSION")" ]]; then
|
|
75
|
+
echo -e "${YELLOW}EXPIRED${NC}: ${old_name} -> ${new_name} — created at ${created_version}, now ${CURRENT_VERSION}; remove the stub"
|
|
76
|
+
continue
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
echo -e "${GREEN}OK${NC}: ${old_name} -> ${new_name} resolves"
|
|
80
|
+
done <<< "$ROWS"
|
|
81
|
+
|
|
82
|
+
exit "$FAIL"
|