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
44 changes: 1 addition & 43 deletions .github/workflows/audit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,49 +25,7 @@ jobs:
cache-dependency-path: tools/loop-audit/package-lock.json

- name: Build, test & run loop-audit on the reference + starters
run: |
cd tools/loop-audit
npm ci
npm test
echo "=== Audit of repo root ==="
node dist/cli.js ../../ --json > /tmp/root-audit.json
echo ""
echo "=== Audit of starters (L1 gate) ==="
FAILED=0
for s in ../../starters/*/; do
NAME=$(basename "$s")
node dist/cli.js "$s" --json > "/tmp/starter-${NAME}.json"
node -e "
const data = JSON.parse(require('fs').readFileSync('/tmp/starter-${NAME}.json', 'utf8'));
console.log('--- ${NAME}: score=' + data.score + ' level=' + data.level);
if (data.score < 38) {
console.error('Starter ${NAME} below L1 threshold (38): ' + data.score);
process.exit(1);
}
" || FAILED=1
done
if [ "$FAILED" -ne 0 ]; then
echo "One or more starters failed L1 gate"
exit 1
fi
cp /tmp/root-audit.json /tmp/audit.json

- name: Report score and gate on critically low (reference is source, not a consumer project)
run: |
node -e '
const fs = require("fs");
try {
const data = JSON.parse(fs.readFileSync("/tmp/audit.json", "utf8"));
console.log("Reference score: " + data.score);
if (data.score < 58) {
console.error("Reference score below L2 threshold (58). Restore dogfood signals: STATE.md, skills/, AGENTS.md.");
process.exit(2);
}
} catch (e) {
console.error("Could not parse audit JSON:", e.message);
process.exit(1);
}
'
run: bash scripts/ci-audit-gates.sh

- name: Comment PR with loop readiness score
if: github.event_name == 'pull_request'
Expand Down
98 changes: 89 additions & 9 deletions .github/workflows/daily-triage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,18 @@ permissions:
contents: write
pull-requests: write
issues: write
statuses: write

jobs:
triage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

- name: Record run start
id: timing
run: echo "started_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"

- uses: actions/setup-node@v6
with:
node-version: '22'
Expand Down Expand Up @@ -82,23 +87,70 @@ jobs:
Run log: Updated by \`.github/workflows/daily-triage.yml\`. See \`LOOP.md\` for cadence and gates.
EOF

- name: Open PR for STATE.md if changed
- name: Append loop-run-log.md
id: runlog
run: |
START="${{ steps.timing.outputs.started_at }}"
END=$(date -u +%Y-%m-%dT%H:%M:%SZ)
DURATION=$(node -e "
const s = new Date('${START}').getTime();
const e = new Date('${END}').getTime();
console.log(Math.max(1, Math.round((e - s) / 1000)));
")
FAILING="${{ steps.workflows.outputs.failing }}"
SCORE="${{ steps.audit.outputs.score }}"
if [ "$FAILING" -gt 0 ]; then
OUTCOME="escalated"
else
OUTCOME="report-only"
fi
ENTRY=$(node -e "
console.log(JSON.stringify({
run_id: '${END}',
pattern: 'daily-triage',
duration_s: Number('${DURATION}'),
items_found: Number('${FAILING}') + 1,
actions_taken: 1,
escalations: Number('${FAILING}'),
tokens_estimate: 52000,
readiness_score: Number('${SCORE}'),
outcome: '${OUTCOME}',
workflow_run: '${{ github.run_id }}'
}));
")
node scripts/append-run-log.mjs "$ENTRY"
echo "outcome=${OUTCOME}" >> "$GITHUB_OUTPUT"

- name: Run validate gates (for PR status)
id: validate_gates
run: bash scripts/ci-validate-gates.sh

- name: Run audit gates (for PR status)
id: audit_gates
run: bash scripts/ci-audit-gates.sh

- name: Open PR for STATE.md + loop-run-log if changed
id: pr
env:
GH_TOKEN: ${{ github.token }}
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

if git diff --quiet STATE.md; then
echo "No STATE.md changes — skip PR"
if git diff --quiet STATE.md loop-run-log.md; then
echo "No STATE.md or loop-run-log.md changes — skip PR"
echo "opened=false" >> "$GITHUB_OUTPUT"
exit 0
fi

BRANCH="automated/daily-triage-$(date -u +%Y-%m-%d)"
git checkout -b "$BRANCH"
git add STATE.md
git commit -m "chore(loop): daily triage update STATE.md [automated]"
git fetch origin "$BRANCH" 2>/dev/null && git checkout "$BRANCH" || git checkout -b "$BRANCH"
git add STATE.md loop-run-log.md
git commit -m "chore(loop): daily triage update STATE.md + run log [automated]"
git push -u origin "$BRANCH"
echo "head_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
echo "branch=${BRANCH}" >> "$GITHUB_OUTPUT"
echo "opened=true" >> "$GITHUB_OUTPUT"

EXISTING=$(gh pr list --head "${{ github.repository_owner }}:${BRANCH}" --base main --state open --json number -q '.[0].number' 2>/dev/null || true)
if [ -n "$EXISTING" ] && [ "$EXISTING" != "null" ]; then
Expand All @@ -112,12 +164,39 @@ jobs:
--body "Automated daily triage update from \`daily-triage.yml\`.

- Loop readiness: **${{ steps.audit.outputs.score }}** (${{ steps.audit.outputs.level }})
- Merges automatically when \`validate\` and \`audit\` pass."
- Run log appended (\`${{ steps.runlog.outputs.outcome }}\`)
- \`validate\` and \`audit\` statuses posted inline (GITHUB_TOKEN does not trigger PR workflows)."
PR_NUMBER=$(gh pr view "$BRANCH" --json number -q '.number')
fi

echo "number=${PR_NUMBER}" >> "$GITHUB_OUTPUT"
gh pr merge "$PR_NUMBER" --auto --squash --delete-branch

- name: Post required commit statuses for branch protection
if: steps.pr.outputs.opened == 'true'
uses: actions/github-script@v9
with:
script: |
const sha = '${{ steps.pr.outputs.head_sha }}';
if (!sha) {
core.setFailed('Missing head_sha for commit statuses');
return;
}
const checks = [
{ context: 'validate', description: 'Pattern/registry gates (daily-triage inline)' },
{ context: 'audit', description: 'Loop readiness gates (daily-triage inline)' },
];
for (const check of checks) {
await github.rest.repos.createCommitStatus({
owner: context.repo.owner,
repo: context.repo.repo,
sha,
state: 'success',
context: check.context,
description: check.description,
target_url: `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`,
});
}

- name: Open weekly loop report issue (Mondays)
if: github.event.schedule == '0 8 * * 1-5' && format('{0}', github.run_attempt) == '1'
run: |
Expand All @@ -135,7 +214,8 @@ jobs:
--body "## Automated daily triage summary

- Loop readiness: **${{ steps.audit.outputs.score }}** (${{ steps.audit.outputs.level }})
- See updated \`STATE.md\` on main
- Latest run outcome: \`${{ steps.runlog.outputs.outcome }}\` (see \`loop-run-log.md\`)
- See updated \`STATE.md\` on main after PR merge
- Review high-priority items and close or re-prioritize

_Generated by daily-triage workflow. Human reviews and decides actions._"
Expand Down
47 changes: 2 additions & 45 deletions .github/workflows/validate-patterns.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,48 +16,5 @@ jobs:
with:
node-version: '22'

- name: Check registry covers all pattern files
run: |
set -e
echo "Patterns declared in registry:"
grep '^\s*-\s*id:' patterns/registry.yaml | sed 's/.*id: //' | sort > /tmp/registry.txt
echo "Pattern markdown files:"
ls patterns/*.md | xargs -n1 basename | sed 's/.md$//' | grep -v README | sort > /tmp/files.txt
echo "=== Registry ==="; cat /tmp/registry.txt
echo "=== Files ==="; cat /tmp/files.txt
comm -23 /tmp/files.txt /tmp/registry.txt | grep . && (echo "ERROR: pattern md file(s) missing from registry.yaml"; exit 1) || echo "All pattern files registered ✓"
comm -23 /tmp/registry.txt /tmp/files.txt | grep . && (echo "ERROR: registry entry without matching .md"; exit 1) || echo "No orphan registry entries ✓"

- name: Verify CONTRIBUTING + template compliance (basic)
run: |
set -e
echo "Checking that key sections exist in patterns (lightweight)"
for f in patterns/*.md; do
if [[ "$f" == *"README.md" ]]; then continue; fi
grep -q "^## Scheduling" "$f" || { echo "Missing Scheduling in $f"; exit 1; }
grep -q "^## Required Skills" "$f" || { echo "Missing Required Skills in $f"; exit 1; }
grep -Eq "maker.*checker|verifier|Maker / Checker|Verification Strategy|reviewer sub-agent" "$f" || { echo "Missing verifier strategy (maker/checker) mention in $f"; exit 1; }
done
echo "Basic pattern structure checks passed ✓"

- name: Ensure templates exist
run: |
test -f templates/pattern-template.md || (echo "Missing pattern-template.md"; exit 1)
test -f templates/STATE.md.template || (echo "Missing STATE template"; exit 1)
test -f templates/loop-run-log.md.template || (echo "Missing loop-run-log template"; exit 1)
test -f templates/loop-budget.md.template || (echo "Missing loop-budget template"; exit 1)
echo "Templates present ✓"

- name: Validate registry.yaml schema and starter paths
run: |
npm install --no-save yaml@2 ajv@8
node scripts/validate-registry.mjs

- name: Verify loop-init pattern sync
run: node scripts/check-loop-init-sync.mjs

- name: Smoke test loop-init package layout
run: |
cd tools/loop-init
npm ci
npm test
- name: Run validate gates
run: bash scripts/ci-validate-gates.sh
2 changes: 1 addition & 1 deletion LOOP.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ See [docs/multi-loop.md](docs/multi-loop.md). Priority: CI Sweeper → PR Babysi
## Budget & Observability

- Token caps: `loop-budget.md`
- Run history: `loop-run-log.md`
- Run history: `loop-run-log.md` (appended each weekday run by `daily-triage.yml`)
- Estimate: `npx @cobusgreyling/loop-cost --pattern daily-triage`
- Kill switch: `loop-pause-all` label or flag in `STATE.md`

Expand Down
48 changes: 48 additions & 0 deletions scripts/append-run-log.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/usr/bin/env node
/**
* Append one JSON run entry to loop-run-log.md and prune entries older than 30 days.
* Usage: node scripts/append-run-log.mjs '<json-object>' [path-to-log]
*/
import { readFile, writeFile } from 'node:fs/promises';

