Skip to content

agentm autonomy

github-actions[bot] edited this page Sep 11, 2026 · 16 revisions

Note

LAUNCHED (2026-07-07). The Autonomy arc's telemetry substrate — a device-local spend ledger, a deterministic console, and a digest ladder into the vault. Cross-cutting, agentm-anchored, points up at the agentm HLD rather than one pillar. See the amendment log below for build history.

Autonomy — observability ledger and console

Objective

Multi-session autonomy only works if you can see what it cost and what it produced without watching it live. This design gives agentm one telemetry ledger — spend and run events, attributed to plan, task, and model — and a console over it: a local dashboard plus periodic digest notes. The subscription window's own rate limit is the spend backstop; this design gives visibility into subscription use and efficiency across projects.

Background

This design evolves the budget-governor draft from the Autonomy arc's front-load. The governor paired a spend ledger with enforcement — budget envelopes, spawn denial, an automatic kill path. At review the design kept the ledger and let the host carry the enforcement: on a subscription plan the window rate limit already bounds a runaway, it resets on its own, and the harness's recoverability doctrine files recoverable outcomes under proceed-with-announcement. The arc's standing lock — an orchestrator blind to cost repeats the runaway-token incident at machine speed — is retired by making cost visible: the Mythos fleet failed because no estimate was ever put in front of the operator, and the ledger, the announcements, and the console close exactly that gap.

The evolution also re-formed the ledger's home. The session-cost capture that shipped in crickets' Wave D writes markdown into the Obsidian vault; this design moves telemetry to a device-local event log built for machine records, and the vault keeps the human-facing digest notes. The capability audit's findings carry forward: the substrate decision (resolved to Agent View by PLAN-autonomy-control-plane's opening task — see Risks) and attribution tags are stamped by the harness itself, since the audit confirmed no host carries a plan field for us.

Overview

Sessions and hooks append small JSON records to a monthly log file on the device. On a schedule, the runner folds those events into a small SQLite rollup with per-plan, per-task, per-model, and per-window tables. Everything downstream reads the rollup: a static HTML console page, a ladder of digest notes into the vault, the fan-out advisory, and — once it ships — dreaming's efficiency trend.

The whole read path is deterministic. The one AI touch is a scheduled analysis pass that writes its findings into the store as another dated artifact; the page renders whatever the store holds, the same way every time.

An unattended run announces its spawns with cost estimates, parks tidily if the window runs out, and ends with a morning report naming what it did, what it spent, and why it ended. The launch-time authorization grade a run states is a control-plane concern (below), recorded here only as one field on its run-start event.

Design

The event log

One JSON line per event, appended to ~/.agentm/telemetry/events-YYYYMM.jsonl — outside both the repo and the vault, rotated monthly. An append can never block a session or raise; if a write fails, the session carries on and the line is simply missing. This is the same graceful contract the shipped session-cost capture holds today.

{"ts": "...", "schema_version": 1, "device": "...", "session_id": "...", "parent_id": "...",
 "event": "session-cost | spawn | run-start | run-end | window-park",
 "model": "...", "tokens_by_kind": {"input": 0, "cache_write": 0, "cache_read": 0, "output": 0},
 "cost_usd": 0.0, "tags": {"plan": "...", "task": "...", "arc": "...", "grade": "..."}}

Every line carries device and schema_version from day one — if a second machine ever joins, the aggregator merges logs by device id, and a cheap field now saves a migration later. The harness stamps the plan and task tags at write time from its own active-plan marker and dispatch context. A run-start line also records the authorization grade the run was launched under, so the console and the morning report can show what authority it claimed — the grade policy itself lives with the control-plane plan.

The rollup

A scheduled runner job folds the raw events into a small SQLite file with per-plan, per-task, per-model, and per-window tables, reusing the five-hour bucketing the token-audit analyzer already has. The log is the source of truth and the rollup is derived from it — if the aggregation logic changes, the rollup rebuilds from the events and loses nothing. The health dashboard's history file and the GitHub board mirror stay where they are; the console reads them alongside the rollup, and one read surface is enough.

The console

A local static HTML page over the rollup, built the way the health dashboard already is. It shows spend by plan, task, and model tier, how full the current window is, and cost per merged PR as a first productivity measure. A shared, published version (a locked-down site, Cloudflare or similar) goes on the roadmap as a backlog note; everything here being static derived data keeps that path simple when its time comes.

Digests

Each horizon reads the rollup at its own window and lands as a note under the memory root's diagnostics/digests/:

Cadence Content
Daily spend and run summary, written only by a hand run; the morning note carries the day's spend
Every 3 days 3-day rollup
Weekly weekly rollup
Monthly trends across all of the above

