# Time-Budgeted Background Loop

> Per-iteration loop spec for `/apt:research --budget <duration>`. Load
> when any budget is set (explicit flag, named alias, or prose-detected)
> OR when running a single session that needs the 4-role discipline.

## Why this exists

A naive "keep researching" loop manufactures false confidence over time —
the agent finds more weak sources that confirm its initial position, prunes
counter-evidence as noise, and arrives at the budget cap with a polished
wrong answer. This appendix encodes the disciplines that prevent that:

- **4-role specialization** so a single agent can't self-rationalize
- **Priority by impact × centrality × uncertainty** so load-bearing claims
  get attacked early, not last
- **Trigger-gated spar** so cross-LLM challenges fire on real conflict, not
  on a clock
- **Citation audit** so fabricated DOIs get quarantined, not amplified
- **Dual stop states (CONVERGED vs STALLED)** so the skill never confuses
  stuckness with done-ness
- **Early termination** so a budget is a CAP not a target — converging at
  hour 3 of an 8h budget stops the loop, doesn't burn the remaining 5h

## Budget as the primitive

The skill accepts one canonical flag plus three named aliases:

| Flag | Resolves to | Typical use |
|---|---|---|
| `--budget <duration>` | Explicit cap; ISO-style `30m`, `4h`, `10h`, `2d` (1m – 72h range) | Any custom horizon |
| `--overnight` | `--budget 8h` | While you sleep |
| `--weekend` | `--budget 48h` | Long horizon, Friday → Monday |
| `--background` | `--budget 4h` | During work day |

Auto-detection from prose (Step 2 of SKILL.md) maps natural-language signals
into a budget value:

| Prose signal | Inferred budget |
|---|---|
| "overnight", "while I sleep" | 8h |
| "weekend", "over the weekend", "by Monday" | 48h |
| "for X hours/days/minutes" | X |
| "while I'm at work" | 8h |
| "in the background" | 4h |
| "really thoroughly", "deep dive", "question my beliefs" | 4h |
| Decomposition produces ≥8 sub-questions OR ≥3 contested perspectives | 4h |
| (no signal AND no flag) | none — single-session pipeline runs |

The auto-detection announcement (Step 2 closing line) MUST surface BOTH the
budget value AND the signal that triggered it:

```
[apt:research] {Mode} / {Domain} — auto-budget 8h from signal "overnight".
Pass --budget <duration> to override or --no-budget to disable.
```

The user can pass `--no-budget` to force the single-session pipeline even
when prose signals would otherwise trigger budget mode. Explicit user
intent always wins.

## Single-session pipeline (no budget set)

If no budget is set AND no prose signal fires:
- Run Step 5 (the 4-role iteration) up to 5 times
- After 5 iterations without terminal state, force `STALLED` with
  `## Cannot resolve in single-session mode — re-run with --budget 4h or higher`
- This is the cap that prevents the lighter single-session mode from
  silently growing into something that should have been a budgeted run

## Agent roles

Per iteration, FOUR `apt-researcher` agents are spawned in a single
parallel message. Each has an isolated context (no shared scratch state)
and a distinct objective.

### Builder

> System prompt seed: "You are a synthesizer. Read the current claim graph
> and the spec. Advance the synthesis on the chosen attack claim. Propose
> what we should now believe, given the evidence, and why. You may NOT
> falsify — that is the Skeptic's job. You may propose new claims if they
> follow from existing evidence."

Tool surface: full read (Read/Grep/Glob), WebSearch, WebFetch, Context7
MCP, plus the domain-specific tools from `domain-tools.md`.

Output shape: a structured block written to the Task return:
```
## Builder
### Position
{one paragraph synthesis}
### Supporting evidence (existing)
- {source_id}: {claim_id} — {one-line why it supports}
### New claims proposed (if any)
- {statement} — {evidence_for source_ids} — {depends_on claim_ids}
```

### Skeptic