const MARKER = '<!-- Loop appends below this line -->';
const MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;

const entryJson = process.argv[2];
const logPath = process.argv[3] || 'loop-run-log.md';

if (!entryJson) {
console.error('Usage: node scripts/append-run-log.mjs \'<json>\' [loop-run-log.md]');
process.exit(1);
}

const entry = JSON.parse(entryJson);
const content = await readFile(logPath, 'utf8');
const markerAt = content.indexOf(MARKER);
if (markerAt === -1) {
console.error(`Marker not found in ${logPath}`);
process.exit(1);
}

const before = content.slice(0, markerAt + MARKER.length);
const after = content.slice(markerAt + MARKER.length);
const now = Date.now();

const kept = [];
for (const line of after.split('\n')) {
const trimmed = line.trim();
if (!trimmed.startsWith('{')) continue;
try {
const obj = JSON.parse(trimmed);
const t = new Date(obj.run_id).getTime();
if (!Number.isNaN(t) && now - t <= MAX_AGE_MS) {
kept.push(trimmed);
}
} catch {
// skip malformed lines
}
}

kept.push(JSON.stringify(entry));
await writeFile(logPath, `${before}\n\n${kept.join('\n')}\n`);
console.log(`Appended run ${entry.run_id} (${kept.length} entries within 30d window)`);
41 changes: 41 additions & 0 deletions scripts/ci-audit-gates.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#!/usr/bin/env bash
# Loop readiness audit gates — shared by audit.yml and daily-triage.yml
set -euo pipefail

REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$REPO_ROOT/tools/loop-audit"
npm ci
npm test
echo "=== Audit of repo root ==="
node dist/cli.js "$REPO_ROOT" --json > /tmp/root-audit.json
echo ""
echo "=== Audit of starters (L1 gate) ==="
FAILED=0
for s in "$REPO_ROOT"/starters/*/; do
NAME=$(basename "$s")
node dist/cli.js "$s" --json > "/tmp/starter-${NAME}.json"
node -e "
const data = JSON.parse(require('fs').readFileSync('/tmp/starter-${NAME}.json', 'utf8'));
console.log('--- ${NAME}: score=' + data.score + ' level=' + data.level);
if (data.score < 38) {
console.error('Starter ${NAME} below L1 threshold (38): ' + data.score);
process.exit(1);
}
" || FAILED=1
done
if [ "$FAILED" -ne 0 ]; then
echo "One or more starters failed L1 gate"
exit 1
fi
cp /tmp/root-audit.json /tmp/audit.json

node -e '
const fs = require("fs");
const data = JSON.parse(fs.readFileSync("/tmp/audit.json", "utf8"));
console.log("Reference score: " + data.score);
if (data.score < 58) {
console.error("Reference score below L2 threshold (58). Restore dogfood signals: STATE.md, skills/, AGENTS.md.");
process.exit(2);
}
'
echo "audit gates passed ✓"
36 changes: 36 additions & 0 deletions scripts/ci-validate-gates.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#!/usr/bin/env bash
# Pattern/registry validation gates — shared by validate-patterns.yml and daily-triage.yml
set -euo pipefail

echo "Patterns declared in registry:"
grep '^\s*-\s*id:' patterns/registry.yaml | sed 's/.*id: //' | sort > /tmp/registry.txt
echo "Pattern markdown files:"
ls patterns/*.md | xargs -n1 basename | sed 's/.md$//' | grep -v README | sort > /tmp/files.txt
echo "=== Registry ==="; cat /tmp/registry.txt
echo "=== Files ==="; cat /tmp/files.txt
comm -23 /tmp/files.txt /tmp/registry.txt | grep . && (echo "ERROR: pattern md file(s) missing from registry.yaml"; exit 1) || echo "All pattern files registered ✓"
comm -23 /tmp/registry.txt /tmp/files.txt | grep . && (echo "ERROR: registry entry without matching .md"; exit 1) || echo "No orphan registry entries ✓"

echo "Checking that key sections exist in patterns (lightweight)"
for f in patterns/*.md; do
if [[ "$f" == *"README.md" ]]; then continue; fi
grep -q "^## Scheduling" "$f" || { echo "Missing Scheduling in $f"; exit 1; }
grep -q "^## Required Skills" "$f" || { echo "Missing Required Skills in $f"; exit 1; }
grep -Eq "maker.*checker|verifier|Maker / Checker|Verification Strategy|reviewer sub-agent" "$f" || { echo "Missing verifier strategy (maker/checker) mention in $f"; exit 1; }
done
echo "Basic pattern structure checks passed ✓"

test -f templates/pattern-template.md || (echo "Missing pattern-template.md"; exit 1)
test -f templates/STATE.md.template || (echo "Missing STATE template"; exit 1)
test -f templates/loop-run-log.md.template || (echo "Missing loop-run-log template"; exit 1)
test -f templates/loop-budget.md.template || (echo "Missing loop-budget template"; exit 1)
echo "Templates present ✓"

npm install --no-save yaml@2 ajv@8
node scripts/validate-registry.mjs
node scripts/check-loop-init-sync.mjs

cd tools/loop-init
npm ci
npm test
echo "validate gates passed ✓"