From 06eea50dae6fc18a836053273fb3e78d1a4a6217 Mon Sep 17 00:00:00 2001 From: Cobus Greyling Date: Tue, 9 Jun 2026 19:15:25 +0200 Subject: [PATCH] feat: make loop-engineering super useful MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - loop-audit v1.4: dynamic 'loopActivity' signal (git history, 'Last run' timestamps in STATE, scheduled workflows, run logs). L3 now requires real usage evidence (not just files). Stronger --suggest output and recommendations. Updated tests + README. - GitHub Pages showcase: Interactive Pattern Picker (click symptom pills → exact npx + first /loop command + links). Live Readiness Simulator (checkboxes for all signals → live score + level + next action, mirrors computeScore + new L3 rules). Hero now has prominent one-click copy npx CTAs for loop-init and loop-audit. - New low-risk pattern: Issue Triage (perfect companion to Daily Triage for noisy GitHub/Linear issues). 7 patterns total. Updated registry.yaml (validated), patterns/README, pattern-picker.md decision flow + table. - Polish: loop-init bumped to 1.2.0. Main + tool READMEs, badges, and quickstart language updated to emphasize 'npx works today' and the new interactive tools + activity proof requirement. Builds and validation green. All changes dogfood the philosophy: structure + actual runs = high L3 score. --- README.md | 8 +- docs/assets/css/showcase.css | 104 +++++++++- docs/index.html | 262 ++++++++++++++++++++++++- docs/pattern-picker.md | 6 +- patterns/README.md | 1 + patterns/issue-triage.md | 94 +++++++++ patterns/registry.yaml | 15 ++ tools/loop-audit/README.md | 7 +- tools/loop-audit/dist/auditor.d.ts | 4 + tools/loop-audit/dist/auditor.js | 84 +++++++- tools/loop-audit/dist/cli.js | 13 +- tools/loop-audit/package.json | 4 +- tools/loop-audit/src/auditor.ts | 86 +++++++- tools/loop-audit/src/cli.ts | 13 +- tools/loop-audit/test/auditor.test.mjs | 20 +- tools/loop-init/README.md | 4 + tools/loop-init/dist/cli.js | 8 + tools/loop-init/package.json | 2 +- tools/loop-init/src/cli.ts | 11 +- 19 files changed, 724 insertions(+), 22 deletions(-) create mode 100644 patterns/issue-triage.md diff --git a/README.md b/README.md index fa06c8e..a2e446f 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@

loop-audit dogfood MIT - Pages + Pages

