bigpowers 2.90.1 → 2.91.0

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 (128) hide show
  1. package/.agents/skills/build-epic/SKILL.md +1 -1
  2. package/.agents/skills/commit-message/SKILL.md +1 -0
  3. package/.agents/skills/release-branch/SKILL.md +4 -5
  4. package/.agents/skills/run-planning/SKILL.md +6 -1
  5. package/.agents/skills/verify-work/SKILL.md +2 -1
  6. package/.cline/skills/build-epic/SKILL.md +1 -1
  7. package/.cline/skills/commit-message/SKILL.md +1 -0
  8. package/.cline/skills/release-branch/SKILL.md +4 -5
  9. package/.cline/skills/run-planning/SKILL.md +6 -1
  10. package/.cline/skills/verify-work/SKILL.md +2 -1
  11. package/.codebuddy/skills/build-epic/SKILL.md +1 -1
  12. package/.codebuddy/skills/commit-message/SKILL.md +1 -0
  13. package/.codebuddy/skills/release-branch/SKILL.md +4 -5
  14. package/.codebuddy/skills/run-planning/SKILL.md +6 -1
  15. package/.codebuddy/skills/verify-work/SKILL.md +2 -1
  16. package/.codex/skills/build-epic/SKILL.md +1 -1
  17. package/.codex/skills/commit-message/SKILL.md +1 -0
  18. package/.codex/skills/release-branch/SKILL.md +4 -5
  19. package/.codex/skills/run-planning/SKILL.md +6 -1
  20. package/.codex/skills/verify-work/SKILL.md +2 -1
  21. package/.copilot/skills/build-epic/SKILL.md +1 -1
  22. package/.copilot/skills/commit-message/SKILL.md +1 -0
  23. package/.copilot/skills/release-branch/SKILL.md +4 -5
  24. package/.copilot/skills/run-planning/SKILL.md +6 -1
  25. package/.copilot/skills/verify-work/SKILL.md +2 -1
  26. package/.cursor/rules/build-epic.mdc +1 -1
  27. package/.cursor/rules/commit-message.mdc +1 -0
  28. package/.cursor/rules/release-branch.mdc +4 -5
  29. package/.cursor/rules/run-planning.mdc +6 -1
  30. package/.cursor/rules/verify-work.mdc +2 -1
  31. package/.gemini/extensions/bigpowers/commands/prompts/build-epic.md +1 -1
  32. package/.gemini/extensions/bigpowers/commands/prompts/commit-message.md +1 -0
  33. package/.gemini/extensions/bigpowers/commands/prompts/release-branch.md +4 -5
  34. package/.gemini/extensions/bigpowers/commands/prompts/run-planning.md +6 -1
  35. package/.gemini/extensions/bigpowers/commands/prompts/verify-work.md +2 -1
  36. package/.gemini/extensions/bigpowers/gemini-extension.json +1 -1
  37. package/.gemini/extensions/bigpowers/skills/build-epic/SKILL.md +1 -1
  38. package/.gemini/extensions/bigpowers/skills/commit-message/SKILL.md +1 -0
  39. package/.gemini/extensions/bigpowers/skills/release-branch/SKILL.md +4 -5
  40. package/.gemini/extensions/bigpowers/skills/run-planning/SKILL.md +6 -1
  41. package/.gemini/extensions/bigpowers/skills/verify-work/SKILL.md +2 -1
  42. package/.hermes/skills/build-epic/SKILL.md +1 -1
  43. package/.hermes/skills/commit-message/SKILL.md +1 -0
  44. package/.hermes/skills/release-branch/SKILL.md +4 -5
  45. package/.hermes/skills/run-planning/SKILL.md +6 -1
  46. package/.hermes/skills/verify-work/SKILL.md +2 -1
  47. package/.kilocode/rules/build-epic.md +1 -1
  48. package/.kilocode/rules/commit-message.md +1 -0
  49. package/.kilocode/rules/release-branch.md +4 -5
  50. package/.kilocode/rules/run-planning.md +6 -1
  51. package/.kilocode/rules/verify-work.md +2 -1
  52. package/.mimocode/skills/build-epic/SKILL.md +1 -1
  53. package/.mimocode/skills/commit-message/SKILL.md +1 -0
  54. package/.mimocode/skills/release-branch/SKILL.md +4 -5
  55. package/.mimocode/skills/run-planning/SKILL.md +6 -1
  56. package/.mimocode/skills/verify-work/SKILL.md +2 -1
  57. package/.opencode/skills/build-epic/SKILL.md +1 -1
  58. package/.opencode/skills/commit-message/SKILL.md +1 -0
  59. package/.opencode/skills/release-branch/SKILL.md +4 -5
  60. package/.opencode/skills/run-planning/SKILL.md +6 -1
  61. package/.opencode/skills/verify-work/SKILL.md +2 -1
  62. package/.pi/package.json +1 -1
  63. package/.pi/prompts/build-epic.md +1 -1
  64. package/.pi/prompts/commit-message.md +1 -0
  65. package/.pi/prompts/release-branch.md +4 -5
  66. package/.pi/prompts/run-planning.md +6 -1
  67. package/.pi/prompts/verify-work.md +2 -1
  68. package/.pi/skills/build-epic/SKILL.md +1 -1
  69. package/.pi/skills/commit-message/SKILL.md +1 -0
  70. package/.pi/skills/release-branch/SKILL.md +4 -5
  71. package/.pi/skills/run-planning/SKILL.md +6 -1
  72. package/.pi/skills/verify-work/SKILL.md +2 -1
  73. package/.qwen/skills/build-epic/SKILL.md +1 -1
  74. package/.qwen/skills/commit-message/SKILL.md +1 -0
  75. package/.qwen/skills/release-branch/SKILL.md +4 -5
  76. package/.qwen/skills/run-planning/SKILL.md +6 -1
  77. package/.qwen/skills/verify-work/SKILL.md +2 -1
  78. package/.trae/skills/build-epic/SKILL.md +1 -1
  79. package/.trae/skills/commit-message/SKILL.md +1 -0
  80. package/.trae/skills/release-branch/SKILL.md +4 -5
  81. package/.trae/skills/run-planning/SKILL.md +6 -1
  82. package/.trae/skills/verify-work/SKILL.md +2 -1
  83. package/.windsurf/rules/build-epic.md +1 -1
  84. package/.windsurf/rules/commit-message.md +1 -0
  85. package/.windsurf/rules/release-branch.md +4 -5
  86. package/.windsurf/rules/run-planning.md +6 -1
  87. package/.windsurf/rules/verify-work.md +2 -1
  88. package/.zcode/skills/build-epic/SKILL.md +1 -1
  89. package/.zcode/skills/commit-message/SKILL.md +1 -0
  90. package/.zcode/skills/release-branch/SKILL.md +4 -5
  91. package/.zcode/skills/run-planning/SKILL.md +6 -1
  92. package/.zcode/skills/verify-work/SKILL.md +2 -1
  93. package/CHANGELOG.md +14 -0
  94. package/hooks/pre-tool-use.sh +23 -1
  95. package/package.json +2 -1
  96. package/scripts/convert-legado.sh +1 -0
  97. package/scripts/lib/__pycache__/atomic_write.cpython-310.pyc +0 -0
  98. package/scripts/lib/__pycache__/link_utils.cpython-310.pyc +0 -0
  99. package/scripts/lib/__pycache__/srp_engine_links.cpython-310.pyc +0 -0
  100. package/scripts/lib/__pycache__/srp_engine_okf.cpython-310.pyc +0 -0
  101. package/scripts/lib/ab_report.py +11 -4
  102. package/scripts/lib/assert-epic-gate.sh +32 -0
  103. package/scripts/lib/assert-planning-artifacts.sh +65 -0
  104. package/scripts/lib/atomic_write.py +71 -0
  105. package/scripts/lib/blind-spot-checks.py +3 -1
  106. package/scripts/lib/blind-spots.py +3 -1
  107. package/scripts/lib/golden-suite-gates.sh +2 -0
  108. package/scripts/lib/run-benchmark.py +7 -5
  109. package/scripts/lib/srp_engine_okf.py +6 -2
  110. package/scripts/lib/trace-stories.py +6 -4
  111. package/scripts/lib/trace_renderer.py +6 -4
  112. package/scripts/okf-add-references.py +4 -1
  113. package/scripts/sync-bugs-registry.sh +5 -2
  114. package/scripts/test-atomic-writes.sh +129 -0
  115. package/scripts/test-generated-path-hook.sh +57 -0
  116. package/scripts/test-test-lock-hook.sh +100 -0
  117. package/scripts/validate-doctrine.sh +4 -4
  118. package/scripts/verify-adversarial.sh +146 -0
  119. package/scripts/verify-tdd-red-commit.sh +39 -1
  120. package/scripts/yaml-tools.py +6 -3
  121. package/skills/build-epic/SKILL.md +1 -1
  122. package/skills/commit-message/SKILL.md +1 -0
  123. package/skills/release-branch/SKILL.md +4 -5
  124. package/skills/run-planning/SKILL.md +6 -1
  125. package/skills/verify-work/SKILL.md +2 -1
  126. package/skills-lock.json +5 -5
  127. package/tests/test-artifact-assertions.sh +86 -0
  128. package/tests/test-verify-adversarial.sh +27 -0
