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.
Files changed (59) hide show
  1. package/.agents/skills/security-review/SKILL.md +3 -1
  2. package/.agents/skills/smoke-test/SKILL.md +1 -1
  3. package/.agents/skills/wire-ci/SKILL.md +78 -59
  4. package/.codebuddy/skills/security-review/SKILL.md +3 -1
  5. package/.codebuddy/skills/smoke-test/SKILL.md +1 -1
  6. package/.codebuddy/skills/wire-ci/SKILL.md +78 -59
  7. package/.codex/skills/security-review/SKILL.md +3 -1
  8. package/.codex/skills/smoke-test/SKILL.md +1 -1
  9. package/.codex/skills/wire-ci/SKILL.md +78 -59
  10. package/.hermes/skills/security-review/SKILL.md +3 -1
  11. package/.hermes/skills/smoke-test/SKILL.md +1 -1
  12. package/.hermes/skills/wire-ci/SKILL.md +78 -59
  13. package/.kilocode/rules/security-review.md +3 -1
  14. package/.kilocode/rules/smoke-test.md +1 -1
  15. package/.kilocode/rules/wire-ci.md +78 -59
  16. package/.mimocode/skills/security-review/SKILL.md +3 -1
  17. package/.mimocode/skills/smoke-test/SKILL.md +1 -1
  18. package/.mimocode/skills/wire-ci/SKILL.md +78 -59
  19. package/.pi/package.json +1 -1
  20. package/.pi/prompts/security-review.md +3 -1
  21. package/.pi/prompts/smoke-test.md +1 -1
  22. package/.pi/prompts/wire-ci.md +78 -59
  23. package/.pi/skills/security-review/SKILL.md +3 -1
  24. package/.pi/skills/smoke-test/SKILL.md +1 -1
  25. package/.pi/skills/wire-ci/SKILL.md +78 -59
  26. package/.trae/skills/security-review/SKILL.md +3 -1
  27. package/.trae/skills/smoke-test/SKILL.md +1 -1
  28. package/.trae/skills/wire-ci/SKILL.md +78 -59
  29. package/.windsurf/rules/security-review.md +3 -1
  30. package/.windsurf/rules/smoke-test.md +1 -1
  31. package/.windsurf/rules/wire-ci.md +78 -59
  32. package/.zcode/skills/security-review/SKILL.md +3 -1
  33. package/.zcode/skills/smoke-test/SKILL.md +1 -1
  34. package/.zcode/skills/wire-ci/SKILL.md +78 -59
  35. package/CHANGELOG.md +21 -0
  36. package/SKILL-INDEX.md +1 -1
  37. package/package.json +1 -1
  38. package/scripts/lib/__pycache__/link_utils.cpython-312.pyc +0 -0
  39. package/scripts/lib/__pycache__/srp_engine_links.cpython-312.pyc +0 -0
  40. package/scripts/lib/context-wire.sh +9 -0
  41. package/scripts/lib/detect-forge.sh +105 -0
  42. package/scripts/lib/extract-story-verify.py +102 -0
  43. package/scripts/lib/fail-open-detect.sh +67 -0
  44. package/scripts/lib/golden-suite-gates.sh +4 -0
  45. package/scripts/lib/plan-consistency-check.sh +26 -2
  46. package/scripts/lib/test-assertions.sh +11 -1
  47. package/scripts/run-skill-verify.sh +5 -46
  48. package/scripts/run-story-verify.sh +208 -0
  49. package/scripts/test-adapters.sh +14 -8
  50. package/scripts/verify-cwe-fixture-sync.sh +88 -0
  51. package/scripts/verify-install.sh +13 -1
  52. package/scripts/wire-ci.sh +159 -0
  53. package/skills/security-review/SKILL.md +3 -1
  54. package/skills/security-review/fixtures/CWE-639-idor-negative.go +48 -0
  55. package/skills/security-review/fixtures/CWE-639-idor-positive.go +29 -0
  56. package/skills/smoke-test/SKILL.md +1 -1
  57. package/skills/wire-ci/REFERENCE.md +15 -4
  58. package/skills/wire-ci/SKILL.md +63 -55
  59. 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 pre-built templates and local validation. Generates GitHub Actions workflows, validates YAML syntax and permissions, supports dry-run via act/gh. The CI equivalent of wire-observability."
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 or when adding CI to an existing project.
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 your project type, copies org templates from `danielvm-git/.github/workflow-templates/`, and provides local verification to catch auth, permissions, and syntax issues before they reach CI.
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
- ## What this sets up
17
+ ## Forge resolution
16
18
 
17
- 1. **Test Build Release workflow** — `.github/workflows/test-build-release.yml` with lint → test → build → release (single pipeline, `needs:` chain)
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
- ## Process
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
- ### 1. Detect project type
27
+ GitHub ships templates (`.github/workflows/`). GitLab, Bitbucket, Codeberg, and Gitea are detected but **unsupported** — `--apply` writes nothing and exits 3.
26
28
 
