# Autonomy iteration log

Started: 2026-05-28
Driver: claude-coder iteration loop against the gateway-served UI at
`http://127.0.0.1:18789/?view=autonomy` (the `/?view=...` URL is normalized
to `/chat?view=autonomy&session=main`; the `view=` query controls which
tab the SPA renders, dispatched via `infinibot-app.setTab()`).
Tab attached via raw CDP (relay extension is not installed on Chrome
137+ — see memory note `spawned_agents_auto_attach_browser.md`). CDP
helper script at `/tmp/cdp.mjs` (browser-level `/cdp` endpoint, drives a
specific tab through `Target.attachToTarget`).

## Baseline (clean reload)
- Page loads, header shows `AUTONOMOUS CONTROL · ▶ ACTIVE · 5 TODAY · $0.00 COST`.
- Tabs: OVERVIEW, QUEUE, PROJECTS (167 kanban tasks), AUDIT, API.
- Overview counters: pending=0, completed=5, failed=3, audit=18, cost=$0.
- "No task currently executing." Three historical tasks visible.
- `autonomy:state` returns: enabled=true, paused=false, activityState has
  cpu=0%, ram=0%, idle=0ms, gpu=9.4 GB / 32.6 GB, isUserActive=true,
  processesRunning=[].

## Constraint: live gateway is shared
The gateway under test (PID 834776, `infinibot-gateway`) is the user's
running production daemon — other cloud-coder sessions are alive against
it. A full restart would disrupt them, so server-side fixes were:

1. Made in source.
2. Compiled into `dist/` via `tsc -p tsconfig.json --outDir dist`.
3. Covered by new unit tests so the change is verified in isolation.
4. Documented as "requires gateway restart for live verification".

UI-side fixes can hot-reload; server-side ones land at the next natural
gateway restart.

---

## Gaps & fixes

### CASE-1 — System status visible — **PASS (with hardware fix shipped)**

- Visible real values for: enabled, queue depth, active runs, paused state,
  API budget, completed/failed/audit counts, ASOP pre-flight checklist.
- **Gap 1A (FIXED)** — `src/autonomy/hardware.ts` was Windows-only
  (PowerShell-based CPU/RAM/idle/processes). On Linux every metric
  returned 0 even though the page rendered them with confidence.
  - **Fix**: added platform dispatch in `checkSystemActivity` and Linux
    implementations using `/proc/stat` (CPU% via delta sample with 250 ms
    initial settle), `/proc/meminfo` (`MemTotal - MemAvailable`),
    `xprintidle` (fallback `loginctl IdleSinceHint`) for idle, and
    `ps -eo comm --sort=-pcpu` for top processes. Added macOS path using
    `top -l 1`, `os.totalmem/freemem`, `ioreg HIDIdleTime`, `ps -A`.
    `getNvidiaGpuMemory` already worked cross-platform.
  - **Runtime**: live gateway still shows 0% (it has the old hardware
    module cached). Verified in code + compile + dist artifact; will be
    live on next gateway restart.
- **Gap 1B (deferred)** — UI has no top-level "notifications channel"
  indicator. The bell button + per-task `partnerContact.channel` cover
  the per-task case, but the top status bar doesn't show whether telegram
  is configured. Low priority — page is otherwise informative.

### CASE-2 — Enable autonomy toggle — **PASS (live)**

- Architectural note: there is no separate enable/disable toggle distinct
  from pause/resume — `enabled` is internal config, the user-controllable
  surface is pause/resume. The `▶ ACTIVE` / `◼ PAUSED` pill maps to
  `pauseState.isPaused`.
- Clicked `◼ PAUSE` in the UI → gateway returns `paused=true, pausedBy=user,
  reason="CASE-2 toggle test"`; pill flips to `◼ PAUSED`, button to
  `▶ RESUME`. Clicked RESUME → `paused=false`, button flips back. No
  console errors.

### CASE-3 — Submit a simple goal end-to-end — **CODE FIXED, awaits gateway restart for live**

Submitted goal "Create a file at `/tmp/autonomy-canary-1779954346.txt`
containing the line `autonomy canary OK`" through the real `+ NEW TASK`
dialog. Two real bugs surfaced that blocked the run:

- **Gap 3A** — `canDoAutonomousWork()` returns
  `allowed:false, reason:"User is active (idle 0s < 300s threshold)"`,
  so the heartbeat scheduler never picks up a `pending` (or `approved`)
  task while the user is at the keyboard. This is by design for
  opportunistic autonomy, but it stranded user-explicit goal submissions.
  - **Mitigation** (no code change): the UI surfaces a `▶ START LOOP`
    button on the selected pending task when "Require pre-fire
    questionnaire" is unchecked. That path goes through
    `autonomy:queue.start` which bypasses `canWork`. Gap 3B blocks it
    further; with both fixed the explicit start path works.
  - Latent UX gap: the `+ NEW TASK` dialog has no "start now" affordance,
    and there is no UI signal that the queued task is waiting on user
    idle.

