bigpowers 2.86.2 → 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.
Files changed (34) hide show
  1. package/.agents/skills/wire-ci/SKILL.md +78 -59
  2. package/.codebuddy/skills/wire-ci/SKILL.md +78 -59
  3. package/.codex/skills/wire-ci/SKILL.md +78 -59
  4. package/.hermes/skills/wire-ci/SKILL.md +78 -59
  5. package/.kilocode/rules/wire-ci.md +78 -59
  6. package/.mimocode/skills/wire-ci/SKILL.md +78 -59
  7. package/.pi/package.json +1 -1
  8. package/.pi/prompts/wire-ci.md +78 -59
  9. package/.pi/skills/wire-ci/SKILL.md +78 -59
  10. package/.trae/skills/wire-ci/SKILL.md +78 -59
  11. package/.windsurf/rules/wire-ci.md +78 -59
  12. package/.zcode/skills/wire-ci/SKILL.md +78 -59
  13. package/CHANGELOG.md +21 -0
  14. package/SKILL-INDEX.md +1 -1
  15. package/package.json +1 -1
  16. package/scripts/golden-g12-status-consistency.sh +232 -0
  17. package/scripts/lib/__pycache__/link_utils.cpython-312.pyc +0 -0
  18. package/scripts/lib/__pycache__/srp_engine_links.cpython-312.pyc +0 -0
  19. package/scripts/lib/context-wire.sh +9 -0
  20. package/scripts/lib/detect-forge.sh +105 -0
  21. package/scripts/lib/extract-story-verify.py +102 -0
  22. package/scripts/lib/fail-open-detect.sh +67 -0
  23. package/scripts/lib/golden-suite-gates.sh +9 -0
  24. package/scripts/lib/plan-consistency-check.sh +26 -2
  25. package/scripts/lib/test-assertions.sh +11 -1
  26. package/scripts/run-skill-verify.sh +5 -46
  27. package/scripts/run-story-verify.sh +208 -0
  28. package/scripts/sync-status-from-epics.sh +8 -0
  29. package/scripts/test-adapters.sh +14 -8
  30. package/scripts/verify-install.sh +13 -1
  31. package/scripts/wire-ci.sh +159 -0
  32. package/skills/wire-ci/REFERENCE.md +15 -4
  33. package/skills/wire-ci/SKILL.md +63 -55
  34. package/skills-lock.json +2 -2
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env python3
2
+ # story: BUG-2026-07-26-story-verify-never-executed
3
+ """Emit executable story-level verify directives from epic capsules.
4
+
5
+ Tier-2 half of the verify arc (GH #106). Tier 1 is SKILL.md -> verify:, run by
6
+ run-skill-verify.sh. This walks specs/epics/*/epic.yaml plus any
7
+ *-tasks.yaml siblings and prints one TSV record per story that declares a
8
+ verify command:
9
+
10
+ <story_id>\t<source_path>\t<status>\t<verify_command>
11
+
12
+ Newlines inside a folded/literal YAML block are collapsed to single spaces so
13
+ each record stays on one line for the bash caller.
14
+ """
15
+
16
+ import glob
17
+ import os
18
+ import sys
19
+
20
+ try:
21
+ import yaml
22
+ except ImportError: # pragma: no cover - resolved by scripts/lib/python-env.sh
23
+ sys.exit("extract-story-verify: PyYAML missing — pip install -r requirements.txt")
24
+
25
+ FIELD_SEPARATOR = "\t"
26
+ UNKNOWN_STATUS = "unknown"
27
+
28
+
29
+ def _flatten(command):
30
+ """Collapse a multi-line YAML scalar into one whitespace-normalized line."""
31
+ return " ".join(str(command).split())
32
+
33
+
34
+ def _story_records(stories, source_path):
35
+ """Yield (id, source, status, verify) for each story declaring a verify."""
36
+ for story in stories or []:
37
+ if not isinstance(story, dict):
38
+ continue
39
+ verify = story.get("verify")
40
+ story_id = story.get("id")
41
+ if not verify or not story_id:
42
+ continue
43
+ status = story.get("status") or UNKNOWN_STATUS
44
+ yield (str(story_id), source_path, str(status), _flatten(verify))
45
+
46
+
47
+ def _load_yaml(path):
48
+ """Parse a YAML file, reporting the offending path on failure."""
49
+ with open(path, encoding="utf-8") as handle:
50
+ try:
51
+ return yaml.safe_load(handle)
52
+ except yaml.YAMLError as exc:
53
+ raise SystemExit(
54
+ f"extract-story-verify: cannot parse {path}: {exc}\n"
55
+ " Expected valid YAML; fix the capsule before running the gate."
56
+ )
57
+
58
+
59
+ def _tasks_records(tasks_path):
60
+ """Extract verify records from a *-tasks.yaml, which nests under `tasks:`."""
61
+ doc = _load_yaml(tasks_path)
62
+ if not isinstance(doc, dict):
63
+ return []
64
+ story_id = doc.get("story") or doc.get("id")
65
+ records = list(_story_records(doc.get("stories"), tasks_path))
66
+ for task in doc.get("tasks") or []:
67
+ if not isinstance(task, dict) or not task.get("verify"):
68
+ continue
69
+ task_id = task.get("id") or story_id
70
+ if not task_id:
71
+ continue
72
+ status = task.get("status") or doc.get("status") or UNKNOWN_STATUS
73
+ records.append(
74
+ (str(task_id), tasks_path, str(status), _flatten(task["verify"]))
75
+ )
76
+ return records
77
+
78
+
79
+ def collect_records(repo_root):
80
+ """Gather every story/task verify directive across all epic capsules."""
81
+ records = []
82
+ epic_glob = os.path.join(repo_root, "specs", "epics", "*", "epic.yaml")
83
+ for epic_path in sorted(glob.glob(epic_glob)):
84
+ doc = _load_yaml(epic_path)
85
+ if isinstance(doc, dict):
86
+ records.extend(_story_records(doc.get("stories"), epic_path))
87
+
88
+ tasks_glob = os.path.join(repo_root, "specs", "epics", "*", "*-tasks.yaml")
89
+ for tasks_path in sorted(glob.glob(tasks_glob)):
90
+ records.extend(_tasks_records(tasks_path))
91
+ return records
92
+
93
+
94
+ def main():
95
+ repo_root = sys.argv[1] if len(sys.argv) > 1 else "."
96
+ for record in collect_records(repo_root):
97
+ rel = os.path.relpath(record[1], repo_root)
98
+ print(FIELD_SEPARATOR.join([record[0], rel, record[2], record[3]]))
99
+
100
+
101
+ if __name__ == "__main__":
102
+ main()
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env bash
2
+ # story: BUG-2026-07-26-story-verify-never-executed
3
+ # fail-open-detect.sh — shared verify-directive fail-open detector.
4
+ #
5
+ # Extracted from run-skill-verify.sh (#96) so tier-1 (SKILL.md → verify:) and
6
+ # tier-2 (epic.yaml story verify:, #106) share ONE detector. A new evasion
7
+ # idiom must be rejected everywhere at once; two copies would drift.
8
+ #
9
+ # Source this file, then call is_fail_open_directive "<cmd>".
10
+
11
+ if [[ -n "${FAIL_OPEN_DETECT_LOADED:-}" ]]; then return 0; fi
12
+ FAIL_OPEN_DETECT_LOADED=1
13
+
14
+ # A directive is fail-open when it cannot exit non-zero on a broken repo.
15
+ # Two families, both checked syntactically because a pipeline's exit status
16
+ # is the status of its LAST command:
17
+ # 1. explicit swallow — `|| echo ...`, `|| true`, `|| :`, `; true`, `; exit 0`
18
+ # 2. non-asserting tail — pipeline ends in a filter that always exits 0
19
+ # (head/wc/cat/tee/sort/tr/sed/awk/echo/true/:). `grep x | wc -l` is the
20
+ # canonical offender: wc always succeeds, so the grep result is discarded.
21
+ # Use an assertion instead, e.g. [ "$(... | wc -l)" -gt 0 ].
22
+ FAIL_OPEN_TAIL_CMDS='head|wc|cat|tee|sort|tr|sed|awk|echo|true|:'
23
+
24
+ is_fail_open_directive() {
25
+ local cmd="$1"
26
+
27
+ # Family 1: explicit exit-status swallowing.
28
+ if echo "$cmd" | grep -qE '\|\|[[:space:]]*(echo|true|:)|;[[:space:]]*(true|:|exit[[:space:]]+0)[[:space:]]*$'; then
29
+ return 0
30
+ fi
31
+
32
+ # Family 2: pipeline whose last stage never fails.
33
+ # Strip $(...) first — a pipe inside a command substitution feeds an
34
+ # assertion, e.g. [ "$(ls x | wc -l)" -gt 0 ], and is NOT fail-open.
35
+ local stripped="$cmd" prev=""
36
+ while [ "$stripped" != "$prev" ]; do
37
+ prev="$stripped"
38
+ stripped=$(printf '%s' "$stripped" | sed 's/\$([^()]*)/SUBST/g')
39
+ done
40
+
41
+ # Only inspect the segment after the final `|` that is not part of `||`.
42
+ local tail_seg
43
+ tail_seg=$(printf '%s' "$stripped" | sed 's/||/\
44
+ /g' | tail -1)
45
+ case "$tail_seg" in
46
+ *\|*)
47
+ tail_seg=${tail_seg##*|}
48
+ tail_seg=$(printf '%s' "$tail_seg" | sed 's/^[[:space:]]*//')
49
+ echo "$tail_seg" | grep -qE "^($FAIL_OPEN_TAIL_CMDS)([[:space:]]|$)" && return 0
50
+ ;;
51
+ esac
52
+
53
+ return 1
54
+ }
55
+
56
+ # A verify string must be a runnable command, not prose. Guards the e60s01 class:
57
+ # `node bin/bigpowers.js setup (interactive in TTY)` is documentation, and a
58
+ # story gate that cannot be executed is as useless as one never executed.
59
+ #
60
+ # Two conditions, both required. The leading-token check alone accepts the
61
+ # example above (it starts with `node`), so shape is also checked with `bash -n`
62
+ # — the parenthetical is a syntax error, which is exactly what marks it as prose.
63
+ is_executable_verify() {
64
+ local cmd="$1"
65
+ echo "$cmd" | grep -qE '^[a-zA-Z][a-zA-Z0-9_.-]*\b|^\[' || return 1
66
+ bash -n -c "$cmd" 2>/dev/null
67
+ }
@@ -14,10 +14,19 @@ 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"
20
25
  "specs-parse:bash scripts/validate-specs-yaml.sh:false"
26
+ # Tier 2 of the verify arc (#106): executes the verify: of every story marked
27
+ # done. Tier 1 (SKILL.md) is run-skill-verify.sh in golden-suite.yml.
28
+ "story-verify-selftest:bash scripts/run-story-verify.sh --self-test:false"
29
+ "story-verify:bash scripts/run-story-verify.sh:false"
21
30
  )