A window with no spend and no events writes no note and adds no history row. The monthly rung skips when its thirty-day total is empty and no history exists.

Delivery — getting it in front of you

A report nobody sees is a report that does not exist. The reports generate on schedule and land in a vault folder you have little reason to open — so the whole observability layer stays invisible unless you run /console by habit, and it goes dark the moment that habit pauses. Three channels close that gap, each honoring the device-and-vault-sync privacy rule that permits local and first-party delivery and bans third-party push.

  • Session-start line — the workhorse. Every session opens with one line: the morning note's headline and how long ago the note was written. The headline is last night's What ran in one line, with a count of the lists that need you. The point is the deadman: when notes stop arriving, the line says so ("none in 3 days … the night has stopped finishing — see runner"), so a stalled night surfaces in your next session instead of staying dark for days. An install with no morning note gets the digest ladder's line instead: the latest digest's headline, the last cycle's age, and the ladder's own deadman ("no digest in 4 days — ladder stalled"). It rides harness-context-session-start — the same visible hook as the "[agentm] Project state" line you already see — and deliberately not the memory-recall hook, whose multi-KB always-load dump the host collapses into a single unread <persisted-output> blob (a line there loads into context but is never seen; that mistaken assumption is why the channel appeared broken — see the 2026-07-17 amendment). The line reads the observability layer's own live signal — runs parked awaiting your resume. It widened in 2026-07-18 with a needs-your-eye count from the auto-organization dedup/merge list (_meta/needs-your-eye.json); that list's only writer retired in agentm v9.20.0, and the count retired with it — the file had never existed on this machine, so it had read zero since filing v2. See the amendment log for the build and retirement detail. No push infrastructure, and a cycle it has already shown stays quiet.
  • On-device notification — opt-in. A local OS notification (macOS osascript or UNUserNotification, a first-party on-device surface) fires once daily with the morning headline. You opt in per device, and nothing else earns a notification. Shipped as scripts/health/session_notify.py — see the amendment log for the build detail.
  • Daily email — opt-in. A once-daily email carries the morning note, for a read away from the machine. It sends inside the night's window, one step after the note is written, and carries the newest digest when no note from this morning or yesterday exists. It rides a first-party mail path you configure — your own SMTP or an on-device mail agent — never a third-party relay. Absent that configuration, the channel skips gracefully and the other two carry delivery. Shipped as scripts/health/session_email.py — see the amendment log for the build detail.

An opt-in that quietly disappears is the failure mode both opt-in channels share. Each one's setting lives as a key in ~/.claude/.agentm-config.json — a file the installer writes too — and on 2026-08-02 a re-run of install.sh deleted every plugins.autonomy.* key while both jobs went on firing daily and delivering nothing. Graceful-skip is right for a channel you never configured, and it looks exactly like a channel whose configuration was taken away. So the quiet is checked rather than assumed: machinery_doctor.py carries an observability-notify-daily:config and an observability-email-daily:config row that ask each channel's own reader whether its settings are still there, and warn when a registered job has nothing left to deliver with.

Together these channels make the delivery-discipline value-gate real: notify-worthy signal appears in one line where you already work, a single daily notification and email carry the rest, and a quiet cycle stays quiet. The FRIDAY arc's "proactive surface" goal lands here. We decline the conversational "save this?" nudge in favor of this delivery, since capture-time instructions already catch save-worthy moments without interrupting a live session.

When the window runs out

The window is the backstop, so hitting it should be tidy. When rate-limit errors arrive mid-run, the run parks: state written to disk, progress recorded, and a park note saying where it stopped, when, and the exact command to resume. Morning resume is one paste, by the operator — a run never resumes itself, because a loop that ate one window would simply eat the next one at reset. The morning report names why the run ended — plan finished, gates green, an escalation parked, or the window ran out — with the spend attached.

The fan-out gate's advisory role

In an interactive session the gate keeps its shipped behavior: it asks the operator before a big dispatch, and they're there to answer. In an unattended run it announces the estimate, logs it as a ledger event, and proceeds. Nothing agentm-side stops, denies, or kills a run over spend; the kill switch stays a lever the operator pulls themselves.

Alternatives considered

  • The enforcing governor (the predecessor draft). A subscription has no surprise bill, the window limit already bounds a runaway, and a hard stop keyed to an estimated meter would either halt healthy runs or sit too loose to fire. Retained as the reopening path if metered spend ever arrives — see Risks.
  • Keeping the ledger in the vault. Telemetry and memory are different things; the vault is curated knowledge on a Google Drive mount with known sync-conflict behavior, and fleet-scale machine records would flood it.
  • One physical store for all observability. One read surface gives everything a merged store would, without disturbing the shipped health history or board mirror.
  • Auto-resume after window exhaustion. It would amplify the exact failure the window bounds.
  • A live dashboard service. Static derived data needs no daemon, and staying static keeps the future shared publish simple.
  • AI in the read path. The analysis runs on a schedule and writes rows; the rendering stays deterministic.

