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.
- package/.agents/skills/build-epic/SKILL.md +1 -1
- package/.agents/skills/commit-message/SKILL.md +1 -0
- package/.agents/skills/release-branch/SKILL.md +4 -5
- package/.agents/skills/run-planning/SKILL.md +6 -1
- package/.agents/skills/verify-work/SKILL.md +2 -1
- package/.cline/skills/build-epic/SKILL.md +1 -1
- package/.cline/skills/commit-message/SKILL.md +1 -0
- package/.cline/skills/release-branch/SKILL.md +4 -5
- package/.cline/skills/run-planning/SKILL.md +6 -1
- package/.cline/skills/verify-work/SKILL.md +2 -1
- package/.codebuddy/skills/build-epic/SKILL.md +1 -1
- package/.codebuddy/skills/commit-message/SKILL.md +1 -0
- package/.codebuddy/skills/release-branch/SKILL.md +4 -5
- package/.codebuddy/skills/run-planning/SKILL.md +6 -1
- package/.codebuddy/skills/verify-work/SKILL.md +2 -1
- package/.codex/skills/build-epic/SKILL.md +1 -1
- package/.codex/skills/commit-message/SKILL.md +1 -0
- package/.codex/skills/release-branch/SKILL.md +4 -5
- package/.codex/skills/run-planning/SKILL.md +6 -1
- package/.codex/skills/verify-work/SKILL.md +2 -1
- package/.copilot/skills/build-epic/SKILL.md +1 -1
- package/.copilot/skills/commit-message/SKILL.md +1 -0
- package/.copilot/skills/release-branch/SKILL.md +4 -5
- package/.copilot/skills/run-planning/SKILL.md +6 -1
- package/.copilot/skills/verify-work/SKILL.md +2 -1
- package/.cursor/rules/build-epic.mdc +1 -1
- package/.cursor/rules/commit-message.mdc +1 -0
- package/.cursor/rules/release-branch.mdc +4 -5
- package/.cursor/rules/run-planning.mdc +6 -1
- package/.cursor/rules/verify-work.mdc +2 -1
- package/.gemini/extensions/bigpowers/commands/prompts/build-epic.md +1 -1
- package/.gemini/extensions/bigpowers/commands/prompts/commit-message.md +1 -0
- package/.gemini/extensions/bigpowers/commands/prompts/release-branch.md +4 -5
- package/.gemini/extensions/bigpowers/commands/prompts/run-planning.md +6 -1
- package/.gemini/extensions/bigpowers/commands/prompts/verify-work.md +2 -1
- package/.gemini/extensions/bigpowers/gemini-extension.json +1 -1
- package/.gemini/extensions/bigpowers/skills/build-epic/SKILL.md +1 -1
- package/.gemini/extensions/bigpowers/skills/commit-message/SKILL.md +1 -0
- package/.gemini/extensions/bigpowers/skills/release-branch/SKILL.md +4 -5
- package/.gemini/extensions/bigpowers/skills/run-planning/SKILL.md +6 -1
- package/.gemini/extensions/bigpowers/skills/verify-work/SKILL.md +2 -1
- package/.hermes/skills/build-epic/SKILL.md +1 -1
- package/.hermes/skills/commit-message/SKILL.md +1 -0
- package/.hermes/skills/release-branch/SKILL.md +4 -5
- package/.hermes/skills/run-planning/SKILL.md +6 -1
- package/.hermes/skills/verify-work/SKILL.md +2 -1
- package/.kilocode/rules/build-epic.md +1 -1
- package/.kilocode/rules/commit-message.md +1 -0
- package/.kilocode/rules/release-branch.md +4 -5
- package/.kilocode/rules/run-planning.md +6 -1
- package/.kilocode/rules/verify-work.md +2 -1
- package/.mimocode/skills/build-epic/SKILL.md +1 -1
- package/.mimocode/skills/commit-message/SKILL.md +1 -0
- package/.mimocode/skills/release-branch/SKILL.md +4 -5
- package/.mimocode/skills/run-planning/SKILL.md +6 -1
- package/.mimocode/skills/verify-work/SKILL.md +2 -1
- package/.opencode/skills/build-epic/SKILL.md +1 -1
- package/.opencode/skills/commit-message/SKILL.md +1 -0
- package/.opencode/skills/release-branch/SKILL.md +4 -5
- package/.opencode/skills/run-planning/SKILL.md +6 -1
- package/.opencode/skills/verify-work/SKILL.md +2 -1
- package/.pi/package.json +1 -1
- package/.pi/prompts/build-epic.md +1 -1
- package/.pi/prompts/commit-message.md +1 -0
- package/.pi/prompts/release-branch.md +4 -5
- package/.pi/prompts/run-planning.md +6 -1
- package/.pi/prompts/verify-work.md +2 -1
- package/.pi/skills/build-epic/SKILL.md +1 -1
- package/.pi/skills/commit-message/SKILL.md +1 -0
- package/.pi/skills/release-branch/SKILL.md +4 -5
- package/.pi/skills/run-planning/SKILL.md +6 -1
- package/.pi/skills/verify-work/SKILL.md +2 -1
- package/.qwen/skills/build-epic/SKILL.md +1 -1
- package/.qwen/skills/commit-message/SKILL.md +1 -0
- package/.qwen/skills/release-branch/SKILL.md +4 -5
- package/.qwen/skills/run-planning/SKILL.md +6 -1
- package/.qwen/skills/verify-work/SKILL.md +2 -1
- package/.trae/skills/build-epic/SKILL.md +1 -1
- package/.trae/skills/commit-message/SKILL.md +1 -0
- package/.trae/skills/release-branch/SKILL.md +4 -5
- package/.trae/skills/run-planning/SKILL.md +6 -1
- package/.trae/skills/verify-work/SKILL.md +2 -1
- package/.windsurf/rules/build-epic.md +1 -1
- package/.windsurf/rules/commit-message.md +1 -0
- package/.windsurf/rules/release-branch.md +4 -5
- package/.windsurf/rules/run-planning.md +6 -1
- package/.windsurf/rules/verify-work.md +2 -1
- package/.zcode/skills/build-epic/SKILL.md +1 -1
- package/.zcode/skills/commit-message/SKILL.md +1 -0
- package/.zcode/skills/release-branch/SKILL.md +4 -5
- package/.zcode/skills/run-planning/SKILL.md +6 -1
- package/.zcode/skills/verify-work/SKILL.md +2 -1
- package/CHANGELOG.md +14 -0
- package/hooks/pre-tool-use.sh +23 -1
- package/package.json +2 -1
- package/scripts/convert-legado.sh +1 -0
- package/scripts/lib/__pycache__/atomic_write.cpython-310.pyc +0 -0
- package/scripts/lib/__pycache__/link_utils.cpython-310.pyc +0 -0
- package/scripts/lib/__pycache__/srp_engine_links.cpython-310.pyc +0 -0
- package/scripts/lib/__pycache__/srp_engine_okf.cpython-310.pyc +0 -0
- package/scripts/lib/ab_report.py +11 -4
- package/scripts/lib/assert-epic-gate.sh +32 -0
- package/scripts/lib/assert-planning-artifacts.sh +65 -0
- package/scripts/lib/atomic_write.py +71 -0
- package/scripts/lib/blind-spot-checks.py +3 -1
- package/scripts/lib/blind-spots.py +3 -1
- package/scripts/lib/golden-suite-gates.sh +2 -0
- package/scripts/lib/run-benchmark.py +7 -5
- package/scripts/lib/srp_engine_okf.py +6 -2
- package/scripts/lib/trace-stories.py +6 -4
- package/scripts/lib/trace_renderer.py +6 -4
- package/scripts/okf-add-references.py +4 -1
- package/scripts/sync-bugs-registry.sh +5 -2
- package/scripts/test-atomic-writes.sh +129 -0
- package/scripts/test-generated-path-hook.sh +57 -0
- package/scripts/test-test-lock-hook.sh +100 -0
- package/scripts/validate-doctrine.sh +4 -4
- package/scripts/verify-adversarial.sh +146 -0
- package/scripts/verify-tdd-red-commit.sh +39 -1
- package/scripts/yaml-tools.py +6 -3
- package/skills/build-epic/SKILL.md +1 -1
- package/skills/commit-message/SKILL.md +1 -0
- package/skills/release-branch/SKILL.md +4 -5
- package/skills/run-planning/SKILL.md +6 -1
- package/skills/verify-work/SKILL.md +2 -1
- package/skills-lock.json +5 -5
- package/tests/test-artifact-assertions.sh +86 -0
- 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
|
-
|
|
60
|
+
### 2b. Audit-code & Traceability gates (HARD GATES — Issue #182)
|
|
62
61
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
Run `gate-trace
|
|
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. **
|
|
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) —
|
|
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
|
-
|
|
60
|
+
### 2b. Audit-code & Traceability gates (HARD GATES — Issue #182)
|
|
62
61
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
Run `gate-trace
|
|
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. **
|
|
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) —
|
|
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
|
|
package/hooks/pre-tool-use.sh
CHANGED
|
@@ -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.
|
|
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"
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/scripts/lib/ab_report.py
CHANGED
|
@@ -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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
194
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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"
|
|
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"
|
|
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
|
|
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
|
|
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"
|
|
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"
|
|
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
|
|
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
|
|
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
|
|
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}/")
|