@@ -56,13 +56,12 @@ git log main...HEAD --format="%B" | grep -qiE 'co[- ]authored[- ]by' && echo "
56
56
  ### 2a. Security gate
57
57
 
58
58
  - [ ] `specs/security/REVIEW.md` exists and is fresh (matches current branch diff)
59
- - [ ] No unresolved HIGH findings with confidence ≥ 8 (or all documented in `specs/security/EXCEPTIONS.md` with sign-off rationale)
60
59
 
61
- If REVIEW.md is missing or stale → run `security-review` inline. Findings block the merge unless documented in EXCEPTIONS.md.
60
+ ### 2b. Audit-code & Traceability gates (HARD GATES — Issue #182)
62
61
 
63
- ### 2b. Traceability gate
64
-
65
- Run `gate-trace` before merge. FAIL blocks merge; CONCERNS requires explicit override in `specs/state.yaml` (`traceability_override: CONCERNS accepted, reason: <explanation>`). WAIVED if no matrix available.
62
+ - [ ] Run `bash scripts/lib/assert-epic-gate.sh release` (asserts `specs/AUDIT_LATEST.md` passed; blocks release on failure).
63
+ - [ ] No unresolved HIGH security findings with confidence ≥ 8 (or documented in `specs/security/EXCEPTIONS.md`).
64
+ - [ ] Run `gate-trace`. FAIL blocks merge; CONCERNS requires explicit override in `specs/state.yaml`. WAIVED if no matrix available.
66
65
 
67
66
  > **Adversarial refute framing (e45s32):** The final pre-merge check is **refute, not rubber-stamp**. Before declaring ready, actively try to disprove traceability completeness — missing story tags, absent verify evidence, stale security review. Only proceed when refutation fails.
68
67
 
@@ -62,7 +62,12 @@ if written:
62
62
  - `plan-release` — sequence epics by WSJF
63
63
  - `slice-tasks` — cut vertical slices
64
64
 
