Automation Audit and Change Report

A continuation controller that works, ten defects found by testing it, and a queue it finished on its own standing between it and running unattended. 26 October 2026.

Machine: Jacks-iMac · Cost of everything below: $0 · Code written: 1,596 lines across two skills · Defects found in my own work: 10 · Improvements reverted after testing: 1

1. The seven asks, and where each one stands

Status of every numbered item in the brief, with the evidence that backs it. An item with no evidence beside it is marked unfinished rather than done.

#AskStatusEvidence
1Five-minute automatic continuationRunning unattended20 launchd runs, last exit code 0. It sent 3 nudges, closed 5 queue steps and marked the work complete with no one at the keyboard.
2Revisit the X.com conversation against realityDoneAll 8 recommendations checked against this machine, 4 of 5 free actions were never implemented
3Verify and improve, not box tickingDone10 defects found in my own work and fixed, 1 improvement reverted after testing proved it a regression
4Lyrics without AI reproducing textBuilt and loaded with real data33 songs from 3 source types, feature export verified to leak 0 verbatim lines
5Complete change reportThis pageEvery claim below carries a command output, file path or live URL
6Second optimization passDone, reported separatelySection 7, which found 4 items the first pass missed, 2 of them real bugs
7Durable scheduled processVerified liveTicks at 14:03, 14:08, 14:13, 14:18, 14:23, 14:28 and 14:33 on a closed app. The 3 failed ticks sit inside the consent window and every tick after 13:08 succeeded.

2. The continuation controller

Claude Code holds the queue and decides what to send. The browser sessions do the work. That is the difference between a feedback loop and a prompt loop.

What it does on each tick

Working
  • Reads the real state of every registered conversation through Chrome's DOM, with no API calls and no cost.
  • Writes a verified checkpoint whenever the conversation changes, holding the tail actually observed plus the queue as it stood.
  • Composes the next instruction from the queue and that checkpoint. The instruction states the last completed step, what remains, and the single next action.
  • Refuses to send during a streaming response, on any state it cannot classify, when another target has already been sent to this tick, and inside the minimum gap.
  • Marks a target blocked after three nudges with no progress, or at once on a platform blocker such as a conversation length limit.

The word continue appears nowhere in anything it sends.

Code: ~/.claude/skills/continuation-controller/ · state and logs: ~/.claude/continuation/

Proven live on a conversation that was already dead

Verified

Your MTG deck conversation had stalled at 12:29 PM on the word continue, with ChatGPT's answer cut off mid-sentence at "After twenty" and the page showing Connection interrupted.

The controller classified it as stalled, quoted the exact point of interruption back, and asked for the one outstanding step. ChatGPT worked for 2m 43s and produced Revision 21: all five decks structurally legal, none demonstrably optimized, each verified at 60 main deck and 15 sideboard with no four-copy violations, plus named gaps on Delney's redundant legendary draws and Gardenize needing black sources that green-only acceleration cannot supply.

Log: sent {state: stalled, submitted: button, chars: 1626} then progress {gptTurns: 5 -> 6}

Recovery when the conversation itself dies

Verified

That conversation then hit You have reached the maximum length for this conversation. Prompting cannot fix that, so the controller marked it blocked with the true reason instead of nudging a corpse.

ctl.py migrate opened a fresh chat, seeded it with the verified checkpoint and the four surviving steps, carried the finished step across with its evidence, and re-pointed the target automatically. ChatGPT's first reply in the new conversation: "I will locate the existing five-deck artifacts, correct only the Mono-White Delney ranking and filename references. I will leave the decklists and other outstanding tests untouched."

It picked up at step 2 and declined to redo step 1.

New target: chatgpt.com/c/6adf9587-074c-83e8-bf23-d26611f3dcfd · seed verified by composer emptying from 1934 chars to 1

It ran unattended, and finished the job

The first scheduled run raised the macOS dialog "python3.13 wants access to control Google Chrome", and every Apple Event blocked until it was answered. Three ticks failed inside that window, at 12:57, 12:58 and 13:03. Once consent was given, every tick since has succeeded.

What it then did with nobody at the keyboard. It sent three context-aware nudges at 13:08, 13:18 and 13:33, closed five queue steps on evidence ChatGPT reported itself, and marked the target complete at 13:38 when the answered turns caught up with the asked ones. It has idled correctly ever since, logging skipped: complete every five minutes rather than prodding finished work.

launchd runs      20        last exit code  0
ticks logged      24        tick errors     3  (all inside the consent window)
nudges sent        3        queue steps closed  5

The spent-evidence guard also fired in production. At 13:18 one report closed two steps, the guard reopened the second, and the conversation earned it again with fresh evidence at 13:33.

3. The 23 October page against the actual machine

Nothing was assumed finished because it had been written down. Each recommendation was checked against local state.

