From 7360436ce033c35088dc176d6e08d4a777b5e31f Mon Sep 17 00:00:00 2001 From: Cobus Greyling Date: Mon, 29 Jun 2026 16:59:19 +0200 Subject: [PATCH 1/3] fix(loop-init): serialize asset bundling (#80) Serialize bundle-assets with a lock and atomic directory replace so concurrent rebuilds no longer race with EEXIST. Adds regression test. Thanks @Jalendar10 --- tools/loop-init/scripts/bundle-assets.mjs | 83 ++++++++++++++++++----- tools/loop-init/test/cli.test.mjs | 12 +++- 2 files changed, 78 insertions(+), 17 deletions(-) diff --git a/tools/loop-init/scripts/bundle-assets.mjs b/tools/loop-init/scripts/bundle-assets.mjs index 0158f6e..b2ab0c2 100644 --- a/tools/loop-init/scripts/bundle-assets.mjs +++ b/tools/loop-init/scripts/bundle-assets.mjs @@ -1,10 +1,11 @@ #!/usr/bin/env node -import { cp, rm, access } from 'node:fs/promises'; +import { cp, rm, access, rename, mkdir } from 'node:fs/promises'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const REPO_ROOT = path.resolve(PACKAGE_ROOT, '../..'); +const LOCK_DIR = path.join(PACKAGE_ROOT, '.bundle-assets.lock'); async function exists(p) { try { @@ -15,21 +16,71 @@ async function exists(p) { } } -for (const dir of ['starters', 'templates']) { - const dest = path.join(PACKAGE_ROOT, dir); - const src = path.join(REPO_ROOT, dir); - if (!(await exists(src))) { - console.error(`bundle-assets: missing ${src}`); - process.exit(1); +async function replaceDirectory(src, dest) { + const tempDest = `${dest}.tmp-${process.pid}-${Date.now()}`; + let moved = false; + await rm(tempDest, { recursive: true, force: true }); + try { + await cp(src, tempDest, { recursive: true }); + + for (let attempt = 0; attempt < 3; attempt += 1) { + await rm(dest, { recursive: true, force: true }); + try { + await rename(tempDest, dest); + moved = true; + return; + } catch (err) { + if (err?.code !== 'EEXIST' || attempt === 2) { + throw err; + } + } + } + } finally { + if (!moved) { + await rm(tempDest, { recursive: true, force: true }); + } } - await rm(dest, { recursive: true, force: true }); - await cp(src, dest, { recursive: true }); - console.log(`bundled ${dir}/ → tools/loop-init/${dir}/`); } -const registrySrc = path.join(REPO_ROOT, 'patterns', 'registry.yaml'); -const registryDest = path.join(PACKAGE_ROOT, 'registry.yaml'); -if (await exists(registrySrc)) { - await cp(registrySrc, registryDest); - console.log('bundled patterns/registry.yaml → tools/loop-init/registry.yaml'); -} \ No newline at end of file +async function sleep(ms) { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} + +async function acquireLock() { + for (let attempt = 0; attempt < 100; attempt += 1) { + try { + await mkdir(LOCK_DIR); + return async () => rm(LOCK_DIR, { recursive: true, force: true }); + } catch (err) { + if (err?.code !== 'EEXIST') { + throw err; + } + await sleep(50); + } + } + throw new Error(`bundle-assets: timed out waiting for ${LOCK_DIR}`); +} + +const releaseLock = await acquireLock(); +try { + for (const dir of ['starters', 'templates']) { + const dest = path.join(PACKAGE_ROOT, dir); + const src = path.join(REPO_ROOT, dir); + if (!(await exists(src))) { + throw new Error(`bundle-assets: missing ${src}`); + } + await replaceDirectory(src, dest); + console.log(`bundled ${dir}/ → tools/loop-init/${dir}/`); + } + + const registrySrc = path.join(REPO_ROOT, 'patterns', 'registry.yaml'); + const registryDest = path.join(PACKAGE_ROOT, 'registry.yaml'); + if (await exists(registrySrc)) { + await cp(registrySrc, registryDest); + console.log('bundled patterns/registry.yaml → tools/loop-init/registry.yaml'); + } +} finally { + await releaseLock(); +} diff --git a/tools/loop-init/test/cli.test.mjs b/tools/loop-init/test/cli.test.mjs index 96951d9..4d74327 100644 --- a/tools/loop-init/test/cli.test.mjs +++ b/tools/loop-init/test/cli.test.mjs @@ -9,6 +9,16 @@ import { promisify } from 'node:util'; const exec = promisify(execFile); const CLI = path.resolve('dist/cli.js'); +test('bundle-assets tolerates concurrent rebuilds', async () => { + await Promise.all([ + exec('node', ['scripts/bundle-assets.mjs']), + exec('node', ['scripts/bundle-assets.mjs']), + ]); + await access(path.join('starters', 'issue-triage', 'README.md')); + await access(path.join('templates', 'SKILL.md.issue-triage')); + await access('registry.yaml'); +}); + test('loop-init --help exits 0', async () => { const { stdout } = await exec('node', [CLI, '--help']); assert.match(stdout, /changelog-drafter/); @@ -75,4 +85,4 @@ test('loop-init scaffolds ci-sweeper with bundled assets', async () => { } finally { await rm(dir, { recursive: true, force: true }); } -}); \ No newline at end of file +}); From b5a69e84d5d0933bbb3e7156e9265b0fa79f0bd8 Mon Sep 17 00:00:00 2001 From: Cobus Greyling Date: Mon, 29 Jun 2026 16:59:19 +0200 Subject: [PATCH 2/3] chore(changelog-drafter): add post-run critique section (#77) Record missed items, false positives, and prompt adjustments in state after each draft review cycle. Thanks @shivamsingh-007 --- patterns/changelog-drafter.md | 16 ++++++++++++++++ .../changelog-drafter-state.md.example | 7 +++++++ 2 files changed, 23 insertions(+) diff --git a/patterns/changelog-drafter.md b/patterns/changelog-drafter.md index 95f700a..2f60751 100644 --- a/patterns/changelog-drafter.md +++ b/patterns/changelog-drafter.md @@ -53,6 +53,21 @@ The loop should prune entries once a release is tagged/published and the draft i - Tone and project voice 6. On approval: the loop can open a PR that updates `CHANGELOG.md` (or the GitHub release body), or simply leave the draft in a file for the maintainer to copy. 7. Record the run + mark items as "published" in state. Prune old entries. +8. Record post-run critique in state: missed items, false positives, grouping issues, retries, and prompt/policy adjustments for next run. + +## Post-Run Critique + +After the human reviews the draft, the human reviewer or operator records what the previous run missed or misclassified so the next run improves. This captures review findings from the last draft run. + +Record in state under a `## Post-Run Critique` heading: + +- **Missed items** — Changes users reported that the draft omitted +- **False positives** — Items in the draft that should not have been user-facing (chores, infra-only) +- **Grouping issues** — Items in the wrong section (e.g., a fix in Features) +- **Retries** — Number of times the draft was revised or regenerated (+ reason) +- **Prompt/policy adjustments** — 1–2 concrete changes to scan, draft, or verifier skill instructions for the next run + +Optional but recommended during L1 dogfood runs. Even occasional critique entries help prevent repeat issues. ## Verification Strategy @@ -96,6 +111,7 @@ The loop should prune entries once a release is tagged/published and the draft i | Overly long / noisy notes | Strict categorization + "user-facing only" rule in the draft skill. Human can trim. | | Tone mismatch with project | Provide a short "Release voice" section in AGENTS.md or a project skill that the drafter reads. | | Accidentally publishing | Never grant the loop write access to tags or the live CHANGELOG without an explicit human gate + PR. | +| Stale critique / never reviewed | Add a human handoff when critique entries accumulate without resolution across a threshold (e.g., 3 runs). | ## Cost Profile diff --git a/starters/changelog-drafter/changelog-drafter-state.md.example b/starters/changelog-drafter/changelog-drafter-state.md.example index f7b0390..3249c96 100644 --- a/starters/changelog-drafter/changelog-drafter-state.md.example +++ b/starters/changelog-drafter/changelog-drafter-state.md.example @@ -13,6 +13,13 @@ Last release tag: v2.14.0 (2026-06-01) ## Recently Published - v2.14.0 — published 2026-06-01 (human reviewed draft) +## Post-Run Critique (from last run) +- **Missed items**: 0 reported +- **False positives**: 2 internal chore PRs — tightened bot filter +- **Grouping issues**: 1 fix in Features — add label check +- **Retries**: 0 +- **Prompt/policy adjustments**: None needed this run + ## Scan Window Notes - Ignore Dependabot / Renovate PRs (handled by dependency-sweeper) - Direct commits on main are included if they have conventional type or linked issue From 2e0d0c8ec3d20feba2fec8ec030398bd51676246 Mon Sep 17 00:00:00 2001 From: Cobus Greyling Date: Mon, 29 Jun 2026 16:59:19 +0200 Subject: [PATCH 3/3] chore(daily-triage): add post-run critique section (#78) Capture high-noise items, false positives, and one adjustment per run to improve triage quality over time. Thanks @shivamsingh-007 --- patterns/daily-triage.md | 12 ++++++++++++ starters/minimal-loop/STATE.md.example | 7 +++++++ 2 files changed, 19 insertions(+) diff --git a/patterns/daily-triage.md b/patterns/daily-triage.md index a3cf364..d20e86a 100644 --- a/patterns/daily-triage.md +++ b/patterns/daily-triage.md @@ -50,6 +50,17 @@ Fields the loop must update every run: 4. (Phase 2) For small, self-contained failures: open worktree → implementer → verifier. 5. (Phase 3) Connectors update PRs/tickets; ambiguous items flagged for human. 6. Prune resolved/merged items from state. +7. Record post-run critique in state: false positives, repeated items, re-prioritized or dropped items, and one adjustment for next run. + +## Post-Run Critique + +After each Daily Triage run, record: + +- High-noise items. +- False positives (items incorrectly flagged). +- Items that should be deprioritized. +- Any human-review friction. +- One change to improve the next cycle. ## Verification Strategy @@ -90,6 +101,7 @@ Fields the loop must update every run: | State file grows unbounded | Prune merged/closed items every run | | Auto-fix on wrong priority | Start report-only; add explicit effort/risk gates | | Missed overnight failures | Add `fireImmediately: true` or run at start of day + mid-day | +| Stale critique / never reviewed | Add human handoff when critique entries accumulate without resolution across N runs. | ## Cost Profile diff --git a/starters/minimal-loop/STATE.md.example b/starters/minimal-loop/STATE.md.example index 1168b1e..d61417d 100644 --- a/starters/minimal-loop/STATE.md.example +++ b/starters/minimal-loop/STATE.md.example @@ -8,5 +8,12 @@ Last run: never ## Recent Noise (ignored this run) +## Post-Run Critique (from last run) +- High-noise: dependabot PRs surfaced again — add to ignore list +- False positives: 1 CI flake (known flaky test) +- Deprioritize: lint warnings moved to Watch List +- Friction: triage missed nightly deploy failure (was infra, not code) +- Adjustment: include infra check status in scan + --- Run log: — \ No newline at end of file