From 1a1e1b34d7bb2cd24f7358333da3909182f93614 Mon Sep 17 00:00:00 2001 From: neerajdad123-byte Date: Fri, 26 Jun 2026 04:42:04 +0530 Subject: [PATCH 1/3] feat(constraints): add loop-constraints file + enforcement skill - New templates/loop-constraints.md: single source of truth for all loop guardrails (denylist paths, push/merge rules, human gates, budget caps) - New templates/SKILL.md.loop-constraints: skill that reads constraints file at start of every run and enforces every rule - loop-audit: scores constraints file (+4 pts) and skill (+2 pts); detects both in auditProject; new finding for missing constraints - loop-init: scaffoldConstraints() generates loop-constraints.md + loop-constraints skill in target project - docs/safety.md: cross-reference to new constraints file Closes: the gap where constraints were only soft prompts scattered across safety.md, LOOP.md, and skill files. Now a single structured file + runtime enforcement skill. Tests: loop-audit 11/11, loop-cost 7/7, loop-init 6/6 --- docs/safety.md | 11 ++++-- templates/SKILL.md.loop-constraints | 48 ++++++++++++++++++++++++++ templates/loop-constraints.md | 31 +++++++++++++++++ tools/loop-audit/dist/auditor.d.ts | 4 +++ tools/loop-audit/dist/auditor.js | 34 +++++++++++++++++- tools/loop-audit/src/auditor.ts | 36 +++++++++++++++++-- tools/loop-audit/test/auditor.test.mjs | 1 + tools/loop-init/dist/cli.js | 9 +++++ tools/loop-init/src/cli.ts | 17 +++++++++ 9 files changed, 186 insertions(+), 5 deletions(-) create mode 100644 templates/SKILL.md.loop-constraints create mode 100644 templates/loop-constraints.md diff --git a/docs/safety.md b/docs/safety.md index a5563b7..ea1b3dc 100644 --- a/docs/safety.md +++ b/docs/safety.md @@ -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: @@ -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). \ No newline at end of file +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) \ No newline at end of file diff --git a/templates/SKILL.md.loop-constraints b/templates/SKILL.md.loop-constraints new file mode 100644 index 0000000..2f921cb --- /dev/null +++ b/templates/SKILL.md.loop-constraints @@ -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 diff --git a/templates/loop-constraints.md b/templates/loop-constraints.md new file mode 100644 index 0000000..b08f058 --- /dev/null +++ b/templates/loop-constraints.md @@ -0,0 +1,31 @@ +# Loop Constraints + +> Add rules below with `/constraints ` 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 + +--- + diff --git a/tools/loop-audit/dist/auditor.d.ts b/tools/loop-audit/dist/auditor.d.ts index a5ebb7d..2d81924 100644 --- a/tools/loop-audit/dist/auditor.d.ts +++ b/tools/loop-audit/dist/auditor.d.ts @@ -49,6 +49,10 @@ export interface LoopSignals { loopMdBudget: boolean; budgetSkill: boolean; }; + constraints: { + present: boolean; + hasConstraintsSkill: boolean; + }; loopActivity: { present: boolean; evidence: string[]; diff --git a/tools/loop-audit/dist/auditor.js b/tools/loop-audit/dist/auditor.js index 5a4ce00..7f17780 100644 --- a/tools/loop-audit/dist/auditor.js +++ b/tools/loop-audit/dist/auditor.js @@ -31,6 +31,8 @@ const SCORE_WEIGHTS = { runLog: 3, loopMdBudget: 2, budgetSkill: 2, + constraintsFile: 4, + constraintsSkill: 2, loopActivity: 6, }; const LEVEL_THRESHOLDS = { @@ -48,7 +50,7 @@ const LOOP_SKILL_NAMES = [ 'dependency-triage', 'rebase-and-clean', 'changelog-scan', - 'draft-release-notes', + 'loop-constraints', 'issue-triage', ]; const SAFETY_FILES = ['safety.md', 'docs/safety.md', 'SECURITY.md']; @@ -209,6 +211,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)); @@ -322,6 +328,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 }, @@ -334,6 +354,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 }, @@ -379,6 +400,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)'); diff --git a/tools/loop-audit/src/auditor.ts b/tools/loop-audit/src/auditor.ts index e908bb0..ffc1eb0 100644 --- a/tools/loop-audit/src/auditor.ts +++ b/tools/loop-audit/src/auditor.ts @@ -22,6 +22,7 @@ export interface LoopSignals { loopMdBudget: boolean; budgetSkill: boolean; }; + constraints: { present: boolean; hasConstraintsSkill: boolean }; loopActivity: { present: boolean; evidence: string[] }; } @@ -71,6 +72,8 @@ const SCORE_WEIGHTS = { runLog: 3, loopMdBudget: 2, budgetSkill: 2, + constraintsFile: 4, + constraintsSkill: 2, loopActivity: 6, } as const; @@ -90,7 +93,7 @@ const LOOP_SKILL_NAMES = [ 'dependency-triage', 'rebase-and-clean', 'changelog-scan', - 'draft-release-notes', + 'loop-constraints', 'issue-triage', ]; @@ -235,6 +238,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)); @@ -281,7 +286,6 @@ export async function auditProject(target: string): Promise { 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)); @@ -357,6 +361,21 @@ export async function auditProject(target: string): Promise { 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 }, @@ -369,6 +388,7 @@ export async function auditProject(target: string): Promise { 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 }, @@ -418,6 +438,18 @@ export async function auditProject(target: string): Promise { 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)'); diff --git a/tools/loop-audit/test/auditor.test.mjs b/tools/loop-audit/test/auditor.test.mjs index 023050b..bfed8e7 100644 --- a/tools/loop-audit/test/auditor.test.mjs +++ b/tools/loop-audit/test/auditor.test.mjs @@ -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: [] }, }; diff --git a/tools/loop-init/dist/cli.js b/tools/loop-init/dist/cli.js index a20b019..08abe35 100644 --- a/tools/loop-init/dist/cli.js +++ b/tools/loop-init/dist/cli.js @@ -194,6 +194,14 @@ async function scaffoldObservability(pattern, tool, targetDir, templatesRoot, dr } await copyTemplateSkill(templatesRoot, 'SKILL.md.loop-budget', targetDir, tool, 'loop-budget', dryRun); } +async function scaffoldConstraints(targetDir, templatesRoot, tool, dryRun) { + const constraintsPath = path.join(targetDir, 'loop-constraints.md'); + const constraintsTemplate = path.join(templatesRoot, 'loop-constraints.md'); + if (!(await exists(constraintsPath)) && (await exists(constraintsTemplate))) { + await copyFile(constraintsTemplate, constraintsPath, dryRun); + } + await copyTemplateSkill(templatesRoot, 'SKILL.md.loop-constraints', targetDir, tool, 'loop-constraints', dryRun); +} async function copyFile(src, dest, dryRun) { if (!(await exists(src))) return false; @@ -352,6 +360,7 @@ Examples: } await copyL2Templates(pattern, tool, targetDir, templatesRoot, dryRun); await scaffoldObservability(pattern, tool, targetDir, templatesRoot, dryRun); + await scaffoldConstraints(targetDir, templatesRoot, tool, dryRun); if (!dryRun && !(await exists(path.join(targetDir, 'AGENTS.md')))) { const agentsTemplate = `# AGENTS.md diff --git a/tools/loop-init/src/cli.ts b/tools/loop-init/src/cli.ts index 31f6ec3..301147c 100644 --- a/tools/loop-init/src/cli.ts +++ b/tools/loop-init/src/cli.ts @@ -245,6 +245,22 @@ async function scaffoldObservability( await copyTemplateSkill(templatesRoot, 'SKILL.md.loop-budget', targetDir, tool, 'loop-budget', dryRun); } +async function scaffoldConstraints( + targetDir: string, + templatesRoot: string, + tool: Tool, + dryRun: boolean, +) { + const constraintsPath = path.join(targetDir, 'loop-constraints.md'); + const constraintsTemplate = path.join(templatesRoot, 'loop-constraints.md'); + + if (!(await exists(constraintsPath)) && (await exists(constraintsTemplate))) { + await copyFile(constraintsTemplate, constraintsPath, dryRun); + } + + await copyTemplateSkill(templatesRoot, 'SKILL.md.loop-constraints', targetDir, tool, 'loop-constraints', dryRun); +} + async function copyFile(src: string, dest: string, dryRun: boolean) { if (!(await exists(src))) return false; if (dryRun) { @@ -420,6 +436,7 @@ Examples: await copyL2Templates(pattern, tool, targetDir, templatesRoot, dryRun); await scaffoldObservability(pattern, tool, targetDir, templatesRoot, dryRun); + await scaffoldConstraints(targetDir, templatesRoot, tool, dryRun); if (!dryRun && !(await exists(path.join(targetDir, 'AGENTS.md')))) { const agentsTemplate = `# AGENTS.md From bb77bd3a2cf2a77508ad2a37796c0bf68c7b36cf Mon Sep 17 00:00:00 2001 From: neerajdad123-byte Date: Fri, 26 Jun 2026 04:46:02 +0530 Subject: [PATCH 2/3] fix(constraints): add tool-specific examples for constraints feature --- examples/claude-code/constraints.md | 30 +++++++++++++++++++++++++++ examples/codex/constraints.md | 29 ++++++++++++++++++++++++++ examples/grok/constraints.md | 32 +++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+) create mode 100644 examples/claude-code/constraints.md create mode 100644 examples/codex/constraints.md create mode 100644 examples/grok/constraints.md diff --git a/examples/claude-code/constraints.md b/examples/claude-code/constraints.md new file mode 100644 index 0000000..683e620 --- /dev/null +++ b/examples/claude-code/constraints.md @@ -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 ` 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 +``` diff --git a/examples/codex/constraints.md b/examples/codex/constraints.md new file mode 100644 index 0000000..163cb3f --- /dev/null +++ b/examples/codex/constraints.md @@ -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 +``` diff --git a/examples/grok/constraints.md b/examples/grok/constraints.md new file mode 100644 index 0000000..34026da --- /dev/null +++ b/examples/grok/constraints.md @@ -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 ` 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 +``` From 12673c7085c1065cb0df37aef31494f4cdbbd92c Mon Sep 17 00:00:00 2001 From: neerajdad123-byte Date: Fri, 26 Jun 2026 07:57:51 +0530 Subject: [PATCH 3/3] fix(auditor): restore draft-release-notes in LOOP_SKILL_NAMES alongside loop-constraints --- tools/loop-audit/dist/auditor.js | 1 + tools/loop-audit/src/auditor.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/tools/loop-audit/dist/auditor.js b/tools/loop-audit/dist/auditor.js index 7f17780..fcf7082 100644 --- a/tools/loop-audit/dist/auditor.js +++ b/tools/loop-audit/dist/auditor.js @@ -51,6 +51,7 @@ const LOOP_SKILL_NAMES = [ 'rebase-and-clean', 'changelog-scan', 'loop-constraints', + 'draft-release-notes', 'issue-triage', ]; const SAFETY_FILES = ['safety.md', 'docs/safety.md', 'SECURITY.md']; diff --git a/tools/loop-audit/src/auditor.ts b/tools/loop-audit/src/auditor.ts index ffc1eb0..9c7b795 100644 --- a/tools/loop-audit/src/auditor.ts +++ b/tools/loop-audit/src/auditor.ts @@ -94,6 +94,7 @@ const LOOP_SKILL_NAMES = [ 'rebase-and-clean', 'changelog-scan', 'loop-constraints', + 'draft-release-notes', 'issue-triage', ];