Dependencies

  • The shipped session-cost capture and analyzer (crickets src/tokens/) — the writer, reader, five-hour window math, and fan-out gate this design retargets.
  • The runner — hosts the aggregator and digest jobs as scheduled tasks.
  • The health dashboard — the pattern (and scorecard machinery) the console page reuses.
  • The harness's active-plan marker — the attribution source the event writer stamps tags from.
  • The capability audit (agent-teams-audit.md, vault _harness/designs/post-ag-frontload/) — the substrate decision it reserved stays with the control-plane plan; its plan-binding finding is answered here by harness-side stamping.

Migrations

The session-cost capture moves off the vault. The Wave D capture writes one markdown entry per model per session into the vault via the memory engine; it retargets to the event log, and the vault write retires. What it touches:

  • session_cost_writer.py targets the event log; its tests keep the same behavior contract against the new sink.
  • session_cost_reader.py and the fan-out gate read the rollup.
  • The session-cost memory kind (agentm's memory index) goes vestigial.
  • Dreaming's efficiency trend is still an unbuilt stub, so pointing it at the rollup is a doc edit.

Risks & open questions

  • The ledger is an estimate. The hosts expose no quota API, so accounting works from local transcripts and dispatch announcements, and undercounts mid-session burn. Re-audit trigger: undercount beyond 15% of observable reality moves estimation to transcript-tail sampling.
  • An overnight loop can burn the rest of a window before anyone sees it. Accepted at review: the loss is bounded, the window resets, and the morning report shows what happened.
  • Enforcement reopens if auto-refilling metered spend ever arrives — a plan change or an API-keyed fleet. The question reopens from this design's record; until then it stays closed.
  • The substrate choice is resolved: Agent View. Decided at PLAN-autonomy-control-plane's opening task (2026-07-08), live-verified against Claude Code 2.1.193 (no drift from the capability audit's own baseline). Agent Teams was rejected — one team per session, no nested teams, and no session resumption at all for in-process teammates, all disqualifying for an unattended overnight fleet. Agent View's background sessions (claude --bg / claude agents) are process-supervised, survive machine sleep and restart, auto-isolate into worktrees, and (as of v2.1.198) auto-commit/push/open a draft PR on completion. See the amendment log for the full record.
  • The Antigravity half of the capability audit rests on third-party coverage. Re-run it when Google's own docs come up.
  • The shared dashboard needs a publish gate designed at pickup: aggregates only, personal detail scrubbed, access locked down.
  • Email delivery is decided: a once-daily, opt-in, first-party mail path (see Design → Delivery), resolving the earlier deferral. The connector is the operator's own SMTP or on-device mail agent, never a third-party push relay; unconfigured, it graceful-skips.
  • The Design section is now as-built, not pending. The event log, the rollup, the console, and the digest ladder all shipped — PLAN-observability-ledger (the aggregator) and PLAN-observability-console (the dashboard, digest ladder, park artifact, morning report) both landed, and the control-plane plan wired dispatch + the launch-time grade statement on top. See the amendment log for the shipped detail and the real N1 acceptance run. What's still genuinely open is narrower: the shared/published dashboard (named above), email delivery (named above), and the Antigravity half of the capability audit.

