diff --git a/docs/loop-design-checklist.md b/docs/loop-design-checklist.md index a48e58d..d97514b 100644 --- a/docs/loop-design-checklist.md +++ b/docs/loop-design-checklist.md @@ -8,6 +8,7 @@ Use this before enabling a loop in production. Score honestly — a loop missing - [ ] **Explicit non-goals** — what will this loop *not* do? - [ ] **Watched scope** — which repos, branches, PRs, or tickets? - [ ] **Phased rollout** — report-only first, then act on small wins? +- [ ] **Ambiguous input handled** — when a work item is too vague to verify "done", the loop clarifies it or escalates instead of guessing (scaffold the `loop-intake` skill via `loop-init` for issue-driven patterns) ## 2. Scheduling diff --git a/templates/SKILL.md.loop-intake b/templates/SKILL.md.loop-intake new file mode 100644 index 0000000..349ec76 --- /dev/null +++ b/templates/SKILL.md.loop-intake @@ -0,0 +1,71 @@ +--- +name: loop-intake +description: > + Clarify an ambiguous goal or work item BEFORE the loop acts on it. Runs at the + front of a run when the assigned task is underspecified. Ask one question at a + time, push for exact values, and escalate instead of guessing. +user_invocable: true +--- + +# Loop Intake - Clarify Before You Act + +Autonomous loops waste attempts (and budget) when they guess at a vague goal. +This skill runs FIRST, before triage or any action skill, whenever the item the +loop is about to work on is not specific enough to verify "done". + +## When to run + +Run intake when the goal or work item is missing any of: + +- a single, testable definition of done +- the exact scope (which repo / branch / paths / tickets) +- concrete values a fix would depend on (error text, thresholds, versions) + +If the goal is already specific and verifiable, skip intake and proceed. + +## How to clarify (one question at a time) + +1. Read the current state file and the item (issue, PR, request) in full. Do NOT + re-ask anything already answered there. +2. Ask the SINGLE most decision-blocking question. Wait for the answer. +3. Push for an exact value. "It should be fast" becomes "What p95 latency, in + ms?". "Add a limit" becomes "How many per minute?". Re-ask once if the answer + is still vague. +4. Stop as soon as the goal is verifiable. Do not interrogate for its own sake. + +Ask in behavior terms, not implementation: what the system must do, what the user +sees, which values matter. Not: which function, table, or endpoint. + +## When an answer stays vague + +Do not guess. Record it and move on: + +- Write the gap as an open question in the state file: `- [ ] OQ: `. +- Mark the item `needs-human` (escalate per the loop's handoff rule). +- If NO blocking question can be resolved, escalate the whole item and stop. + Acting on a guess burns attempts the circuit breaker should not have to catch. + +## Output + +Append a short, sharpened goal to the state file (or the item): + +``` +Goal (clarified): +Done when: +Open questions: (needs-human) - +``` + +Hand back to triage or the action skill only if "Done when" is now verifiable. + +## Interaction with other skills + +- Runs BEFORE `loop-triage` / `issue-triage` and any action skill. +- Feeds `loop-verifier`: the "Done when" line becomes the verifier's check. +- Pairs with `loop-guard` (circuit breaker): intake prevents wasted attempts on a + goal that was never well defined; the breaker catches the ones that slip through. +- Respects `loop-constraints`: never ask for, or act on, denylisted scope. + +## Default behavior + +Report-only in week one: propose the clarified goal and open questions, but let a +human confirm before the loop acts on the sharpened goal. diff --git a/tools/loop-init/README.md b/tools/loop-init/README.md index 38b2f82..f88bcf9 100644 --- a/tools/loop-init/README.md +++ b/tools/loop-init/README.md @@ -38,6 +38,10 @@ Fix-capable patterns (`pr-babysitter`, `ci-sweeper`, `dependency-sweeper`, `post The ledger's `pattern`/`level` let `loop-guard` size the breaker's `--token-budget` from [`loop-cost`](../loop-cost)'s realistic per-run estimate instead of a hand-typed number. The breaker escalates (same error N× in a row, too many consecutive failures, token budget, or iteration cap) instead of looping in vain. Report-only patterns skip it. +Patterns that act on human-authored, often underspecified input (`issue-triage`) also get an **intake** skill: + +- `loop-intake` skill — when a work item is too vague to verify "done", it asks one question at a time, pushes for exact values, and writes an open question + `needs-human` escalation instead of guessing. Clarifying up front keeps the loop from burning fix attempts on a goal that was never well defined. + Every scaffold also creates: - `loop-budget.md` — pattern-specific daily caps and kill switch diff --git a/tools/loop-init/dist/cli.js b/tools/loop-init/dist/cli.js index cfabecd..dc72211 100644 --- a/tools/loop-init/dist/cli.js +++ b/tools/loop-init/dist/cli.js @@ -30,6 +30,12 @@ const PATTERNS_NEEDING_FIX = new Set([ 'dependency-sweeper', 'post-merge-cleanup', ]); +/** + * Patterns that act on human-authored, often underspecified input (issues). + * They get the loop-intake skill so the loop clarifies a vague item or escalates + * it instead of guessing and burning fix attempts. + */ +const PATTERNS_NEEDING_INTAKE = new Set(['issue-triage']); const STATE_FILES = { 'daily-triage': 'STATE.md', 'pr-babysitter': 'pr-babysitter-state.md', @@ -190,6 +196,15 @@ async function scaffoldCircuitBreaker(pattern, tool, targetDir, templatesRoot, d await writeFile(ledgerPath, seed); console.log(' created: loop-ledger.json (circuit breaker)'); } +/** + * Scaffold the loop-intake skill for patterns that receive ambiguous human + * input, so the loop sharpens the goal (or escalates) before acting on it. + */ +async function scaffoldIntake(pattern, tool, targetDir, templatesRoot, dryRun) { + if (!PATTERNS_NEEDING_INTAKE.has(pattern)) + return; + await copyTemplateSkill(templatesRoot, 'SKILL.md.loop-intake', targetDir, tool, 'loop-intake', dryRun); +} function formatTokenCap(n) { if (n >= 1_000_000) return `${n / 1_000_000}M`; @@ -492,6 +507,7 @@ Examples: await copyL2Templates(pattern, tool, targetDir, templatesRoot, dryRun); await scaffoldCircuitBreaker(pattern, tool, targetDir, templatesRoot, dryRun); await scaffoldObservability(pattern, tool, targetDir, templatesRoot, dryRun); + await scaffoldIntake(pattern, tool, targetDir, templatesRoot, dryRun); await scaffoldConstraints(targetDir, templatesRoot, tool, dryRun); if (tool !== 'opencode' && !dryRun && !(await exists(path.join(targetDir, 'AGENTS.md')))) { const agentsTemplate = `# AGENTS.md @@ -529,6 +545,10 @@ npm run lint console.log('Circuit breaker wired (loop-guard skill + loop-ledger.json):'); console.log(' npx @cobusgreyling/loop-context --check --ledger loop-ledger.json'); } + if (PATTERNS_NEEDING_INTAKE.has(pattern)) { + console.log(''); + console.log('Intake wired (loop-intake skill): clarify a vague item or escalate before acting.'); + } console.log(''); console.log(`First loop (${tool}):`); console.log(` ${firstLoopCommand(pattern, tool)}`); diff --git a/tools/loop-init/package.json b/tools/loop-init/package.json index b906df5..c884875 100644 --- a/tools/loop-init/package.json +++ b/tools/loop-init/package.json @@ -1,6 +1,6 @@ { "name": "@cobusgreyling/loop-init", - "version": "1.3.3", + "version": "1.4.0", "description": "Scaffold loop engineering patterns and starters into any project. Supports Grok, Claude Code, Codex, Opencode and more. npx @cobusgreyling/loop-init . --pattern daily-triage --tool opencode", "type": "module", "bin": { diff --git a/tools/loop-init/src/cli.ts b/tools/loop-init/src/cli.ts index ba42b85..75b1561 100644 --- a/tools/loop-init/src/cli.ts +++ b/tools/loop-init/src/cli.ts @@ -47,6 +47,13 @@ const PATTERNS_NEEDING_FIX: Set = new Set([ 'post-merge-cleanup', ]); +/** + * Patterns that act on human-authored, often underspecified input (issues). + * They get the loop-intake skill so the loop clarifies a vague item or escalates + * it instead of guessing and burning fix attempts. + */ +const PATTERNS_NEEDING_INTAKE: Set = new Set(['issue-triage']); + const STATE_FILES: Record = { 'daily-triage': 'STATE.md', 'pr-babysitter': 'pr-babysitter-state.md', @@ -245,6 +252,21 @@ async function scaffoldCircuitBreaker( console.log(' created: loop-ledger.json (circuit breaker)'); } +/** + * Scaffold the loop-intake skill for patterns that receive ambiguous human + * input, so the loop sharpens the goal (or escalates) before acting on it. + */ +async function scaffoldIntake( + pattern: Pattern, + tool: Tool, + targetDir: string, + templatesRoot: string, + dryRun: boolean, +) { + if (!PATTERNS_NEEDING_INTAKE.has(pattern)) return; + await copyTemplateSkill(templatesRoot, 'SKILL.md.loop-intake', targetDir, tool, 'loop-intake', dryRun); +} + function formatTokenCap(n: number): string { if (n >= 1_000_000) return `${n / 1_000_000}M`; if (n >= 1_000) return `${n / 1_000}k`; @@ -587,6 +609,7 @@ Examples: await copyL2Templates(pattern, tool, targetDir, templatesRoot, dryRun); await scaffoldCircuitBreaker(pattern, tool, targetDir, templatesRoot, dryRun); await scaffoldObservability(pattern, tool, targetDir, templatesRoot, dryRun); + await scaffoldIntake(pattern, tool, targetDir, templatesRoot, dryRun); await scaffoldConstraints(targetDir, templatesRoot, tool, dryRun); if (tool !== 'opencode' && !dryRun && !(await exists(path.join(targetDir, 'AGENTS.md')))) { @@ -628,6 +651,11 @@ npm run lint console.log(' npx @cobusgreyling/loop-context --check --ledger loop-ledger.json'); } + if (PATTERNS_NEEDING_INTAKE.has(pattern)) { + console.log(''); + console.log('Intake wired (loop-intake skill): clarify a vague item or escalate before acting.'); + } + console.log(''); console.log(`First loop (${tool}):`); console.log(` ${firstLoopCommand(pattern, tool)}`); diff --git a/tools/loop-init/test/cli.test.mjs b/tools/loop-init/test/cli.test.mjs index bfea216..d041bbb 100644 --- a/tools/loop-init/test/cli.test.mjs +++ b/tools/loop-init/test/cli.test.mjs @@ -79,6 +79,27 @@ test('loop-init scaffolds issue-triage with bundled assets', async () => { } }); +test('loop-init scaffolds loop-intake for issue-triage', async () => { + const dir = await mkdtemp(path.join(tmpdir(), 'loop-init-intake-')); + try { + const { stdout } = await exec('node', [CLI, dir, '--pattern', 'issue-triage', '--tool', 'grok']); + await access(path.join(dir, '.grok', 'skills', 'loop-intake', 'SKILL.md')); + assert.match(stdout, /Intake wired/); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); + +test('loop-init does NOT scaffold loop-intake for report-only daily-triage', async () => { + const dir = await mkdtemp(path.join(tmpdir(), 'loop-init-no-intake-')); + try { + await exec('node', [CLI, dir, '--pattern', 'daily-triage', '--tool', 'grok']); + await assert.rejects(() => access(path.join(dir, '.grok', 'skills', 'loop-intake', 'SKILL.md'))); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); + test('loop-init rejects unknown pattern', async () => { await assert.rejects( () => exec('node', [CLI, '.', '--pattern', 'not-a-pattern', '--tool', 'grok', '--dry-run']),