#RecommendationClaimedFound todayNow
1Obsidian 1.12.7 to 1.14.55 minStill 1.12.7Done, 1.14.5 installed
2Claude Duet waitlist1 minNeeds your accountYours to join
3Bash sandbox10 minNo sandbox key at allConfigured, left off, see section 7
4mcp-obsidian write access15 minmcpServers was emptyWired and proven
5financial-analysis plugin10 minNot installedInstalled, 18 skills including xlsx-author and audit-xls
6Claude Dashboards trial20 minNeeds your accountYours to try
7Game-Studios hook reading20 minNot readRead, 18 hooks; the rigor dial is below
8Obsidian Sync decision$48/yrCosts moneyOut of scope, free only

Four of the five free actions had never been implemented. The page was correct about what to do and nothing had been done with it.

4. What actually changed today, all of it free

Obsidian write access from Claude Code

Done

The bridge was half built and sitting idle. obsidian-local-rest-api was enabled in the vault and answering on both ports, and nothing was configured against it.

Now registered at user scope with mcp-obsidian 1.30.0, exposing 15 tools. The two that matter are obsidian_patch_content and obsidian_append_content, because the previous retrieval path could read the vault and could not write to it. Session notes, artifact rows and decision logs can now be written straight into Brandon-Vault.

obsidian_delete_file is denied in permissions.deny, so no delete call can reach the vault through it.

Authed vault listing returned 200; MCP handshake returned serverInfo: mcp-obsidian 1.30.0; no mcp 2.0 incompatibility

A continuation controller, where there was none

Done

Forty launchd agents already run on this Mac and not one of them did this. 1,103 lines across the bridge, the store, the tick and the operator CLI, plus a skill file so it is invoked as /continuation-controller rather than by raw path.

4 checkpoints written, 23 events logged, target migrated once

A lyrics database that keeps AI out of the text path

Done

Covered in section 5. 33 songs loaded from material already on your disk.

~/Projects/_outputs/lyrics-db/lyrics.sqlite

Sandbox configured and deliberately left off

Reverted

Section 7 explains why. Applying it broke two of your standing gates, so it is written, tuned to your real write paths, and switched off with the reason recorded in the file.

settings.json now carries sandbox._why_off

5. The lyrics problem

The lyrics problem is a transport problem wearing a copyright costume. Asking a model to retype protected text is both the thing it should refuse and the slowest possible courier. The two jobs are now separate.

Exact text moves locally, AI works on features

Built

Transport. Embedded lyric frames in files you own, .lrc sidecars, JSON packs already on disk, and your own clipboard through capture_clip.py. Byte for byte, with no model in the path. Every row carries source, source_ref and rights_note, so the Joni pack's own "personal, non-commercial study only" note survived import rather than being dropped.

Intelligence. features --out emits hashes, counts, structure and quality flags with no lyric text in it. That is enough for identification, matching, indexing, deduplication and quality checking.

Loaded today from material already on your disk: 33 songs, 22 rows from JSON packs, 10 from .lrc sidecars, 1 from an embedded tag.

Export tested against every line over 12 characters in the database: 0 verbatim lines leaked in 15,060 bytes

What the quality pass caught on real data

Verified
  • 3 rows carry invisible unicode. Two Beach House lyrics hold U+2005 and one holds U+205F. These are the exact characters your voice gate hunts, and they are invisible in every editor you would open them in.
  • 1 row is not a song. A Chipperish Media podcast episode at 151 characters was flagged as suspiciously short, correctly, because its lyrics tag is not a lyric.
  • 0 duplicates in the current corpus. Duplicates get marked, never removed.
lyrics_db.py qc

5. Before and after, where it is measurable

Numbers taken from the machine on both sides of the work. Nothing here is an estimate.

WhatBeforeAfter
Continuing a browser AI sessionYou type continue by hand3 nudges sent and 5 queue steps closed with nobody at the keyboard
Your MTG conversationStalled 12:29, dead on a cut-off sentenceComplete 13:38, all five steps evidenced
launchd jobs doing continuation0 of 401, with 20 runs at exit code 0
Obsidian1.12.71.14.5, 14 community plugins carried over, old bundle archived
Claude Code access to the vaultRead only, mcpServers empty15 tools including patch and append, delete denied
Lyrics on disk3 formats, no index, no quality check33 songs in one database, 4 real defects surfaced
Lyric text reaching a modelThe blocker that stopped the work0 verbatim lines in the AI-facing export, tested
Truncation flag on the same corpus17 of 33 rows, 52%0 of 33
Reading of the stalled conversationmainLen 7130, userTurns 8, contaminatedmainLen 5366, userTurns 6, true
Turn counting on claude.ai0 and 0, silently wrong1 and 1, correct

6. The rigor dial, read from Game-Studios

The transferable piece from that repository, which is why it was on the 23 October page at all.

One dial fronting six settings

Read

The repository carries 18 hooks. The part worth taking is modes.rigor, a single setting whose whole job is to expand into six knobs: modes.review_mode, modes.workflow, docs.density, qa.level, modes.story_granularity and team.size. Its own config file warns that seeding any of the six pins that value and shadows the expansion, so the dial stops changing anything.