> System prompt seed: "You are an adversary. Read the chosen attack claim.
> Your only job is to find concrete counter-evidence or a logical/empirical
> falsification. Confirming the claim or hedging is failure. Produce at
> least one specific challenge with a citation OR explicit reasoning."

Tool surface: same as Builder.

Output shape:
```
## Skeptic
### Falsification target
{claim_id}: {statement}
### Counter-evidence
- {source_id or new source}: {quote} — {why it falsifies}
### Failure mode of the claim
{one paragraph — when does this claim break? What context invalidates it?}
### Verdict
{retract | downgrade | contest | no-falsification-found}
```

If `no-falsification-found` AND the claim was at `confidence: high`,
the Skeptic must explicitly state: "I attempted falsification via [search
queries / reasoning paths] and found no counter-evidence. The claim
survives this attack."

### Retriever

> System prompt seed: "You are an evidence finder. Read the spec, the
> claim graph, and the list of sources already in sources.json. Your
> ONLY job is to find NEW sources (not already in sources.json) that
> bear on any open or contested claim. Do NOT synthesize. Do NOT take
> sides. Return URLs + extracted quotes + source-type classification."

Tool surface: WebSearch, WebFetch, Context7, plus domain-specific
search tools (PubMed for empirical, GitHub Code Search for code, arxiv
for mechanistic).

Output shape:
```
## Retriever
### New sources found
- {url}: tier={tier}, type={type}, derivation_chain={[urls cited]},
  quote_anchor={"..."}
### Search trail
- query: "{q}" — engine: {WebSearch|PubMed|GitHub|...} — useful_hits: {N}
### Frontier status
{exhausted | active | promising} — {one-line why}
```

The `frontier status: exhausted` signal is load-bearing for the STALLED
stop state. Retriever must report it honestly — if every new search
returns sources already in `sources.json` (modulo near-duplicates),
the frontier IS exhausted for this iteration's queries. Judge uses this
to decide stop state.

### Judge

> System prompt seed: "You are the arbiter. Read the Builder, Skeptic, and
> Retriever outputs plus the current claim graph. Update claim statuses
> with explicit evidence deltas. You may NOT propose new claims of your
> own; you may accept or reject claims proposed by Builder. Apply domain
> gates: demote first-principles output to `hypothesis` in non-mechanistic
> domains; require Crossref/PubMed-verified citations in empirical
> domains."

Tool surface: read-only access to `claims.json`, `sources.json`, the three
role outputs, and `research-spec.md`. NO web tools (no temptation to
re-research).

Output shape (mandatory JSON, parsed and merged into the graph):
```json
{
  "iteration": N,
  "attacked_claim": "C-007",
  "claim_deltas": [
    {
      "claim_id": "C-007",
      "old_status": "live",
      "new_status": "contested",
      "old_confidence": "medium",
      "new_confidence": "low",
      "reason": "Skeptic surfaced S-019 (peer-reviewed) contradicting; downgrade per source-tier weighting."
    }
  ],
  "new_claims": [
    {
      "id": "C-021",
      "statement": "...",
      "source_tier": "...",
      "evidence_for": ["S-019"],
      "depends_on": [],
      "contradicts": ["C-007"],
      "impact": "medium"
    }
  ],
  "new_sources_accepted": ["S-019", "S-020"],
  "new_sources_quarantined": ["S-021"],
  "frontier_exhausted_this_iter": false,
  "tier1_conflict_introduced": true
}
```

## Priority formula

```
priority(C) = impact(C) × centrality(C) × uncertainty(C)
```

- `impact(C)`: `high=1.0, medium=0.6, low=0.3` — sourced from
  `research-spec.md ## Critical claims`, may be bumped by Judge when
  graph topology changes.
- `centrality(C)`: 0-1, computed by Judge each iteration (see
  `claim-graph-schema.md`).
- `uncertainty(C)`: 0-1, computed by Judge each iteration.