22
31
 
23
32
  GOLDEN_GREEN='\033[0;32m'
@@ -10,6 +10,15 @@ if [[ -z "$CAPSULE" || ! -d "$CAPSULE" ]]; then
10
10
  exit 2
11
11
  fi
12
12
 
13
+ SCRIPT_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
14
+ REPO_ROOT="$(cd "$SCRIPT_LIB_DIR/../.." && pwd)"
15
+ # shellcheck source=fail-open-detect.sh
16
+ source "$SCRIPT_LIB_DIR/fail-open-detect.sh"
17
+ # shellcheck source=python-env.sh
18
+ source "$SCRIPT_LIB_DIR/python-env.sh"
19
+ resolve_python >/dev/null 2>&1 || true
20
+ PYTHON_BIN="${PYTHON:-python3}"
21
+
13
22
  EPIC_YAML="$CAPSULE/epic.yaml"
14
23
  CRITICAL=0
15
24
  HIGH=0
@@ -74,8 +83,23 @@ for tasks in "${TASKS[@]+"${TASKS[@]}"}"; do
74
83
  && report MED "Story $sid spec contains ambiguous/TBD markers"
75
84
  fi
76
85
 
77
- grep -qE '^[[:space:]]*verify:' "$tasks" \
78
- || report CRITICAL "Story $sid tasks.yaml missing runnable verify: commands"
86
+ # Presence is necessary but not sufficient — the word "runnable" in the old
87
+ # message was aspirational: nothing checked shape, and nothing executed it.
88
+ # Execution happens later in scripts/run-story-verify.sh (#106), which gates
89
+ # only `done` stories. At plan time the code may not exist yet, so assert what
90
+ # can be asserted now: the directive parses as a command and can fail.
91
+ if ! grep -qE '^[[:space:]]*verify:' "$tasks"; then
92
+ report CRITICAL "Story $sid tasks.yaml missing runnable verify: commands"
93
+ else
94
+ verify_cmd="$("$PYTHON_BIN" "$SCRIPT_LIB_DIR/extract-story-verify.py" "$REPO_ROOT" 2>/dev/null \
95
+ | awk -F'\t' -v s="$sid" '$1 == s { print $4; exit }')"
96
+ if [[ -n "$verify_cmd" ]]; then
97
+ is_executable_verify "$verify_cmd" \
98
+ || report CRITICAL "Story $sid verify: is prose, not a runnable command: $verify_cmd"
99
+ is_fail_open_directive "$verify_cmd" \
100
+ && report CRITICAL "Story $sid verify: is fail-open (cannot exit non-zero): $verify_cmd"
101
+ fi
102
+ fi
79
103
  grep -qE '^status:[[:space:]]*(failing|todo|passing)' "$tasks" \
