This release is a harness: no charts, no pages, nothing to point a browser at. So the demo is the behaviour itself — every capture below is real output from this machine, taken after the release commit, with the command that produced it. Where the change is a fix, the old behaviour is captured beside the new one.
Typing /s-init used to start a conversation: the agent read the scratchpad, chased the previous session's notes, then painted a list — several round trips before anything appeared. The hand-off bundle is now assembled by a shell script that runs during command expansion, so its output is already in the prompt when the model produces its first token. The paint costs zero tool calls.
$ /usr/bin/time -p bash obot.agent/tools/session-init/handoff.sh === HANDOFF (preprocessed 2026-08-14 19:50) === === SCRATCHPAD === file: /Users/jwildfire/Documents/obot2/.claude/session-notes/2026-08-14.md (age: 0m) --- previous scratchpad (pointer-chase guard) --- file: /Users/jwildfire/Documents/obot2/.claude/session-notes/2026-08-04.md (age: 13205m) ### Agent-actionable - [ ] 1. /s-init acceptance re-run on the merged fast path … - [ ] 2. Rebase pass: oa#63 + oa#67 (single-file conflicts vs PR#76) … - [ ] 3. #140 impl after D1–D8: ONE oa PR closing oa#65 + oa#75 … ### Waiting on @jwildfire - [ ] 9. D-lists: #140 D1–D8 · #91 D1–D7 + formal sign-off … - [ ] 12. PR queue ×7 — 5 CONFLICTING … real 0.03 user 0.01 sys 0.02
110 lines and 20 KB of carried state, assembled in 30 ms of shell time — versus the several model round trips the old init spent rediscovering it. /session-init and /s-init are now the same skill (#81), so the long form no longer takes the slow path, and unattended runs launched by obot-auto ride the same fast paint (#80).
cd ~/Documents/obot2 && /usr/bin/time -p bash obot.agent/tools/session-init/handoff.sh
Eleven session commands gained one-word aliases (#64), and two new ones arrived: /s-idea files a half-formed thought to the triage queue, /s-reviews walks the PRs waiting on a decision.
$ ls obot.agent/commands/
s-dashboard.md s-init.md s-reviews.md s-todo.md
s-idea.md s-note.md s-scaffold.md s-update.md
s-inbox.md s-spawn.md s-wrapup.md session-wrap.md
Reaching the ops hub meant running /session-dashboard and waiting, or remembering where live.html lives. Every status line now ends with a clickable link — the live hub inside the obot workspace, the deployed roadmap hub anywhere else. The captures below are raw cat -v output, so the OSC 8 hyperlink escapes are visible rather than rendered.
[a1554f0d] Opus 5 ~/Documents/obot2 68% left $1.20 # no link. To reach the hub: # run /session-dashboard, or find # .claude/session-hub/live.html # by hand.
[a1554f0d] Opus 5 ~/Documents/obot2 68% left $1.20 ↗ ops hub # the segment is an OSC 8 hyperlink: ^[]8;;file:///…/session-hub/live.html^G ↗ ops hub^[]8;;^G
Outside the workspace the same segment points at the deployed hub instead, so the link is never dead:
[deadbeef] Opus 5 ~/Documents/github 91% left $0.02 ↗ obot hub ^[]8;;https://jwildfire.github.io/obot.roadmap/^G↗ obot hub^[]8;;^G
git ls-tree origin/main | grep statusline returned nothing before this release. It is now tracked at tools/statusline/ with an idempotent installer, so a second machine can reproduce it. And the new --serve flag on the watch loop turns the in-workspace link into an http://127.0.0.1:7325/ URL, because Ghostty hands file:// links to Finder instead of the browser.echo '{"model":{"display_name":"Opus 5"},"workspace":{"current_dir":"/Users/jwildfire/Documents/obot2"},"context_window":{"remaining_percentage":68},"session_id":"a1554f0d","cost":{"total_cost_usd":1.2}}' \
| bash ~/Documents/obot2/obot.agent/tools/statusline/statusline.sh | cat -v
# swap current_dir to somewhere outside the workspace for the deployed-hub variant,
# or prefix OBOT_STATUSLINE_LINK=off to drop the segment on terminals that can't click.
What an agent may write used to live in two files — merge-policy.json for merge tiers and autonomy-grants.json for unattended grants — each needing a separate edit per repo, and they had already drifted: two repos were listed for merges but missing from the grants matrix, a state neither file could express. They are now one policy.json where a repo gets a single decision, its profile, and you can ask what that means in plain words.
$ obot.agent/scripts/obot-policy explain jwildfire/obot.agent jwildfire/obot.agent profile: auto approved: @jwildfire on 2026-07-22 — obot.roadmap#18 design review merge main (integration) -> standard merge <any other branch> -> refused --auto: branch=True draftPR=main mergeStandard=['main'] issues=full+closes
Branches are matched by role, not by name, so a repo whose release branch is called master or site needs no special-casing. Merges are gated by the same file: the dry run below refuses to say yes for a branch with no declared role, and every real merge in this release went through it.
$ obot.agent/scripts/obot-merge 52 -R jwildfire/obot.agent --check PR #52 https://github.com/jwildfire/obot.agent/pull/52 title: Add a clickable hub link to the agent status line base: main state: OPEN draft: False mergeable: MERGEABLE policy: profile auto, role integration obot-merge: CHECK PASSED - policy permits merging PR #52 in jwildfire/obot.agent
cd ~/Documents/obot2 obot.agent/scripts/obot-policy explain jwildfire/obot.agent obot.agent/scripts/obot-policy explain jwildfire/safety-histogram # release role is 'master' obot.agent/scripts/obot-merge <any-open-pr> -R jwildfire/obot.agent --check
Claude Code auto-approves worktrees under .claude/worktrees/ and prompts for anything else. Spawned audit agents were being handed a literal command placing their worktree in a sibling directory, so each one parked on a permission click — in an unattended run, until morning. The spawn prompt now names the auto-approved path.
session-audit.mjs:137 git worktree add \ ../obot.roadmap-worktrees/audit-apply-${t} \ -b audit-apply-${t} origin/main # → permission prompt, # run parks until clicked
session-audit.mjs:137 git worktree add \ .claude/worktrees/audit-apply-${t} \ -b audit-apply-${t} origin/main # → auto-approved, # run continues unattended
grep -n "worktree add" ~/Documents/obot2/obot.agent/tools/session-hub/session-audit.mjs
A background session that never appears on claude.ai/code had four possible causes and no way to tell them apart. docs/remote-control.md now maps each job's template and respawnFlags signature to the lane that launched it and the one fix that works for that lane — including agents-view sessions, which take no flag at all because there is nowhere to pass one. That was the lane that kept the lead session off Remote Control for six days, and it is why hardening the spawn skill never fixed it: the lead was never spawned by that skill.
$ obot.agent/scripts/obot-auto --preflight-only obot-auto: pre-flight OK (level=A1, goal=charts → hub#78) obot-auto: would run: claude --bg --permission-mode auto --remote-control \ -n '👯🤖 2026-08-14 charts' --model opus /s-init --auto --goal charts
~/Documents/obot2/obot.agent/scripts/obot-auto --preflight-only
A risk named only in a diary entry is a risk nobody is tracking — the entry scrolls off the news page and the concern goes with it. Wrapups now file every actionable blocker or risk as a tracked issue (#71), each carrying a proposed mitigation rather than just a warning (#72), and refresh the Analytics page's cost data before posting (#67) — that dashboard had been frozen at 2026-07-29 while the builder had data through today.
$ python3 obot.roadmap/scripts/build_usage_data.py --dry-run claude-opus-4-8 $ 818.65 5,183 calls claude-sonnet-5 $ 9.39 171 calls claude-haiku-4-5 $ 0.88 49 calls <synthetic> $ 0.00 8 calls # the committed usage.json the site renders was frozen at 2026-07-29; # the builder has data through today. The wrapup is the only thing that # closes that gap — the site deploy cannot, because the source is this # machine's local transcripts.
cd ~/Documents/obot2 && python3 obot.roadmap/scripts/build_usage_data.py --dry-run
The roadmap audit queue (#55, #56) renders findings locally and spends no model tokens until you click: a click spawns the apply agent, and batches submit in parallel with per-agent worktrees so concurrent applies stop colliding on the same push.
Until this release, "what reaches @jwildfire" was an unwritten habit, and it leaked: a convention PR sat in his queue tonight waiting on a review it never needed. docs/rc-framework.md now says it outright — release candidates and decision artifacts reach him, nothing else does — and two mechanisms make the rule survive contact: release notes live in a repo's NEWS.md and publish from it unedited, and every open decision gets a Q&A thread where he answers in one place. The page you are reading is that contract's output for v0.4.0.
A release's notes are no longer typed into the GitHub release form. They are a section of NEWS.md on the integration branch, reviewable in a diff like anything else, and the section opens with its demo link before a word of prose — the rule the skill states, and the reason you landed on this page from the release rather than the other way round.
$ head -11 obot.agent/NEWS.md <!-- NEWS.md is the running release log and the draft of each release's notes. Shape (per skills/rc-release-notes/SKILL.md): newest release first; every release section opens with its demo-artifact link, then a text-only, functionality-first account of what a user can now do. The GitHub release publishes from the section here, copied verbatim, when the release is approved and tagged. --> # obot.agent v0.4.0 **See it move:** the [annotated v0.4.0 demo](https://jwildfire.github.io/obot.roadmap/reports/oa-v0.4-demo/) walks each change as a real terminal capture with the command that produced it.
head -11 obot.agent/NEWS.md
Updated 2026-08-15: R2 accepted (decision record) — v0.4.0 now reaches you as a real main → stable RC PR, obot.agent#99, with the RC roles applied; the draft release below stays as the tag-time body, kept byte-identical to NEWS.md.
"A news.md file that will be copied to release notes once I approve the PR" — his words on the v1.6.0 candidate, now a checkable property rather than an intention. The notes on the v0.4.0 draft release and the notes in NEWS.md are the same bytes; a diff is the test, and it is silent.
$ python3 - <<'PY' # compare the NEWS.md section with the draft release body ... PY NEWS.md section : 7,369 bytes release body : 7,369 bytes IDENTICAL — the release publishes the NEWS.md section unedited
python3 - <<'PY'
import pathlib, subprocess
news = pathlib.Path("obot.agent/NEWS.md").read_text()
section = news[news.index("# obot.agent v0.4.0"):news.index("# Earlier releases")].rstrip()
body = subprocess.run(["gh","release","view","v0.4.0","-R","jwildfire/obot.agent",
"--json","body","-q",".body"], capture_output=True, text=True).stdout
notes = body[:body.rindex("\n---\n")].rstrip() # everything above the attribution rule
print(f"NEWS.md section : {len(section):,} bytes")
print(f"release body : {len(notes):,} bytes")
print("IDENTICAL — the release publishes the NEWS.md section unedited" if notes == section
else "DIFFERS — the release body has drifted from NEWS.md")
PY
rindex slice drops. Exactly one thing: the drafted-by attribution block this house style appends below a --- rule on every published artifact. Everything above it — all 7 369 bytes — is the NEWS.md section unchanged. Nothing is reworded on the way to the release form, which is the whole point: what you review in the diff is what ships.
The convention is loadable, not folklore. rc-release-notes joins the skill library alongside the session-lifecycle and renderer-migration families, so a session told to "prep the RC" or "draft release notes" loads the procedure by description match instead of improvising a house style per repo.
$ ls obot.agent/skills/ | column -c 78 chartjs-migration requirements-harvesting session-scaffold gxp-test-framework session-dashboard session-spawn obot-identity session-idea session-todo p004-test-driver session-inbox session-update p004-test-setup session-init session-wrapup p004-write-tests session-note stakeholder-interview rc-release-notes session-prime renderer-modernization session-reviews
ls obot.agent/skills/ | column -c 78
A decision artifact used to be a page with nowhere to reply. Every open decision now also posts a brief summary — question, options, recommendation — to the hub's Q&A discussions, and the thread is where the answer is recorded. Three are live and waiting as of this release.
$ gh api graphql -f query='{repository(owner:"jwildfire",name:"obot.roadmap"){ discussions(first:5,orderBy:{field:CREATED_AT,direction:DESC}){nodes{ number title category{name} answer{id}}}}}' -q "$FMT" #154 [Q&A] Decide: context management for obot-prime (C1–C6) — state fi answered=false #151 [Ideas] Document Monitor-authoring rules: zsh word-splitting trap + answered=false #150 [Q&A] Decision needed: demo-301's site branch — which size mitigat answered=false #149 [Q&A] Decision needed: the app plan rewrite — four calls (A1–A4) answered=false #141 [Ideas] Make obot-merge --check report mergeability, not just policy answered=false
FMT='.data.repository.discussions.nodes[] | "#\(.number) [\(.category.name)] \(.title[0:60]) answered=\(.answer != null)"'
gh api graphql -f query='{repository(owner:"jwildfire",name:"obot.roadmap"){discussions(first:5,orderBy:{field:CREATED_AT,direction:DESC}){nodes{number title category{name} answer{id}}}}}' -q "$FMT"
NEWS.md half of the contract is what makes the first list reviewable: an RC without notes and a deployed demo page is not an RC.
gsm.qtl's report workflow, each with a runnable repro script, live at drafts/gsm.qtl/ (#62). Every finding was re-verified against current upstream on 2026-08-14 and none has been fixed. They stay drafts by rule — agents do not write outside the jwildfire org.main as its integration lane and the published site branch in the release role (#59).obot-merge calls in this window were denied by the Claude Code auto-mode classifier — including a read-only --check dry run — which read as a threat to the whole "increments merge unattended" model. It was not: 3 denials in 99 real invocations, traced to the one command spelling the workspace permission allowlist did not cover. The rule landed on 2026-08-14, and the three merges that closed this release went through on the first attempt. Full reasoning in the decision artifact.