The next claim to attack is `argmax(priority)` over claims with
`status ∈ {open, contested, live}`. Tie-break by: oldest
`last_attacked_iter`, then by `impact`, then alphabetical claim ID.

**Why this beats "lowest confidence first":**

A high-confidence claim that everything else depends on (high `impact`,
high `centrality`) MUST be attacked early. If it survives, the rest of
the graph rests on firmer ground. If it falls, the cascade reveals which
descendants were also wrong before more time is spent building on them.

"Lowest confidence first" chases the noisiest claims and leaves the
load-bearing assumptions untested until late, when their failure is
maximally expensive.

## Spar triggers (NOT every-N-iterations)

`/apt:spar` fires from Step 8a when ANY of these conditions hold after
Judge has updated the graph:

| Trigger | Detection |
|---|---|
| **Tier-1 conflict** | Two claims with `confidence ≥ medium` and a `contradicts` edge between them |
| **Critical-claim stall** | A claim flagged in `## Critical claims` has had `status ∈ {contested, open}` for ≥ 2 consecutive iterations with no confidence change |
| **Vendor-only evidence** | A claim's `evidence_for[]` contains ONLY sources with `tier: vendor-doc` AND `impact ≥ medium` |

Spar prompt MUST include:
1. The current synthesis position (1 paragraph)
2. The specific claim under challenge (statement + current evidence)
3. Explicit instruction: "Produce a concrete claim status delta with citation diff. Reject debate summaries with no actionable delta."
4. The current `claims.json` excerpt for the challenged claim + neighbors

Spar return is parsed for a Judge-shaped delta. If the return does NOT
include `claim_deltas[]`, mark the spar `## status: theater` in
`spar/{round-slug}.md` and DO NOT update the graph. This prevents
"both LLMs trained on the same corpus rephrased each other" from
corrupting the synthesis.

## Stop states + early termination

Two terminal states. The skill MUST distinguish them in `RESEARCH.md`
status header and in the final report. **A budget is a CAP, not a target.**
The loop stops the instant a terminal state fires — remaining budget is
not consumed.

### CONVERGED

All of:
- Every claim in `## Critical claims` is at `confidence: high`
- Zero unresolved tier-1 conflicts (every contradicts-edge with both ends
  `confidence ≥ medium` has been resolved by retracting one side)
- Retriever reported `frontier_exhausted_this_iter: true` for the last 2
  iterations (no new sources to be found)
- Citation audit ran in one of the last 4 iterations with zero new
  quarantines