80
104
  || report MED "Story $sid tasks.yaml should use status: failing|passing ledger (e45s06)"
81
105
  done
@@ -4,7 +4,17 @@
4
4
  if [[ -n "${TEST_ASSERTIONS_LOADED:-}" ]]; then return 0; fi
5
5
  TEST_ASSERTIONS_LOADED=1
6
6
 
7
- ta_cleanup() { [[ -n "${TA_TMPDIR:-}" && -d "$TA_TMPDIR" ]] && rm -rf "$TA_TMPDIR"; }
7
+ # Registered as `trap ta_cleanup EXIT`. The exit status of an EXIT trap's last
8
+ # command replaces the script's own, so this MUST end with an explicit success:
9
+ # the old `[[ ... ]] && rm -rf ...` one-liner returned 1 whenever TA_TMPDIR was
10
+ # unset, silently forcing every caller to exit 1 while printing "0 failed".
11
+ # That single line broke 22 story verifies across the integration epics (#106).
12
+ ta_cleanup() {
13
+ if [[ -n "${TA_TMPDIR:-}" && -d "$TA_TMPDIR" ]]; then
14
+ rm -rf "$TA_TMPDIR"
15
+ fi
16
+ return 0
17
+ }
8
18
 
9
19
  ta_pass() { echo " PASS $1"; TA_PASS=$((TA_PASS + 1)); }
