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.
- package/.agents/skills/wire-ci/SKILL.md +78 -59
- package/.codebuddy/skills/wire-ci/SKILL.md +78 -59
- package/.codex/skills/wire-ci/SKILL.md +78 -59
- package/.hermes/skills/wire-ci/SKILL.md +78 -59
- package/.kilocode/rules/wire-ci.md +78 -59
- package/.mimocode/skills/wire-ci/SKILL.md +78 -59
- package/.pi/package.json +1 -1
- package/.pi/prompts/wire-ci.md +78 -59
- package/.pi/skills/wire-ci/SKILL.md +78 -59
- package/.trae/skills/wire-ci/SKILL.md +78 -59
- package/.windsurf/rules/wire-ci.md +78 -59
- package/.zcode/skills/wire-ci/SKILL.md +78 -59
- package/CHANGELOG.md +21 -0
- package/SKILL-INDEX.md +1 -1
- package/package.json +1 -1
- package/scripts/golden-g12-status-consistency.sh +232 -0
- package/scripts/lib/__pycache__/link_utils.cpython-312.pyc +0 -0
- package/scripts/lib/__pycache__/srp_engine_links.cpython-312.pyc +0 -0
- package/scripts/lib/context-wire.sh +9 -0
- package/scripts/lib/detect-forge.sh +105 -0
- package/scripts/lib/extract-story-verify.py +102 -0
- package/scripts/lib/fail-open-detect.sh +67 -0
- package/scripts/lib/golden-suite-gates.sh +9 -0
- package/scripts/lib/plan-consistency-check.sh +26 -2
- package/scripts/lib/test-assertions.sh +11 -1
- package/scripts/run-skill-verify.sh +5 -46
- package/scripts/run-story-verify.sh +208 -0
- package/scripts/sync-status-from-epics.sh +8 -0
- package/scripts/test-adapters.sh +14 -8
- package/scripts/verify-install.sh +13 -1
- package/scripts/wire-ci.sh +159 -0
- package/skills/wire-ci/REFERENCE.md +15 -4
- package/skills/wire-ci/SKILL.md +63 -55
- 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
|
-
|
|
78
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
|
|
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"
|
package/scripts/test-adapters.sh
CHANGED
|
@@ -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
|
-
|
|
60
|
-
cp "$SCRIPT_DIR/../docs/templates/AGENTS.md" "$
|
|
61
|
-
(
|
|
62
|
-
|
|
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
|
|
66
|
-
wire_context
|
|
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
|
-
|
|
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()"
|