@@ -42,10 +42,10 @@ A loop is a recursive goal: you define a purpose and the AI iterates (often with | [Pattern Picker](docs/pattern-picker.md) | Which loop to run first — **start here if unsure** | | [Primitives Matrix](docs/primitives-matrix.md) | Grok vs Claude Code vs Codex — bookmark this | | [Loop Design Checklist](docs/loop-design-checklist.md) | Ship readiness rubric | -| [Patterns](patterns/README.md) | 6 production patterns including the new low-risk Changelog Drafter | +| [Patterns](patterns/README.md) | 6 production patterns (new low-risk Changelog Drafter) + interactive picker | | [Starters](starters/) | Clone-and-run kits (Grok, Claude Code, Codex) | -| [loop-audit](tools/loop-audit/) | Loop Readiness Score CLI — `npx @cobusgreyling/loop-audit` | -| [loop-init](tools/loop-init/) | Scaffold starters — `npx @cobusgreyling/loop-init` | +| [loop-audit](tools/loop-audit/) | Loop Readiness Score CLI (v1.4 + dynamic activity) — `npx @cobusgreyling/loop-audit . --suggest` | +| [loop-init](tools/loop-init/) | Scaffold starters (v1.2) — `npx @cobusgreyling/loop-init . --pattern daily-triage --tool grok` | | [Stories](stories/) | Real wins and honest failures | ## Why This Matters diff --git a/docs/assets/css/showcase.css b/docs/assets/css/showcase.css index baf7adf..2bb301f 100644 --- a/docs/assets/css/showcase.css +++ b/docs/assets/css/showcase.css @@ -506,4 +506,106 @@ section { .nav-links { display: none; } .hero { padding: 48px 0 40px; } .flow-arrow { display: none; } -} \ No newline at end of file +} + +/* === Interactive widgets (v1.4 site upgrade) === */ +.interactive { + background: var(--bg-elevated); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 28px; + margin: 24px 0; +} + +.symptom-pills { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin: 16px 0; +} + +.symptom-pill { + padding: 8px 14px; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 999px; + font-size: 0.82rem; + color: var(--text-muted); + cursor: pointer; + user-select: none; + transition: all 0.15s; +} + +.symptom-pill:hover { border-color: var(--accent); color: var(--text); } +.symptom-pill.active { + background: rgba(62, 232, 197, 0.15); + border-color: var(--accent); + color: var(--accent); + font-weight: 600; +} + +.reco-card { + margin-top: 16px; + padding: 16px 18px; + background: #050810; + border: 1px solid var(--accent); + border-radius: 10px; + font-size: 0.9rem; +} + +.reco-card .cmd { + font-family: var(--font-mono); + background: rgba(255,255,255,0.04); + padding: 2px 6px; + border-radius: 4px; +} + +.check-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 8px 16px; + margin: 16px 0; +} + +.check-item { + display: flex; + align-items: center; + gap: 8px; + font-size: 0.85rem; + color: var(--text-muted); + cursor: pointer; +} + +.check-item input { accent-color: var(--accent); } + +.live-score { + display: flex; + align-items: center; + gap: 16px; + margin: 12px 0; + padding: 12px 16px; + background: var(--bg-card); + border-radius: 10px; +} + +.live-score .big { + font-size: 2.1rem; + font-weight: 800; + color: var(--accent); + line-height: 1; +} + +.live-score .meta { font-size: 0.85rem; } + +.copy-btn { + font-size: 0.7rem; + padding: 2px 8px; + border-radius: 4px; + background: rgba(62,232,197,0.1); + color: var(--accent); + border: 1px solid rgba(62,232,197,0.3); + cursor: pointer; + margin-left: 8px; +} + +.copy-btn:hover { background: rgba(62,232,197,0.2); } \ No newline at end of file diff --git a/docs/index.html b/docs/index.html index 03670d2..1cf596b 100644 --- a/docs/index.html +++ b/docs/index.html @@ -32,7 +32,7 @@
-
Open source · Tool-agnostic · Production-ready · L3 dogfood score · 6 patterns
+
Open source · Tool-agnostic · Production-ready · L3 dogfood · 6 patterns · Interactive tools

Design the system
that prompts your agents

Loop engineering moves you from "I prompt → agent responds → I prompt again" @@ -43,6 +43,12 @@

Design the system
that prompts your agents

Read the Essay Primitives Matrix +

+ Or try instantly: npx @cobusgreyling/loop-init . --pattern daily-triage --tool grok +   +    npx @cobusgreyling/loop-audit . --suggest + +

Get started

+ +
+ +

Interactive Pattern Picker & Readiness Simulator

+

Pick your pain. See the exact loop + commands. Simulate your score live.

+ +
+

What's hurting right now?

+
+
CI red / flaky checks
+
PRs stalling on review/CI
+
Morning chaos — what should I do?
+
Dependabot / CVE noise
+
Merge debt / TODOs piling up
+
Stale release notes / changelogs
+
+ + +
+ +
+

Live Loop Readiness Simulator (mirrors loop-audit)

+

Check the boxes you already have (or plan to add). Score updates live.

+ +
+ + + + + + + + + + +
+ +
+
+
10
+
/ 100
+
+
+
L0Not loop-ready — start with a starter.
+
+
+
+ +
+ This is a close client-side approximation of the real loop-audit scoring + L3 rules (activity now required for top level). +
+
+
+

Engineering, not hype

@@ -330,5 +398,197 @@

Ready to stop prompting?

+ + \ No newline at end of file diff --git a/docs/pattern-picker.md b/docs/pattern-picker.md index e3c3aa8..04c17f2 100644 --- a/docs/pattern-picker.md +++ b/docs/pattern-picker.md @@ -8,8 +8,8 @@ flowchart TD B -->|yes| C[CI Sweeper] B -->|no| D{PRs stalling?} D -->|yes| E[PR Babysitter] - D -->|no| F{Morning chaos / unknown priorities?} - F -->|yes| G[Daily Triage] + D -->|no| F{Morning chaos or noisy issues?} + F -->|yes| G[Daily Triage + Issue Triage] F -->|no| H{Dependabot / CVE noise?} H -->|yes| I[Dependency Sweeper] H -->|no| J{Merge debt / TODOs piling up?} @@ -25,7 +25,7 @@ flowchart TD |---------|---------|------------| | CI failing on main or PRs | [CI Sweeper](../patterns/ci-sweeper.md) | L2, 15m cadence, max 3 attempts | | PRs waiting on review/CI/rebase | [PR Babysitter](../patterns/pr-babysitter.md) | L1 watch → L2 assisted | -| "What should I work on?" every morning | [Daily Triage](../patterns/daily-triage.md) | **L1 report-only week one** | +| "What should I work on?" every morning or noisy GitHub issues | [Daily Triage](../patterns/daily-triage.md) + [Issue Triage](../patterns/issue-triage.md) (new) | **L1 report-only week one** — low risk, excellent pair | | Outdated packages / CVE alerts | [Dependency Sweeper](../patterns/dependency-sweeper.md) | L2 patch-only, denylist majors | | TODOs and cleanup after merges | [Post-Merge Cleanup](../patterns/post-merge-cleanup.md) | L1 off-peak, small fixes only | | Stale or missing release notes | [Changelog Drafter](../patterns/changelog-drafter.md) | **L1** (draft only first), very low risk | diff --git a/patterns/README.md b/patterns/README.md index c0e1a9a..a12ac01 100644 --- a/patterns/README.md +++ b/patterns/README.md @@ -16,6 +16,7 @@ Each pattern answers: |---------|---------|------|------| | PR Babysitter | 5–15m | Medium | [pr-babysitter.md](./pr-babysitter.md) | | Daily Triage | 1d–2h | Low | [daily-triage.md](./daily-triage.md) | +| Issue Triage (new) | 2h–1d | Low | [issue-triage.md](./issue-triage.md) | | CI Sweeper | 5–15m | Medium | [ci-sweeper.md](./ci-sweeper.md) | | Post-Merge Cleanup | 1d–6h | Low | [post-merge-cleanup.md](./post-merge-cleanup.md) | | Dependency Sweeper | 6h–1d | Medium | [dependency-sweeper.md](./dependency-sweeper.md) | diff --git a/patterns/issue-triage.md b/patterns/issue-triage.md new file mode 100644 index 0000000..a530c80 --- /dev/null +++ b/patterns/issue-triage.md @@ -0,0 +1,94 @@ +# Issue Triage Loop + +**Goal**: Continuously discover, deduplicate, prioritize, and label incoming issues, feature requests, and discussions so the team (and other loops) always have a clean, actionable top-of-queue. Pure report / proposal mode in week one. Extremely low risk, high leverage. + +## Scheduling + +**Recommended**: +- `/loop 2h` or `1d` (morning + end of day for busy repos) +- GitHub Action on `issues` / `discussion` events + scheduled fallback +- Pairs beautifully with Daily Triage (this loop feeds the "what should I work on" report) + +This is an excellent always-on, low-cost companion loop. + +## Required Skills + +- `issue-triage` — Scans open issues, discussions, and (optionally via MCP) Linear / Jira. Dedupes, extracts signals (labels, comments, linked PRs, age, reactions), proposes priority + suggested labels + one-sentence summary. +- `loop-verifier` (light or human) — Sanity check on the proposed triage actions / new labels before anything is applied. + +## State + +Filename: `issue-triage-state.md` + +Compact rolling view of the current backlog health: + +```markdown +# Issue Triage State +Last run: 2026-06-09 09:15 UTC +Open actionable: 14 (was 17) +New since last run: 3 +Needs human: 2 (one potential duplicate of #412, one unclear spec) + +## Top 5 (by loop score) +- #487 (bug, p1, 2d old) — "Crash on export with large files" — suggested: bug + needs-repro + area:export +- ... +``` + +The loop prunes closed/merged items and only keeps "needs attention" items. + +## How the Loop Runs (Typical Cycle) + +1. Discover new/updated issues + discussions since last run (or all open if first run). +2. For each: summarize intent, detect duplicates (title + embedding hints or simple text match), pull signals (age, author, linked PRs, reactions, existing labels). +3. Score / bucket: P0 (security, prod breakage), P1 (high impact + clear), P2, P3, needs-info, duplicate. +4. Write or update a clean prioritized list + suggested label set + short "why this matters" into the state file (and optionally a comment on the issue itself as "Loop triage note"). +5. Verifier (or human) reviews only the "needs human" bucket and any proposed label changes on sensitive areas. +6. Record run, prune resolved items, update counts. + +## Verification Strategy + +- The loop **never auto-labels or closes** in L1. +- In L2 it can apply allowlisted labels only (e.g. `area:*`, `needs-repro`) after verifier passes. +- Human always owns P0/P1 assignment for the first weeks and for anything touching auth, payments, security, or public API. + +## Human Handoff Points + +- Any issue touching security, auth, billing, or infra +- Duplicate detection that is uncertain (>30% chance wrong) +- Issues older than N days that the loop wants to close as "stale" (human confirms) +- When > X new issues appear in a single run (context overload signal) + +## Tool-Specific Notes + +**Grok Build TUI**: +``` +/loop 2h Run issue-triage skill. Read issue-triage-state.md first. Produce updated state + suggested labels for new items only. No auto-label or close. Escalate anything ambiguous. +``` + +**Claude Code**: +``` +/loop 2h $issue-triage — update issue-triage-state.md. Propose labels only on allowlisted areas. Human review for P0/P1. +``` + +**Codex**: +Automation every 2h or on `issues` event: run issue-triage → update state. Report mode. + +**GitHub Actions**: +See `examples/github-actions/` for a starter workflow that can react to issue events + scheduled run. + +## Failure Modes & Mitigations + +| Failure | Mitigation | +|--------------------------|----------| +| Over-prioritizing noisy reporter | Weight by signals the team actually cares about (reactions, linked PRs, internal +1s). Human overrides recorded in state. | +| Duplicate false positives | Conservative matching + always surface "possible duplicate of #NNN" for human confirmation in L1. | +| Alert fatigue on every new issue | Only notify human for the "needs human" slice. Everything else lives in the state file that Daily Triage or the engineer reads. | + +## Success Metrics + +- Reduction in time from issue open → first meaningful label or "needs info" comment. +- % of issues that have a clear priority within 24h. +- Engineer-reported "I always know what the top 5 things are" score (qualitative, from state file reviews). +- Number of duplicates caught before two people start working on them. + +See also: [Daily Triage](../daily-triage.md) (this loop is a feeder), [Multi-Loop Coordination](../docs/multi-loop.md), and the [Loop Design Checklist](../../docs/loop-design-checklist.md). diff --git a/patterns/registry.yaml b/patterns/registry.yaml index 0dc7235..e482511 100644 --- a/patterns/registry.yaml +++ b/patterns/registry.yaml @@ -90,4 +90,19 @@ patterns: human_gates: [breaking-changes, security, major-features, marketing-sensitive] starter: starters/changelog-drafter week_one_mode: L1 + token_cost: low + + - id: issue-triage + name: Issue Triage + file: issue-triage.md + goal: Discover, deduplicate, prioritize and label incoming issues/discussions so the team always has a clean actionable queue. Excellent low-risk companion to Daily Triage. + cadence: 2h-1d + risk: low + tools: [grok, claude-code, codex, github-actions] + skills: [issue-triage, loop-verifier] + state: issue-triage-state.md + phases: [discover, dedupe, score, propose-labels, human-review] + human_gates: [security, p0-p1, ambiguous-duplicates, stale-closures] + starter: starters/minimal-loop + week_one_mode: L1 token_cost: low \ No newline at end of file diff --git a/tools/loop-audit/README.md b/tools/loop-audit/README.md index c5a7324..04628a9 100644 --- a/tools/loop-audit/README.md +++ b/tools/loop-audit/README.md @@ -2,6 +2,8 @@ CLI that scores a project's **Loop Readiness** (0–100) and suggests next steps. +**npx @cobusgreyling/loop-audit . --suggest** works immediately (published package). + ## Install & Run **npm (recommended):** @@ -34,7 +36,7 @@ bash scripts/before-after-demo.sh loop-audit . # human-readable (default) loop-audit . --json # machine-readable loop-audit . --md # markdown report -loop-audit . --suggest # copy-from-template commands (all tools) +loop-audit . --suggest # copy-from-template commands + activity tips (all tools) ``` Exit code `2` if score < 40 (useful for CI gates once your project is loop-ready). @@ -49,7 +51,7 @@ npm run build npm publish --access public ``` -## Signals Checked (v1.2+) +## Signals Checked (v1.4+) | Signal | Notes | |-------------------------|-------| @@ -63,6 +65,7 @@ npm publish --access public | MCP / connectors | Mentions or config files | | Worktree evidence | Isolation patterns in docs | | patterns/registry.yaml | Machine index for tooling | +| **loopActivity (new)** | **Dynamic proof**: "Last run" timestamps in state, loop-related git commits, scheduled workflows, run logs. This is what separates "configured on paper" from "actually running loops". L3 now requires it. | ## Levels diff --git a/tools/loop-audit/dist/auditor.d.ts b/tools/loop-audit/dist/auditor.d.ts index 65548c9..52f63a2 100644 --- a/tools/loop-audit/dist/auditor.d.ts +++ b/tools/loop-audit/dist/auditor.d.ts @@ -43,6 +43,10 @@ export interface LoopSignals { registry: { present: boolean; }; + loopActivity: { + present: boolean; + evidence: string[]; + }; } export interface Finding { level: 'ok' | 'warn' | 'fail'; diff --git a/tools/loop-audit/dist/auditor.js b/tools/loop-audit/dist/auditor.js index 24d2e2b..015903c 100644 --- a/tools/loop-audit/dist/auditor.js +++ b/tools/loop-audit/dist/auditor.js @@ -1,5 +1,6 @@ import { readdir, readFile, stat } from 'node:fs/promises'; import path from 'node:path'; +import { execSync } from 'node:child_process'; const STATE_FILES = [ 'STATE.md', 'pr-babysitter-state.md', @@ -71,6 +72,73 @@ async function findSkills(root) { } return found; } +async function detectLoopActivity(root) { + const evidence = []; + const stateCandidates = [...STATE_FILES, 'STATE.md']; + // 1. Look for "Last run" timestamps or dated entries inside state files (strong real-usage signal) + for (const sf of stateCandidates) { + try { + const p = path.join(root, sf); + if (await fileExists(p)) { + const txt = await readFile(p, 'utf8'); + if (/last\s*run|last updated|^\s*-\s*\d{4}-\d{2}-\d{2}/im.test(txt) || /triage|loop run|changelog drafter/i.test(txt)) { + evidence.push(`state:${sf}`); + } + } + } + catch { } + } + // 2. Presence of run log artifacts or dedicated log templates being used + const logHints = ['loop-run-log', 'run-log', 'loop.log', 'audit-report']; + try { + const entries = await readdir(root, { withFileTypes: true }); + for (const e of entries) { + if (e.isFile() && logHints.some(h => e.name.toLowerCase().includes(h))) { + evidence.push(`log:${e.name}`); + } + } + } + catch { } + // 3. Workflow or LOOP evidence of scheduled execution + try { + const wfDir = path.join(root, '.github', 'workflows'); + if (await fileExists(wfDir)) { + const wfs = await readdir(wfDir); + if (wfs.some(w => /triage|changelog|daily|loop|audit|pr-babysit/i.test(w))) { + evidence.push('github:loop-workflows'); + } + } + } + catch { } + // 4. Light git history scan for loop-related commits (best dynamic proof) + try { + const log = execSync('git log --oneline -25 -- .', { + cwd: root, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'ignore'], + timeout: 1500, + }); + const lower = log.toLowerCase(); + if (/state\.md|loop| t riage |changelog-drafter|post-merge|daily triage|audit/i.test(lower)) { + const firstMatch = log.trim().split('\n')[0] || ''; + evidence.push(`git:${firstMatch.slice(0, 60)}`); + } + } + catch { + // git not available or not a repo — ignore gracefully + } + // 5. Check LOOP.md or a state for explicit "Last run" human-readable proof + try { + const loopP = path.join(root, 'LOOP.md'); + if (await fileExists(loopP)) { + const txt = await readFile(loopP, 'utf8'); + if (/last run|cadence|scheduled|automation/i.test(txt)) + evidence.push('LOOP.md:active'); + } + } + catch { } + return { present: evidence.length > 0, evidence: Array.from(new Set(evidence)).slice(0, 4) }; +} export function computeScore(signals) { let score = 10; if (signals.stateFile.present) @@ -101,9 +169,12 @@ export function computeScore(signals) { score += 3; if (signals.registry.present) score += 2; + if (signals.loopActivity.present) + score += 6; // dynamic proof the loops are actually being exercised score = Math.min(100, Math.max(0, score)); let level = 'L0'; - if (score >= 78 && signals.verifier.present && signals.stateFile.present) + const hasRealActivity = signals.loopActivity.present; + if (score >= 78 && signals.verifier.present && signals.stateFile.present && hasRealActivity) level = 'L3'; else if (score >= 58 && signals.triage.present) level = 'L2'; @@ -182,6 +253,8 @@ export async function auditProject(target) { catch { } } const registryPresent = await fileExists(path.join(root, 'patterns', 'registry.yaml')); + // Dynamic real-world usage evidence (the key new v1.4 signal) + const loopActivity = await detectLoopActivity(root); const signals = { stateFile: { present: statePaths.length > 0, paths: statePaths }, loopConfig: { present: loopMd, path: loopMd ? 'LOOP.md' : undefined }, @@ -196,6 +269,7 @@ export async function auditProject(target) { mcp: { present: mcpPresent }, worktreeEvidence: { present: worktreeEvidence }, registry: { present: registryPresent }, + loopActivity, }; if (!signals.stateFile.present) { findings.push({ level: 'fail', message: 'No state file (STATE.md or pattern-specific state).' }); @@ -260,6 +334,14 @@ export async function auditProject(target) { findings.push({ level: 'warn', message: 'No patterns/registry.yaml (machine-readable index for future tools).' }); recommendations.push('Add patterns/registry.yaml following the existing format'); } + // New dynamic activity signal (v1.4) + if (!signals.loopActivity.present) { + findings.push({ level: 'warn', message: 'No evidence of actual loop runs detected (no "Last run" entries in state, loop-related git activity, or scheduled workflows yet).' }); + recommendations.push('Run one loop (report-only), update + commit STATE.md (or pattern state). This turns structure into proven usage.'); + } + else { + findings.push({ level: 'ok', message: `Loop activity detected — real usage signals present (${signals.loopActivity.evidence.length} sources).` }); + } const { score, level, assessment } = computeScore(signals); return { target: root, diff --git a/tools/loop-audit/dist/cli.js b/tools/loop-audit/dist/cli.js index 462d404..df2df81 100644 --- a/tools/loop-audit/dist/cli.js +++ b/tools/loop-audit/dist/cli.js @@ -8,7 +8,7 @@ const md = args.includes('--md'); const suggest = args.includes('--suggest') || args.includes('--fix'); const help = args.includes('--help') || args.includes('-h'); if (help) { - console.log(`loop-audit — Loop Readiness Score CLI (v1.1+) + console.log(`loop-audit — Loop Readiness Score CLI (v1.4+) Usage: loop-audit [path] [options] @@ -19,14 +19,20 @@ Options: --suggest Show copy-from-template commands for missing pieces (recommended on first runs) --help, -h This help +New in v1.4: + • Dynamic "loop activity" detection (git history, "Last run" in STATE, scheduled workflows) + • Higher L3 bar requires proven usage, not just files + • Stronger recommendations when structure exists but no runs yet + Exit codes: 0 score >= 40 2 score < 40 (early stage or gate) Examples: loop-audit . - loop-audit starters/minimal-loop --suggest + loop-audit . --suggest npx @cobusgreyling/loop-audit . --json + npx @cobusgreyling/loop-audit starters/minimal-loop --suggest bash scripts/before-after-demo.sh `); process.exit(0); @@ -70,6 +76,9 @@ try { console.log(' # Or scaffold automatically:'); console.log(' npx @cobusgreyling/loop-init . --pattern daily-triage --tool grok'); console.log(''); + console.log(' # IMPORTANT (v1.4): After scaffolding, actually RUN a loop (report-only) and commit the updated STATE.md.'); + console.log(' # This creates the "loopActivity" evidence that pushes you toward real L2/L3 scores.'); + console.log(''); console.log('See docs/loop-design-checklist.md and patterns/ for full guidance.'); } if (result.score < 40) diff --git a/tools/loop-audit/package.json b/tools/loop-audit/package.json index 1a8e98a..987b4bf 100644 --- a/tools/loop-audit/package.json +++ b/tools/loop-audit/package.json @@ -1,7 +1,7 @@ { "name": "@cobusgreyling/loop-audit", - "version": "1.3.0", - "description": "Loop Readiness Score — audit a project for loop engineering readiness (L0-L3). Supports --suggest for copy commands.", + "version": "1.4.0", + "description": "Loop Readiness Score — audit a project for loop engineering readiness (L0-L3). Dynamic activity detection + --suggest. npx @cobusgreyling/loop-audit . --suggest", "type": "module", "bin": { "loop-audit": "./dist/cli.js" diff --git a/tools/loop-audit/src/auditor.ts b/tools/loop-audit/src/auditor.ts index de7bad8..710559e 100644 --- a/tools/loop-audit/src/auditor.ts +++ b/tools/loop-audit/src/auditor.ts @@ -1,5 +1,6 @@ import { readdir, readFile, stat } from 'node:fs/promises'; import path from 'node:path'; +import { execSync } from 'node:child_process'; export interface LoopSignals { stateFile: { present: boolean; paths: string[] }; @@ -15,6 +16,7 @@ export interface LoopSignals { mcp: { present: boolean }; worktreeEvidence: { present: boolean }; registry: { present: boolean }; + loopActivity: { present: boolean; evidence: string[] }; } export interface Finding { @@ -104,6 +106,74 @@ async function findSkills(root: string): Promise { return found; } +async function detectLoopActivity(root: string): Promise<{ present: boolean; evidence: string[] }> { + const evidence: string[] = []; + const stateCandidates = [...STATE_FILES, 'STATE.md']; + + // 1. Look for "Last run" timestamps or dated entries inside state files (strong real-usage signal) + for (const sf of stateCandidates) { + try { + const p = path.join(root, sf); + if (await fileExists(p)) { + const txt = await readFile(p, 'utf8'); + if (/last\s*run|last updated|^\s*-\s*\d{4}-\d{2}-\d{2}/im.test(txt) || /triage|loop run|changelog drafter/i.test(txt)) { + evidence.push(`state:${sf}`); + } + } + } catch {} + } + + // 2. Presence of run log artifacts or dedicated log templates being used + const logHints = ['loop-run-log', 'run-log', 'loop.log', 'audit-report']; + try { + const entries = await readdir(root, { withFileTypes: true }); + for (const e of entries) { + if (e.isFile() && logHints.some(h => e.name.toLowerCase().includes(h))) { + evidence.push(`log:${e.name}`); + } + } + } catch {} + + // 3. Workflow or LOOP evidence of scheduled execution + try { + const wfDir = path.join(root, '.github', 'workflows'); + if (await fileExists(wfDir)) { + const wfs = await readdir(wfDir); + if (wfs.some(w => /triage|changelog|daily|loop|audit|pr-babysit/i.test(w))) { + evidence.push('github:loop-workflows'); + } + } + } catch {} + + // 4. Light git history scan for loop-related commits (best dynamic proof) + try { + const log = execSync('git log --oneline -25 -- .', { + cwd: root, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'ignore'], + timeout: 1500, + }); + const lower = log.toLowerCase(); + if (/state\.md|loop| t riage |changelog-drafter|post-merge|daily triage|audit/i.test(lower)) { + const firstMatch = log.trim().split('\n')[0] || ''; + evidence.push(`git:${firstMatch.slice(0, 60)}`); + } + } catch { + // git not available or not a repo — ignore gracefully + } + + // 5. Check LOOP.md or a state for explicit "Last run" human-readable proof + try { + const loopP = path.join(root, 'LOOP.md'); + if (await fileExists(loopP)) { + const txt = await readFile(loopP, 'utf8'); + if (/last run|cadence|scheduled|automation/i.test(txt)) evidence.push('LOOP.md:active'); + } + } catch {} + + return { present: evidence.length > 0, evidence: Array.from(new Set(evidence)).slice(0, 4) }; +} + export function computeScore(signals: LoopSignals): { score: number; level: 'L0' | 'L1' | 'L2' | 'L3'; assessment: string } { let score = 10; @@ -121,11 +191,13 @@ export function computeScore(signals: LoopSignals): { score: number; level: 'L0' if (signals.mcp.present) score += 3; if (signals.worktreeEvidence.present) score += 3; if (signals.registry.present) score += 2; + if (signals.loopActivity.present) score += 6; // dynamic proof the loops are actually being exercised score = Math.min(100, Math.max(0, score)); let level: 'L0' | 'L1' | 'L2' | 'L3' = 'L0'; - if (score >= 78 && signals.verifier.present && signals.stateFile.present) level = 'L3'; + const hasRealActivity = signals.loopActivity.present; + if (score >= 78 && signals.verifier.present && signals.stateFile.present && hasRealActivity) level = 'L3'; else if (score >= 58 && signals.triage.present) level = 'L2'; else if (score >= 38 && signals.stateFile.present) level = 'L1'; else level = 'L0'; @@ -207,6 +279,9 @@ export async function auditProject(target: string): Promise { const registryPresent = await fileExists(path.join(root, 'patterns', 'registry.yaml')); + // Dynamic real-world usage evidence (the key new v1.4 signal) + const loopActivity = await detectLoopActivity(root); + const signals: LoopSignals = { stateFile: { present: statePaths.length > 0, paths: statePaths }, loopConfig: { present: loopMd, path: loopMd ? 'LOOP.md' : undefined }, @@ -221,6 +296,7 @@ export async function auditProject(target: string): Promise { mcp: { present: mcpPresent }, worktreeEvidence: { present: worktreeEvidence }, registry: { present: registryPresent }, + loopActivity, }; if (!signals.stateFile.present) { @@ -291,6 +367,14 @@ export async function auditProject(target: string): Promise { recommendations.push('Add patterns/registry.yaml following the existing format'); } + // New dynamic activity signal (v1.4) + if (!signals.loopActivity.present) { + findings.push({ level: 'warn', message: 'No evidence of actual loop runs detected (no "Last run" entries in state, loop-related git activity, or scheduled workflows yet).' }); + recommendations.push('Run one loop (report-only), update + commit STATE.md (or pattern state). This turns structure into proven usage.'); + } else { + findings.push({ level: 'ok', message: `Loop activity detected — real usage signals present (${signals.loopActivity.evidence.length} sources).` }); + } + const { score, level, assessment } = computeScore(signals); return { diff --git a/tools/loop-audit/src/cli.ts b/tools/loop-audit/src/cli.ts index d66181e..a9be6cf 100644 --- a/tools/loop-audit/src/cli.ts +++ b/tools/loop-audit/src/cli.ts @@ -10,7 +10,7 @@ const suggest = args.includes('--suggest') || args.includes('--fix'); const help = args.includes('--help') || args.includes('-h'); if (help) { - console.log(`loop-audit — Loop Readiness Score CLI (v1.1+) + console.log(`loop-audit — Loop Readiness Score CLI (v1.4+) Usage: loop-audit [path] [options] @@ -21,14 +21,20 @@ Options: --suggest Show copy-from-template commands for missing pieces (recommended on first runs) --help, -h This help +New in v1.4: + • Dynamic "loop activity" detection (git history, "Last run" in STATE, scheduled workflows) + • Higher L3 bar requires proven usage, not just files + • Stronger recommendations when structure exists but no runs yet + Exit codes: 0 score >= 40 2 score < 40 (early stage or gate) Examples: loop-audit . - loop-audit starters/minimal-loop --suggest + loop-audit . --suggest npx @cobusgreyling/loop-audit . --json + npx @cobusgreyling/loop-audit starters/minimal-loop --suggest bash scripts/before-after-demo.sh `); process.exit(0); @@ -71,6 +77,9 @@ try { console.log(' # Or scaffold automatically:'); console.log(' npx @cobusgreyling/loop-init . --pattern daily-triage --tool grok'); console.log(''); + console.log(' # IMPORTANT (v1.4): After scaffolding, actually RUN a loop (report-only) and commit the updated STATE.md.'); + console.log(' # This creates the "loopActivity" evidence that pushes you toward real L2/L3 scores.'); + console.log(''); console.log('See docs/loop-design-checklist.md and patterns/ for full guidance.'); } diff --git a/tools/loop-audit/test/auditor.test.mjs b/tools/loop-audit/test/auditor.test.mjs index 804b030..1c14672 100644 --- a/tools/loop-audit/test/auditor.test.mjs +++ b/tools/loop-audit/test/auditor.test.mjs @@ -20,6 +20,7 @@ function emptySignals() { mcp: { present: false }, worktreeEvidence: { present: false }, registry: { present: false }, + loopActivity: { present: false, evidence: [] }, }; } @@ -49,7 +50,7 @@ test('computeScore: full L2 signals', () => { assert.ok(score >= 58 && score < 78); }); -test('computeScore: L3 requires verifier and high score', () => { +test('computeScore: L3 requires verifier, high score, and real loop activity (v1.4)', () => { const s = emptySignals(); s.stateFile = { present: true, paths: ['STATE.md'] }; s.triage = { present: true }; @@ -62,11 +63,28 @@ test('computeScore: L3 requires verifier and high score', () => { s.mcp = { present: true }; s.worktreeEvidence = { present: true }; s.registry = { present: true }; + s.loopActivity = { present: true, evidence: ['git:state update', 'state:STATE.md'] }; const { level, score } = computeScore(s); assert.equal(level, 'L3'); assert.ok(score >= 78); }); +test('computeScore: high structure without activity caps at L2 (v1.4 dynamic signal)', () => { + const s = emptySignals(); + s.stateFile = { present: true, paths: ['STATE.md'] }; + s.triage = { present: true }; + s.loopConfig = { present: true, path: 'LOOP.md' }; + s.agentsMd = { present: true }; + s.skills = { count: 3, loopSkills: ['loop-triage', 'minimal-fix', 'loop-verifier'] }; + s.verifier = { present: true }; + s.safety = { loopMdMentionsSafety: true, safetyDocPresent: true }; + s.github = { present: true, workflows: true }; + s.registry = { present: true }; + // deliberately no loopActivity + const { level } = computeScore(s); + assert.equal(level, 'L2'); +}); + test('auditProject: empty directory scores low', async () => { const dir = await mkdtemp(path.join(tmpdir(), 'loop-audit-empty-')); try { diff --git a/tools/loop-init/README.md b/tools/loop-init/README.md index 8479119..fd69b6c 100644 --- a/tools/loop-init/README.md +++ b/tools/loop-init/README.md @@ -2,6 +2,8 @@ Scaffold loop engineering starters into your project by pattern and tool. +**npx @cobusgreyling/loop-init . --pattern daily-triage --tool grok** works immediately. + ## Install & Run ```bash @@ -12,6 +14,8 @@ npx @cobusgreyling/loop-init . -p dependency-sweeper --dry-run See [docs/RELEASE.md](../../docs/RELEASE.md) for npm publish tags. The published package bundles `starters/` and `templates/` from this monorepo. +After scaffolding, always run `npx @cobusgreyling/loop-audit . --suggest` and actually execute the first report-only loop to generate activity signals. + ## Patterns | Pattern | Default state file | diff --git a/tools/loop-init/dist/cli.js b/tools/loop-init/dist/cli.js index 16d1001..8c85ebf 100644 --- a/tools/loop-init/dist/cli.js +++ b/tools/loop-init/dist/cli.js @@ -13,6 +13,7 @@ const PATTERN_STARTERS = { 'dependency-sweeper': 'dependency-sweeper', 'post-merge-cleanup': 'post-merge-cleanup', 'changelog-drafter': 'changelog-drafter', + 'issue-triage': 'minimal-loop', // reuses daily-triage starter + new skill can be added manually or via templates later }; const TOOL_SUFFIX = { grok: '', @@ -33,6 +34,7 @@ const STATE_FILES = { 'dependency-sweeper': 'dependency-sweeper-state.md', 'post-merge-cleanup': 'post-merge-state.md', 'changelog-drafter': 'changelog-drafter-state.md', + 'issue-triage': 'issue-triage-state.md', }; function parseArgs(argv) { let pattern = 'daily-triage'; @@ -170,6 +172,11 @@ function firstLoopCommand(pattern, tool) { claude: '/loop 1d $changelog-scan + draft-release-notes — write RELEASE_NOTES_DRAFT.md and update state. Human approves before publish.', codex: 'Automation daily: changelog-scan + draft-release-notes → RELEASE_NOTES_DRAFT.md. Human review.', }, + 'issue-triage': { + grok: '/loop 2h Run issue-triage. Update issue-triage-state.md. Propose labels and priority only. No auto-apply. Human reviews the needs-human slice.', + claude: '/loop 2h $issue-triage — update issue-triage-state.md. Suggest labels on allowlisted areas only. Report mode week one.', + codex: 'Automation 2h: issue-triage → issue-triage-state.md. Propose only.', + }, }; return cmds[pattern][tool]; } @@ -188,6 +195,7 @@ Patterns: dependency-sweeper post-merge-cleanup changelog-drafter (new low-risk release notes pattern) + issue-triage (new low-risk issue queue health companion to daily triage) Options: -p, --pattern Pattern to scaffold diff --git a/tools/loop-init/package.json b/tools/loop-init/package.json index 5a15c1c..1c1da0c 100644 --- a/tools/loop-init/package.json +++ b/tools/loop-init/package.json @@ -1,6 +1,6 @@ { "name": "@cobusgreyling/loop-init", - "version": "1.1.0", + "version": "1.2.0", "description": "Scaffold loop engineering starters into your project by pattern and tool.", "type": "module", "bin": { diff --git a/tools/loop-init/src/cli.ts b/tools/loop-init/src/cli.ts index dbc36e8..ea4d968 100644 --- a/tools/loop-init/src/cli.ts +++ b/tools/loop-init/src/cli.ts @@ -14,7 +14,8 @@ type Pattern = | 'ci-sweeper' | 'dependency-sweeper' | 'post-merge-cleanup' - | 'changelog-drafter'; + | 'changelog-drafter' + | 'issue-triage'; type Tool = 'grok' | 'claude' | 'codex'; @@ -25,6 +26,7 @@ const PATTERN_STARTERS: Record = { 'dependency-sweeper': 'dependency-sweeper', 'post-merge-cleanup': 'post-merge-cleanup', 'changelog-drafter': 'changelog-drafter', + 'issue-triage': 'minimal-loop', // reuses daily-triage starter + new skill can be added manually or via templates later }; const TOOL_SUFFIX: Record = { @@ -49,6 +51,7 @@ const STATE_FILES: Record = { 'dependency-sweeper': 'dependency-sweeper-state.md', 'post-merge-cleanup': 'post-merge-state.md', 'changelog-drafter': 'changelog-drafter-state.md', + 'issue-triage': 'issue-triage-state.md', }; function parseArgs(argv: string[]) { @@ -207,6 +210,11 @@ function firstLoopCommand(pattern: Pattern, tool: Tool): string { claude: '/loop 1d $changelog-scan + draft-release-notes — write RELEASE_NOTES_DRAFT.md and update state. Human approves before publish.', codex: 'Automation daily: changelog-scan + draft-release-notes → RELEASE_NOTES_DRAFT.md. Human review.', }, + 'issue-triage': { + grok: '/loop 2h Run issue-triage. Update issue-triage-state.md. Propose labels and priority only. No auto-apply. Human reviews the needs-human slice.', + claude: '/loop 2h $issue-triage — update issue-triage-state.md. Suggest labels on allowlisted areas only. Report mode week one.', + codex: 'Automation 2h: issue-triage → issue-triage-state.md. Propose only.', + }, }; return cmds[pattern][tool]; } @@ -227,6 +235,7 @@ Patterns: dependency-sweeper post-merge-cleanup changelog-drafter (new low-risk release notes pattern) + issue-triage (new low-risk issue queue health companion to daily triage) Options: -p, --pattern Pattern to scaffold