10
20
 
@@ -25,47 +25,11 @@ SKILLS_ROOT="$REPO_ROOT"
25
25
  PASS=0; FAIL=0; SKIP=0
26
26
  TARGET="${1:-}"
27
27
 
28
- # A directive is fail-open when it cannot exit non-zero on a broken repo.
29
- # Two families, both checked syntactically because a pipeline's exit status
30
- # is the status of its LAST command:
31
- # 1. explicit swallow — `|| echo ...`, `|| true`, `|| :`, `; true`, `; exit 0`
32
- # 2. non-asserting tail — pipeline ends in a filter that always exits 0
33
- # (head/wc/cat/tee/sort/tr/sed/awk/echo/true/:). `grep x | wc -l` is the
34
- # canonical offender: wc always succeeds, so the grep result is discarded.
35
- # Use an assertion instead, e.g. [ "$(... | wc -l)" -gt 0 ].
36
- FAIL_OPEN_TAIL_CMDS='head|wc|cat|tee|sort|tr|sed|awk|echo|true|:'
37
-
38
- is_fail_open_directive() {
39
- local cmd="$1"
40
-
41
- # Family 1: explicit exit-status swallowing.
42
- if echo "$cmd" | grep -qE '\|\|[[:space:]]*(echo|true|:)|;[[:space:]]*(true|:|exit[[:space:]]+0)[[:space:]]*$'; then
43
- return 0
44
- fi
45
-
46
- # Family 2: pipeline whose last stage never fails.
47
- # Strip $(...) first — a pipe inside a command substitution feeds an
48
- # assertion, e.g. [ "$(ls x | wc -l)" -gt 0 ], and is NOT fail-open.
49
- local stripped="$cmd" prev=""
50
- while [ "$stripped" != "$prev" ]; do
51
- prev="$stripped"
52
- stripped=$(printf '%s' "$stripped" | sed 's/\$([^()]*)/SUBST/g')
53
- done
54
-
55
- # Only inspect the segment after the final `|` that is not part of `||`.
56
- local tail_seg
57
- tail_seg=$(printf '%s' "$stripped" | sed 's/||/\
58
- /g' | tail -1)
59
- case "$tail_seg" in
60
- *\|*)
61
- tail_seg=${tail_seg##*|}
62
- tail_seg=$(printf '%s' "$tail_seg" | sed 's/^[[:space:]]*//')
63
- echo "$tail_seg" | grep -qE "^($FAIL_OPEN_TAIL_CMDS)([[:space:]]|$)" && return 0
64
- ;;
65
- esac
66
-
67
- return 1
68
- }
28
+ # is_fail_open_directive / is_executable_verify now live in the shared detector,
29
+ # so tier-1 (here) and tier-2 (run-story-verify.sh, #106) reject the same
30
+ # idioms. A new evasion must be closed in one place, not two.
31
+ # shellcheck source=lib/fail-open-detect.sh
32
+ source "$REPO_ROOT/scripts/lib/fail-open-detect.sh"
69
33
 
70
34
  normalize_verify_cmd() {
71
35
  local raw="$1"
@@ -74,11 +38,6 @@ normalize_verify_cmd() {
74
38
  echo "$raw"
75
39
  }
76
40
 
77
- is_executable_verify() {
78
- local cmd="$1"
79
- echo "$cmd" | grep -qE '^[a-zA-Z][a-zA-Z0-9_.-]*\b|^\['
80
- }
81
-
82
41
  run_verify_cmd() {
83
42
  local skill="$1"
84
43
  local cmd="$2"
@@ -0,0 +1,208 @@
1
+ #!/usr/bin/env bash
2
+ # story: BUG-2026-07-26-story-verify-never-executed
3
+ # Execute story-level verify: commands declared in specs/epics/*/epic.yaml.
4
+ # Tier 2 of the verify arc (GH #106). Tier 1 is run-skill-verify.sh.
5
+ #
6
+ # Only stories marked `status: done` are gated: a done story asserts its own
7
+ # acceptance criteria hold. Backlog/in-progress stories are reported as SKIP.
8
+ #
9
+ # Usage:
10
+ # bash scripts/run-story-verify.sh # gate all done stories
11
+ # bash scripts/run-story-verify.sh e80s05 # one story
12
+ # bash scripts/run-story-verify.sh --self-test # prove the gate can fail
13
+
14
+ set -uo pipefail
15
+
16
+ REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
17
+ cd "$REPO_ROOT"
18
+
19
+ # shellcheck source=lib/fail-open-detect.sh
20
+ source "$REPO_ROOT/scripts/lib/fail-open-detect.sh"
21
+ # shellcheck source=lib/python-env.sh
22
+ source "$REPO_ROOT/scripts/lib/python-env.sh"
23
+ resolve_python || exit 1
24
+
25
+ VERIFY_TIMEOUT_SECONDS=120
26
+
27
+ # Portable timeout: macOS lacks GNU timeout; use perl alarm as fallback.
28
+ # stdin is closed for every verify command: gate_all reads its record list on
29
+ # fd 0, and a child that reads stdin (any test-*.sh sourcing a read loop) would
30
+ # otherwise swallow the remaining records and silently shrink the gate.
31
+ if command -v timeout >/dev/null 2>&1; then
32
+ run_with_timeout() { timeout "$VERIFY_TIMEOUT_SECONDS" bash -c "$1" </dev/null 2>&1; }
33
+ elif command -v perl >/dev/null 2>&1; then
34
+ run_with_timeout() { perl -e "alarm $VERIFY_TIMEOUT_SECONDS; exec @ARGV" bash -c "$1" </dev/null 2>&1; }
35
+ else
36
+ run_with_timeout() { bash -c "$1" </dev/null 2>&1; }
37
+ fi
38
+
39
+ GATED_STATUS="done"
40
+ PASS=0; FAIL=0; SKIP=0
41
+ TARGET="${1:-}"
42
+
43
+ extract_records() {
44
+ "$PYTHON" "$REPO_ROOT/scripts/lib/extract-story-verify.py" "$REPO_ROOT"
45
+ }
46
+
47
+ # Report a story whose directive can never fail. Kept separate from execution so
48
+ # a fail-open directive is a FAIL even when the command happens to exit 0.
49
+ reject_fail_open() {
50
+ local sid="$1" src="$2" cmd="$3"
51
+ echo "FAIL: $sid [$src] — fail-open directive (cannot exit non-zero): $cmd"
52
+ FAIL=$((FAIL + 1))
53
+ }
54
+
55
+ reject_non_executable() {
56
+ local sid="$1" src="$2" cmd="$3"
57
+ echo "FAIL: $sid [$src] — verify is prose, not a runnable command: $cmd"
58
+ FAIL=$((FAIL + 1))
59
+ }
60
+
61
+ run_story_verify() {
62
+ local sid="$1" src="$2" cmd="$3"
63
+
64
+ if ! is_executable_verify "$cmd"; then
65
+ reject_non_executable "$sid" "$src" "$cmd"
66
+ return 1
67
+ fi
68
+ if is_fail_open_directive "$cmd"; then
69
+ reject_fail_open "$sid" "$src" "$cmd"
70
+ return 1
71
+ fi
72
+
73
+ local output
74
+ if output=$(run_with_timeout "$cmd"); then
75
+ echo "PASS: $sid"
76
+ PASS=$((PASS + 1))
77
+ return 0
78
+ fi
79
+ echo "FAIL: $sid [$src] — $cmd"
80
+ echo " output: $(echo "$output" | tail -3 | tr '\n' ' ')"
81
+ FAIL=$((FAIL + 1))
82
+ return 1
83
+ }
84
+
85
+ gate_all() {
86
+ local sid src status cmd
87
+ while IFS=$'\t' read -r sid src status cmd; do
88
+ [ -z "${sid:-}" ] && continue
89
+ if [ -n "$TARGET" ] && [ "$sid" != "$TARGET" ]; then
90
+ continue
91
+ fi
92
+ if [ -z "$TARGET" ] && [ "$status" != "$GATED_STATUS" ]; then
93
+ echo "SKIP: $sid (status: $status)"
94
+ SKIP=$((SKIP + 1))
95
+ continue
96
+ fi
97
+ run_story_verify "$sid" "$src" "$cmd" || true
98
+ done < <(extract_records)
99
+ }
100
+
101
+ # Anti-vacuity proof: seed a capsule whose done story must fail, and one that
102
+ # must pass, then require the runner to agree. A gate that cannot fail is the
103
+ # defect this script exists to close (#96/#106) — so it must prove otherwise.
104
+ run_self_test() {
105
+ echo "=== run-story-verify self-test ==="
106
+ local tmp_root
107
+ tmp_root=$(mktemp -d)
108
+ trap 'rm -rf "$tmp_root"' EXIT
109
+
110
+ mkdir -p "$tmp_root/specs/epics/e99-selftest"
111
+ cat > "$tmp_root/specs/epics/e99-selftest/epic.yaml" <<'YAML'
112
+ id: e99
113
+ title: self-test capsule
114
+ stories:
115
+ - id: e99s01
116
+ title: passing story
117
+ status: done
118
+ verify: test -d specs
119
+ - id: e99s02
120
+ title: failing story
121
+ status: done
122
+ verify: test -f /nonexistent/path/for/story-verify-self-test
123
+ - id: e99s03
124
+ title: fail-open story
125
+ status: done
126
+ verify: test -f /nonexistent || true
127
+ - id: e99s04
128
+ title: prose story
129
+ status: done
130
+ verify: node bin/bigpowers.js setup (interactive in TTY)
131
+ - id: e99s05
132
+ title: backlog story is not gated
133
+ status: backlog
134
+ verify: test -f /nonexistent/should/not/run
135
+ YAML
136
+
137
+ local records
138
+ records=$("$PYTHON" "$REPO_ROOT/scripts/lib/extract-story-verify.py" "$tmp_root")
139
+
140
+ local checked=0 problems=0
141
+ local sid src status cmd
142
+ while IFS=$'\t' read -r sid src status cmd; do
143
+ [ -z "${sid:-}" ] && continue
144
+ checked=$((checked + 1))
145
+ case "$sid" in
146
+ e99s01)
147
+ if is_fail_open_directive "$cmd" || ! is_executable_verify "$cmd"; then
148
+ echo "FAIL: self-test — genuine assertion rejected: $cmd"; problems=$((problems + 1))
149
+ elif ! run_with_timeout "$cmd" >/dev/null; then
150
+ echo "FAIL: self-test — passing fixture did not pass"; problems=$((problems + 1))
151
+ fi
152
+ ;;
153
+ e99s02)
154
+ if run_with_timeout "$cmd" >/dev/null; then
155
+ echo "FAIL: self-test — failing fixture exited 0 (gate cannot fail)"; problems=$((problems + 1))
156
+ fi
157
+ ;;
158
+ e99s03)
159
+ if ! is_fail_open_directive "$cmd"; then
160
+ echo "FAIL: self-test — fail-open directive not detected: $cmd"; problems=$((problems + 1))
161
+ fi
162
+ ;;
163
+ e99s04)
164
+ if is_executable_verify "$cmd"; then
165
+ echo "FAIL: self-test — prose accepted as a command: $cmd"; problems=$((problems + 1))
166
+ fi
167
+ ;;
168
+ e99s05)
169
+ if [ "$status" = "$GATED_STATUS" ]; then
170
+ echo "FAIL: self-test — backlog story reported as done"; problems=$((problems + 1))
171
+ fi
172
+ ;;
173
+ esac
174
+ done <<EOF
175
+ $records
176
+ EOF
177
+
178
+ rm -rf "$tmp_root"
179
+ trap - EXIT
180
+
181
+ if [ "$checked" -lt 5 ]; then
182
+ echo "FAIL: self-test — extractor found $checked/5 fixture stories"
183
+ return 1
184
+ fi
185
+ if [ "$problems" -gt 0 ]; then
186
+ echo "FAIL: self-test — $problems assertion(s) failed"
187
+ return 1
188
+ fi
189
+ echo "PASS: self-test — gate fails on broken stories, passes on sound ones"
190
+ return 0
191
+ }
192
+
193
+ if [ "$TARGET" = "--self-test" ]; then
194
+ run_self_test
195
+ exit $?
196
+ fi
197
+
198
+ gate_all
199
+
200
+ echo ""
201
+ echo "Results: $PASS PASS, $FAIL FAIL, $SKIP SKIP"
202
+
203
+ if [ -n "$TARGET" ] && [ "$((PASS + FAIL))" -eq 0 ]; then
204
+ echo "ERROR: no story matching '$TARGET' declares a verify: command" >&2
205
+ exit 2
206
+ fi
207
+
208
+ [ "$FAIL" -eq 0 ]
@@ -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"
@@ -21,7 +21,6 @@ bash "$SCRIPT_DIR/validate-targets-yaml.sh" >/dev/null
21
21
 
