Skip to content
Open
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
11 changes: 9 additions & 2 deletions docs/safety.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ Always require human for:
- Do not disable tests to make CI green
- Do not increase timeouts blindly without root-cause note
- Quarantine flakes via explicit ticket + human approval

## Incident Response

If a loop merges bad code:
Expand All @@ -93,4 +92,12 @@ Before L3 (unattended):
- [ ] Human gates documented in pattern
- [ ] Kill switch documented ([operating-loops.md](./operating-loops.md))

See also [loop-design-checklist.md](./loop-design-checklist.md).
See also [loop-design-checklist.md](./loop-design-checklist.md).

## Machine-Readable Constraints

For runtime enforcement, define constraints in `loop-constraints.md` at the project root.
The `loop-constraints` skill reads this file at the start of every loop run and enforces
every rule. Template: [templates/loop-constraints.md](../templates/loop-constraints.md).

Tool examples: [Grok](../examples/grok/constraints.md) · [Claude Code](../examples/claude-code/constraints.md) · [Codex](../examples/codex/constraints.md)
30 changes: 30 additions & 0 deletions examples/claude-code/constraints.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Constraints — Claude Code

Add rules your loop must never break. The `loop-constraints` skill reads `loop-constraints.md` at the start of every run.

## Quick start

```bash
# Add a constraint (appends to loop-constraints.md)
/constraints Don't push before telling me. Never edit auth/. Always run tests first.
```

## Before every loop run

```bash
# The loop-constraints skill runs first — it reads loop-constraints.md and bakes
# every rule into the agent's context BEFORE triage or any action skill runs.
/loop 1d Run $loop-constraints, then $loop-triage. Update STATE.md. No auto-fix in week one.
```

## How it works

1. `/constraints <rule>` appends your rule to `loop-constraints.md`
2. The `loop-constraints` skill (from `templates/SKILL.md.loop-constraints`) must be installed in `.claude/skills/loop-constraints/SKILL.md`
3. Every loop run starts with `$loop-constraints` — it reads the file, loads rules, enforces them

## Scaffold automatically

```bash
npx @cobusgreyling/loop-init . --pattern daily-triage --tool claude
```
29 changes: 29 additions & 0 deletions examples/codex/constraints.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Constraints — Codex

Add rules your loop must never break. The `loop-constraints` skill reads `loop-constraints.md` at the start of every run.

## Quick start

Add to your Automation prompt:

```text
Before any triage or fix: run $loop-constraints. Read loop-constraints.md and enforce every rule.
```

## Adding constraints

Edit `loop-constraints.md` directly:

```
loop-constraints.md rules:
- Don't push before telling me
- Never edit auth/
- Always run tests first
- Max 3 fix attempts per item
```

## Scaffold automatically

```bash
npx @cobusgreyling/loop-init . --pattern daily-triage --tool codex
```
32 changes: 32 additions & 0 deletions examples/grok/constraints.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Constraints — Grok Build TUI

Add rules your loop must never break. The `loop-constraints` skill reads `loop-constraints.md` at the start of every run.

## Quick start

```bash
# Add a constraint (appends to loop-constraints.md)
/constraints Don't push before telling me. Never edit auth/. Always run tests first.
```

The agent appends your rules to `loop-constraints.md`.

## Before every loop run

```bash
# The loop-constraints skill runs first — it reads loop-constraints.md and bakes
# every rule into the agent's context BEFORE triage or any action skill runs.
/loop 1d Run loop-constraints, then loop-triage. Update STATE.md. No auto-fix in week one.
```

## How it works

1. `/constraints <rule>` appends your rule to `loop-constraints.md`
2. The `loop-constraints` skill (from `templates/SKILL.md.loop-constraints`) must be installed in `.grok/skills/loop-constraints/SKILL.md`
3. Every loop run starts with `loop-constraints` — it reads the file, loads rules, enforces them

## Scaffold automatically

```bash
npx @cobusgreyling/loop-init . --pattern daily-triage --tool grok
```
48 changes: 48 additions & 0 deletions templates/SKILL.md.loop-constraints
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
name: loop-constraints
description: >
Read loop-constraints.md at the start of every run and enforce every rule.
This skill runs BEFORE triage or any action skill. Constraints are binding.
user_invocable: true
---

# Loop Constraints Enforcer

You are the guardrail. Before any other work begins, you MUST:

1. Read `loop-constraints.md` from the project root.
2. Load every rule into your working memory.
3. Check if `loop-pause-all` is active → exit immediately.
4. Apply these rules to EVERY action that follows.

## How to enforce

- Before pushing: re-read the Push & Merge section. If ANY rule blocks it, stop and tell the human.
- Before editing a file: re-read the Paths section. If the path matches a denylist pattern, escalate.
- Before proposing a fix: re-read the Code section. Run tests. One fix per run.
- Before merging: re-read the Push & Merge section. Human must approve.