Output: full synthesis with confidence statements. Budget remaining is
reported in the final report (e.g. "CONVERGED at iter 12 of estimated 24,
budget used: 3h 14m of 8h").

### STALLED

Retriever frontier exhausted (no new sources for 2 iterations) but
critical claims remain at `contested` or `confidence < high`. Output
MUST include a `## Cannot resolve without` section listing what new
evidence would be required:

- Real-world experiment (specify the experiment)
- Paywalled paper access (specify the citation)
- Vendor disclosure (specify what disclosure)
- Domain expert interview (specify the question)
- Code execution / falsification harness (specify the test)
- Telemetry / instrumented data (specify the measurement)

STALLED is NOT a failure — it is the honest answer when evidence
genuinely runs out. Confident-sounding synthesis on a stalled question
is the worst outcome the skill can produce; STALLED prevents it.

### Budget-cap exhaustion

When `elapsed ≥ budget`, force `STALLED` with `## Stopped at budget cap`
regardless of mid-iter state. Honest, even if mid-attack. The reason
this is STALLED and not its own state: hitting the cap means evidence
acquisition was not yet complete, which IS the STALLED condition. The
`## Stopped at budget cap` note distinguishes it from a frontier-exhausted
STALLED for the reader.

### Early-termination rule

Whichever terminal state fires first wins. The skill body MUST check
terminal-state criteria at the END of every iteration before scheduling
the next wake. A 3-hour CONVERGED at hour 3 of an 8-hour budget exits
cleanly; the saved compute is reported in the final summary, NOT spent
on extra iterations "just in case."

## Progress checkpoints (tail-able)

Long-running loops need observable progress without interrupting the
loop. At the START of every iteration, the skill body appends ONE line
to `.aperant/research/{task-id}/progress.log`:

```
{ISO timestamp} iter={N}/{est_max} budget={used}/{cap} next_attack={C-id} priority={value} status={INVESTIGATING|SYNTHESIZING|...}
```

Example:
```
2026-05-24T10:14:22Z iter=4/24 budget=42m/8h next_attack=C-007 priority=0.84 status=INVESTIGATING
2026-05-24T10:34:15Z iter=5/24 budget=1h12m/8h next_attack=C-012 priority=0.71 status=INVESTIGATING
2026-05-24T10:55:08Z iter=6/24 budget=1h33m/8h next_attack=C-007 priority=0.69 status=INVESTIGATING
```

Users can `tail -f .aperant/research/{task-id}/progress.log` to watch the
loop without blocking it. The line is intentionally one-per-iteration
(not one-per-agent-return) to keep the log scannable. The richer
per-iteration trace lives in `iteration-log.md`.

At terminal state, the final line includes the verdict:

```
2026-05-24T13:28:51Z iter=12/24 budget=3h14m/8h verdict=CONVERGED critical_claims_high=5/5 unresolved_conflicts=0
```

`est_max` is recomputed each iteration based on average iteration
duration so far × remaining budget. It is a hint, not a guarantee.

## Wake-loop continuation

End of each iteration (when status = CONTINUE), the skill calls
`ScheduleWakeup`:

```
ScheduleWakeup({
  delaySeconds: 1200,                                          // 20 min — past cache TTL
  prompt: "/apt:research --resume {task-id} --budget {remaining}",
  reason: "apt:research budget loop iter {N+1}, next attack: {claim_id}, budget remaining: {remaining}"
})
```

The `--budget {remaining}` arg keeps the budget value monotonically
decreasing across wakes so a crash + manual restart preserves the cap.

Then exits cleanly. The host CLI delivers the wake event when ready and
the skill resumes from `RESEARCH.md` status (which was set to
`INVESTIGATING` before the spawn).

This is the same primitive `/apt:watch-ci` uses. No daemons, no
cross-CLI re-exec, no external scheduler.

If `ScheduleWakeup` is unavailable in the current host CLI environment,
the skill MUST refuse `--budget` with an explicit error pointing the
user at the manual `--resume` workflow.

## Resume semantics

`--resume <task-id>` re-enters at the iteration boundary:
1. Read `research-spec.md` (immutable, locked at Step 3)
2. Read `claims.json`, `sources.json`, `RESEARCH.md` status
3. Read `## Next attack` from `RESEARCH.md` (written by previous iteration)
4. Compute remaining budget = `original_budget - elapsed_at_start_of_iter`
   (Step 9 of SKILL.md records elapsed in the iteration log before
   scheduling the wake)
5. Go to Step 5 with that claim as the attack target

If the previous run was killed mid-iteration (incomplete Judge output in
`iteration-log.md`), discard the partial iteration entry and re-run from
the last fully-recorded iteration. The graph state on disk is canonical;
the partial log entry is lost work, not corrupted state.

## Budget override precedence

| Source of budget value | Precedence |
|---|---|
| Explicit `--budget <duration>` flag | wins all |
| Explicit `--no-budget` flag | wins all (forces single-session mode) |
| Named alias (`--overnight`, `--weekend`, `--background`) | beats prose detection |
| Prose detection (from Step 2 of SKILL.md) | beats default |
| Default | single-session mode (no budget loop) |

Bounds: `1m ≤ budget ≤ 72h`. Outside-range values are rejected with an
explicit error. The 72h ceiling is intentional — longer than that and
the wall-clock predictability of the artifact starts to suffer.