22
22
  PASS=0
23
23
  FAIL=0
24
- TMPDIR=""
25
24
  TA_PASS=0
26
25
  TA_FAIL=0
27
26
  TA_TMPDIR=""
@@ -56,15 +55,22 @@ test_adapter() {
56
55
  if [[ "$has_context" == "true" && "$mode" != "native" ]]; then
57
56
  declare -f wire_context >/dev/null 2>&1 && ta_pass "$id: wire_context defined" || ta_fail "$id: wire_context missing"
58
57
 
59
- TMPDIR=$(mktemp -d)
60
- cp "$SCRIPT_DIR/../docs/templates/AGENTS.md" "$TMPDIR/AGENTS.md" 2>/dev/null || echo "# test" > "$TMPDIR/AGENTS.md"
61
- (
62
- cd "$TMPDIR"
58
+ TA_TMPDIR=$(mktemp -d)
59
+ cp "$SCRIPT_DIR/../docs/templates/AGENTS.md" "$TA_TMPDIR/AGENTS.md" 2>/dev/null || echo "# test" > "$TA_TMPDIR/AGENTS.md"
60
+ # The idempotency probe runs in a subshell (it cd's away), so its ta_pass/
61
+ # ta_fail would increment counters in a child and be lost — the script used
62
+ # to print 5 PASS lines and report 4. Capture the status, tally in-process.
63
+ if (
64
+ cd "$TA_TMPDIR"
63
65
  # shellcheck source=/dev/null
64
66
  source "$adapter_file"
65
- wire_context 2>/dev/null || true
66
- wire_context 2>/dev/null && ta_pass "$id: idempotent wire_context" || ta_fail "$id: wire_context failed"
67
- )
67
+ wire_context >/dev/null 2>&1 || true
68
+ wire_context >/dev/null 2>&1
69
+ ); then
70
+ ta_pass "$id: idempotent wire_context"
71
+ else
72
+ ta_fail "$id: wire_context failed"
73
+ fi
68
74
  fi
69
75
  }
