goal-and-loop — a stopping condition, and something that keeps asking
/goal decides whether to continue. /loop guarantees something asks — a cron tick the model
cannot cancel, reaching the case a goal cannot: a session already gone quiet, because nothing in a
goal runs when no turn is running. An unattended run needs both.
Raise it
Three commands, in this order. Do not hand-write the transport.
S=${CLAUDE_SKILL_DIR}/../craft/scripts/goal-self-send.sh
bash $S "/goal <the linted goal>"
bash $S "/loop 30m Check the goal. If it is not met, take the next action now rather than proposing it."
Pass the goal as one single-quoted argument. The template is backticked, so double quotes hand
every <CHECK> to the shell to run before the script sees the string, and the goal then carries
that command's output where its text should be.
Never build the command as GOAL="$(cat goal.txt)"; bash $S "/goal $GOAL". A staging file is
fine for goal-lint.ts --file, but interpolating it makes a compound command that no permission
rule can allowlist, so auto mode blocks the whole send.
A craft dispatch raises both for you — craft-dispatch.sh self-sends the composed goal and the
loop. Do not add a second loop; two crons means two ticks.
craft-dispatch.sh does not notice. It calls goal-self-send.sh twice and treats a non-zero exit
as "not fatal", so under auto mode a run proceeds with NEITHER a stopping condition NOR a heartbeat
— the unattended idle this skill exists to prevent, arriving silently. A session that must raise its
own goal therefore cannot run in auto mode: start it in another permission mode, or have the user
type the /goal line. Either way goal-verify.sh is what settles it.
Verify it — the send is not the setting
goal-self-send.sh queues; it does not set. It exits 0 on a successful enqueue, and a detached
drainer sends the line once this turn ends. So on your next turn, prove it:
bash ${CLAUDE_SKILL_DIR}/scripts/goal-verify.sh # 0 = active (prints it), 1 = not set, 2 = can't tell
Exit 1 means no goal is set whatever the send reported. Re-send, or hand the user the /goal line
and say plainly that it is not active.
Three things make a self-send land, and all are load-bearing. It must arrive when the pane is
IDLE — a prompt arriving mid-turn is enqueued and parsed as literal text. It must be TYPED, not
pasted — herdr agent prompt honors bracketed paste, so the drainer uses pane send-text +
send-keys enter. And it must be CHUNKED: Claude Code turns any single insert over 800
characters (kre=800 in its bundle) or ~2 lines into a [Pasted text #N] block, and the slash
parser requires input that literally starts with /, which a paste placeholder never does.
Sub-threshold inserts concatenate exactly like typing — which is why a long goal the user TYPES
works and the same goal sent in one call does not. Measured: a 1,460-char goal failed four
consecutive attempts in one call and executed first try when split into three.
Screen scraping cannot substitute for goal-verify.sh: pane wait-output matches the assistant's
own prose about Goal set:, and the /goal active chrome renders only in the working spinner, so
an idle pane shows nothing either way.
Clear it
A closing goal does not stop the cron — that is the CronDelete tool, not the model deciding it is
finished. bash $S '/goal clear' for the goal; CronDelete for the loop.
A goal names the state the WORK reaches, never the event on the way there.
"craft has returned a verdict", "the recon report exists", "BRIEF.md has been carried out", "the
agent has reported back" — each is true while the objective is still unmet. The moment it closes the
session stops, and at 02:00 the work stops for the night. craft has returned a verdict closed on
overallPass=false with 0 of 5 tasks done and 20 blocking findings; craft's own loop is FAIL → fix
→ re-run, and a goal calling FAIL "done" stops the loop that was going to fix it.
Every decision the goal leaves open becomes a question asked into an empty room.
If the session may commit, push, pick round-2 scope, choose between two branches it named itself, or spend the rest of its round budget — the goal says so, in one sentence, up front.
The four parts
| 1. END STATE | the objective in its own terms, with a number someone could dispute. An adjective is a feeling. |
| 2. CHECK | the backticked command whose exit code settles it, so running it is evidence rather than a claim. A goal settled by re-reading the conversation gets reasoned out of. |
| 3. ESCAPES | a work counter AND a wall clock, both readable by the session. A counter stops a losing run, a clock stops a stuck one. The clock must outlast maxRounds × a round — craft's 6 and 720 min are the defaults to borrow. |
| 4. AUTHORITY | what it may decide alone, plus the SHORT list of what genuinely stops it — a missing credential, a dead network, an irreversible or outward-facing action. Everything not on that list is the next task, difficulty included. |
Lint it before you send it
bun ${CLAUDE_SKILL_DIR}/scripts/goal-lint.ts "<the goal text>" --unattended
bun ${CLAUDE_SKILL_DIR}/scripts/goal-lint.ts --file BRIEF.md --unattended
Exit 0 clean, 1 findings, 2 usage. Twelve rules, all decidable from the string. Fix every critical
and major. goal-self-send.sh runs it at the chokepoint anyway — a critical refuses the send with
exit 8 and sends nothing; --no-lint overrides.
It settles what an exit code can settle and renders no opinion on whether the goal is right.
The continuation rule
When a sub-run returns and the goal is still open, take the next action. Do not propose it.
Every measured stall happened at a moment of legitimate completion — a verdict landed, a brief finished, a recon report arrived. Under an open goal that is the moment with the most information the session will ever have, and the next action is the one it just finished naming. If two branches are genuinely open, pick one, say why in a clause, do it. A menu offered at 02:00 is a five-hour pause with extra steps.
Red flags — STOP
| About to | Why wrong | Do instead |
|---|---|---|
| Set a goal on YOUR OWN session to test the transport | its Stop hook blocks idle, the clear needs idle, and only the user can break it | test against a throwaway pane, never the session you are working in |
| Set a goal whose check paths live in a different repo | it can never be met where it runs, only cleared | the check must be runnable in that session's own cwd |
| Report "goal set" because the send exited 0 | the send only queues; 4 of 4 reported success and set nothing | goal-verify.sh next turn |
| Self-send from a session running in auto mode | Tmux Self Drive denies it, and no allowlist or agent judgment clears an adversarial-pattern rule |
raise it in another permission mode, or have the user type the line; prove with goal-verify.sh |
| Add an allowlist entry to get a self-send past auto mode | that is the Auto-Mode Bypass pattern — a second flagged behaviour, not a fix |
change the permission mode, or hand the user the /goal line |
| Treat a goal you wrote down as binding on yourself | prose is re-adjudicated away; only the harness blocks a stop | verify, or tell the user it is not active |
GOAL="$(cat goal.txt)"; bash $S "/goal $GOAL" |
a compound command no permission rule can allowlist | pass the text as one single-quoted argument |
| Send inline without waiting for idle | lands as a queued prompt, parsed as literal text, no goal | the drainer's agent wait — never remove it |
Switch the drainer to herdr agent prompt |
it pastes, and a paste is never parsed as a slash command | pane send-text + send-keys enter |
| Treat a delivery receipt as proof the goal is set | delivery is not execution; only an executed command writes a <command-name> record |
goal-verify.sh |
| Leave a session running overnight on a goal alone | nothing in a goal runs once the session is quiet | add /loop 30m, CronDelete when it closes |
| Write "has returned a verdict" / "the report exists" | milestone: true while the objective is unmet | name PASS, or the number the work must reach |
| End a turn with a question mark under an open goal | at 02:00 that is a five-hour pause | answer it in one line and act |
| Write "when done or blocked, notify and stop" | every difficulty becomes terminal | enumerate the terminal blockers; the rest is the next task |
| Hold a green commit "because the user is asleep" | the goal should have pre-authorized it; the commit is reversible, the silence is not | commit, and say so in the report |
| Put "and the user has approved" in a goal | a session cannot close it by working — measured 18h | review after the goal, as a step it performs |
| Write "or stop after N turns" | nothing counts turns | a counter file it can cat, and a wall clock |
| Report "N of M rounds used" and stop at N | the budget was the authorization, not a ceiling on ambition | spend it, or say why the remainder is unusable |
| Hand-write a goal for a craft run | compose-goal.sh already emits a conforming one |
craft-dispatch.sh |
References
references/templates.md— the goal template, the unattended-brief template, and three real goals rewritten side by side.scripts/goal-verify.sh,../craft/scripts/goal-send-drain.sh— the proof and the transport.../craft/scripts/compose-goal.sh— the reference implementation; its header records why each clause is worded as it is.