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
209 changes: 209 additions & 0 deletions tools/loop-audit/src/autofixer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
import { writeFile, mkdir } from 'node:fs/promises';
import path from 'node:path';
import { fileExists } from '@cobusgreyling/readiness-core';
import { AuditResult } from './auditor.js';

const STATE_MD_TEMPLATE = `# Loop State — YOUR_PROJECT

Last run: (set by loop on each run)

## High Priority (loop is acting or waiting on human)

<!-- Format:
- [ ] ID — one-line description
Loop action: what the loop did last
Human decision: (if any)
-->

## Watch List

<!-- Items to monitor but not act on yet -->

## Recent Noise (ignored this run)

<!-- Brief list — helps tune triage skill -->

---
Run log: (timestamp) | findings | actions | escalations
`;

const LOOP_BUDGET_TEMPLATE = `# Loop Budget — YOUR_PROJECT

## Daily limits

| Loop | Max runs/day | Max tokens/day | Max sub-agent spawns/run |
|------|--------------|----------------|--------------------------|
| Daily Triage | 2 | 100k | 0 (L1) / 2 (L2) |
| PR Babysitter | 288 | 2M | 3 |
| CI Sweeper | 96 | 1M | 3 |
| Dependency Sweeper | 4 | 500k | 3 |
| Post-Merge Cleanup | 1 | 200k | 2 |

## On budget exceed

1. Pause all schedulers (\`scheduler_delete\` or disable automations)
2. Append event to \`loop-run-log.md\`
3. Notify human (Slack / issue / STATE.md High Priority)

## Kill switch

- Command or issue label: \`loop-pause-all\`
- Resume only after human clears the flag in STATE.md
`;

const LOOP_RUN_LOG_TEMPLATE = `# Loop Run Log — YOUR_PROJECT

Append one entry per run. Prune entries older than 30 days.

## Format

\`\`\`json
{
"run_id": "2026-06-09T08:15:00Z",
"pattern": "daily-triage",
"duration_s": 45,
"items_found": 4,
"actions_taken": 1,
"escalations": 0,
"tokens_estimate": 52000,
"outcome": "report-only | fix-proposed | escalated | no-op"
}
\`\`\`

## Recent Runs

<!-- Loop appends below this line -->
`;

const LOOP_CONSTRAINTS_TEMPLATE = `# Loop Constraints

> Add rules below with \`/constraints <rule>\` in your agent.
> The \`loop-constraints\` skill reads this file at the start of every run.
> Constraints here are **binding** — the agent MUST follow them.

## Push & Merge
- Don't push before telling me
- Never auto-merge to main without human approval
- Always create a draft PR first; let me review before marking ready

## Paths
- Never edit .env, .env.*, auth/, payments/, secrets/, credentials/
- Never edit infrastructure configs without human approval

## Code
- Always run tests before proposing a fix
- Never disable tests to make CI green
- Never refactor unrelated code — one fix per run
- Max 3 fix attempts per item; escalate after
- Enforce the attempt limit mechanically: log each try to \`loop-ledger.json\` and run \`loop-context --check\` before retrying (see the \`loop-guard\` skill)

## Communication
- Always tell me what you're about to do before doing it
- Never close an issue or PR without my approval

## Budget
- If token spend hits 80% of daily cap, switch to report-only
- If loop-pause-all is active, exit immediately

---
<!-- Add your own rules below. Use plain English. The loop reads this verbatim. -->
`;

const LOOP_MD_TEMPLATE = `# Loop Configuration — Minimal Triage

## Active Loops

| Pattern | Cadence | Status | Command |
|---------|---------|--------|---------|
| Daily Triage | 1d | L1 report-only | See README |

## Human Gates

- No auto-fix until L2 checklist complete
- All high-risk paths: human review required

## Budget

- Max sub-agent spawns per run: 0 (L1) / 2 (L2)
- Max tokens/day: 100k (see \`loop-budget.md\`)
- Append each run to \`loop-run-log.md\`; use \`loop-budget\` skill at start/end
- Kill switch: \`loop-pause-all\` — pause schedulers and notify human
- Estimate: \`npx @cobusgreyling/loop-cost --pattern daily-triage\`
`;