70
76
 
@@ -168,7 +168,19 @@ grep -q "'antigravity'" "$REPO_ROOT/bin/setup.js" && grep -q 'SUPPORTED_IDS' "$R
168
168
  grep -q "'agy'" "$REPO_ROOT/bin/setup.js" && ta_pass "setup.js: agy supported" || ta_fail "setup.js: agy not in SUPPORTED_IDS"
169
169
  grep -q "case 'antigravity'" "$REPO_ROOT/scripts/lib/install-helpers.js" && ta_pass "install-helpers: antigravity case" || ta_fail "install-helpers: missing antigravity case"
170
170
  grep -q "case 'agy'" "$REPO_ROOT/scripts/lib/install-helpers.js" && ta_pass "install-helpers: agy case" || ta_fail "install-helpers: missing agy case"
171
- ! install_grep -q '\.gemini/extensions/bigpowers' <<< "$(sed -n '/install_agy/,/^}/p')" && ta_pass "source: agy avoids gemini extension path" || ta_fail "source: agy touches gemini extension path"
171
+ # Scope this to install_agy's OWN body. The previous form piped a herestring into
172
+ # install_grep(), which is `grep "$@" $(install_grep_paths)` — it always greps the
173
+ # install files and ignores stdin, so the assertion actually searched every target.
174
+ # install_gemini/install_claude legitimately use .gemini/extensions/bigpowers
175
+ # (install-targets-a.sh:21,22,70), so the check could never pass. (#106)
176
+ AGY_BODY="$(sed -n '/^install_agy()/,/^}/p' $(install_grep_paths) 2>/dev/null)"
177
+ if [[ -z "$AGY_BODY" ]]; then
178
+ ta_fail "source: install_agy() body not found — assertion would pass vacuously"
179
+ elif printf '%s' "$AGY_BODY" | grep -q '\.gemini/extensions/bigpowers'; then
180
+ ta_fail "source: agy touches gemini extension path"
181
+ else
182
+ ta_pass "source: agy avoids gemini extension path"
183
+ fi
172
184
 
173
185
  install_grep -q 'install_codex()' && ta_pass "source: install_codex()" || ta_fail "source: missing install_codex()"
174
186
  install_grep -q 'uninstall_codex()' && ta_pass "source: uninstall_codex()" || ta_fail "source: missing uninstall_codex()"