## Output at start of run

Always begin with a one-line confirmation:

```
Constraints loaded from loop-constraints.md: N rules active.
```

If no `loop-constraints.md` exists, say so and proceed with default safety rules from `docs/safety.md`.

## Interaction with other skills

- `loop-triage` — constraints may override triage priority (e.g. "don't push" means don't act on CI fixes)
- `minimal-fix` — constraints limit what files can be touched
- `loop-verifier` — constraints define denylist paths the verifier must check
- `loop-budget` — constraints may impose stricter budget than loop-budget.md

## Default constraints (when no file exists)

If `loop-constraints.md` is absent, enforce these minimums:
- Never edit `.env`, `.env.*`, `auth/`, `payments/`, `secrets/`, `credentials/`
- Never auto-merge to main
- Never disable tests
- Escalate after 3 failed fix attempts
31 changes: 31 additions & 0 deletions templates/loop-constraints.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# 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

## 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. -->
4 changes: 4 additions & 0 deletions tools/loop-audit/dist/auditor.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ export interface LoopSignals {
loopMdBudget: boolean;
budgetSkill: boolean;
};
constraints: {
present: boolean;
hasConstraintsSkill: boolean;
};
loopActivity: {
present: boolean;
evidence: string[];
Expand Down
33 changes: 33 additions & 0 deletions tools/loop-audit/dist/auditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ const SCORE_WEIGHTS = {
runLog: 3,
loopMdBudget: 2,
budgetSkill: 2,
constraintsFile: 4,
constraintsSkill: 2,
loopActivity: 6,
};
const LEVEL_THRESHOLDS = {
Expand All @@ -48,6 +50,7 @@ const LOOP_SKILL_NAMES = [
'dependency-triage',
'rebase-and-clean',
'changelog-scan',
'loop-constraints',
'draft-release-notes',
'issue-triage',
];
Expand Down Expand Up @@ -209,6 +212,10 @@ export function computeScore(signals) {
score += w.loopMdBudget;
if (signals.cost.budgetSkill)
score += w.budgetSkill;
if (signals.constraints.present)
score += w.constraintsFile;
if (signals.constraints.hasConstraintsSkill)
score += w.constraintsSkill;
if (signals.loopActivity.present)
score += w.loopActivity;
score = Math.min(100, Math.max(0, score));
Expand Down Expand Up @@ -322,6 +329,20 @@ export async function auditProject(target) {
}
}
const loopActivity = await detectLoopActivity(root);
const constraintsFile = await fileExists(path.join(root, 'loop-constraints.md'));
const constraintsSkillDirs = [
path.join(root, 'skills', 'loop-constraints'),
path.join(root, '.grok', 'skills', 'loop-constraints'),
path.join(root, '.claude', 'skills', 'loop-constraints'),
path.join(root, '.codex', 'skills', 'loop-constraints'),
];
let constraintsSkill = false;
for (const dir of constraintsSkillDirs) {
if (await fileExists(path.join(dir, 'SKILL.md'))) {
constraintsSkill = true;
break;
}
}
const signals = {
stateFile: { present: statePaths.length > 0, paths: statePaths },
loopConfig: { present: loopMd, path: loopMd ? 'LOOP.md' : undefined },
Expand All @@ -334,6 +355,7 @@ export async function auditProject(target) {
starters: { used: loopSkills.includes('loop-triage') },
github: { present: githubDir, workflows: hasWorkflows },
mcp: { present: mcpPresent },
constraints: { present: constraintsFile, hasConstraintsSkill: constraintsSkill },
worktreeEvidence: { present: worktreeEvidence },
registry: { present: registryPresent },
cost: { budgetDoc, runLog, loopMdBudget, budgetSkill },
Expand Down Expand Up @@ -379,6 +401,17 @@ export async function auditProject(target) {
else {
findings.push({ level: 'ok', message: 'Safety documentation present.' });
}
if (!signals.constraints.present) {
findings.push({ level: 'warn', message: 'No loop-constraints.md — structured constraints file missing.' });
recommendations.push('Create loop-constraints.md with denylist paths, push/merge rules, and human gates (see templates/loop-constraints.md)');
}
else {
findings.push({ level: 'ok', message: 'loop-constraints.md present.' });
}
if (signals.constraints.present && !signals.constraints.hasConstraintsSkill) {
findings.push({ level: 'warn', message: 'loop-constraints.md exists but no loop-constraints skill — rules not enforced at runtime.' });
recommendations.push('Add loop-constraints skill via loop-init or templates/SKILL.md.loop-constraints');
}
if (!signals.github.present) {
findings.push({ level: 'warn', message: 'No .github/ directory (templates, workflows for dogfooding).' });
recommendations.push('Add .github/ISSUE_TEMPLATE, PULL_REQUEST_TEMPLATE, and workflows (see this repo for examples)');
Expand Down
35 changes: 34 additions & 1 deletion tools/loop-audit/src/auditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface LoopSignals {
loopMdBudget: boolean;
budgetSkill: boolean;
};
constraints: { present: boolean; hasConstraintsSkill: boolean };
loopActivity: { present: boolean; evidence: string[] };
}

Expand Down Expand Up @@ -71,6 +72,8 @@ const SCORE_WEIGHTS = {
runLog: 3,
loopMdBudget: 2,
budgetSkill: 2,
constraintsFile: 4,
constraintsSkill: 2,
loopActivity: 6,
} as const;

Expand All @@ -90,6 +93,7 @@ const LOOP_SKILL_NAMES = [
'dependency-triage',
'rebase-and-clean',
'changelog-scan',
'loop-constraints',
'draft-release-notes',
'issue-triage',
];
Expand Down Expand Up @@ -235,6 +239,8 @@ export function computeScore(signals: LoopSignals): { score: number; level: 'L0'
if (signals.cost.runLog) score += w.runLog;
if (signals.cost.loopMdBudget) score += w.loopMdBudget;
if (signals.cost.budgetSkill) score += w.budgetSkill;
if (signals.constraints.present) score += w.constraintsFile;
if (signals.constraints.hasConstraintsSkill) score += w.constraintsSkill;
if (signals.loopActivity.present) score += w.loopActivity;

score = Math.min(100, Math.max(0, score));
Expand Down Expand Up @@ -281,7 +287,6 @@ export async function auditProject(target: string): Promise<AuditResult> {
const loopMd = await fileExists(path.join(root, 'LOOP.md'));
const agentsMd = await fileExists(path.join(root, 'AGENTS.md')) ||
await fileExists(path.join(root, 'CLAUDE.md'));

const skillNames = await findSkills(root);
const loopSkills = skillNames.filter((s) => LOOP_SKILL_NAMES.includes(s));

Expand Down Expand Up @@ -357,6 +362,21 @@ export async function auditProject(target: string): Promise<AuditResult> {

const loopActivity = await detectLoopActivity(root);

const constraintsFile = await fileExists(path.join(root, 'loop-constraints.md'));
const constraintsSkillDirs = [
path.join(root, 'skills', 'loop-constraints'),
path.join(root, '.grok', 'skills', 'loop-constraints'),
path.join(root, '.claude', 'skills', 'loop-constraints'),
path.join(root, '.codex', 'skills', 'loop-constraints'),
];
let constraintsSkill = false;
for (const dir of constraintsSkillDirs) {
if (await fileExists(path.join(dir, 'SKILL.md'))) {
constraintsSkill = true;
break;
}
}

const signals: LoopSignals = {
stateFile: { present: statePaths.length > 0, paths: statePaths },
loopConfig: { present: loopMd, path: loopMd ? 'LOOP.md' : undefined },
Expand All @@ -369,6 +389,7 @@ export async function auditProject(target: string): Promise<AuditResult> {
starters: { used: loopSkills.includes('loop-triage') },
github: { present: githubDir, workflows: hasWorkflows },
mcp: { present: mcpPresent },
constraints: { present: constraintsFile, hasConstraintsSkill: constraintsSkill },
worktreeEvidence: { present: worktreeEvidence },
registry: { present: registryPresent },
cost: { budgetDoc, runLog, loopMdBudget, budgetSkill },
Expand Down Expand Up @@ -418,6 +439,18 @@ export async function auditProject(target: string): Promise<AuditResult> {
findings.push({ level: 'ok', message: 'Safety documentation present.' });
}


if (!signals.constraints.present) {
findings.push({ level: 'warn', message: 'No loop-constraints.md — structured constraints file missing.' });
recommendations.push('Create loop-constraints.md with denylist paths, push/merge rules, and human gates (see templates/loop-constraints.md)');
} else {
findings.push({ level: 'ok', message: 'loop-constraints.md present.' });
}

if (signals.constraints.present && !signals.constraints.hasConstraintsSkill) {
findings.push({ level: 'warn', message: 'loop-constraints.md exists but no loop-constraints skill — rules not enforced at runtime.' });
recommendations.push('Add loop-constraints skill via loop-init or templates/SKILL.md.loop-constraints');
}
if (!signals.github.present) {
findings.push({ level: 'warn', message: 'No .github/ directory (templates, workflows for dogfooding).' });
recommendations.push('Add .github/ISSUE_TEMPLATE, PULL_REQUEST_TEMPLATE, and workflows (see this repo for examples)');
Expand Down
1 change: 1 addition & 0 deletions tools/loop-audit/test/auditor.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ function emptySignals() {
mcp: { present: false },
worktreeEvidence: { present: false },
registry: { present: false },
constraints: { present: false, hasConstraintsSkill: false },
cost: { budgetDoc: false, runLog: false, loopMdBudget: false, budgetSkill: false },
loopActivity: { present: false, evidence: [] },
};
Expand Down
Loading