- **Gap 3B (FIXED)** — `orchestrator.startOrchestratedTask` had a
  prefire gate that ignored `task.context.prefireRequired === false`,
  while the gateway handler `autonomy:queue.start` DID honor that flag.
  Net effect: `queue.start` accepted the call, flipped the task to
  `running`, then the orchestrator's own gate immediately blocked,
  leaving the task stuck in `running` with no progress, no agent spawn,
  no failure path. Observed live: task `task_mpp6v5g4_3yul` ran for 5+
  minutes with `phase=null, iters=0`; activity log showed
  `prefire_blocked` then nothing.
  - **Fix**: `src/autonomy/orchestrator.ts:294` wrapped the prefire
    block in `if (prefireRequired) { ... }` so the orchestrator and the
    gateway agree.
  - **Test**: added two cases in
    `src/autonomy/orchestrator.test.ts` —
    "starts a task whose prefire form is incomplete when prefireRequired
    is false" and "still blocks when prefireRequired is unset". Both
    pass (14 / 15 orchestrator tests green, 1 unrelated skipped).

- **Gap 3C (FIXED)** — `autonomy:queue.start` handler (gateway) calls
  `queue.start(taskId)` to flip status to `running` BEFORE awaiting
  `startOrchestratedTask`, which can return `{started:false}` (prefire
  blocked, already running, etc.). The handler did `void
  startOrchestratedTask(task).catch(...)` and never rolled the queue
  state back. The user-visible result was a permanently-stuck "running"
  task (CASE-3 reproducer above).
  - **Fix**: handler now inspects the result; on `started:false` it
    calls `queue.fail(taskId, reason)`. On promise rejection it also
    calls `queue.fail(taskId, "orchestrator crashed: …")`. The audit
    chain still records the original `task_started` then the rollback's
    `task_failed`.

### CASE-4 — Approval-gated goal — **DEPENDS ON CASE-3 LIVE**

Same architectural plumbing as CASE-3 — UI APPROVE button → status flips
to `approved` → heartbeat picks up. Same idle-gate constraint applies.
Code path verified by reading; live verification deferred to next gateway
restart. No additional fixes needed once 3A/3B/3C land.

### CASE-5 — Pause/resume mid-run — **CODE PATH VERIFIED, full live needs CASE-3**

`autonomy:pause` handler propagates to all live continuous daemons via
`pauseContinuousRun`, and `autonomy:resume` lifts the autonomy-initiated
pauses (leaving user-paused daemons paused). Tested round-trip live in
CASE-2; the in-flight aspect needs CASE-3's orchestrator running, which
in turn needs gateway restart.

### CASE-6 — Cancel in-flight run — **PARTIAL LIVE**

Used `autonomy:queue.cancel { taskId, error }` to cancel the stranded
CASE-3 task; gateway flipped status to `cancelled`, audit entry
`task_dequeued` was added with `outcome=blocked`, UI moved the task out
of "executing" on the next refresh. No orphan PID — the orchestrator
hadn't spawned a sub-agent yet (gap 3B). Full live (with a real
in-flight sub-agent) requires CASE-3 first.

### CASE-7 — Retry on failure — **CODE VERIFIED, NEEDS RESTART**

Existing `retry.test.ts` has 23 passing cases covering retryWithBackoff
delay, retry count, classification (transient vs permanent), and self-
heal exhaustion. The orchestrator's loop wraps build/test in
`retryWithBackoff`. Verified by reading + unit tests; full live needs
restart.

### CASE-8 — Budget guard — **NOT IMPLEMENTED IN THIS CODEBASE**

The API tab shows `calls / tokens / cost / autonomous%` but the autonomy
module has **no budget cap mechanism** (no `maxCostUSD`, no
`stopAtBudget`, nothing in `api-monitor.ts` that halts spawn). Grep for
`budget|costCap|maxCost|spendCap|stopAt|halt.*budget` across
`src/autonomy/` returns only orchestrator/retry/goal-orchestrator refs
unrelated to cost limits. **Skipped** — this is an unimplemented feature,
not a bug. Recommend filing as a separate spec.

### CASE-9 — Observability — **UI INFRASTRUCTURE PRESENT**