27
- Read the project root for manifest files to determine which template pair to copy:
29
+ ## Template source — configurable, bundled by default
28
30
 
29
- | Manifest | Type | Template pair (from `.github` repo) |
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
- **CLI/library repos** (no hosted target): copy only `test-build-release-<stack>.yml` → `test-build-release.yml`. The `release` job is terminal; skip `deploy.yml`. See `docs/how-to/migrate-ci-cd/migration-plan.md` § CLI and library repos.
33
+ ## What this sets up
40
34
 
41
- If no manifest is found, prompt the user to specify the type or pass `--type <rust|node|python|go|static|swift>`.
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
- ### 2. Copy test-build-release template
40
+ Deploy workflows are **not** bundled: they are platform-specific. See [REFERENCE.md](REFERENCE.md) for a worked example.
44
41
 
45
- Copy the matching template from `~/Developer/.github/workflow-templates/` to `.github/workflows/test-build-release.yml`. **Do not rename the workflow `name:` field** — deploy listens for `"Test Build Release"`.
42
+ ## Process
43
+
44
+ ### 1. Detect forge and stack
45
+
46
+ ```bash
47
+ bash scripts/wire-ci.sh --detect
48
+ ```
46
49
 
47
- Edit placeholders: `APP_TYPE`, `SITE_URL`, language versions.
50
+ Stack detection reads the project root:
48
51
 
49
- See [REFERENCE.md](REFERENCE.md) for stack-specific job shapes.
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
- ### 3. Copy deploy template (hosted stacks only)
59
+ No recognized manifest → exit 3 with the list of manifests it looked for. Do not guess.
52
60
 
53
- If the project deploys to BigBase or GitHub Pages:
61
+ ### 2. Apply the template
54
62
 
55
63
  ```bash
56
- cp ~/Developer/.github/workflow-templates/deploy-${STACK}.yml .github/workflows/deploy.yml
64
+ bash scripts/wire-ci.sh --apply
57
65
  ```
58
66
 
59
- Configure `BIGBASE_SITE_ID`, `BIGBASE_DEPLOY_TOKEN`, and matching `SITE_URL`. Deploy runs via `workflow_run` after TBR succeeds on `main` with `cancel-in-progress: false`.
67
+ **Do not rename the workflow `name:` field** — deploy listens for `"Test Build Release"`.
60
68
 
61
- Skip this step for CLI/library repos.
69
+ Edit placeholders after copying: language versions, `APP_TYPE`, `SITE_URL`.
62
70
 
63
- ### 4. Validate workflows (`--validate`)
71
+ ### 3. Unsupported forges
64
72
 
65
- Run `wire-ci --validate` to check all generated workflow files:
73
+ `--apply` writes nothing and exits 3. Your options, in the order the runner prints them:
66
74
 
67
- See [REFERENCE.md](REFERENCE.md)
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
- **Exit codes:**
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
- ### 5. Dry-run workflows (`--dry-run`)
81
+ ### 4. Validate workflows (`--validate`)
75
82
 
76
- Attempt to run the generated workflows locally to catch errors before push:
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 doesn't execute locally — useful for checking YAML parsing but not for testing the actual steps.
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 GitHub repo secrets |
90
- | `semantic-release` fails on push | Missing `permissions: contents: write` | Add `permissions: contents: write` to release job |
91
- | `cargo publish` auth fail | `CARGO_REGISTRY_TOKEN` not set | Add token to `~/.cargo/config.toml` or env |
92
- | `go vet` fails | Go version mismatch | Match `go.mod` `go` directive with setup-go version |
93
- | `cargo clippy` errors | New lints in Rust nightly | `cargo clippy --fix` or allow specific lints |
94
- | `act` not found | Docker not running or act not installed | `brew install act` / `docker ps` to verify Docker |
95
- | Hardcoded Node version stale | `.nvmrc` exists but workflow uses hardcoded version | Use `node-version-file: .nvmrc` instead |
96
- | Deploy never runs | TBR workflow renamed | Keep `name: Test Build Release` in test-build-release.yml |
97
- | Release rebuilds binary | Artifact not downloaded | `release` job must `download-artifact` from `build` |
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: `test -f skills/wire-ci/REFERENCE.md && test -f .github/workflows/sync-skills.yml`
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
- # Copy org templates
125
- cp ~/Developer/.github/workflow-templates/test-build-release-go.yml .github/workflows/test-build-release.yml
126
- cp ~/Developer/.github/workflow-templates/deploy-go.yml .github/workflows/deploy.yml
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
- cp ~/Developer/.github/workflow-templates/test-build-release-go.yml .github/workflows/test-build-release.yml
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 pre-built templates and local validation. Generates GitH | ✅ Active |
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bigpowers",
3
- "version": "2.86.1",
3
+ "version": "2.86.3",
4
4
  "description": "73 agent skills synthesizing 17 years of software engineering discipline into a prescriptive methodology for solo developers",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -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
- 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