65
- 4. **Update status** — On successful completion, set `status: done` for that workflow key in `planning-status.yaml`.
65
+ 4. **Mechanical artifact assertion before status update (HARD GATE — Issue #183):**
66
+ Before marking any workflow key `status: done`, run:
67
+ ```bash
68
+ bash scripts/lib/assert-planning-artifacts.sh <workflow-key>
69
+ ```
70
+ If the artifact assertion fails, do NOT advance status. Remediate missing output files before continuing. On success, set `status: done` for that workflow key in `planning-status.yaml`.
66
71
 
67
72
  5. **Advance** — Set `state.yaml` `active_flow: planning` while in this chain. When all required keys are done, set `handoff.next_skill` to `plan-work`.
68
73
 
@@ -62,7 +62,8 @@ Review answers "is the code good?"; Verify answers "does the built thing do what
62
62
  5a. **Blind-spot check** — run `bash scripts/check-blind-spots.sh`. This detects structural quality gaps (verify-gap, test-gap, stale-tag, etc.) beyond percentage coverage. If any HIGH-severity findings exist → **block the verify-work PASS gate**. Findings are written to `specs/blind-spots.json`. MEDIUM/LOW findings warn but don't block.
63
63
  5a2. **Completeness critic (e45s05)** — `bash scripts/lib/completeness-critic.sh`. **BLOCKER** aborts merge gate; WARNING → gaps loop; FILLED = evidence only.
64
64
 
65
- 5b. **NFR Evidence Gate** (P0 only) — Produces go/no-go output on three dimensions: Performance (response time, throughput), Reliability (error rate, recovery), and Operability (logging, health checks). Reads thresholds from `specs/tech-architecture/eNN-TEST_PLAN_LATEST.md` and writes evidence as OKF verification-report bundles to `specs/verifications/NFR-eNNsYY.json`. FAIL on any dimension blocks the gate.
65
+ 5b. **NFR Evidence Gate** (P0 only) — Go/no-go on Performance, Reliability, Operability against `specs/tech-architecture/eNN-TEST_PLAN_LATEST.md`. Evidence written to `specs/verifications/NFR-eNNsYY.json`. FAIL blocks gate.
66
+ 5c. **Adversarial Defect Mutation Gate (HARD GATE — e86 / ab-20)** — `bash scripts/verify-adversarial.sh`. Boundary fuzzing & parameter injection against modified interfaces. Crashes or failures block PASS gate; evidence in `specs/verifications/ADVERSARIAL_LATEST.yaml`.
66
67
  6. **Step-by-step UAT** (skip if P2/P3) — one user-observable action at a time.
67
68
  7. **Gaps loop** — failures → log → `plan-work` → re-verify. Unaddressed HIGH findings from step 5 feed into this loop alongside other quality gaps.
68
69
 
@@ -66,7 +66,7 @@ Orchestrates the **build** flow for a single epic: survey → plan tasks → kic
66
66
  After step 5 (verify-work) completes successfully, step 6 runs `audit-code` automatically in `--gate` mode:
67
67
 
68
68
  1. **Run audit:** Invoke `audit-code --gate` on the complete diff for this story.
69
- 2. **Pass (exit 0):** All checklist sections pass → advance to step 7 (commit-message). Record `epic_cycle.audit_result: pass` in `state.yaml`.
69
+ 2. **Pass (exit 0):** All checklist sections pass → run `bash scripts/lib/assert-epic-gate.sh advance` to mechanically confirm the audit pass, then advance to step 7 (commit-message). Record `epic_cycle.audit_result: pass` in `state.yaml`.
70
70
  3. **Fail (exit 1):** One or more checklist sections fail → **reset `epic_cycle.current_step` to `4`** (develop-tdd) and add the failing section IDs to `completed_steps` as `"1,2,3,4,5,6(fail: ...)"`. Record `epic_cycle.audit_result: fail` in `state.yaml`. Do NOT advance past step 6 until audit passes.
71
71
  4. **Audit artifact:** Full audit report saved to `specs/verifications/AUDIT-<epic>-<story>.md` regardless of pass/fail, for reviewer traceability.
72
72
  5. **Enforce F.I.R.S.T:** After audit-code passes, run `enforce-first --quick` on new/modified tests. Append F.I.R.S.T violations (if any) to the audit report. Failing F.I.R.S.T criteria trigger the same loop-back to step 4.
@@ -36,6 +36,7 @@ description: "Reviews working-tree changes, then drafts a Conventional Commits t
36
36
 
37
37
  ## Checklist before finalizing
38
38
 
39
+ - [ ] **Audit pass confirmed (HARD GATE — Issue #182):** In active epic flow, `bash scripts/lib/assert-epic-gate.sh commit` passes.
39
40
  - [ ] Type matches the **dominant** user-visible outcome (`feat` vs `fix` vs `perf`, etc.).
40
41
  - [ ] **Scope** is a short noun in parentheses if it helps (e.g. `fix(api): …`).
41
42
  - [ ] Breaking changes are explicit (`!` and/or `BREAKING CHANGE:` in the body/footer).
@@ -56,13 +56,12 @@ git log main...HEAD --format="%B" | grep -qiE 'co[- ]authored[- ]by' && echo "
56
56
  ### 2a. Security gate
57
57
 
58
58
  - [ ] `specs/security/REVIEW.md` exists and is fresh (matches current branch diff)
59
- - [ ] No unresolved HIGH findings with confidence ≥ 8 (or all documented in `specs/security/EXCEPTIONS.md` with sign-off rationale)
60
59
 
61
- If REVIEW.md is missing or stale → run `security-review` inline. Findings block the merge unless documented in EXCEPTIONS.md.
60
+ ### 2b. Audit-code & Traceability gates (HARD GATES — Issue #182)
62
61
 
63
- ### 2b. Traceability gate
64
-
65
- Run `gate-trace` before merge. FAIL blocks merge; CONCERNS requires explicit override in `specs/state.yaml` (`traceability_override: CONCERNS accepted, reason: <explanation>`). WAIVED if no matrix available.
62
+ - [ ] Run `bash scripts/lib/assert-epic-gate.sh release` (asserts `specs/AUDIT_LATEST.md` passed; blocks release on failure).
63
+ - [ ] No unresolved HIGH security findings with confidence ≥ 8 (or documented in `specs/security/EXCEPTIONS.md`).
64
+ - [ ] Run `gate-trace`. FAIL blocks merge; CONCERNS requires explicit override in `specs/state.yaml`. WAIVED if no matrix available.
66
65
 
67
66
  > **Adversarial refute framing (e45s32):** The final pre-merge check is **refute, not rubber-stamp**. Before declaring ready, actively try to disprove traceability completeness — missing story tags, absent verify evidence, stale security review. Only proceed when refutation fails.
68
67
 
@@ -62,7 +62,12 @@ if written:
62
62
  - `plan-release` — sequence epics by WSJF
63
63
  - `slice-tasks` — cut vertical slices
64
64
 
65
- 4. **Update status** — On successful completion, set `status: done` for that workflow key in `planning-status.yaml`.
65
+ 4. **Mechanical artifact assertion before status update (HARD GATE — Issue #183):**
66
+ Before marking any workflow key `status: done`, run:
67
+ ```bash
68
+ bash scripts/lib/assert-planning-artifacts.sh <workflow-key>
69
+ ```
70
+ If the artifact assertion fails, do NOT advance status. Remediate missing output files before continuing. On success, set `status: done` for that workflow key in `planning-status.yaml`.
66
71
 
67
72
  5. **Advance** — Set `state.yaml` `active_flow: planning` while in this chain. When all required keys are done, set `handoff.next_skill` to `plan-work`.
68
73
 
@@ -62,7 +62,8 @@ Review answers "is the code good?"; Verify answers "does the built thing do what
62
62
  5a. **Blind-spot check** — run `bash scripts/check-blind-spots.sh`. This detects structural quality gaps (verify-gap, test-gap, stale-tag, etc.) beyond percentage coverage. If any HIGH-severity findings exist → **block the verify-work PASS gate**. Findings are written to `specs/blind-spots.json`. MEDIUM/LOW findings warn but don't block.
63
63
  5a2. **Completeness critic (e45s05)** — `bash scripts/lib/completeness-critic.sh`. **BLOCKER** aborts merge gate; WARNING → gaps loop; FILLED = evidence only.
64
64
 
65
- 5b. **NFR Evidence Gate** (P0 only) — Produces go/no-go output on three dimensions: Performance (response time, throughput), Reliability (error rate, recovery), and Operability (logging, health checks). Reads thresholds from `specs/tech-architecture/eNN-TEST_PLAN_LATEST.md` and writes evidence as OKF verification-report bundles to `specs/verifications/NFR-eNNsYY.json`. FAIL on any dimension blocks the gate.
65
+ 5b. **NFR Evidence Gate** (P0 only) — Go/no-go on Performance, Reliability, Operability against `specs/tech-architecture/eNN-TEST_PLAN_LATEST.md`. Evidence written to `specs/verifications/NFR-eNNsYY.json`. FAIL blocks gate.
66
+ 5c. **Adversarial Defect Mutation Gate (HARD GATE — e86 / ab-20)** — `bash scripts/verify-adversarial.sh`. Boundary fuzzing & parameter injection against modified interfaces. Crashes or failures block PASS gate; evidence in `specs/verifications/ADVERSARIAL_LATEST.yaml`.
66
67
  6. **Step-by-step UAT** (skip if P2/P3) — one user-observable action at a time.
67
68
  7. **Gaps loop** — failures → log → `plan-work` → re-verify. Unaddressed HIGH findings from step 5 feed into this loop alongside other quality gaps.
68
69
 
package/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ # [2.91.0](https://github.com/danielvm-git/bigpowers/compare/v2.90.2...v2.91.0) (2026-11-03)
2
+
3
+
4
+ ### Features
5
+
6
+ * **hooks:** enforce mechanical gates for red-commit, artifacts, and adversarial mutation (e86) ([#191](https://github.com/danielvm-git/bigpowers/issues/191)) ([7a1b090](https://github.com/danielvm-git/bigpowers/commit/7a1b0907aa20962ee71e825c4996aa80c213be3f))
7
+
8
+ ## [2.90.2](https://github.com/danielvm-git/bigpowers/compare/v2.90.1...v2.90.2) (2026-11-03)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * **scripts:** write spec state atomically instead of in place ([#192](https://github.com/danielvm-git/bigpowers/issues/192)) ([6198138](https://github.com/danielvm-git/bigpowers/commit/61981388c6ab80e493ae7582749730b41c89fa24)), closes [#181](https://github.com/danielvm-git/bigpowers/issues/181)
14
+
1
15
  ## [2.90.1](https://github.com/danielvm-git/bigpowers/compare/v2.90.0...v2.90.1) (2026-11-03)
2
16
 
3
17
 
@@ -42,8 +42,30 @@ deny() {
42
42
  # Read input from stdin
43
43
  INPUT=$(cat)
44
44
 
45
- # Extract command using jq (standard in bigpowers)
45
+ # Extract command and file path using jq (standard in bigpowers)
46
46
  COMMAND=$(echo "$INPUT" | jq -r '.command // .tool_input.command // empty')
47
+ FILE_PATH=$(echo "$INPUT" | jq -r '.file_path // .tool_input.file_path // .path // .tool_input.path // .target_file // .tool_input.target_file // empty')
48
+ TOOL_NAME=$(echo "$INPUT" | jq -r '.tool // .name // .tool_name // empty')
49
+
50
+ # 0. Check file editing rules
51
+ if [ -n "$FILE_PATH" ]; then
52
+ # e86s01: Generated paths are blocked at edit time
53
+ if [[ "$FILE_PATH" =~ ^\.cursor/rules/ ]] || [[ "$FILE_PATH" =~ ^\.gemini/extensions/ ]] || [[ "$FILE_PATH" =~ ^\.windsurf/ ]]; then
54
+ deny "BLOCKED: Generated file '$FILE_PATH' cannot be edited directly. Edit the source SKILL.md instead and run bash scripts/sync-skills.sh."
55
+ fi
56
+
57
+ # e86s02: Tests are locked during fix_bug flow after RED commit
58
+ if [[ "$FILE_PATH" =~ (^|/)test/ ]] || [[ "$FILE_PATH" =~ (^|/)tests/ ]] || [[ "$FILE_PATH" =~ \.test\.[^/]+$ ]] || [[ "$FILE_PATH" =~ _test\.[^/]+$ ]] || [[ "$FILE_PATH" =~ (^|/)test_[^/]+$ ]]; then
59
+ if [ -f specs/state.yaml ] && grep -qE "active_flow:[[:space:]]*fix_bug" specs/state.yaml 2>/dev/null; then
60
+ if git rev-parse --git-dir >/dev/null 2>&1; then
61
+ recent_log=$(git log -n 5 --format=%s 2>/dev/null || true)
62
+ if echo "$recent_log" | grep -qE '^test(\(.+\))?:'; then
63
+ deny "BLOCKED: Test files ($FILE_PATH) are locked during fix_bug after the RED commit. Write the fix in production code (src/), not in tests."
64
+ fi
65
+ fi
66
+ fi
67
+ fi
68
+ fi
47
69
 
48
70
  if [ -z "$COMMAND" ]; then
49
71
  [ "$MODE" = "gemini" ] && echo '{"decision":"allow"}'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bigpowers",
3
- "version": "2.90.1",
3
+ "version": "2.91.0",
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": {
@@ -68,6 +68,7 @@
68
68
  "zod": "^3.24.2"
69
69
  },
70
70
  "devDependencies": {
71
+ "@ctxo/lang-typescript": "^0.11.0",
71
72
  "@semantic-release/changelog": "^6.0.3",
72
73
  "@semantic-release/exec": "^7.1.0",
73
74
  "@semantic-release/git": "^10.0.1",
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env bash
2
2
  # convert-legado.sh — RELEASE-PLAN.md + SCOPE.md → YAML layout (one-time migration helper)
3
+ # bp-manual-utility: legacy MD spec converter (pre-e44 migration)
3
4
  set -euo pipefail
4
5
  source "$(dirname "${BASH_SOURCE[0]}")/lib/python-env.sh"
5
6
  SPECS="$REPO_ROOT/specs"
@@ -7,11 +7,15 @@ from __future__ import annotations
7
7
  import json
8
8
  import random
9
9
  import subprocess
10
+ import sys
10
11
  from datetime import date
11
12
  from pathlib import Path
12
13
 
13
14
  import yaml
14
15
 
16
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
17
+ from atomic_write import atomic_write_text # noqa: E402
18
+
15
19
  REPO_ROOT = Path(__file__).resolve().parents[2]
16
20
  BOOTSTRAP_RESAMPLES = 1000
17
21
  BOOTSTRAP_SEED = 58
@@ -96,10 +100,13 @@ def write_reports(out: Path, meta: dict, result: dict, records: list[dict]) -> P
96
100
  out.mkdir(parents=True, exist_ok=True)
97
101
  today = date.today().isoformat()
98
102
  report = out / f"AB-{today}.yaml"
99
- report.write_text(yaml.safe_dump({**meta, **result}, sort_keys=False))
100
- with (out / f"ab-runs-{today}.jsonl").open("w") as f:
101
- for rec in records:
102
- f.write(json.dumps({**rec, "bigpowers_sha": meta["bigpowers_sha"]}) + "\n")
103
+ # Atomic (#181): the dashboard reads these while a run is still appending.
104
+ atomic_write_text(report, yaml.safe_dump({**meta, **result}, sort_keys=False))
105
+ records_path = out / f"ab-runs-{today}.jsonl"
106
+ atomic_write_text(
107
+ records_path,
108
+ "".join(json.dumps({**rec, "bigpowers_sha": meta["bigpowers_sha"]}) + "\n" for rec in records),
109
+ )
103
110
  return report
104
111
 
105
112
 
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env bash
2
+ # story: e86 / GH #182
3
+ # Mechanical audit-code pass gate before commit / release.
4
+ set -euo pipefail
5
+
6
+ STAGE="${1:-commit}"
7
+
8
+ AUDIT_PASSED=false
9
+
10
+ # Check specs/AUDIT_LATEST.md
11
+ if [[ -f specs/AUDIT_LATEST.md ]]; then
12
+ if grep -qiE "VERDICT:[[:space:]]*PASS|RESULT:[[:space:]]*PASS" specs/AUDIT_LATEST.md; then
13
+ AUDIT_PASSED=true
14
+ fi
15
+ fi
16
+
17
+ # Check specs/execution-status.yaml
18
+ if [[ "$AUDIT_PASSED" == "false" ]] && [[ -f specs/execution-status.yaml ]]; then
19
+ if grep -qE "audit_passed:[[:space:]]*true" specs/execution-status.yaml 2>/dev/null; then
20
+ AUDIT_PASSED=true
21
+ fi
22
+ fi
23
+
24
+ if [[ "$AUDIT_PASSED" == "false" ]]; then
25
+ echo "ERROR [$STAGE]: audit-code check is missing or did not pass (Issue #182)." >&2
26
+ echo "Remediation: Run audit-code and ensure all checks pass before advancing to $STAGE." >&2
27
+ echo "If audit fails, loop back to develop-tdd to address findings." >&2
28
+ exit 1
29
+ fi
30
+
31
+ echo "PASS [$STAGE]: audit-code verified."
32
+ exit 0
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env bash
2
+ # story: e86 / GH #183
3
+ # Mechanical artifact existence assertion for run-planning workflow steps.
4
+ set -euo pipefail
5
+
6
+ STEP="${1:-}"
7
+
8
+ if [[ -z "$STEP" ]]; then
9
+ echo "Usage: bash scripts/lib/assert-planning-artifacts.sh <step_name>" >&2
10
+ exit 1
11
+ fi
12
+
13
+ case "$STEP" in
14
+ survey-context)
15
+ if [[ ! -f specs/tech-architecture/tech-stack.md ]] && [[ ! -f specs/state.yaml ]]; then
16
+ echo "ERROR [survey-context]: Neither specs/tech-architecture/tech-stack.md nor specs/state.yaml exists." >&2
17
+ exit 1
18
+ fi
19
+ ;;
20
+
21
+ scope-work)
22
+ if [[ ! -f specs/product/SCOPE_LATEST.yaml ]] || [[ ! -s specs/product/SCOPE_LATEST.yaml ]]; then
23
+ echo "ERROR [scope-work]: specs/product/SCOPE_LATEST.yaml is missing or empty." >&2
24
+ exit 1
25
+ fi
26
+ ;;
27
+
28
+ research-first)
29
+ # Research can record in specs/product/PRIOR_ART.md or directly in specs/
30
+ if [[ ! -f specs/product/PRIOR_ART.md ]] && [[ ! -f specs/product/SCOPE_LATEST.yaml ]]; then
31
+ echo "ERROR [research-first]: Neither specs/product/PRIOR_ART.md nor specs/product/SCOPE_LATEST.yaml found." >&2
32
+ exit 1
33
+ fi
34
+ ;;
35
+
36
+ elaborate-spec)
37
+ if [[ ! -f specs/planning-context.yaml ]] && [[ ! -f specs/product/SCOPE_LATEST.yaml ]]; then
38
+ echo "ERROR [elaborate-spec]: specs/planning-context.yaml is missing." >&2
39
+ exit 1
40
+ fi
41
+ ;;
42
+
43
+ plan-release)
44
+ if [[ ! -f specs/release-plan.yaml ]] || [[ ! -s specs/release-plan.yaml ]]; then
45
+ echo "ERROR [plan-release]: specs/release-plan.yaml is missing or empty." >&2
46
+ exit 1
47
+ fi
48
+ ;;
49
+
50
+ slice-tasks)
51
+ # Assert at least one task file exists in specs/epics/
52
+ if ! find specs/epics -name "*-tasks.yaml" 2>/dev/null | grep -q .; then
53
+ echo "ERROR [slice-tasks]: No tasks files (*-tasks.yaml) found in specs/epics/." >&2
54
+ exit 1
55
+ fi
56
+ ;;
57
+
58
+ *)
59
+ echo "WARNING: Unknown step '$STEP' — skipping artifact assertion" >&2
60
+ exit 0
61
+ ;;
62
+ esac
63
+
64
+ echo "PASS: Planning artifacts asserted for step '$STEP'."
65
+ exit 0
@@ -0,0 +1,71 @@
1
+ """Atomic file replacement for spec state (BUG-2026-11-03-atomic-yaml-writes, #181).
2
+
3
+ `Path.write_text()` truncates the destination first and then streams the new
4
+ bytes into it. Any concurrent reader — the dashboard snapshot loop, a subagent,
5
+ a background gate — can observe the file after the truncate and before the last
6
+ chunk lands, and fails with `yaml.scanner.ScannerError` on a file that a moment
7
+ later is perfectly valid. Interrupting the agent mid-write leaves the file
8
+ truncated for good.
9
+
10
+ `atomic_write_text()` never exposes a partial file: the bytes go to a temporary
11
+ file in the same directory, are flushed and fsynced, and only then replace the
12
+ destination with `os.replace()` (a single atomic rename on POSIX). Readers see
13
+ the old content or the new content, never a mix.
14
+
15
+ Same-directory temp file is required: `os.replace` is atomic only within one
16
+ filesystem, and `tempfile.mkstemp(dir=...)` guarantees that.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import os
22
+ import stat
23
+ import tempfile
24
+ from pathlib import Path
25
+
26
+
27
+ def atomic_write_bytes(path: str | Path, data: bytes, mode: int | None = None) -> None:
28
+ """Replace `path` with `data` atomically. Preserves the target's mode."""
29
+ target = Path(path)
30
+ target.parent.mkdir(parents=True, exist_ok=True)
31
+
32
+ if mode is None and target.exists():
33
+ mode = stat.S_IMODE(target.stat().st_mode)
34
+
35
+ fd, tmp_name = tempfile.mkstemp(dir=str(target.parent), prefix=f".{target.name}.", suffix=".tmp")
36
+ try:
37
+ with os.fdopen(fd, "wb") as handle:
38
+ handle.write(data)
39
+ handle.flush()
40
+ os.fsync(handle.fileno())
41
+ if mode is not None:
42
+ os.chmod(tmp_name, mode)
43
+ os.replace(tmp_name, target)
44
+ except BaseException:
45
+ try:
46
+ os.unlink(tmp_name)
47
+ except OSError:
48
+ pass
49
+ raise
50
+
51
+ _fsync_dir(target.parent)
52
+
53
+
54
+ def atomic_write_text(path: str | Path, text: str, encoding: str = "utf-8", mode: int | None = None) -> None:
55
+ """Atomic replacement for `Path.write_text()`."""
56
+ atomic_write_bytes(path, text.encode(encoding), mode=mode)
57
+
58
+
59
+ def _fsync_dir(directory: Path) -> None:
60
+ """Persist the rename itself. Best-effort: not every platform allows it."""
61
+ flags = getattr(os, "O_DIRECTORY", 0)
62
+ try:
63
+ dir_fd = os.open(str(directory), os.O_RDONLY | flags)
64
+ except OSError:
65
+ return
66
+ try:
67
+ os.fsync(dir_fd)
68
+ except OSError:
69
+ pass
70
+ finally:
71
+ os.close(dir_fd)
@@ -6,6 +6,8 @@
6
6
  import json, os, re, sys