const SAFETY_MD_TEMPLATE = `# Loop Safety Policy

## Auto-merge policy
- Never auto-merge to main. All code changes require human review.

## Path denylist
- .github/workflows/
- secrets/
- credentials/

## MCP Scopes
- Read-only unless explicitly allowed.
`;

const AGENTS_MD_TEMPLATE = `# AGENTS.md — Project Conventions

## Build & Test
- Run \`npm test\` to verify changes.

## Review Norms
- Never auto-merge changes.
- Ensure all loops isolate their work in worktrees.
`;

export async function autoFixProject(target: string, result: AuditResult): Promise<void> {
const root = path.resolve(target);
let fixesApplied = 0;

console.log('\n=== Auto-Fixing Repository ===\n');

const safeWriteFile = async (relPath: string, content: string, label: string) => {
const fullPath = path.join(root, relPath);
if (!(await fileExists(fullPath))) {
await mkdir(path.dirname(fullPath), { recursive: true });
await writeFile(fullPath, content, 'utf8');
console.log(`✅ Generated ${relPath} (${label})`);
fixesApplied++;
} else {
console.log(`⏭️ Skipped ${relPath} (already exists)`);
}
};

if (!result.signals.stateFile.present) {
await safeWriteFile('STATE.md', STATE_MD_TEMPLATE, 'Base state tracking');
}

if (!result.signals.loopConfig.present) {
await safeWriteFile('LOOP.md', LOOP_MD_TEMPLATE, 'Loop configuration');
}

if (!result.signals.cost.budgetDoc) {
await safeWriteFile('loop-budget.md', LOOP_BUDGET_TEMPLATE, 'Cost caps');
}

if (!result.signals.cost.runLog) {
await safeWriteFile('loop-run-log.md', LOOP_RUN_LOG_TEMPLATE, 'Run history');
}

if (!result.signals.constraints.present) {
await safeWriteFile('loop-constraints.md', LOOP_CONSTRAINTS_TEMPLATE, 'Agent rules');
}

if (!result.signals.safety.safetyDocPresent) {
await safeWriteFile('docs/safety.md', SAFETY_MD_TEMPLATE, 'Safety policy');
}

if (!result.signals.agentsMd.present) {
await safeWriteFile('AGENTS.md', AGENTS_MD_TEMPLATE, 'Project conventions');
}

if (fixesApplied > 0) {
console.log(`\n✨ Applied ${fixesApplied} automatic fixes. Run loop-audit again to see your new score!`);
} else {
console.log('\nNo safe automatic fixes could be applied. (Check the suggestions manually).');
}
}
7 changes: 6 additions & 1 deletion tools/loop-audit/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@
import { auditProject } from './auditor.js';
import { printContributorCta } from './contributor-cta.js';
import { formatBadge, formatHuman, formatJson, formatMarkdown } from './reporter.js';
import { autoFixProject } from './autofixer.js';

const args = process.argv.slice(2);
const target = args.find((a) => !a.startsWith('-')) || '.';
const json = args.includes('--json');
const md = args.includes('--md');
const suggest = args.includes('--suggest') || args.includes('--fix');
const autoFix = args.includes('--auto-fix');
const badge = args.includes('--badge');
const help = args.includes('--help') || args.includes('-h');

Expand All @@ -21,6 +23,7 @@ Options:
--json JSON output (for CI / scripting)
--md Markdown report
--suggest Show copy-from-template commands for missing pieces (recommended on first runs)
--auto-fix Auto-heal missing repository structure (STATE, LOOP, budgets, etc.)
--badge Markdown README badge (Loop Ready level + score)
--help, -h This help

Expand Down Expand Up @@ -58,7 +61,9 @@ try {
else if (md) console.log(formatMarkdown(result));
else console.log(formatHuman(result));

if (suggest) {
if (autoFix) {
await autoFixProject(target, result);
} else if (suggest) {
console.log('\n=== Suggested actions (copy & customize) ===');
console.log('From the root of this repo (or after cloning the reference):');
console.log('');
Expand Down