Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions patterns/changelog-drafter.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
12 changes: 12 additions & 0 deletions patterns/daily-triage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
7 changes: 7 additions & 0 deletions starters/minimal-loop/STATE.md.example
Original file line number Diff line number Diff line change
Expand Up @@ -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: —
83 changes: 67 additions & 16 deletions tools/loop-init/scripts/bundle-assets.mjs
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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');
}
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();
}
12 changes: 11 additions & 1 deletion tools/loop-init/test/cli.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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/);
Expand Down Expand Up @@ -75,4 +85,4 @@ test('loop-init scaffolds ci-sweeper with bundled assets', async () => {
} finally {
await rm(dir, { recursive: true, force: true });
}
});
});