References

  • Code: session_cost_writer.py · session_cost_reader.py · analyzer.py · fanout_cost_gate.py (crickets src/tokens/) · the health dashboard (scripts/health/)
  • Designs: token-audit · memory index · model + effort routing · the HLD this seeds under
  • Predecessor: BUDGET-GOVERNOR-DESIGN-DRAFT.md (v1 + v2, both superseded 2026-07-07, vault _harness/designs/post-ag-frontload/) · agent-teams-audit.md (the capability audit, same directory)
  • Built by: PLAN-observability-ledger → PLAN-observability-console → PLAN-autonomy-control-plane (carries the launch-time grade statement + substrate decision) → PLAN-board-tracking-model (gated on the control-plane plan's first real team run) — staged in the vault _harness/queued-plans/

Amendment log

2026-09-11 — the morning note is what the three channels deliver (agentm-vault plan 04, task 6). morning_note.py now writes one note a morning at <memory root>/diagnostics/morning/, as the night's last step. The session-start line shows its headline and goes deadman after two days without a note. The email sends the whole note as its body, at order 6 in the 02:00-06:00 window, one step after the note. The notification relays the line as it always has. The digest ladder stays as each channel's fallback. The daily rung's job template is deleted, since the note carries the day's spend, and a window with no spend and no events writes no digest and no history row. The Digests section also moves the notes to diagnostics/digests/, where they have landed since filing-v2 part 2a. resolve_vault() now joins the configured plugins.obsidian-vault.memory_root onto the vault path, which ends the line's false "no digest note delivered" on a nested layout. Why not keep the daily digest beside the note: the note already carries the day's spend, the digest said nothing on 2026-09-06, and a page you read has to be one page (AgentM Vault § Dreaming). Re-audit trigger: the deadman firing on a morning when the runner shows the night's other steps finished; the note's own job is then the fault.

2026-09-06 — the needs-your-eye count retires; the session-start line and the on-device notification narrow to the signal that's left. inbox_triage.py was the sole writer of _meta/needs-your-eye.json, and this design's own Delivery subsection read it on two of its three channels. inbox_triage.py is retired (agentm v9.20.0) — it walked memory/_inbox/, a directory filing v2 had already emptied, so its promote/merge/expire proposed nothing since, and its three jobs now belong to the heat policy, the dreaming binary's copies job, and the lifecycle axis. session_brief.py's count_needs_your_eye() is gone, and with it the parked_clause widening the 2026-07-18 entry below describes; the session-start line now reads the observability layer's own parked-runs signal alone, and the on-device notification carries the headline without a needs-your-eye count — it has always just relayed whatever build_brief() computes, so it narrowed on its own. The Delivery subsection's closing paragraph drops the auto-triaged inbox and the needs-your-eye list from its list of reasons the declined "save this?" nudge is safe to decline; capture-time instructions are what's left of that argument. Nothing replaces the lost signal: nothing now detects a fuzzy-similar, fingerprint-distinct pair and parks it for you — the copies job takes content-identical families, and the dedup stage proposes a merge. Re-audit trigger: if a successor surface for ambiguous dedup/merge candidates ships, this design's Delivery subsection should widen again to carry it.

2026-08-02 — both opt-in channels lost their config to an installer re-run, and the doctor said nothing; the config is now checked, not assumed. install.sh --hooks --update --scope user deleted plugins.autonomy.notify_enabled, .email_to, .email_smtp_url and .email_from from ~/.claude/.agentm-config.json — persist_install_state() rebuilt the file from an allowlist of the five keys the installer knew about rather than merging over what was already there, so every key belonging to agentm_config.py was destroyed on re-persist. The vault pair alongside them (plugins.obsidian-vault.vault_path, storage.backend) survived only because harness_memory re-derives them from the legacy top-level vault_path the rebuild happened to keep; the four autonomy keys had no such path and were simply gone. Both jobs kept reporting registered (live) in machinery_doctor.py throughout, because registration was the only question it asked. The installer is fixed to merge (see CHANGELOG), and the Delivery subsection above now names the detector: two <job>:config rows delegating to notify_enabled() and email_config() — the same readers the jobs themselves call at fire time, so a key rename can't drift the check away from the behavior. Config-absent is WARN rather than FAIL, since declining to opt in stays a legitimate state; what's being caught is silence, not misconfiguration. Re-audit trigger: these two rows are hand-listed in _JOB_CONFIG_CHECKS — a third config-dependent job added later gets no row until someone adds it, so revisit whether the list should derive from the job manifests when a third one appears. The cadence/double-fire trigger from the entries below stays open and untouched by this.

2026-07-18 — the needs-your-eye count widens: auto-organization's ambiguous dedup/merge list joins the observability signal (PLAN-auto-org-dedup-and-lint.md task 5 of 9). The Session-start line bullet above named this as a forward reference — "until the auto-organization needs-your-eye list ships and widens it" — and this is that widening. scripts/health/session_brief.py gains count_needs_your_eye(vault), reading _meta/needs-your-eye.json (the list AgentM Auto-Organization task 3 already writes), folded into build_brief()'s parked_clause alongside the existing parked-runs count — the line now reads, for example, "· 2 runs parked, awaiting resume · 1 dedup candidate needs your eye" — and into the anti-fatigue signature string, so a count change alone re-surfaces the line even when nothing else changed. The two signals are additive, not a replacement: parked runs and ambiguous dedup candidates are different kinds of "needs your eye," and the brief now names both. harness/skills/console/scripts/console.py gains a matching section_needs_your_eye(vault) terminal section (honest-dark before the first triage cycle, "nothing" once every candidate resolves, else a bulleted list by file name and reason), completing the third surface the auto-organization design's own Deduplication section named alongside the digest (task 3) and this count. Tested by scripts/test_needs_your_eye_surface.py (3 tests): a fixture ambiguous pair appears on all three surfaces after one cycle, and resolving it clears all three on the next. Re-audit trigger: unchanged from the entries below — the cadence/double-fire check on the runner-scheduled channels stays open; this task only widens what the count reads, not how or when it delivers.

2026-07-17 — Runner job manifests registered for both opt-in channels, closing PLAN-proactive-delivery.md (task 5, FRIDAY ladder feature 1). templates/jobs/observability-notify-daily.yaml and templates/jobs/observability-email-daily.yaml mirror the existing digest jobs' shape — schedule: daily, command: python3 -m health.session_notify / -m health.session_email, dry_run: false (the opt-in config gate each script already reads is the safety gate; a second dry-run layer on top would be redundant). .harness/jobs/ is gitignored, so each is opt-in per device via a one-time copy from the template — see Enable on-device notifications and Enable email digest delivery for the copy step. Smoke-tested with a real runner cycle: both jobs were discovered and ran cleanly, correctly no-op'd since unconfigured on the test device. Re-audit trigger: the two entries below both flagged "confirm the job's cadence doesn't double-fire across overlapping runner cycles" as an open trigger for this task — re-verify that against the live runner's actual tick interval before treating it as settled.

2026-07-17 — The daily email channel shipped, closing out all three Delivery channels (PLAN-proactive-delivery.md task 4, FRIDAY ladder feature 1). scripts/health/session_email.py is the built version of the Delivery bullet above. Unlike session_notify.py, it reads session_brief.latest_digest() (session_email.py:112, reading session_brief.py:116) rather than build_brief() — an email has no session-boot moment to gate on, so it doesn't need the staleness/deadman logic build_brief() carries; it just wants the newest delivered digest, stale or not. It gates on both plugins.autonomy.email_to and plugins.autonomy.email_smtp_url (email_config(), session_email.py:53-74) — either alone reads as unconfigured and the channel silently no-ops, deliberately stricter than notify_enabled()'s single boolean gate, because a half-set email destination (a recipient with no relay, or a relay with no recipient) isn't a state worth sending from. It sends over stdlib smtplib only, parsing an smtp://[user@]host[:port] URL (_send_smtp(), session_email.py:124-145) — never a third-party relay, per the same privacy rule the design names above; the operator's own configured host is the only destination it ever talks to. Anti-fatigue is calendar-day, the same shape as the notification channel's: a state file at ~/.cache/agentm/telemetry/email-state.json (default_state_path(), session_email.py:77-78) records last_sent_date, checked by _already_sent_today() (session_email.py:87-92) and written by _record_sent() (session_email.py:95-103) only after a confirmed send — an SMTP failure never marks the day as sent, so the next invocation retries. Twenty tests in scripts/health/test_session_email.py cover the both-keys-required gate, SMTP send/failure/parsing paths (mocking smtplib.SMTP directly — no SMTP test double existed in this repo before this task), the digest-reuse body, and the once-a-day contract end to end. Scope: with this task, all three Delivery channels named above are built; task 5 (the runner job manifests scheduling both the notification and email channels) has since shipped — see the entry above. Re-audit trigger: see the task-5 entry above for the outstanding cadence/double-send check.

Addendum, same day — auth + TLS added before real dogfood. Configuring the channel against a real transactional-email relay (Resend) surfaced a real gap this entry's "shipped" claim missed: _send_smtp() only ever parsed smtp://[user@]host[:port] with no password and no TLS, so it could talk to an unauthenticated local relay but not an actual authenticated one — every relay worth using for real delivery requires both. Fixed in the same task, before the operator's first supervised send: the URL now parses smtp://[user[:password]@]host[:port], negotiates implicit TLS (SMTP_SSL) on port 465 or opportunistic STARTTLS otherwise (swallowing SMTPNotSupportedError for a relay that offers neither), and calls server.login() when a password is present (_send_smtp(), session_email.py:131-). A new optional plugins.autonomy.email_from key (--email-from, agentm_config.py:270 cmd_set_email_from()) carries the domain-verified sending address a relay like Resend requires distinct from the auth username — email_config() now returns a 3-tuple with email_from optional, falling back to email_to when absent (session_email.py:54-). Eight new tests cover the STARTTLS attempt/failure-swallow, SSL-on-465, login-with-credentials, from-address precedence, and auth-failure paths (test_session_email.py); four more cover the new config flag's set/get/unset/validate round trip (test_agentm_config.py). Re-audit trigger: unchanged from the task-5 entry above — the cadence/double-send check is still the outstanding one; this addendum touches only the transport and sender-identity layer, not the send-gating or anti-fatigue logic.

2026-07-17 — The on-device notification channel shipped (PLAN-proactive-delivery.md task 3, FRIDAY ladder feature 1). The Delivery bullet above named this channel as designed but unbuilt; scripts/health/session_notify.py is the built version. It reuses session_brief.build_brief() — the same digest reader the SessionStart line already reads — rather than forking new digest-parsing logic (notify_body(), session_notify.py:120-135), strips the "[agentm] " prefix since a native notification banner already carries its own title, and fires via osascript -e 'display notification ...' (_fire_osascript(), session_notify.py:99-110). It gates on the plugins.autonomy.notify_enabled key the prior task added (notify_enabled(), session_notify.py:53-67); absent or false is a silent no-op, matching the opt-in framing above. Because this fires on a runner cadence rather than a session boot, its anti-fatigue check is calendar-day, not hours-based like session_brief.py's cooldown: a state file at ~/.cache/agentm/telemetry/notify-state.json (default_state_path(), session_notify.py:70-71) records last_fired_date, so a second call the same day is a no-op regardless of how many times it's invoked (_already_fired_today(), session_notify.py:80-85). Twenty tests in scripts/health/test_session_notify.py cover the opt-in gate, the osascript firing/quoting/failure paths, the digest-reuse and prefix-strip, and the once-a-day contract end to end. Scope: only the notification-firing code and its config gate are done here — task 4 (the email channel) and task 5 (the runner job manifests that schedule both channels) shipped afterward on this same plan; see the two entries above. Re-audit trigger: see the task-5 entry above for the outstanding cadence/double-fire check.

2026-07-17 — The invisible digest_stale_days deadman is retired (the follow-up the entry below named). The entry below closed with "#320's digest_stale_days pending-block entry is now a harmless invisible duplicate of this visible line and can be retired in a follow-up." This is that follow-up. orchestration_briefing.py no longer computes or renders the digest-ladder deadman — the count_digest_stale_days / latest_digest_date helpers, the gather_signals / _over_threshold / build_briefing wiring, the digest_ladder_stale_days_threshold config key, and their tests are gone. scripts/health/session_brief.py on the visible harness-context-session-start hook is now the single surface for the deadman; the memory pending-briefing keeps its other signals (inbox, watchlist, incubator, idea-ledger, staged-adapt, the nudges), each still tested and unchanged. Why retire rather than leave it as belt-and-suspenders: a second deadman on an already-invisible surface adds a vault scan and a config knob every session open for a line no one can read — cost with no signal. Config safety: the key came from DEFAULT_CONFIG, so the seeded config template drops it automatically, and _parse_config_md already ignores unknown keys, so an operator whose config was seeded before this still loads cleanly. Re-audit trigger: if session_brief.py is ever removed or its hook stops being visible, the deadman has to move back onto a visible surface — do not restore it here (this block is invisible by construction).

2026-07-17 — The session-start line is now actually visible (the wiring bug the entries below flagged). The re-audit trigger two entries down said it plainly: "if the SessionStart line is not actually appearing in sessions, that is a wiring bug to fix before judging the channel." It wasn't appearing, and this is that fix. The diagnosis: the design's premise — "the SessionStart hook already assembles a briefing at every session open" — was half-true. A briefing does assemble (orchestration_briefing.py, and #320's digest_stale_days deadman rode it), but it is tail-appended to the memory-recall hook's multi-KB always-load dump, which the host collapses into one unread <persisted-output> blob. So the line was emitted into context and never seen — the operator's "I've never seen it" was exactly right. The fix is a separate visible surface: scripts/health/session_brief.py reads the digest ladder's own delivered artifacts (_briefs/*-digest-*.md + the park-state files) and emits one line — headline + last-cycle age, or the deadman — from the already-visible harness-context-session-start hook (the "[agentm] Project state" line the operator confirmed they do see). Against the live vault it renders the real stall today: "no digest in 4 days, ladder stalled (last: 2026-07-13). Digests computed through 2026-07-17 but not delivered — see runner" — composing this diagnosis with #320's runner fix in one line. Why not fix orchestration_briefing.py's visibility instead: a hook's stdout is one stream → one host "success" line, collapsed by size; the recall dump is inherently large, so nothing appended there can be made visible without splitting the hook — a separate small-output hook is the only lever. Why not a brand-new dedicated hook: harness-context is already registered, already visible, already the agentm session line, and already resolves the source clone the script lives under — a new hook adds settings-merge + global-config surface for no gain. Scope: the deadman + headline + last-cycle-age + parked-runs (needs-your-eye) half is built and tested; the every-day non-stall notification and the email channel below stay open, and the needs-your-eye count reads parked runs until the auto-organization needs-your-eye list ships. #320's digest_stale_days pending-block entry is now a harmless invisible duplicate of this visible line and can be retired in a follow-up. Re-audit trigger: if a host ever renders large hook stdout inline (no <persisted-output> collapse), the separate-surface rationale weakens and the line could fold back into the memory briefing; re-check the collapse behavior before doing so.

2026-07-17 — The session-start deadman line shipped (#319). The stalled ladder the entry below flagged had two real causes, not one: agentm-runner.sh resolved its job-manifest directory relative to scripts/ (the cd it needs for a sibling import), so every launchd-triggered cycle since the runner was first built discovered zero jobs and exited clean — the digest ladder had never once run from the actual background scheduler, only from manual sessions; and separately, the launchd plist never set MEMORY_VAULT_PATH, so a job that did run could silently write into the repo checkout instead of the vault. Both are fixed. This also exposed a marker-honesty gap worth closing on its own: cycle.py's lookback re-anchor wrote the same "status": "done" marker a real completion does, so a job that had never actually run read as healthy — the runner state now carries a separate last_real_run field a reader can trust. Of the three delivery channels below, only the session-start deadman half of the workhorse line shipped here (orchestration_briefing.py's new digest_stale_days signal — "no digest in Nd — stalled", riding the existing pending-briefing mechanism, not a new hook): the every-day headline content (spend, run summary), the on-device notification, and the email channel are still open, exactly as designed below. Re-audit trigger: if the digest ladder goes quiet again with these fixes in place, the next suspect is the delivery line itself, not the runner.

2026-07-17 — Delivery channels added; the FRIDAY proactive-surface goal lands here (F1 design session). The observability layer generated reports but never delivered them: the digest ladder lands notes in _briefs/ (a vault-root folder), and the operator confirmed in the F1 session that they had never seen a digest, a review reminder, or the console in normal use — the whole layer stayed invisible outside a /console habit, and a real ladder stall (no digest between 2026-07-13 and the session) had gone unnoticed for days (diagnosis chipped separately). Added a Delivery subsection with three privacy-honoring channels: a SessionStart brief line with a deadman variant (the workhorse, in-band where the operator already is), an opt-in on-device OS notification (once daily), and an opt-in once-daily first-party email (resolving the long-deferred email question in Risks). This is where the FRIDAY arc's "proactive surface" goal resolves: the daily-brief half is delivery (built here), the delivery-discipline value-gate is these channels plus quiet-stays-quiet, and the conversational "save this?" nudge is declined — the auto-triaged inbox, capture-time instructions, and the auto-organization design's needs-your-eye list already catch save-worthy moments without interrupting a live session. Also corrected the Digests section's stale _inbox/ to the as-built _briefs/ (the F2 fix during the V8 proving phase). Re-audit trigger: if a channel proves noisy in real use, tighten its threshold; if the SessionStart line is not actually appearing in sessions, that is a wiring bug to fix before judging the channel (the operator reported never seeing the existing briefing line — a build-side check, not a design gap).

2026-07-15 — Unattended merge-on-green needs a clear global permission path; provisioning is dev-setup's, detection is doctor's. The N1 retest (vault _harness/designs/v8-proving/PROVING-LEDGER.md item 19) drove a real unattended dispatch all the way to gh pr merge, then stalled: the operator's global ~/.claude/settings.json carried Bash(gh pr merge:*) in its ask list, and Claude Code resolves permissions deny > ask > allow — an ask entry beats any allow at any scope, so no project- or job-scoped allow could clear it (which is why the project-scoped override, agentm 052de06/a279734, was reverted). The working fix is a move — remove the rule from the global ask list, add it to allow. Decision on where that provisioning lives: dev-setup owns it — its capture.sh already declares dev-setup the sole owner of the permissions block in ~/.claude/settings.json (agentm owns only hooks there), so a new idempotent ensure_gh_pr_merge_allowed step (mirroring its existing ensure_claude_co_authored_by_disabled) is the natural home. agentm's install.sh stays silent on the global permission surface — a project-level install silently deleting a user's global security-gate entry is past the trust line, breaks that ownership seam, and over-provisions for the ~all agentm users who never run unattended merge. agentm's doctor detects and points, never writes: machinery_doctor.py's check_unattended_merge_gate WARNs (with a copy-pasteable remedy — the opt-in scripts/enable-unattended-merge.sh) when n1-overnight is registered but the global gate would block, matching doctor's "reports gaps, never mutates state" contract. Why not agentm owns it: it can't be done correctly without editing/removing a guardrail from a stranger's global config during a project install. Why not a runner-injected scoped grant: wrong locus (the operator's own ruling put the risk on "running the fan-out/merge flow at all," not per-repo) and mechanically a scoped allow can't override a global ask anyway. Re-audit trigger: if Claude Code's permission precedence ever changes so a more-specific scoped allow can override a broader global ask, the scoped-grant option reopens — re-verify the deny > ask > allow assumption before relying on it. Full reasoning: vault v8-proving/unattended-merge-gate-verdict.md.

2026-07-08 — Board tracking model decided: substrate-native shape, no by-agent axis (PLAN-board-tracking-model task 1). With the control-plane's real N1 run as evidence, the operator and this session decided the tracking-model question the 2026-06-19 A1 verdict and the 2026-07-03 field-taxonomy rethink (ROADMAP-MASTER § ⑫) both left open: there is no agent-identity concept anywhere in the dispatch substrate to track by — Agent View names a session <plan>-<task> (n1-demo-1, n1-demo-2), never an agent or persona identity. What every dispatch does carry is a tier (classify_work_type()'s T0-Mechanical…T4-Deep), already real and populated, that nothing on the board tracks today. Decision: no net-new by-agent tracking (there is nothing for it to track) — repurpose the board's Track single-select field for dispatch tier instead of version (per ⑫'s own finding, versions already live in the issue hierarchy, a job Track was redundantly also doing). The field-taxonomy mechanics (option-set migration, board-items.json + renderer reconciliation) land in crickets-github-projects.md, its designated home per ⑫, in task 2. Re-audit trigger: if a real agent/persona-identity concept is ever added to the dispatch substrate, this decision is the one to revisit — it was made against "no such concept exists," not "such a concept exists but isn't worth tracking."

2026-07-08 — Acceptance demo closed: the N1 rerun ran for real (PLAN-autonomy-control-plane task 6). PLAN-autonomy-control-plane merged (#251) with task 6 escalated rather than simulated — a sandboxed build session's real dispatch test found claude --bg sessions couldn't authenticate there, and the plan's own constraints forbade faking the demo. The operator ran it for real afterward, from their own terminal, against a clean origin/main worktree: n1_run.run_n1_sequence() dispatched two real Agent View background sessions tagged plan: n1-demo, declared the launch grade (G-ship) on a real run-start event, and both sessions completed. The harvest step surfaced the actual root cause behind the earlier sandbox finding — not a sandbox artifact at all, but the base claude CLI never having a persistent login (claude auth status read loggedIn: false); the operator's regular usage runs through a session-managing host that injects auth per session, which a --bg-spawned process doesn't inherit. claude auth login fixed it for the base CLI same as it would for any machine in this state. Once fixed: the aggregator built a real rollup, the console rendered it, and the morning report read "Ended because the plan finished (plan-finished). Spend: $0.4607" — matched exactly by an independent grep across the raw event log ($0.1672 + $0.2935 = $0.4607, plus four $0.00 <synthetic>-model rows from the pre-fix failed attempts, priced at zero by the pricing table's own graceful-unknown-model fallback rather than crashing). Morning report, console rollup, and raw ledger all agree — the plan's own acceptance criterion, met. PLAN-board-tracking-model's gate ("the control-plane plan's first real team run") is now satisfied. Re-audit trigger: none outstanding for this plan; the auth-setup step (claude auth login) is worth naming in onboarding docs for any other machine that dispatches through Agent View for the first time.

2026-07-08 — R1 resolved: Agent View (PLAN-autonomy-control-plane task 1). The capability audit (agent-teams-audit.md, vault _harness/designs/post-ag-frontload/) reserved the fleet-substrate decision rather than resolving it. This plan's opening task made it explicit: re-verified live against the installed Claude Code (2.1.193, unchanged from the audit's own baseline — no version drift to re-check for) that claude --bg / claude agents (Agent View's background-session primitive) is real, current, and behaves as the audit described (process-supervised; claude agents --json confirms the live session schema is {pid, cwd, kind, startedAt, sessionId, name} — no plan-ID field, confirming the audit's R2 finding independently). Agent Teams was rejected on the audit's own findings: one team per session, no nested teams, and — the disqualifying one — no session resumption at all for in-process teammates, which a fleet meant to survive an unattended overnight stretch cannot tolerate. Antigravity's equivalent stays out of scope for this decision; the control plane is Claude-Code-first, matching the "Claude-first scheduling" precedent already set elsewhere in this harness (crickets' wiki-watch) — Antigravity support is a forward reference, not a rejected option. Re-audit trigger: if Antigravity's own docs become reachable (the audit's own named gap) and the arc later needs cross-host dispatch, re-open this decision for that host specifically; it does not reopen the Claude Code half.

2026-07-07 — Seeded at AA2 as the evolution of the budget-governor draft, lifted from the approved vault draft (OBSERVABILITY-DESIGN-DRAFT.md, status: final, approved: 2026-07-07) after two operator review passes: the first dropped enforcement (subscription billing has no surprise bill; the window limit is the host-run backstop), the second homed the authorization-ladder remnant — the launch-time grade statement — to PLAN-autonomy-control-plane, leaving this design only the grade recorded on run-start events.

Clone this wiki locally