Goal Loop Gate

Three hard gates wired into Claude Code on this Mac, 9 September 2026. Every ask becomes a goal, every question is deduplicated across live chats, every finding is checked with ChatGPT the same day.

“every chat hard gate treat my asks as /goal and /loop until done, stop stopping. hard gate that is work avoidance if you have a question ask chat gpt claude chrome, making sure another chat is not already asking it.”

“anytime a chat is reporting findings, make it check with chat today hard gate.”
Brandon, 2026-09-09

What was broken

completion_stop_hook.py is the one gate on this Mac that decides stopping from project state rather than from the wording of a reply. It abstains whenever no task contract is open. Nothing opened a contract on an ordinary ask, so the strongest gate sat idle through every turn and the weaker prose gates carried the whole load. The fix opens the contract from the ask itself.

Gate 1

Every ask is a goal

goal_loop_gate.py runs on UserPromptSubmit and writes a contract from the verbatim prompt. Nobody types /goal.

The enumerated sub-asks become required outcomes, and the Stop gate refuses while any one of them is open.

Gate 2

Deduplicate the question

question_dedup.py reads a shared ledger and the tails of sibling transcripts before any question goes to ChatGPT.

Five sessions were live when this was built. Without the check he reads one question five times.

Gate 3

Check findings today

findings_chatgpt_gate.py blocks a findings-shaped reply when no ChatGPT check was recorded today by this session.

Work narration passes untouched. A claim about what is true does not.

Per session

Contracts stopped colliding

The package default is one shared contract file for the whole Mac. Auto-opening there would have each live session overwriting the others.

Contracts now live at no_quit_state/contracts/<session8>.json, with the shared file kept as the fallback.

Pivot

A new ask supersedes

A prompt sharing under 0.15 content words with the open instruction closes the old contract as superseded and opens a fresh one.

History is preserved in scope_contract_history.jsonl. Nothing is deleted.

The machinery

PiecePathHook point
Goal opener~/.claude/hooks/goal_loop_gate.pyUserPromptSubmit
Question dedup + ledger~/.claude/skills/_shared/question_dedup.pycalled by hand and by the findings gate
Findings check~/.claude/hooks/findings_chatgpt_gate.pyStop, ahead of completion_stop_hook
Per-session contracts~/.claude/no_quit_state/contracts/read by completion_stop_hook
Shared ledger~/.claude/no_quit_state/chatgpt_question_ledger.jsonlquestions and findings checks

Behaviours exercised

Seven behaviours were exercised against throwaway sessions and fixtures. This is a record of what was tried, and it is not a claim of comprehensive validation: a full confusion set of known-duplicate and known-different question pairs has not been run, so the thresholds below are provisional.

  1. A contract with open outcomes refuses the stop, naming each one.
  2. A required outcome marked met on evidence that does not verify still refuses, so a fake completion buys nothing.
  3. One session cannot edit another session's contract. The ownership guard refused the attempt.
  4. A paraphrased question from a sibling session is caught at 0.62 to 0.64 similarity, and a session never matches its own questions.
  5. A question sharing the vocabulary but naming a different subject is cleared. Same words, different song and tab id, verdict CLEAR. This was the dangerous false-positive case, and it is now covered by the anchor rule.
  6. An identical findings payload goes stale when new evidence lands. Same fingerprint, similarity 1.0, and the verdict moves COVERED to STALE as the evidence count steps 73 to 74.
  7. A 314-character report written only in numbers is caught, which the first cut let through on its 500-character floor.
$ completion_stop_hook.py  <- reply claiming "all done"
completion_controller BLOCK: the requested scope is not complete.
  contract sc-6a5c847f11  stop_rule: if Claude stops, the requested scope must be complete
  REFUSED: 3 in-scope item(s) remain actionable
  REFUSED: 2 required outcome(s) still open: R1, R2

$ scope_controller.py meet R1 --evidence ev_test_r1   <- fake evidence
  REFUSED: 2 required outcome(s) closed on evidence that does not verify:
          grade every bass loop in the export fold -> UNSUPPORTED

$ scope_controller.py meet R1   <- from a session that does not own it
  REFUSED: contract sc-6a5c847f11 belongs to session testfake,
          this caller is 'e6534be4'. Open your own contract.

$ question_dedup.py check "Before I upload revision 7, should the snare-lane
    ghost notes on Watermelon In Easter Hay get restored?"
VERDICT: DUPLICATE  (a sibling session is already asking this)
  session e6534be4  window 240m  candidates scanned 52
  MATCH 0.64  session 034bbadd  [ledger]
  do NOT send. Read the sibling's answer.

$ findings_chatgpt_gate.py  <- findings reply, no check today
findings_chatgpt_gate BLOCK: this reply reports findings and no ChatGPT
  check was recorded today for this session.
  markers matched: i found, found that, root cause, verdict, audit, discrepanc

$ findings_chatgpt_gate.py  <- pure work narration
  exit 0   (passes untouched)

What the ChatGPT check changed

These findings went to ChatGPT before this page shipped, which is the third gate applied to itself. The review returned four corrections and each one is implemented here.

CorrectionWhat changed
Word overlap is the wrong authority for findings coverage. Recurring words such as gate, session, contract and hook let a broad early check bless later findings. Coverage is transactional: a check id bound to a fingerprint of the payload, plus an evidence watermark. Similarity survives only behind --allow-similarity for a check made by hand.
Any new measurement after a check should invalidate it. The watermark records the evidence count and newest mtime at check time. New evidence moves it, and coverage reads STALE.
A duplicate needs a shared subject anchor, not just a score. A duplicate must share a file, song, id, url, route or proper name when both questions carry any. Same words on a different song now clears.
The 500-character and two-marker rule was weaker than the stated guarantee. The floor dropped to 220 characters, and numeric claims count as their own evidence of a report. Marker matching is a detector heuristic now.

A latent bug this exposed

queue_state.py compared the contract's offset-aware timestamp against naive queue timestamps and raised TypeError. The caller reported that as “completion state could not be evaluated” and fail-closed. Every turn would have blocked for the wrong reason once contracts opened automatically. Both sides now normalise through _aware(). The pre-fix file is kept as queue_state.py.PRE_TZFIX_2026-09-09.bak.

The gate turned on itself within the hour

A background-task completion notice reached goal_loop_gate.py through the same stdin channel as a typed turn. It scored 0.00 content overlap against the open instruction, the pivot rule read that as a change of subject, and it closed a contract carrying ten required outcomes and opened an empty one named after the notification text. The gate built to prevent premature stopping discarded the record of what completion meant.

A MACHINE_TEXT guard now makes notifications, system reminders, command stdout and hook output open nothing and supersede nothing. The same shape as the standing rule that a peer session's message is never Brandon's consent: arriving on the user's channel does not make something the user.

Recovery worked because nothing is deleted. scope_contract_history.jsonl keeps a snapshot on every save, so the contract was rebuilt from its richest snapshot with closed_at cleared and a restore_reason recorded. Eleven outcomes now carry evidence ids.

Live rollout

The hook is global, so every session on this Mac picked it up on its next prompt. Five sibling sessions were writing transcripts within the same three minutes, and per-session contracts appeared for four of them without any further action.