self-nudge-loop
Build a deadline-preserving same-session autonomy loop using Kiro Crew's AutoNudgeService. One long-lived session keeps working toward a goal; each completed loop turn schedules the next nudge, while
SKILL.mdskills/self-nudge-loop/SKILL.md
GitHubSelf-Nudge Loop
Overview
AutoNudgeService (in kiro_crew.autonudge) keeps a single chat session working toward a goal by re-feeding a nudge on its persisted deadline, deferred while a user turn is active. Unlike cron_add, the nudge runs in the same session — warm memory, same tools, same conversation history. State persists across gateway restarts via ~/.kiro/crew/autonudge.json.
Usage
Use when the user wants:
- "continuous improvement" that doesn't die between chats
- one long-lived session that auto-resumes whenever idle
- a north-star / roadmap / tasks anchor-file pattern driving autonomous work
- the loop to survive logout and gateway restart
Do NOT use for:
- work best run in fresh isolated session → use plain
cron_add - parallel independent tasks → use
spawn_run - external-system callbacks → use
register_hook
Feature flag
On by default. To disable:
export KIROCREW_AUTONUDGE=0 # add to systemd unit Environment= or ~/.kiro/crew/env
kirocrew restart
When disabled, the REST API returns 503 and the UI popover surfaces the error.
Core Concepts
Deadline-preserving timer — after a loop turn completes, the service stores an absolute next_due_ts and arms a per-slot timer. A user turn cancels the pending timer task so it cannot race the human, but does not move the deadline; when that turn ends, the timer resumes toward the same deadline and may fire shortly afterward if already due. A delivered loop turn starts the next full interval from that turn's end.
Three files the agent owns (convention, not enforced):
| File | Role |
|---|---|
north_star.md |
Immutable goal. Why the loop exists. |
roadmap.md |
Phases / milestones. Agent may edit as it learns. |
tasks.md |
Active checklist. Agent checks off / adds items each cycle. |
Kill switches (any of the below):
- The looping agent itself calls the session-bound
autonudge_stopMCP tool — preferred, with no loop ID or token handling needed. Ordinary prompt loops are removed; structured monitor records follow their own retained-stop contract. - In the UI popover, click the Pause icon button (named "Pause loop") to deactivate the loop. To delete it for good, Pause first, then click Clear stopped goal and confirm.
- Create the configured
STOPsentinel file — next cycle halts. max_cyclesreached — loop deactivates (not removed, so you can resume).DELETE /api/autonudge/{loop_id}orautonudge_svc.remove(id).
Warning: a STOP sentinel file is ONLY checked if the loop has a non-empty stop_sentinel_path. When a UI or REST arm leaves it blank on a live dashboard or channel slot, the service fills in a per-session default, <workspace>/.stop-<slot>, and replaces {{STOP_FILE}} in the nudge message with that path on every send. Set the field explicitly over REST to use a custom path. If the stored path is empty, the sentinel file is ignored and nudges keep firing. A path pointing at a sensitive location is refused at arm time, and a persisted path is re-homed onto the current data home on reload — silently dropped if it cannot be repaired. Prefer the autonudge_stop MCP tool for in-loop halting.
Overlap with babysit: the bundled babysit skill is the monitor_*-native
guide for same-session loops and is the one to reach for when the job is watching a
PR, a CI run, a ticket, or a deployment. This skill covers the scaffolded
goal/roadmap/tasks pattern and the raw service surface underneath it. Use one
vocabulary: interval_secs and max_cycles as the MCP tools name them.
Restart survival — on AutoNudgeService.start(), loops marked active:true in ~/.kiro/crew/autonudge.json are reloaded and re-armed from their persisted next_due_ts. A lost best-effort deadline write degrades to one fresh interval after restart; the normal path does not reset the countdown.
How to start a loop
From an agent session (preferred): call the MCP tools. monitor_start(message, interval_secs?, gate?, max_cycles?, max_runtime_secs?, banner?) arms a loop on the
calling session, monitor_update(message?, interval_secs?, max_cycles?, max_runtime_secs?) revises it in place without losing its cycle count, and
autonudge_stop() halts it. No token handling is involved. The tool's
interval_secs (default 300) is stored as the loop's idle_secs; the raw
REST/dataclass field defaults to 60.
monitor_start defaults to 24 delivered cycles and a 14,400-second wall-clock
budget; use explicit positive finite bounds for unattended work. Raw REST defaults
both bounds to 0 (unlimited). Reaching either bound is a runaway backstop, not a
successful finish: check the exit condition every cycle and call autonudge_stop
deliberately.
From the UI:
- Click the
🎯 Set a goal(bullseye) icon in the chat composer toolbar (lit green when active, dim when off). - In the popover: paste your nudge message, set idle seconds (min 15, default 60), set max cycles (0 = unlimited), then click the Play icon button (named "Start loop").
- Close the popover. Loop runs in the background. Icon stays lit across tab closes / logins.
From an external script or when debugging (REST, human-operated only):
/api/autonudge is a user-scoped endpoint. Agents should use the MCP tools
above and must not read, print, or request local-secret/token material. If a human
needs the raw REST surface, obtain a short-lived dashboard token in a private
terminal (for example with kirocrew token) and keep it out of chat and logs:
# Human-controlled terminal only. Obtain this without exposing it to an agent.
TOKEN='<short-lived dashboard token>'
curl -sf -X POST "http://127.0.0.1:5476/api/autonudge?token=$TOKEN" \
-H "Content-Type: application/json" \
-d '{
"slot_key": "chat-3-1777089677",
"message": "Read ~/project/north_star.md, pick next task, execute.",
"idle_secs": 60,
"max_cycles": 30,
"max_runtime_secs": 14400,
"stop_sentinel_path": "/home/user/project/STOP"
}'
Common 403 traps (all have the same error body {"error":"Token required"}):
- Forgetting the
?token=…query param. The/api/autonudgearm/list/edit/delete routes do not acceptX-Internal-Secretauth; they require the dashboard owner (_require_monitor_ownerindashboard/handlers/autonudge.py). Which paths accept the machine secret is defined by_STRICT_INTERNAL_API_PATHS/_MIXED_INTERNAL_API_PATHSindashboard/server.py. - Calling
/api/token/localwithoutX-Local-Secret— the endpoint is in_BYPASS_EXACT(no token needed) but still validates the machine secret via HMAC compare. Missing/wrong secret →{"error":"invalid secret"}403. - Non-loopback source. Token issuance and most internal paths require
is_loopback(request.remote)regardless of auth material.
Slot key — the slot_key in the POST body is your dashboard session ID. Inside MCP subprocess code, read KIROCREW_SESSION_KEY (format dashboard:chat-<N>-<epoch>) and strip the dashboard: prefix.
Endpoints:
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | /api/token/local |
X-Local-Secret header, loopback only |
issue a user token for local bootstrap |
| GET | /api/autonudge |
?token=… |
list all loops |
| GET | /api/autonudge/slot/{slot_key} |
?token=… |
loop bound to slot (or null) |
| POST | /api/autonudge |
?token=… |
start a loop; 409 if the slot already has any automation record |
| PATCH | /api/autonudge/{loop_id} |
?token=… |
edit message / idle / active |
| DELETE | /api/autonudge/{loop_id} |
?token=… |
stop and remove |
Preferred path for agents: use the autonudge_stop MCP tool for self-halt (emits a stop directive the session applies to its own bound loop — no token handling needed on your side; the tool reply is a request, not confirmation). The REST flow above is for external scripts, new-loop arming, and debugging.
Per-cycle agent behaviour (to put in your nudge message)
Good nudges tell the agent to:
- Check for a halt condition (STOP sentinel, goal achieved, unrecoverable error). If halt → call the
autonudge_stopMCP tool with a brief reason, then return. Do NOT just stop working silently: if the AutoNudgeService loop isn't removed, it will keep firing nudges everyidle_secsand eventually blow the context window. - Read north_star.md, roadmap.md, tasks.md (cheap, keeps context fresh).
- Pick the single highest-leverage next step.
- Execute it (suggest ≤5 tool calls per cycle).
- Update tasks.md (check off / add / reorder).
- Append a one-line entry to a
## Cycle Logsection of tasks.md. - Stay silent unless phase boundary / blocker / user-facing decision.
Common Mistakes
Forgetting the feature flag — service loads but does nothing. Check kirocrew logs | grep AutoNudge for the "disabled" message.
interval_secs too short — sub-30s nudges thrash context. 300s is the value to use for external polling (CI, review bots); reserve 60s for tight local iteration.
No kill switch in the nudge text — user ends up hunting the loop id. Always instruct the agent to check a STOP sentinel.
Expecting the interval to reset across restart — the persisted next_due_ts normally survives and is re-armed. A fresh interval occurs only when the best-effort deadline write was lost. Do not depend on a restart to postpone a due cycle.
Agent re-pings the same blocker every cycle — explicitly tell it to post a blocker ONCE, then stay silent until it clears.
Multiple loops on the same slot — the service enforces one automation per session. monitor_start is create-only and refuses while an active loop or structured monitor exists; revise the bound prompt loop with monitor_update instead of re-arming it.
Hardened Loop Pattern (production recipe)
Lessons from shipping two long-running loops (RRL self-improvement + HAA verification). Adopt this pattern any time a loop will run overnight or unattended.
1. Definition of Done, not "stop when I say"
A loop without a self-retire rule becomes a forever-firing context-eater. Every loop's anchor doc (usually LOOP.md next to the project) MUST have a "Definition of Done" section listing N auditable boolean criteria. The nudge message MUST instruct the agent to evaluate these criteria first, and call autonudge_stop(reason="definition of done met") when all are true.
Template:
## Definition of Done
The loop retires itself when **all** of these are true:
1. <boolean criterion 1 — shell-checkable if possible>
2. <boolean criterion 2>
3. <boolean criterion 3>
4. **Zero stale markers.** `grep -rn "TODO\|FIXME\|XXX\|TBD" <planning-tree>` returns empty.
5. **Board drained.** No cards in todo/in-progress/review (only backlog/done/archived).
When all true: agent posts the DoD checklist with ticks, calls `autonudge_stop(reason="DoD met")`, stays silent.
2. Three overnight-failure mitigations — bake them into every arming
| Failure | Mitigation (REQUIRED) |
|---|---|
Context-window overflow after ~100 cycles (each nudge+reply adds ~170 B; session SIGTERMs at ContentWindowOverflow) |
max_cycles: 30 per arming. Re-arm manually for more. |
| STOP file does not stop before delivery when no service sentinel was configured | UI arming has no stop-file input, but the service gives the loop its own default stop file; put {{STOP_FILE}} in the nudge so the loop knows that path, and still check it in the nudge and call autonudge_stop. Raw REST callers may set stop_sentinel_path to a custom path. |
Credential-file leak via urllib ValueError echoing raw cookie-jar contents (e.g. ~/.config/<app>/credentials) into transcripts |
Use http.cookiejar.MozillaCookieJar(path).load() + urllib opener, OR curl -b <cookie-jar>. Scrub auth-path exceptions to type(e).__name__ only. |
3. kanban-md integration (optional but recommended)
If the project has a kanban-md board, the nudge MUST instruct the agent to drive it via the CLI (atomic flocked writes), never via strReplace on task .md files.
Minimal kanban-md ops the agent needs (keep loop-<project> unchanged across cycles):
BOARD=<abs path to board dir>
kanban-md --dir $BOARD list --status todo --json # read
kanban-md --dir $BOARD list --claimed-by loop-<project> --json # resume a held card if any
kanban-md --dir $BOARD pick --claim loop-<project> --status todo # only if no held card
kanban-md --dir $BOARD edit <id> --block "…" --release # held card is blocked: mark it, drop the claim, pick instead
kanban-md --dir $BOARD show <id> # read spec
kanban-md --dir $BOARD edit <id> --claim loop-<project> --append-body "…" # append progress note
kanban-md --dir $BOARD handoff <id> --claim loop-<project> --note "…" --release # move to review + release
4. Hardened nudge template
Replace <PROJECT>, <project>, <ANCHOR>, <STOP_PATH>, and <BOARD_PATH> for your project. This template encodes all failure mitigations above.
Continue the <PROJECT> loop.
Definition of Done: <ANCHOR>/LOOP.md §Definition of Done.
STOP / EXIT CHECKS (every cycle, in order, before anything else):
1. If <STOP_PATH> exists: call autonudge_stop(reason="sentinel"), post "Loop halted by sentinel.", do nothing else.
2. If the Definition-of-Done criteria in LOOP.md are all met: call autonudge_stop(reason="DoD met"), post the DoD checklist with ticks, stop.
BOARD (skip this block if the project has no kanban-md board):
3. `kanban-md --dir <BOARD_PATH> list --status todo --json` — read todo column.
4. `kanban-md --dir <BOARD_PATH> list --claimed-by loop-<project> --json` — resume a held card if any; if the held card is blocked, `kanban-md --dir <BOARD_PATH> edit <id> --block "<reason>" --release` (drops the claim so this scan stops returning it) and pick instead; otherwise `kanban-md --dir <BOARD_PATH> pick --claim loop-<project> --status todo`. Keep the same project claimant every cycle.
5. If pick returns nothing and a dep-met backlog card exists, promote: `kanban-md --dir <BOARD_PATH> move <id> todo` then pick.
6. If everything blocked AND you already posted a blocker this arming: autonudge_stop(reason="all blocked") and stop.
EXECUTE (≤5 tool calls per cycle, hard cap):
7. Read the single spec file for the claimed task.
8. Do ONE atomic thing: draft a fix, write a test, run a test, invoke one SOP. Never all at once.
9. NEVER git push. NEVER destructive ops. NEVER reply to real production tickets during verification — sandbox / read-only only.
10. Cookie-jar auth: http.cookiejar.MozillaCookieJar(path).load() + urllib opener, OR `curl -b <cookie-jar> -f -s`. NEVER read a credential/cookie file as text. NEVER echo cookie contents in any error — scrub exceptions to type(e).__name__.
RECORD:
11. Append progress to the claimed card (`kanban-md --dir <BOARD_PATH> edit <id> --claim loop-<project> --append-body "<note>"`) or to the anchor doc's Cycle Log section.
12. Notify only on a real phase boundary, blocker, threshold crossing, or completion. Use `send_message` with the intended destination when conversational delivery is required; an omitted destination produces a dashboard notification, not an owner DM. Do not emit a routine per-cycle tick.
13. If task complete: `kanban-md --dir <BOARD_PATH> handoff <id> --claim loop-<project> --note "<summary>" --release` to Review (NOT Done — human approves Done). `--release` drops the claim so the next cycle does not resume a card already in Review.
STAY SILENT in the chat panel unless:
- Phase boundary reached / DoD met (then autonudge_stop + summary).
- Hard blocker needs user decision.
- STOP sentinel tripped.
One cycle = one step. Compound cycles build features.
5. Arming checklist (do not skip)
Before clicking 🎯 "Set a goal" → the Play icon button ("Start loop"):
- Anchor doc (
LOOP.md) exists with a Definition of Done section. (Runscaffold.shin this skill dir to generate a hardened template in one command.) - STOP sentinel absent:
ls <STOP_PATH>says "No such file". - Popover fields: Goal description (the nudge, from the template), Seconds between nudges
60, and a finite Max cycles such as 30. The popover has no stop-file input; the service assigns a default stop file, which{{STOP_FILE}}in the nudge names. The nudge itself must still check its stop path and callautonudge_stop. - If arming with
monitor_start, set positive finitemax_cyclesandmax_runtime_secs; usemonitor_updaterather than a second arm when the instruction changes. - If a human is arming via REST, keep the dashboard token in their private terminal and optionally set
stop_sentinel_path; agents must not read local-secret or cookie files. - If the loop touches authenticated APIs, the user must establish or refresh access through the supported client; never copy credential contents into the nudge or transcript.
6. Ten invariants every loop must respect
- Never
git push. Humans push. - Never run destructive ops.
- Never read credential files as text (
~/.aws/*,~/.ssh/*, cookie jars). - Never echo credential content in errors — scrub to
type(e).__name__. - If the project has a kanban-md board,
kanban-mdis the only board writer. - Test execution lives in a sandbox — never in the local workspace for ops that touch live systems.
- One cycle, one step. Compound cycles build features.
- Human approval required for Done. Loop only moves cards to Review.
max_cycles: 30cap every arming — this recipe's own recommendation, not a code default (monitor_startdefaults to 24, the raw surface to 0 = unlimited). Re-arm manually for more.- Every nudge checks an explicit halt condition and calls
autonudge_stop; UI and REST arming get a defaultstop_sentinel_path, and only raw REST arming can set a custom one.