bigpowers 2.86.1 → 2.86.3
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/security-review/SKILL.md +3 -1
- package/.agents/skills/smoke-test/SKILL.md +1 -1
- package/.agents/skills/wire-ci/SKILL.md +78 -59
- package/.codebuddy/skills/security-review/SKILL.md +3 -1
- package/.codebuddy/skills/smoke-test/SKILL.md +1 -1
- package/.codebuddy/skills/wire-ci/SKILL.md +78 -59
- package/.codex/skills/security-review/SKILL.md +3 -1
- package/.codex/skills/smoke-test/SKILL.md +1 -1
- package/.codex/skills/wire-ci/SKILL.md +78 -59
- package/.hermes/skills/security-review/SKILL.md +3 -1
- package/.hermes/skills/smoke-test/SKILL.md +1 -1
- package/.hermes/skills/wire-ci/SKILL.md +78 -59
- package/.kilocode/rules/security-review.md +3 -1
- package/.kilocode/rules/smoke-test.md +1 -1
- package/.kilocode/rules/wire-ci.md +78 -59
- package/.mimocode/skills/security-review/SKILL.md +3 -1
- package/.mimocode/skills/smoke-test/SKILL.md +1 -1
- package/.mimocode/skills/wire-ci/SKILL.md +78 -59
- package/.pi/package.json +1 -1
- package/.pi/prompts/security-review.md +3 -1
- package/.pi/prompts/smoke-test.md +1 -1
- package/.pi/prompts/wire-ci.md +78 -59
- package/.pi/skills/security-review/SKILL.md +3 -1
- package/.pi/skills/smoke-test/SKILL.md +1 -1
- package/.pi/skills/wire-ci/SKILL.md +78 -59
- package/.trae/skills/security-review/SKILL.md +3 -1
- package/.trae/skills/smoke-test/SKILL.md +1 -1
- package/.trae/skills/wire-ci/SKILL.md +78 -59
- package/.windsurf/rules/security-review.md +3 -1
- package/.windsurf/rules/smoke-test.md +1 -1
- package/.windsurf/rules/wire-ci.md +78 -59
- package/.zcode/skills/security-review/SKILL.md +3 -1
- package/.zcode/skills/smoke-test/SKILL.md +1 -1
- 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/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 +4 -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/test-adapters.sh +14 -8
- package/scripts/verify-cwe-fixture-sync.sh +88 -0
- package/scripts/verify-install.sh +13 -1
- package/scripts/wire-ci.sh +159 -0
- package/skills/security-review/SKILL.md +3 -1
- package/skills/security-review/fixtures/CWE-639-idor-negative.go +48 -0
- package/skills/security-review/fixtures/CWE-639-idor-positive.go +29 -0
- package/skills/smoke-test/SKILL.md +1 -1
- package/skills/wire-ci/REFERENCE.md +15 -4
- package/skills/wire-ci/SKILL.md +63 -55
- package/skills-lock.json +4 -4
|
@@ -1,104 +1,112 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: wire-ci
|
|
3
3
|
model: sonnet
|
|
4
|
-
description: "CI pipeline setup with
|
|
4
|
+
description: "CI pipeline setup with bundled, forge-neutral templates and local validation. Detects the forge from the git remote, generates workflows for supported forges, and skips honestly for the rest. The CI equivalent of wire-observability."
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Wire CI
|
|
8
8
|
|
|
9
|
-
> **HARD GATE** — Do not ship a project without CI. Run this skill before first merge to main
|
|
9
|
+
> **HARD GATE (supported forges only)** — Do not ship a project without CI. Run this skill before first merge to main.
|
|
10
|
+
>
|
|
11
|
+
> On a forge bigpowers ships no templates for, this skill **is not a gate**: it reports the forge, explains what it cannot do, and exits 3. A gate that cannot run must not claim it did. See § Unsupported forges.
|
|
10
12
|
>
|
|
11
13
|
> **HARD GATE** — CI that is untestable locally will break every cycle. Always run `--validate` after generating workflows and `--dry-run` before pushing.
|
|
12
14
|
|
|
13
|
-
Generate, validate, and test CI workflows. Detects
|
|
15
|
+
Generate, validate, and test CI workflows. Detects the forge and project type, copies a **bundled** template, and verifies locally before anything reaches CI.
|
|
14
16
|
|
|
15
|
-
##
|
|
17
|
+
## Forge resolution
|
|
16
18
|
|
|
17
|
-
|
|
18
|
-
2. **Deploy workflow** — `.github/workflows/deploy.yml` triggered via `workflow_run` (BigBase/Pages stacks only; CLI/library repos omit this)
|
|
19
|
-
3. **`--validate` mode** — checks YAML syntax, workflow permissions, required secrets, and common pitfalls
|
|
20
|
-
4. **`--dry-run` mode** — runs workflows locally via `act` or `gh workflow run` to prove correctness before push
|
|
21
|
-
5. **Failure pattern documentation** — common CI failure categories and their fixes
|
|
19
|
+
`scripts/lib/detect-forge.sh` resolves the forge, first match wins: `BIGPOWERS_FORGE` env var → `forge:` in `specs/forge.yaml` → the `origin` remote URL → `unknown`.
|
|
22
20
|
|
|
23
|
-
|
|
21
|
+
```bash
|
|
22
|
+
bash scripts/wire-ci.sh --detect # report forge + stack, write nothing
|
|
23
|
+
bash scripts/wire-ci.sh --plan # show the template that would be used
|
|
24
|
+
bash scripts/wire-ci.sh --apply # write the workflow
|
|
25
|
+
```
|
|
24
26
|
|
|
25
|
-
|
|
27
|
+
GitHub ships templates (`.github/workflows/`). GitLab, Bitbucket, Codeberg, and Gitea are detected but **unsupported** — `--apply` writes nothing and exits 3.
|
|
26
28
|
|
|
27
|
-
|
|
29
|
+
## Template source — configurable, bundled by default
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
|----------|------|-------------------------------------|
|
|
31
|
-
| `Cargo.toml` | Rust | `test-build-release-monorepo.yml` + `deploy-monorepo.yml` (or monorepo subset) |
|
|
32
|
-
| `package.json` | Node | `test-build-release-node.yml` + `deploy-node.yml` |
|
|
33
|
-
| `setup.py` / `pyproject.toml` | Python | `test-build-release-python.yml` + `deploy-python.yml` |
|
|
34
|
-
| `go.mod` | Go | `test-build-release-go.yml` + `deploy-go.yml` |
|
|
35
|
-
| Static site (no server) | Static | `test-build-release-static.yml` + `deploy-static.yml` |
|
|
36
|
-
| `Package.swift` | Swift | `test-build-release-swift.yml` only (no deploy yet) |
|
|
37
|
-
| Multiple detected | Polyglot | `test-build-release-monorepo.yml` + `deploy-monorepo.yml` |
|
|
31
|
+
Templates live in `docs/templates/ci/<forge>/` **inside the bigpowers package**, so there is no network dependency on any third party's repository. Override with `BIGPOWERS_CI_TEMPLATES=/path/to/your/templates`, laid out as `<root>/<forge>/test-build-release-<stack>.yml`.
|
|
38
32
|
|
|
39
|
-
|
|
33
|
+
## What this sets up
|
|
40
34
|
|
|
41
|
-
|
|
35
|
+
1. **Test Build Release workflow** — lint → test → build → release in one `needs:` chain
|
|
36
|
+
2. **`--validate` mode** — YAML syntax, workflow permissions, required secrets, common pitfalls
|
|
37
|
+
3. **`--dry-run` mode** — runs workflows locally via `act` before push
|
|
38
|
+
4. **Failure pattern documentation** — see the table below
|
|
42
39
|
|
|
43
|
-
|
|
40
|
+
Deploy workflows are **not** bundled: they are platform-specific. See [REFERENCE.md](REFERENCE.md) for a worked example.
|
|
44
41
|
|
|
45
|
-
|
|
42
|
+
## Process
|
|
43
|
+
|
|
44
|
+
### 1. Detect forge and stack
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
bash scripts/wire-ci.sh --detect
|
|
48
|
+
```
|
|
46
49
|
|
|
47
|
-
|
|
50
|
+
Stack detection reads the project root:
|
|
48
51
|
|
|
49
|
-
|
|
52
|
+
| Manifest | Stack | Bundled template |
|
|
53
|
+
|----------|-------|------------------|
|
|
54
|
+
| `Cargo.toml` | Rust | `test-build-release-rust.yml` |
|
|
55
|
+
| `package.json` | Node | `test-build-release-node.yml` |
|
|
56
|
+
| `pyproject.toml` / `setup.py` | Python | `test-build-release-python.yml` |
|
|
57
|
+
| `go.mod` | Go | `test-build-release-go.yml` |
|
|
50
58
|
|
|
51
|
-
|
|
59
|
+
No recognized manifest → exit 3 with the list of manifests it looked for. Do not guess.
|
|
52
60
|
|
|
53
|
-
|
|
61
|
+
### 2. Apply the template
|
|
54
62
|
|
|
55
63
|
```bash
|
|
56
|
-
|
|
64
|
+
bash scripts/wire-ci.sh --apply
|
|
57
65
|
```
|
|
58
66
|
|
|
59
|
-
|
|
67
|
+
**Do not rename the workflow `name:` field** — deploy listens for `"Test Build Release"`.
|
|
60
68
|
|
|
61
|
-
|
|
69
|
+
Edit placeholders after copying: language versions, `APP_TYPE`, `SITE_URL`.
|
|
62
70
|
|
|
63
|
-
###
|
|
71
|
+
### 3. Unsupported forges
|
|
64
72
|
|
|
65
|
-
|
|
73
|
+
`--apply` writes nothing and exits 3. Your options, in the order the runner prints them:
|
|
66
74
|
|
|
67
|
-
|
|
75
|
+
- point `BIGPOWERS_CI_TEMPLATES` at templates for your forge
|
|
76
|
+
- pin `forge: github` in `specs/forge.yaml` if the remote is misdetected
|
|
77
|
+
- write the CI config by hand
|
|
68
78
|
|
|
69
|
-
|
|
70
|
-
- `0` — all checks pass (no errors)
|
|
71
|
-
- `1` — YAML syntax errors found
|
|
72
|
-
- `2` — validation warnings only (missing permissions, secrets, etc.)
|
|
79
|
+
Contributing a `docs/templates/ci/gitlab/` set and adding `gitlab` to `FORGE_SUPPORTED_LIST` is the natural next slice — per-forge command mapping (`gh pr checks` → `glab ci status`) is not implemented yet.
|
|
73
80
|
|
|
74
|
-
###
|
|
81
|
+
### 4. Validate workflows (`--validate`)
|
|
75
82
|
|
|
76
|
-
|
|
83
|
+
See [REFERENCE.md](REFERENCE.md). Exit codes: `0` clean, `1` YAML syntax errors, `2` warnings only.
|
|
84
|
+
|
|
85
|
+
### 5. Dry-run workflows (`--dry-run`)
|
|
77
86
|
|
|
78
|
-
See [REFERENCE.md](REFERENCE.md)
|
|
87
|
+
See [REFERENCE.md](REFERENCE.md).
|
|
79
88
|
|
|
80
89
|
> **act** runs workflows in a local Docker environment — the most accurate pre-push validation.
|
|
81
|
-
> **gh workflow run** sends the workflow to GitHub but
|
|
90
|
+
> **gh workflow run** sends the workflow to GitHub but does not execute locally.
|
|
82
91
|
|
|
83
92
|
### 6. Document common CI failure patterns
|
|
84
93
|
|
|
85
|
-
Add the following to the project's documentation or CLAUDE.md after setup:
|
|
86
|
-
|
|
87
94
|
| Failure | Cause | Fix |
|
|
88
95
|
|---------|-------|-----|
|
|
89
|
-
| `npm publish` fails | `NPM_TOKEN` not set as repo secret | Add `NPM_TOKEN` to
|
|
90
|
-
| `semantic-release` fails on push | Missing `permissions: contents: write` | Add
|
|
91
|
-
| `cargo publish` auth fail | `CARGO_REGISTRY_TOKEN` not set | Add token to `~/.cargo/config.toml`
|
|
92
|
-
| `go vet` fails | Go version mismatch |
|
|
93
|
-
| `cargo clippy` errors | New lints
|
|
94
|
-
| `act` not found | Docker not running or act
|
|
95
|
-
| Hardcoded Node version stale | `.nvmrc` exists but workflow
|
|
96
|
-
| Deploy never runs | TBR workflow renamed | Keep `name: Test Build Release`
|
|
97
|
-
| Release rebuilds binary | Artifact not downloaded | `release`
|
|
96
|
+
| `npm publish` fails | `NPM_TOKEN` not set as repo secret | Add `NPM_TOKEN` to repo secrets |
|
|
97
|
+
| `semantic-release` fails on push | Missing `permissions: contents: write` | Add it to the release job |
|
|
98
|
+
| `cargo publish` auth fail | `CARGO_REGISTRY_TOKEN` not set | Add token to env or `~/.cargo/config.toml` |
|
|
99
|
+
| `go vet` fails | Go version mismatch | Use `go-version-file: go.mod` |
|
|
100
|
+
| `cargo clippy` errors | New nightly lints | Pin the toolchain; `cargo clippy --fix` |
|
|
101
|
+
| `act` not found | Docker not running or act missing | `brew install act`; `docker ps` |
|
|
102
|
+
| Hardcoded Node version stale | `.nvmrc` exists but workflow hardcodes | Use `node-version-file: .nvmrc` |
|
|
103
|
+
| Deploy never runs | TBR workflow renamed | Keep `name: Test Build Release` |
|
|
104
|
+
| Release rebuilds binary | Artifact not downloaded | `release` must `download-artifact` from `build` |
|
|
98
105
|
|
|
99
106
|
## Verify
|
|
100
107
|
|
|
101
|
-
→ verify: `
|
|
108
|
+
→ verify: `bash scripts/wire-ci.sh --self-test`
|
|
109
|
+
→ verify: `test -f docs/templates/ci/github/test-build-release-node.yml && test -f scripts/lib/detect-forge.sh`
|
|
102
110
|
→ verify: `grep -q wire-ci SKILL-INDEX.md`
|
|
103
111
|
|
|
104
112
|
---
|
|
@@ -121,19 +129,26 @@ Add the following to the project's documentation or CLAUDE.md after setup:
|
|
|
121
129
|
### Create CI for a Go project (TBR + optional deploy)
|
|
122
130
|
|
|
123
131
|
```bash
|
|
124
|
-
#
|
|
125
|
-
|
|
126
|
-
|
|
132
|
+
# Resolve forge + stack, then apply the bundled template
|
|
133
|
+
bash scripts/wire-ci.sh --detect
|
|
134
|
+
bash scripts/wire-ci.sh --apply
|
|
127
135
|
|
|
128
136
|
wire-ci --validate
|
|
129
137
|
wire-ci --dry-run
|
|
130
138
|
```
|
|
131
139
|
|
|
140
|
+
To use your own org templates instead of the bundled ones:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
BIGPOWERS_CI_TEMPLATES=/path/to/your/templates bash scripts/wire-ci.sh --apply
|
|
144
|
+
```
|
|
145
|
+
|
|
132
146
|
### Create CI for a CLI tool (TBR only, no deploy)
|
|
133
147
|
|
|
134
148
|
```bash
|
|
135
|
-
|
|
149
|
+
bash scripts/wire-ci.sh --apply
|
|
136
150
|
# Edit release job to download build artifact — see big-release dogfood
|
|
151
|
+
# CLI/library repos: delete deploy.yml; the release job is terminal.
|
|
137
152
|
|
|
138
153
|
wire-ci --validate
|
|
139
154
|
```
|
|
@@ -277,6 +292,10 @@ jobs:
|
|
|
277
292
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
278
293
|
run-id: ${{ github.event.workflow_run.id }}
|
|
279
294
|
path: deploy-meta
|
|
295
|
+
# BigBase-specific composite action, not a bigpowers default: this deploy
|
|
296
|
+
# example targets the author's own hosting stack. Replace it with your
|
|
297
|
+
# platform's deploy step (or drop the deploy job entirely for CLI and
|
|
298
|
+
# library repos). bigpowers itself ships no deploy templates. (GH #104)
|
|
280
299
|
- uses: danielvm-git/.github/actions/bigbase-deploy@9c56ac10c629f3baa110ddb19136579e3c90d690 # v1
|
|
281
300
|
with:
|
|
282
301
|
site_id: ${{ secrets.BIGBASE_SITE_ID }}
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,24 @@
|
|
|
1
|
+
## [2.86.3](https://github.com/danielvm-git/bigpowers/compare/v2.86.2...v2.86.3) (2026-07-26)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **adapters:** make copy-mode context wiring idempotent ([2154e5f](https://github.com/danielvm-git/bigpowers/commit/2154e5f55140c8cb8f9c0ea22a7ea7bacf55f99e)), closes [#106](https://github.com/danielvm-git/bigpowers/issues/106)
|
|
7
|
+
* **specs:** give e60s01 a runnable verify ([585f0aa](https://github.com/danielvm-git/bigpowers/commit/585f0aa78ecc25dba5c13fccd1090c1b626900af)), closes [#106](https://github.com/danielvm-git/bigpowers/issues/106)
|
|
8
|
+
* **sync:** restore .pi and .gemini artifacts dropped by a concurrent sync ([b901340](https://github.com/danielvm-git/bigpowers/commit/b901340d08cbd74e34853beb8e9aad6fbec66698))
|
|
9
|
+
* **test:** scope agy gemini-path assertion to install_agy body ([1ac37f0](https://github.com/danielvm-git/bigpowers/commit/1ac37f0c0673e031996b7d9dde8ba0304d2a6ca1)), closes [#106](https://github.com/danielvm-git/bigpowers/issues/106)
|
|
10
|
+
* **test:** stop ta_cleanup EXIT trap forcing exit 1 ([cd409ef](https://github.com/danielvm-git/bigpowers/commit/cd409efcf2d8d92f8796114f50871cd55538067d)), closes [#106](https://github.com/danielvm-git/bigpowers/issues/106)
|
|
11
|
+
* **verify:** execute story-level verify: commands ([#106](https://github.com/danielvm-git/bigpowers/issues/106)) ([9b3f82a](https://github.com/danielvm-git/bigpowers/commit/9b3f82a18c18014a70380e1062de81c268639ef9)), closes [#96](https://github.com/danielvm-git/bigpowers/issues/96)
|
|
12
|
+
* **verify:** story-level verify and CI template fix ([#104](https://github.com/danielvm-git/bigpowers/issues/104), [#106](https://github.com/danielvm-git/bigpowers/issues/106)) ([049c7c9](https://github.com/danielvm-git/bigpowers/commit/049c7c9529fa0584b6823033e68e2fb7281ff750))
|
|
13
|
+
* **wire-ci:** un-hardcode template source, skip unsupported forges ([#104](https://github.com/danielvm-git/bigpowers/issues/104)) ([31a6fac](https://github.com/danielvm-git/bigpowers/commit/31a6fac06f823b55a00b38637d06f01b555b25c8))
|
|
14
|
+
|
|
15
|
+
## [2.86.2](https://github.com/danielvm-git/bigpowers/compare/v2.86.1...v2.86.2) (2026-07-26)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Bug Fixes
|
|
19
|
+
|
|
20
|
+
* **verify:** repair two gates that could not fail ([#97](https://github.com/danielvm-git/bigpowers/issues/97), [#98](https://github.com/danielvm-git/bigpowers/issues/98)) ([5f54699](https://github.com/danielvm-git/bigpowers/commit/5f54699f11875da2db1bae939f313d69c2fa7adf))
|
|
21
|
+
|
|
1
22
|
## [2.86.1](https://github.com/danielvm-git/bigpowers/compare/v2.86.0...v2.86.1) (2026-07-26)
|
|
2
23
|
|
|
3
24
|
|
package/SKILL-INDEX.md
CHANGED
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
| 37 | Build | `quick-fix` | "Streamlined fast-path for trivial data-only fixes — no TDD, no branching cere | ✅ Active |
|
|
66
66
|
| 38 | Build | `setup-environment` | Pre-install dependencies and configure tools before development work begins. Use | ✅ Active |
|
|
67
67
|
| 39 | Build | `spike-prototype` | Throw-away prototype for unknown problem spaces. Output is learning notes in spe | ✅ Active |
|
|
68
|
-
| 40 | Build | `wire-ci` | "CI pipeline setup with
|
|
68
|
+
| 40 | Build | `wire-ci` | "CI pipeline setup with bundled, forge-neutral templates and local validation. D | ✅ Active |
|
|
69
69
|
| 41 | Build | `wire-observability` | Add structured JSON logging, observability commands, and idempotent setup script | ✅ Active |
|
|
70
70
|
| 42 | Verify | `audit-code` | Self-review checklist for the coding agent to run before dispatching a reviewer. | ✅ Active |
|
|
71
71
|
| 43 | Verify | `context7-mcp` | Fetch current library docs via Context7 MCP instead of training data. Use when u | ✅ Active |
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
@@ -54,6 +54,15 @@ wire_context_mode() {
|
|
|
54
54
|
symlink|copy)
|
|
55
55
|
[[ -n "$file" ]] || { echo "context-wire: file required for mode=$mode" >&2; return 1; }
|
|
56
56
|
if [[ "$mode" == "copy" ]]; then
|
|
57
|
+
[[ -f "$src" ]] || { echo "context-wire: source missing: $src" >&2; return 1; }
|
|
58
|
+
# Already wired — nothing to do. kilocode and windsurf declare
|
|
59
|
+
# `mode: copy, file: AGENTS.md` in targets.yaml, and the source is also
|
|
60
|
+
# AGENTS.md, so a bare `cp` fails with "are identical" and made
|
|
61
|
+
# wire_context non-idempotent. wire_symlink_or_copy already guards this;
|
|
62
|
+
# the copy branch did not. (#106)
|
|
63
|
+
if [[ -f "$file" ]] && cmp -s "$src" "$file" 2>/dev/null; then
|
|
64
|
+
return 0
|
|
65
|
+
fi
|
|
57
66
|
cp "$src" "$file"
|
|
58
67
|
else
|
|
59
68
|
wire_symlink_or_copy "$src" "$file"
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# story: GH-104-forge-neutrality
|
|
3
|
+
# detect-forge.sh — resolve which git forge a project uses.
|
|
4
|
+
#
|
|
5
|
+
# bigpowers ships to solo devs generally, but its CI advice assumed GitHub
|
|
6
|
+
# everywhere (GH #104). This is the seam: skills ask which forge is in play and
|
|
7
|
+
# what it supports, instead of hardcoding `gh` and `.github/workflows/`.
|
|
8
|
+
#
|
|
9
|
+
# Resolution order (first match wins):
|
|
10
|
+
# 1. BIGPOWERS_FORGE environment variable — explicit override
|
|
11
|
+
# 2. forge: key in specs/forge.yaml — per-project setting
|
|
12
|
+
# 3. the `origin` remote URL — inferred
|
|
13
|
+
# 4. "unknown" — no remote, or an unrecognized host
|
|
14
|
+
#
|
|
15
|
+
# Source this file, then call detect_forge / forge_is_supported / forge_ci_path.
|
|
16
|
+
|
|
17
|
+
if [[ -n "${DETECT_FORGE_LOADED:-}" ]]; then return 0; fi
|
|
18
|
+
DETECT_FORGE_LOADED=1
|
|
19
|
+
|
|
20
|
+
FORGE_UNKNOWN="unknown"
|
|
21
|
+
|
|
22
|
+
# Forges whose CI templates bigpowers actually ships. Anything else must be
|
|
23
|
+
# reported honestly and skipped — never silently handed GitHub Actions config.
|
|
24
|
+
FORGE_SUPPORTED_LIST="github"
|
|
25
|
+
|
|
26
|
+
_forge_from_remote_url() {
|
|
27
|
+
local url="$1"
|
|
28
|
+
case "$url" in
|
|
29
|
+
"") echo "$FORGE_UNKNOWN" ;;
|
|
30
|
+
*github.com[:/]*|*github.*.com[:/]*) echo "github" ;;
|
|
31
|
+
*gitlab.com[:/]*|*gitlab.*[:/]*) echo "gitlab" ;;
|
|
32
|
+
*bitbucket.org[:/]*) echo "bitbucket" ;;
|
|
33
|
+
*codeberg.org[:/]*) echo "codeberg" ;;
|
|
34
|
+
*gitea.*[:/]*) echo "gitea" ;;
|
|
35
|
+
*) echo "$FORGE_UNKNOWN" ;;
|
|
36
|
+
esac
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
_forge_from_config() {
|
|
40
|
+
local repo_root="$1"
|
|
41
|
+
local config="$repo_root/specs/forge.yaml"
|
|
42
|
+
[[ -f "$config" ]] || return 1
|
|
43
|
+
local value
|
|
44
|
+
value=$(grep -E '^forge:' "$config" 2>/dev/null | head -1 \
|
|
45
|
+
| sed -E 's/^forge:[[:space:]]*//; s/^"(.*)"$/\1/; s/[[:space:]]*$//')
|
|
46
|
+
[[ -n "$value" ]] || return 1
|
|
47
|
+
printf '%s' "$value"
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# Echo the forge id. Never fails — an undetectable forge is "unknown", which
|
|
51
|
+
# callers must treat as "skip honestly", not as "assume GitHub".
|
|
52
|
+
detect_forge() {
|
|
53
|
+
local repo_root="${1:-.}"
|
|
54
|
+
|
|
55
|
+
if [[ -n "${BIGPOWERS_FORGE:-}" ]]; then
|
|
56
|
+
printf '%s\n' "$BIGPOWERS_FORGE"
|
|
57
|
+
return 0
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
local configured
|
|
61
|
+
if configured=$(_forge_from_config "$repo_root"); then
|
|
62
|
+
printf '%s\n' "$configured"
|
|
63
|
+
return 0
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
local url
|
|
67
|
+
url=$(git -C "$repo_root" remote get-url origin 2>/dev/null || echo "")
|
|
68
|
+
_forge_from_remote_url "$url"
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
forge_is_supported() {
|
|
72
|
+
local forge="$1"
|
|
73
|
+
case " $FORGE_SUPPORTED_LIST " in
|
|
74
|
+
*" $forge "*) return 0 ;;
|
|
75
|
+
*) return 1 ;;
|
|
76
|
+
esac
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# Where a forge expects its CI config. Empty for unsupported forges — callers
|
|
80
|
+
# must check forge_is_supported first rather than treating "" as a path.
|
|
81
|
+
forge_ci_path() {
|
|
82
|
+
local forge="$1"
|
|
83
|
+
case "$forge" in
|
|
84
|
+
github) echo ".github/workflows" ;;
|
|
85
|
+
gitlab) echo ".gitlab-ci.yml" ;;
|
|
86
|
+
*) echo "" ;;
|
|
87
|
+
esac
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# Root of the bundled CI templates. Overridable so a user can point at their own
|
|
91
|
+
# org templates; the previous hardcoded `danielvm-git/.github` made every
|
|
92
|
+
# installation depend on one person's personal repository.
|
|
93
|
+
#
|
|
94
|
+
# Resolved against the bigpowers package — NOT the target project. Templates
|
|
95
|
+
# ship with bigpowers; deriving this from the project being wired would look for
|
|
96
|
+
# them inside the user's repo, where they do not exist.
|
|
97
|
+
forge_template_root() {
|
|
98
|
+
if [[ -n "${BIGPOWERS_CI_TEMPLATES:-}" ]]; then
|
|
99
|
+
printf '%s' "$BIGPOWERS_CI_TEMPLATES"
|
|
100
|
+
return 0
|
|
101
|
+
fi
|
|
102
|
+
local package_root
|
|
103
|
+
package_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
104
|
+
printf '%s' "$package_root/docs/templates/ci"
|
|
105
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -18,6 +18,10 @@ GOLDEN_GATES=(
|
|
|
18
18
|
"import-boundaries:bash scripts/check-import-boundaries.sh:false"
|
|
19
19
|
"skill-catalog:bash scripts/validate-skill-catalog.sh:false"
|
|
20
20
|
"specs-parse:bash scripts/validate-specs-yaml.sh:false"
|
|
21
|
+
# Tier 2 of the verify arc (#106): executes the verify: of every story marked
|
|
22
|
+
# done. Tier 1 (SKILL.md) is run-skill-verify.sh in golden-suite.yml.
|
|
23
|
+
"story-verify-selftest:bash scripts/run-story-verify.sh --self-test:false"
|
|
24
|
+
"story-verify:bash scripts/run-story-verify.sh:false"
|
|
21
25
|
)
|
|
22
26
|
|
|
23
27
|
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
|
|