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
1 change: 1 addition & 0 deletions docs/loop-design-checklist.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
71 changes: 71 additions & 0 deletions templates/SKILL.md.loop-intake
Original file line number Diff line number Diff line change
@@ -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: <question>`.
- 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): <one testable sentence>
Done when: <verifiable condition>
Open questions: <N> (needs-human) - <list>
```

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.
4 changes: 4 additions & 0 deletions tools/loop-init/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 20 additions & 0 deletions tools/loop-init/dist/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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`;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)}`);
Expand Down
2 changes: 1 addition & 1 deletion tools/loop-init/package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
28 changes: 28 additions & 0 deletions tools/loop-init/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@ const PATTERNS_NEEDING_FIX: Set<Pattern> = 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<Pattern> = new Set(['issue-triage']);

const STATE_FILES: Record<Pattern, string> = {
'daily-triage': 'STATE.md',
'pr-babysitter': 'pr-babysitter-state.md',
Expand Down Expand Up @@ -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`;
Expand Down Expand Up @@ -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')))) {
Expand Down Expand Up @@ -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)}`);
Expand Down
21 changes: 21 additions & 0 deletions tools/loop-init/test/cli.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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']),
Expand Down