obot.agentRelease candidate2026-08-14

obot.agent v0.4.0 — what changed, shown running

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.

Release v0.4.0 (draft) Since v0.3.0 · 2026-07-26 Merged 21 pull requests Tests 104 / 104 passing Goal #73 autonomy
01  The session framework

A session now opens before the model thinks

#76 · #77 · #80 · #81

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.

The bundle assembles in 30 milliseconds

after — v0.4.0obot2 · 2026-08-14
$ /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).

Reproduce
cd ~/Documents/obot2 && /usr/bin/time -p bash obot.agent/tools/session-init/handoff.sh

The command family got short forms

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.

after — v0.4.0obot.agent/commands
$ 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
02  Getting to the dashboard

The status line carries a live hub link

#52

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.

before — v0.3.0
[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.
after — v0.4.0
[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:

after — outside the workspacecwd: ~/Documents/github
[deadbeef] Opus 5 ~/Documents/github 91% left $0.02 ↗ obot hub
^[]8;;https://jwildfire.github.io/obot.roadmap/^G↗ obot hub^[]8;;^G
Two things this fixes beyond the link. The status line's source was an untracked file on one machine — 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.
Reproduce — render the status line without a session
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.
03  Write policy

One decision per repo, in one file

#59 · #61

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.

after — v0.4.0the authority on any repo's lane
$ 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.

after — dry run, merges nothing
$ 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
Reproduce
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
04  Unattended runs

Three things that used to stall an overnight run

#69 · #63 · #67 · #71 · #72

Worktrees no longer ask permission

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.

before — the spawn prompt
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
after — v0.4.0
session-audit.mjs:137
git worktree add \
  .claude/worktrees/audit-apply-${t} \
  -b audit-apply-${t} origin/main

# → auto-approved,
#   run continues unattended
Reproduce
grep -n "worktree add" ~/Documents/obot2/obot.agent/tools/session-hub/session-audit.mjs

You can tell why a session is missing from Remote Control

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.

after — the flag survives into the spawn
$ 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
Reproduce — prints the command, launches nothing
~/Documents/obot2/obot.agent/scripts/obot-auto --preflight-only

The wrapup leaves a trail instead of prose

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.

after — v0.4.0the step the wrapup now runs
$ 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.
Reproduce — prints the summary, writes nothing
cd ~/Documents/obot2 && python3 obot.roadmap/scripts/build_usage_data.py --dry-run

And the audit lane that costs nothing to 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.

 Also in this release
05  The release contract

This release is the first one with a written contract

#83 · #85 · #86 · #87

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.

The notes live in the repo, demo link first

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.

obot.agent/NEWS.mdobot2 · 2026-08-14
$ 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.
Reproduce
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.

The release body is that section, byte for byte

"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.

verbatim checkobot2 · 2026-08-14
$ 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
Reproduce
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
What that 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 skill that says how, next to the ones that say what

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.

skills libraryobot2 · 2026-08-14
$ 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
Reproduce
ls obot.agent/skills/ | column -c 78

And decisions get one place to be answered

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.

hub discussionsobot2 · 2026-08-14
$ 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
Reproduce
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"
The same contract shows up at the other end of a session. Every wrapup output now opens with two lists — 🚦 Release candidates needing review, then 🧭 Decisions needed — before any work log, and both carry forward until they are closed rather than scrolling away with the diary entry. The 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.
The one thing that looked like it would not work. Three 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.