7
7
  from pathlib import Path
8
8
  from datetime import datetime, timezone
9
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
10
+ from atomic_write import atomic_write_text # noqa: E402
9
11
 
10
12
  ROOT = Path(sys.argv[1])
11
13
  BLIND_SPOTS_JSON = Path(sys.argv[2])
@@ -202,7 +204,7 @@ blind_spots = {
202
204
  }
203
205
 
204
206
  BLIND_SPOTS_JSON.parent.mkdir(parents=True, exist_ok=True)
205
- BLIND_SPOTS_JSON.write_text(json.dumps(blind_spots, indent=2), encoding="utf-8")
207
+ atomic_write_text(BLIND_SPOTS_JSON, json.dumps(blind_spots, indent=2), encoding="utf-8")
206
208
 
207
209
  print(f"check-blind-spots.sh: {len(findings)} findings ({high_count} HIGH, {medium_count} MEDIUM, {low_count} LOW)")
208
210
  for f in findings:
@@ -15,6 +15,8 @@ from __future__ import annotations
15
15
  import json, os, re, sys
16
16
  from pathlib import Path
17
17
  from datetime import datetime, timezone
18
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
19
+ from atomic_write import atomic_write_text # noqa: E402
18
20
 
19
21
  ROOT = Path(sys.argv[1])