Your honesty gate has no such dial. Six hooks fire at full strength on every turn, whether the turn is a one-line edit or a shipped chapter. A rigor setting that expanded into which gates run would cost nothing and is the one idea in that repository worth having.

6. Nine defects in my own work, found by testing it

Each of these would have made the automation lie to you. Every one was caught by running the thing rather than by reading it.

#DefectHow it would have liedFix
1Insert verified by a 24-character prefixA stale composer passed verification because two prompts shared an opening clause. The wrong text was submitted and reported as success.Exact whitespace-normalised equality, proven with a nonce the model echoed back
2A regex backslash in the JS payloadAppleScript string literals escape backslashes, so the bridge died with a syntax errorWhitespace normalised by char code, and run_js now refuses backslashes and double quotes outright
3Enter fallback reported success and submitted nothing1,764 characters sat unsent in the composer while the log said sent. Synthetic key events carry isTrusted: false and ChatGPT ignores them.Insert and submit split into two calls with a render gap, and submission confirmed by the composer emptying
4The composer lives inside mainUnsent text counted as conversation progress. Readings of mainLen 7130, userTurns 8 were really 5366 and 6.The probe subtracts the composer's own text before measuring anything
5The prompt quoted the strings the probe countsOne unanswered nudge inflated the turn counters from 6 to 8 and 5 to 6, so outstanding work would have looked permanentTurn markers neutralised in any quoted tail
6Truncation flagged anything ending on a letter17 of 33 rows, a 52% truncation rate that was the measure reporting its own bias. Lyrics almost never end in punctuation.Only unambiguous mid-sentence breaks count. Same corpus now yields 0.
7A patch that printed success and changed nothingI replaced a code block without asserting the match, saw my own confirmation message, and the old rule was still runningEvery subsequent patch asserts the replacement landed before writing
8The step-closing marker could match my own instructionA line beginning EVIDENCE: would have closed a queue step and recorded my own request as its evidenceEchoes of the controller's own phrasing are rejected
10The skill file was wrong about claude.aiIt claimed Claude renders Claude said:. The real string is Claude responded:, .font-claude-message does not exist, and claude.ai repeats the user's text in innerText so text counting double counts. Claude turn counting would have read zero forever.User turns take the larger of the text count and the data-testid=user-message DOM count, which is correct on both sites. Claude now reads 1 and 1 where it read 0 and 0.
9One evidence line closed two queue stepsAn EVIDENCE: report stays visible in the tail on later ticks, so it closed step 4 as well as step 3 on one Azorius deck report. Caught by reading the queue output, where steps 3 and 4 carried an identical evidence string.Evidence already spent cannot close another step. Step 4 was reopened.

8. Second pass, run after the work was finished

An independent re-examination, looking for what the first pass missed and challenging what it had already concluded. Four findings, two of them real defects that were still live.

The sandbox upgrade is a regression on this machine

Reverted

The 23 October page scored the Bash sandbox 9 out of 10, and the reasoning was sound: it replaces bypass mode with a real boundary. I enabled it with your actual write paths.

It took effect immediately and turned off Apple Events. osascript to Chrome began failing with -600 with Chrome demonstrably running, which breaks the continuation controller's in-session bridge and your auto-open-deliverables gate, since open -R and launching Firefox both go through Launch Services.

A change that breaks two standing gates fails its own test. It is configured, tuned and switched off, with the reason written into the settings file. The launchd controller is unaffected because it runs outside this sandbox. For a single hardened session: claude --settings '{"sandbox":{"enabled":true}}'

Confirmed by testing before and after: -600 with sandbox on, 1 window with it off

The doctor command was misreporting permission

Fixed

It reported Automation permission: GRANTED because it inherits the grant of whatever shell invokes it. The launchd job runs as a different responsible process and holds no such grant, which is the entire reason the scheduled run hung. A health check that passes while the thing it checks is broken is worse than no check.

It now states that caveat and prints the launchd job's own last tick outcome.

The step-closing marker was still vulnerable

Fixed

Defect 8 in the table above surfaced on this pass. The first pass had concluded it was safe because the controller's prompt is a single line that starts with a bracket. That is luck rather than a guard, and a direct test proved a line beginning EVIDENCE: would close a step with my own instruction text as its evidence.

One stall marker is too broad

Noted

please try again is in the stall marker list and could appear in ordinary model prose. The consequence is mild, since both stalled and idle lead to the same action, and it is recorded here rather than quietly left.

9. What is unfinished, and why

Named rather than dropped. Nothing here is being reported as done.

ItemWhy it is not finishedWhat it needs
Claude Duet waitlistAccount action on your login, which I cannot take for youJoin from claude.com
Claude Dashboards trialAccount action on an existing routeYours to try
A second live targetThe claude.ai path is verified end to end, including send and turn counting, and no Claude conversation currently carries outstanding work worth registeringctl.py add --id claude-main --url claude.ai/chat/<id> --kind claude
Obsidian SyncCosts money, outside a free-only brief$48 a year, your call

One unrelated thing the screen showed while I was working: your keyboard battery is very low and is asking to be connected to USB.