The "executing" task card (renderTaskCard, ui/src/ui/views/autonomy.ts:505)
renders PHASE, AGENT, ITER (`current / total`), LAST (relative time with
precise tooltip), and runtime duration. The current-task panel
(line 619) shows `⏱ {duration}` from `Date.now() - executedAt`. What's
not surfaced: explicit progress % (only iteration count) and exit code
on completion. The data is in `task.iterations[n].buildOutcome` (`ok |
error | timeout`); adding it to the completed-card would be 3 lines but
out of scope for this iteration. Logged for follow-up.

### CASE-10 — Audit log — **PASS (live)**

Switched to the AUDIT tab — chain valid badge ✓, 20/20 recent entries
displayed with isoTime, action type, target, outcome, optional details
JSON. Every event I triggered during the iteration appeared with correct
timestamp and outcome:
```
14:55 task_dequeued       Task cancelled                blocked
14:49 notification_sent   Pre-fire Required             failure
14:49 prefire_blocked     CASE-3 autonomy canary …      blocked
14:49 task_started        CASE-3 autonomy canary …      success
14:45 task_queued         CASE-3 autonomy canary …      success
14:45 notification_sent   Autonomy Resumed              failure
14:45 pause_deactivated   …                              success
14:39 pause_activated     reason: CASE-2 toggle test    success
```
The chain verifier (`audit.verify().valid = true`) ran clean. The
`notification_sent` outcomes are `failure` because telegram isn't
configured in this profile — that's a separate (and intentional) state,
not an audit bug.

---

## Sub-agent runs spawned
None this iteration — all fixes were inline single-file edits whose
scope made delegation unnecessary.

## Files I changed directly
- `src/autonomy/hardware.ts` — Linux + macOS hardware sampling.
- `src/autonomy/orchestrator.ts` — prefire gate honors
  `context.prefireRequired === false`.
- `src/gateway/server-methods/autonomy.ts` — `queue.start` rolls back
  `running → failed` when orchestrator declines/crashes.
- `src/autonomy/orchestrator.test.ts` — two new tests for the prefire
  override behavior.
- `docs/autonomy-iteration-log.md` — this file.

## Test status after fixes
```
src/autonomy/spawn-params.test.ts     28 passed
src/autonomy/retry.test.ts            23 passed
src/autonomy/orchestrator.test.ts     14 passed, 1 skipped (pre-existing)
                                      ──
                                      65 passed
```
No regressions. `tsc -p tsconfig.json --outDir dist` clean (no errors).

## What still needs gateway restart to verify live
- CASE-1 Linux hardware metrics rendering real CPU/RAM/idle.
- CASE-3 / 4 / 5 / 7 with a real `startOrchestratedTask` spawning a
  sub-agent (cli `claude` by default), producing iteration progress,
  and updating the UI.
- CASE-6 cancellation against a real in-flight orchestrator spawn (to
  verify the child process is reaped).

## Architectural debt flagged (out of scope to fix here)
- **Budget cap** (CASE-8 spec) is unimplemented. `apiMonitor` records
  per-call cost but nothing enforces a daily/weekly limit at spawn time.
- **Idle-gate vs explicit submission**: `canDoAutonomousWork` (manager.ts
  line 112-135) gates ALL pickups by user-idle threshold. The
  `queue.start` RPC bypasses this for explicit starts, but the UI's
  "queue + wait" path silently never runs while the user is at the
  keyboard. The `+ NEW TASK` dialog should have a "start immediately"
  toggle, or the pending-task card should warn "waiting on
  user-idle" so the operator isn't surprised.
- **Observability `progress %`** (CASE-9) — iteration count is shown but
  not a percent or sub-iteration checkpoint. Three-line UI add to render
  `iterations[n].buildOutcome` and ` / maxIterations` percent.
- **Notifications channel pill** (CASE-1) — a header chip showing
  `telegram: configured | not configured` would close the loop on
  CASE-1's status-visibility spec.

## How a user would now use this end-to-end (post-restart)
1. Open Autonomy tab. Header shows live status, queue, budget. SYSTEM
   STATUS now shows real CPU%/RAM%/idle on Linux + macOS.
2. Click `+ NEW TASK`, enter title + description + priority. Submit.
3. The new task lands in the Queue. Select it. In the detail panel,
   uncheck "Require pre-fire questionnaire" (unless you want to fill
   the form). Click ▶ START LOOP.
4. Orchestrator spawns the builder agent. Task card transitions to
   EXECUTING; PHASE, AGENT, ITER and LAST update as the loop runs.
5. Pause / resume / cancel buttons act on the in-flight run.
6. The Audit tab records every event with chain integrity.