20
22
  BLIND_SPOTS_JSON = Path(sys.argv[2])
@@ -260,7 +262,7 @@ blind_spots = {
260
262
  }
261
263
 
262
264
  BLIND_SPOTS_JSON.parent.mkdir(parents=True, exist_ok=True)
263
- BLIND_SPOTS_JSON.write_text(json.dumps(blind_spots, indent=2), encoding="utf-8")
265
+ atomic_write_text(BLIND_SPOTS_JSON, json.dumps(blind_spots, indent=2), encoding="utf-8")
264
266
 
265
267
  print(f"check-blind-spots.sh: {len(findings)} findings ({high_count} HIGH, {medium_count} MEDIUM, {low_count} LOW)")
266
268
  for f in findings:
@@ -81,6 +81,8 @@ GOLDEN_GATES=(
81
81
  "golden-gate-runner:bash scripts/test-golden-gate-runner.sh:false"
82
82
  # Pipefail SIGPIPE races: fix shapes hold, git output never feeds grep -q/head.
83
83
  "pipefail-pipe-race:bash scripts/test-pipefail-pipe-race.sh:false"
84
+ # Atomic state writes: no reader ever sees a truncated YAML/JSON file (#181).
85
+ "atomic-writes:bash scripts/test-atomic-writes.sh:false"
84
86
  # One pipeline, release needs: every gate, pinned runner and actions.
85
87
  "ci-pipeline-contract-selftest:bash scripts/test-ci-pipeline-contract.sh --self-test:false"
86
88
  "ci-pipeline-contract:bash scripts/test-ci-pipeline-contract.sh:false"
@@ -12,6 +12,9 @@ from pathlib import Path
12
12
 
13
13
  import yaml
14
14
 
15
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
16
+ from atomic_write import atomic_write_text # noqa: E402
17
+
15
18
  REPO_ROOT = Path(__file__).resolve().parents[2]
16
19
  BENCH_DIR = REPO_ROOT / "specs" / "benchmarks"
17
20
  REPORTS_DIR = BENCH_DIR / "reports"
@@ -169,8 +172,7 @@ def run_benchmark(skill: str, baseline: bool = False) -> dict:
169
172
  REPORTS_DIR.mkdir(parents=True, exist_ok=True)
170
173
  yaml_path = REPORTS_DIR / f"BENCHMARK-{skill}-{today}.yaml"
171
174
  json_path = REPORTS_DIR / f"benchmark-{skill}.json"
172
- with yaml_path.open("w") as f:
173
- yaml.dump(
175
+ yaml_text = yaml.dump(
174
176
  {
175
177
  "skill": skill,
176
178
  "run_date": today,
@@ -189,9 +191,9 @@ def run_benchmark(skill: str, baseline: bool = False) -> dict:
189
191
  },
190
192
  f,
191
193
  default_flow_style=False,
192
- )
193
- with json_path.open("w") as f:
194
- json.dump(report, f, separators=(",", ":"))
194
+ )
195
+ atomic_write_text(yaml_path, yaml_text)
196
+ atomic_write_text(json_path, json.dumps(report, separators=(",", ":")))
195
197
  print(f"Wrote {yaml_path}")
196
198
  print(f"Wrote {json_path}")
197
199
  if baseline:
@@ -1,7 +1,12 @@
1
1
  """OKF concept-page rendering for srp-engine (sync-skills.sh --okf)."""
2
2
  # story: e39s04
3
3
  # story: BUG-2026-10-30-compliance-akita-masked
4
+ # story: BUG-2026-11-03-atomic-yaml-writes
4
5
  import os
6
+ import sys
7
+
8
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
9
+ from atomic_write import atomic_write_text # noqa: E402
5
10
 
6
11
 
7
12
  def render_okf_concept(skill_data, okf_wiki_skills):
@@ -61,5 +66,4 @@ def render_okf_concept(skill_data, okf_wiki_skills):
61
66
  f"> Auto-generated by sync-skills.sh --okf (e39s04) from skills/{name}/SKILL.md"
62
67
  ])
63
68
 
64
- with open(okf_file, 'w', encoding='utf-8') as f:
65
- f.write('\n'.join(lines) + '\n')
69
+ atomic_write_text(okf_file, '\n'.join(lines) + '\n')
@@ -15,6 +15,8 @@ import json, os, re, subprocess, sys
15
15
  from pathlib import Path
16
16
  from datetime import datetime, timezone
17
17
  import yaml
18
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
19
+ from atomic_write import atomic_write_text # noqa: E402
18
20
 
19
21
  ROOT = Path(sys.argv[1])
20
22
  MATRIX_JSON = Path(sys.argv[2])
@@ -189,7 +191,7 @@ matrix = {
189
191
  }
190
192
  }
191
193
  MATRIX_JSON.parent.mkdir(parents=True, exist_ok=True)
192
- MATRIX_JSON.write_text(json.dumps(matrix, indent=2), encoding="utf-8")
194
+ atomic_write_text(MATRIX_JSON, json.dumps(matrix, indent=2), encoding="utf-8")
193
195
 
194
196
  # --- 6. Emit TRACEABILITY_LATEST.md
195
197
  lines = ["# Traceability Matrix", "",
@@ -224,7 +226,7 @@ if stale_tags:
224
226
  for st in stale_tags: lines.append(f"- `{st}`")
225
227
  lines.append("")
226
228
  TRACE_MD.parent.mkdir(parents=True, exist_ok=True)
227
- TRACE_MD.write_text("\n".join(lines), encoding="utf-8")
229
+ atomic_write_text(TRACE_MD, "\n".join(lines))
228
230
 
229
231
  # --- 7. Emit OKF bundle (specs/codebase-wiki/)
230
232
  OKF_DIR.mkdir(parents=True, exist_ok=True)
@@ -240,7 +242,7 @@ for s in matrix_stories:
240
242
  mc = "high" if "high" in confs else ("medium" if "medium" in confs else ("low" if confs else "none"))
241
243
  idx_lines.append(f"| [{s['id']}](./{s['id']}.md) | {s['title'][:60]} | {mc} | {s['link_count']} |")
242
244
  idx_lines.append("")
243
- (OKF_DIR / "index.md").write_text("\n".join(idx_lines), encoding="utf-8")
245
+ atomic_write_text(OKF_DIR / "index.md", "\n".join(idx_lines))
244
246
  for s in matrix_stories:
245
247
  concept = ["---", f"type: Story", f"id: {s['id']}", f"epic: {s['epic_id']}",
246
248
  f"bcps: {s['bcp']}", f"wsjf: {s['wsjf']}",
@@ -263,7 +265,7 @@ for s in matrix_stories:
263
265
  else:
264
266
  concept.append("*No code links found — this is a dark story.*")
265
267
  concept.append("")
266
- (OKF_DIR / f"{s['id']}.md").write_text("\n".join(concept), encoding="utf-8")
268
+ atomic_write_text(OKF_DIR / f"{s['id']}.md", "\n".join(concept))
267
269
 
268
270
  # --- 8. Strict mode
269
271
  if STRICT:
@@ -3,6 +3,8 @@
3
3
  import json
4
4
  from pathlib import Path
5
5
  from datetime import datetime, timezone
6
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
7
+ from atomic_write import atomic_write_text # noqa: E402
6
8
 
7
9
  def emit_matrix_json(matrix_json_path: Path, stories: dict, tagged_sids: set, dark_stories: list, orphan_tags: list, stale_tags: list, matrix_stories: list) -> dict:
8
10
  matrix = {
@@ -26,7 +28,7 @@ def emit_matrix_json(matrix_json_path: Path, stories: dict, tagged_sids: set, da
26
28
  }
27
29
  }
28
30
  matrix_json_path.parent.mkdir(parents=True, exist_ok=True)
29
- matrix_json_path.write_text(json.dumps(matrix, indent=2), encoding="utf-8")
31
+ atomic_write_text(matrix_json_path, json.dumps(matrix, indent=2), encoding="utf-8")
30
32
  return matrix
31
33
 
32
34
  def emit_trace_md(trace_md_path: Path, stories: dict, tagged_sids: set, dark_stories: list, orphan_tags: list, stale_tags: list, matrix_stories: list, matrix: dict, dev_status: dict) -> None:
@@ -80,7 +82,7 @@ def emit_trace_md(trace_md_path: Path, stories: dict, tagged_sids: set, dark_sto
80
82
  lines.append("")
81
83
 
82
84
  trace_md_path.parent.mkdir(parents=True, exist_ok=True)
83
- trace_md_path.write_text("\n".join(lines), encoding="utf-8")
85
+ atomic_write_text(trace_md_path, "\n".join(lines))
84
86
 
85
87
  def emit_okf_bundle(okf_dir: Path, stories: dict, matrix_stories: list) -> None:
86
88
  okf_dir.mkdir(parents=True, exist_ok=True)
@@ -104,7 +106,7 @@ def emit_okf_bundle(okf_dir: Path, stories: dict, matrix_stories: list) -> None:
104
106
  max_conf = "high" if "high" in confs else ("medium" if "medium" in confs else ("low" if confs else "none"))
105
107
  idx_lines.append(f"| [{s['id']}](./{s['id']}.md) | {s['title'][:60]} | {max_conf} | {s['link_count']} |")
106
108
  idx_lines.append("")
107
- (okf_dir / "index.md").write_text("\n".join(idx_lines), encoding="utf-8")
109
+ atomic_write_text(okf_dir / "index.md", "\n".join(idx_lines))
108
110
 
109
111
  # Generate one concept per story
110
112
  for s in matrix_stories:
@@ -144,4 +146,4 @@ def emit_okf_bundle(okf_dir: Path, stories: dict, matrix_stories: list) -> None:
144
146
  else:
145
147
  concept.append("*No code links found — this is a dark story.*")
146
148
  concept.append("")
147
- (okf_dir / f"{s['id']}.md").write_text("\n".join(concept), encoding="utf-8")
149
+ atomic_write_text(okf_dir / f"{s['id']}.md", "\n".join(concept))
@@ -8,6 +8,9 @@ import re
8
8
  import sys
9
9
  from pathlib import Path
10
10
 
11
+ sys.path.insert(0, str(Path(__file__).resolve().parent / "lib"))
12
+ from atomic_write import atomic_write_text # noqa: E402
13
+
11
14
 
12
15
  def main() -> int:
13
16
  graph_json = Path(sys.argv[1])
@@ -69,7 +72,7 @@ def main() -> int:
69
72
  content,
70
73
  )
71
74
  count_updated += 1
72
- path.write_text(content, encoding="utf-8")
75
+ atomic_write_text(path, content)
73
76
 
74
77
  print(f" concepts with cross-references: {count_updated}", file=sys.stderr)
75
78
  return 0
@@ -12,6 +12,9 @@ import re
12
12
  import sys
13
13
  from pathlib import Path
14
14
 
15
+ sys.path.insert(0, str(Path(sys.argv[2]) / "scripts" / "lib"))
16
+ from atomic_write import atomic_write_text
17
+
15
18
  bugs_dir = Path(sys.argv[1])
16
19
  repo_root = Path(sys.argv[2])
17
20
 
@@ -61,7 +64,7 @@ for e in entries:
61
64
  val = e[opt].replace('"', "'")
62
65
  lines.append(f' {opt}: "{val}"')
63
66
  lines.append("")
64
- out.write_text("\n".join(lines), encoding="utf-8")
67
+ atomic_write_text(out, "\n".join(lines))
65
68
  print(f"sync-bugs-registry: {len(entries)} bugs -> {out}")
66
69
 
67
70
  # ---- Phase 2: Emit OKF concept bundles (e45s03) ----
@@ -96,7 +99,7 @@ references:
96
99
 
97
100
  See `specs/bugs/{bug_id}.md` for full investigation and fix details.
98
101
  """
99
- bundle_path.write_text(bundle, encoding="utf-8")
102
+ atomic_write_text(bundle_path, bundle)
100
103
  okf_count += 1
101
104
 
102
105
  print(f"sync-bugs-registry: {okf_count} OKF concept bundles -> {bugs_dir}/")