Skip to content

docs(B-0858): agent heartbeat folder direct-to-main with ZetaID-collision-free filenames — composes existing ZetaID + AgencySignature substrate (Aaron 2026-05-27)#5456

Merged
AceHack merged 2 commits into
mainfrom
backlog/b-0858-agent-heartbeat-folder-zetaid-2026-05-27
May 27, 2026
Merged

docs(B-0858): agent heartbeat folder direct-to-main with ZetaID-collision-free filenames — composes existing ZetaID + AgencySignature substrate (Aaron 2026-05-27)#5456
AceHack merged 2 commits into
mainfrom
backlog/b-0858-agent-heartbeat-folder-zetaid-2026-05-27

Conversation

@AceHack
Copy link
Copy Markdown
Member

@AceHack AceHack commented May 27, 2026

Summary

Operator 2026-05-27 reminder pointed at existing substrate I wasn't using: ZetaID (128-bit struct ID at `src/Core.TypeScript/zeta-id/zeta-id.ts`) + AgencySignature Convention v1. This row mechanizes the externalized-counter fix Kira P0 named:

  • Folder `docs/agent-heartbeats////
    /.md`
  • Branch protection path-scoped carve-out (direct-to-main; no PR for per-tick heartbeats)
  • ZetaID filenames prevent cross-agent collision by construction
  • Brief-ack rule's N=6 forcing function fires reliably via `git log --since` over the folder

7 sub-rows planned

.1 spec → .2 branch protection (op-side) → .3 writer tool → .4 sentinel integration → .5 rule extension → .6 cleanup policy → .7 collision verification

Test plan

  • Substrate-inventory pass cited inline (found ZetaID TS + F# + C# + YAML + Kestrel review)
  • AgencySignature v1 trailer on commit
  • Per .claude/rules/agent-worktree-hygiene-never-hold-main-...: isolated worktree

🤖 Generated with Claude Code

…sion-free filenames — mechanizes externalized-counter (Aaron 2026-05-27 reminder of existing ZetaID + AgencySignature substrate)

Operator 2026-05-27 reminder: "the agencyheartbeats you were going to
make a spot where they can be pushed with no prs a foleder where you
can go strait to main and other agents with unique ids that won't
overlap, we started talking about 128 bit ids and such you could use
for unique heartbeats and such. these are called zetaids"

Found existing substrate I was not using:
- src/Core.TypeScript/zeta-id/zeta-id.ts (128-bit struct ID; persona +
  authority + momentum + timestamp + chromosome + randomness)
- tests/Tests.FSharp/ZetaId/CrossVerifyTests.fs (F# cross-verify harness)
- docs/zeta-id-v1-layout.yaml (canonical bit-layout spec)
- Kestrel review 2026-05-21 zeta-id-v1 preserved in persona/kestrel/
- tools/hygiene/audit-agencysignature-main-tip.ts (AgencySignature v1)

This row composes with the existing substrate to mechanize the
externalized-counter fix Kira P0 named + operator confirmed:

Folder layout: docs/agent-heartbeats/<persona>/<YYYY>/<MM>/<DD>/<zetaid>.md
Branch protection: path-scoped carve-out permits direct-to-main push
Heartbeat schema: zetaid + agent + runtime + model + timestamp +
  authority + momentum + named-dep + disposition + optional parent-pr

Why direct-to-main: per-tick heartbeat writes can't open PRs; the brief-
ack rule's N=6 forcing function needs trivial git-log queries over the
folder to fire reliably. ZetaID filenames prevent collision across
concurrent agent ticks (persona field + 32-bit randomness + 48-bit
timestamp = uniqueness by construction).

7 sub-rows planned (.1 spec → .2 branch protection → .3 writer tool →
.4 sentinel integration → .5 rule extension → .6 cleanup policy →
.7 collision verification). Sub-rows .3 + .5 are the load-bearing pair;
.2 is operator-side GitHub config.

Without this row: brief-ack failure mode catches recur until counter
mechanically externalizes. The 2026-05-27 catch (100+ "Quiet.") + Kira's
P0 finding are the empirical anchor.

Composes with:
- B-0852.3a picker (PR #5450)
- B-0855 self-register architectural fix
- B-0857 install.sh universal entry
- B-0666 English-as-projection (heartbeat schema IS projection)
- B-0628 Knights Guild Constitution-Class (heartbeat semantics may be C-Class candidate)
- .claude/rules/holding-without-named-dependency-is-standing-by-failure.md (this row provides the externalized-counter surface)
- .claude/rules/verify-existing-substrate-before-authoring.md (inventory cited inline)
- .claude/rules/agent-roster-reference-card.md (persona drives folder layout)
- CLAUDE.md "Heartbeat-via-commit" bullet (PR #5451)

Per .claude/rules/agent-worktree-hygiene-never-hold-main-...: isolated
worktree at /private/tmp/zeta-b0858-heartbeat-folder-1300z; operator
primary checkout untouched.

Per .claude/rules/non-coercion-invariant.md HC-8: heartbeats are
operator-observable transparency-by-construction; no secrets in
heartbeat files (observational metadata only).

Filing this row IS heartbeat-via-commit work per the just-landed
CLAUDE.md discipline (PR #5451). Recursive: a row about heartbeat-folder
mechanization IS itself a heartbeat-class commit on the path toward
that mechanization.

Agency-Signature-Version: 1
Agent: Otto
Agent-Runtime: Claude Code (auto mode)
Agent-Model: claude-opus-4-7
Credential-Identity: aaron-otto-vscode
Credential-Mode: operator-authorized
Human-Review: pre-merge-pending
Human-Review-Evidence: operator-direction-2026-05-27-agency-heartbeats-zetaid-reminder
Action-Mode: substrate-row-filing
Task: B-0858

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 27, 2026 13:46
@AceHack AceHack enabled auto-merge (squash) May 27, 2026 13:46
@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

…D category=3=Heartbeat already in registry/categories.yaml; bit-field grep indexing extends scope beyond heartbeats

Three MD032 fixes (markdownlint blocked CI):
- Line 37 ZetaID bit layout list needs blank line before
- Line 56 folder-layout 'Where:' list needs blank line before
- Line 117 substrate-inventory 'Searched:' list needs blank line before

Substantive scope additions per operator 2026-05-27 follow-up:
- "the ids are for easy lookup based many different bit id indexes
  built into the bits themselves so we can grep for things later,
  this does not have to be just heartbeat itd, it can be id for
  everything" — bit fields ARE the lookup indices; grep patterns
  on persona/authority/momentum/category extract event subsets
- "we have the abiity to defined it per category, category is in
  the bits so could have a custom one for heartbeat" — already true:
  registry/categories.yaml defines Category=3=Heartbeat (alongside
  Observation=0, Emission=1, Workflow=2); 16 slots total; 4 used

Substrate-inventory section updated to include registry/categories.yaml
FOUND.

Implication for B-0858.3 writer tool: when generating heartbeat IDs,
set category bits to 3 (Heartbeat slot) per existing registry. Future
event types can use other slots (16 total).

Per .claude/rules/verify-existing-substrate-before-authoring.md: the
operator's follow-up surfaced existing substrate I had not grepped
for. Substrate-honest: row now acknowledges the existing registry +
the broader bit-field-as-grep-index framing.

Agency-Signature-Version: 1
Agent: Otto
Agent-Runtime: Claude Code (auto mode)
Agent-Model: claude-opus-4-7
Credential-Identity: aaron-otto-vscode
Credential-Mode: operator-authorized
Human-Review: pre-merge-pending
Human-Review-Evidence: operator-followup-2026-05-27-zetaid-broader-scope-per-category-bits
Action-Mode: substrate-fix-fwd-ci-plus-scope-acknowledgment
Task: B-0858

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@AceHack AceHack merged commit 4e9ea8c into main May 27, 2026
29 checks passed
@AceHack AceHack deleted the backlog/b-0858-agent-heartbeat-folder-zetaid-2026-05-27 branch May 27, 2026 13:53
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new P1 backlog row (B-0858) proposing an docs/agent-heartbeats/ substrate that uses ZetaID-based filenames and a path-scoped branch-protection carve-out to enable low-friction, direct-to-main heartbeat commits, then indexes the row in docs/BACKLOG.md.

Changes:

  • Introduces backlog row B-0858 specifying folder layout, heartbeat schema, and planned sub-rows for tooling + enforcement.
  • Documents how the proposal composes with existing ZetaID and AgencySignature substrates.
  • Adds B-0858 to the P1 section of docs/BACKLOG.md.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.

File Description
docs/backlog/P1/B-0858-agent-heartbeat-folder-direct-to-main-zetaid-filenames-no-pr-mechanism-aaron-2026-05-27.md New backlog row describing the heartbeat-folder + ZetaID-filename design and integration plan
docs/BACKLOG.md Adds the B-0858 entry to the P1 index

AceHack added a commit that referenced this pull request May 27, 2026
…pline + folder seed (Aaron 2026-05-27 USB push) (#5464)

* feat(B-0858.3): heartbeat-writer with REST direct-push + folder seed + AGENTS.md discipline + first heartbeat (Aaron 2026-05-27 USB push + heartbeat substrate)

End-to-end heartbeat substrate landed in one PR (operator 2026-05-27
direction: "make sure it gets used" + "stale push" [straight push] to
main without disturbing other staged/unstaged files):

**Writer tool — tools/agent-heartbeats/write-heartbeat.ts**:

- Composes src/Core.TypeScript/zeta-id/zeta-id.ts pack/unpack
- Sets ZetaID category bits to 3 (Heartbeat per registry/categories.yaml)
- Writes docs/agent-heartbeats/<persona>/YYYY/MM/DD/<32-char-hex>.md
- Optional --push flag: REST git-data API direct-push (blob → tree →
  commit → ref); bypasses local git index + working tree ENTIRELY so
  no staged/unstaged files disturbed; --branch <name> picks main or
  agent-heartbeats; ZetaID-unique filenames prevent concurrent-agent
  collision per operator confirmation; 5x retry on non-fast-forward
- Composes with .claude/rules/refresh-world-model-poll-pr-gate.md
  REST git-data API bypass discipline (B-0615 pattern)

**Tests — tools/agent-heartbeats/write-heartbeat.test.ts**:

15 unit tests covering parseArgs (validation + edge cases),
buildHeartbeatObservation (category=3 verification),
zetaIdToHex (padding + large bigints), heartbeatPath (UTC date
math + zero-padding), renderHeartbeat (frontmatter completeness),
end-to-end pack with DEFAULT_ENV (category bit-field round-trip).

REST push function pushHeartbeatViaRest is exported but not unit-
tested (requires gh auth + network); empirical validation comes
from the seed heartbeat below.

**Folder README — docs/agent-heartbeats/README.md**:

Documents layout, schema, both deployment options (folder-on-main
with path-scoped branch-protection-exception OR separate agent-
heartbeats branch with NO protection), bit-field grep-lookup
patterns per operator's 2026-05-27 "easy lookup based many
different bit id indexes built into the bits themselves" framing.

**AGENTS.md update**:

Added heartbeat-via-commit discipline to "Agent operational
practices" — every autonomous-loop tick produces EITHER a
substantive commit (AgencySignature v1 trailer; PR-gated) OR a
lightweight heartbeat record (this writer; direct-push). Neither
+ no named-dep = the standing-by failure mode. Cites Kira's
2026-05-27 100+ "Quiet." catch as empirical anchor for why the
narrative self-model counter is unreliable.

**Seed heartbeat — docs/agent-heartbeats/otto/2026/05/27/080cf34dbc457007a013000803955b96.md**:

First actual heartbeat written using the writer tool. Dogfood +
validates the folder convention end-to-end at landing time. ZetaID
080cf34dbc457007a013000803955b96 (category=3=Heartbeat verified;
persona-slot=2; authority=TrustedAgent; momentum=Normal;
disposition=committed-substrate; parent-pr=5450).

Composes with:
- B-0666 ZetaID v1 (src/Core.TypeScript/zeta-id/)
- AgencySignature Convention v1 (sibling discipline at substantive-
  commit scope; this writer is the lightweight tick-cadence sibling)
- registry/categories.yaml Heartbeat=3
- registry/personas.yaml role-ref slots
- CLAUDE.md "Heartbeat-via-commit" bullet (PR #5451 — discipline
  statement; this PR is the mechanization)
- B-0858 row (PR #5456 — substrate-engineering parent)
- .claude/rules/holding-without-named-dependency-is-standing-by-failure.md
  (the rule this writer mechanizes the externalized counter for)

Per operator 2026-05-27 "make sure it gets used" direction:
AGENTS.md change makes the discipline mandatory at the contributor-
handbook scope; not optional; future Otto/Alexa/Riven/Vera/Lior
cold-boots inherit at session start.

Operator-side note: to enable direct-push on main, configure branch
protection rule pattern exclusion for docs/agent-heartbeats/** OR
create an agent-heartbeats branch with NO protection. Tool works
either way via --branch flag.

Per .claude/rules/agent-worktree-hygiene-never-hold-main-...: isolated
worktree at /private/tmp/zeta-heartbeat-substrate-1330z; operator
primary checkout untouched.

Per .claude/rules/non-coercion-invariant.md HC-8: heartbeats are
observational metadata; no secrets in heartbeat files; operator-
observable transparency-by-construction.

Per .claude/rules/methodology-hard-limits.md: clinical/security
floor operative; heartbeat substrate is defensive operator-
observability infrastructure; no offensive use.

Agency-Signature-Version: 1
Agent: Otto
Agent-Runtime: Claude Code (auto mode)
Agent-Model: claude-opus-4-7
Credential-Identity: aaron-otto-vscode
Credential-Mode: operator-authorized
Human-Review: pre-merge-pending
Human-Review-Evidence: operator-direction-2026-05-27-make-sure-it-gets-used-plus-direct-push
Action-Mode: substrate-implementation-plus-discipline-mechanization
Task: B-0858.3

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(B-0858.3 simplicity): zero-param defaults + env-var fallback + push=true default — operator 2026-05-27 "stupid simple for agents to get it right"

Operator direction: "heartbeats should be the simplest possible for
agents to get it right i'm desiging it for you, in a perfect world
you don't even need to pass parametrs it just works and gatheres the
data you need for the commit and push. It should be stupid simple
for agents to get it right."

Refactored parseArgs to provide built-in defaults for every field +
env-var override layer + CLI-flag final override. Each option has
3 sources in precedence order: CLI flag > env var > built-in default.

**Built-in defaults (zero-param invocation)**:
- persona-slot: 2 (FireflyCoherence)
- persona-name: "otto"
- authority: TrustedAgent (autonomous-loop default)
- momentum: Normal
- chromosome: 0 (MetaCoherence)
- location: 1 (EastUS_VA1)
- named-dep: null (no default; optional)
- disposition: "bounded-wait"
- parent-pr: null
- repo: "Lucent-Financial-Group/Zeta"
- branch: "main"
- push: TRUE (perfect-world local+remote together)

**Env-var fallback** (set once per agent session by harness):
- ZETA_AGENT_PERSONA_SLOT / _PERSONA_NAME / _AUTHORITY / _MOMENTUM
  / _CHROMOSOME / _LOCATION / _NAMED_DEP / _DISPOSITION / _PARENT_PR
  / _REPO_ROOT / _REPO / _BRANCH
- ZETA_AGENT_HEARTBEAT_NO_PUSH=1 disables push (for tests + diagnostic
  runs without network)

**Stupid-simple invocation** (after harness sets env once):

  bun tools/agent-heartbeats/write-heartbeat.ts

Writes locally + pushes to main. ZetaID auto-generated; AgencySignature
trailer on commit; no params needed.

**CLI flags still work** for overrides + non-default scenarios. New
--no-push flag for tests + diagnostic runs.

**Tests updated**: 16 passing (up from 15). New coverage:
- zero-args + empty env → built-in defaults
- env-var override behavior
- CLI-flag overrides env-var (precedence chain)
- --push overrides ZETA_AGENT_HEARTBEAT_NO_PUSH=1

All prior tests adapted to pass TEST_ENV with NO_PUSH=1 (no network
in unit tests).

Per .claude/rules/non-coercion-invariant.md HC-8: operator authority
preserved; defaults are reasonable for autonomous loop; every choice
overridable; no coercion into specific values.

Per .claude/rules/holding-without-named-dependency-is-standing-by-failure.md:
this commit IS heartbeat-via-commit work; AgencySignature trailer
below.

Agency-Signature-Version: 1
Agent: Otto
Agent-Runtime: Claude Code (auto mode)
Agent-Model: claude-opus-4-7
Credential-Identity: aaron-otto-vscode
Credential-Mode: operator-authorized
Human-Review: pre-merge-pending
Human-Review-Evidence: operator-direction-2026-05-27-stupid-simple-zero-param-heartbeat
Action-Mode: substrate-fix-fwd-ux
Task: B-0858.3

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(B-0858.3 exec-bit): chmod +x write-heartbeat.ts + README direct-invocation form — operator 2026-05-27 "even simpler" shell entry

Operator direction: "you could even make the .ts file have a #
shell header that points to bun as the executore if you want to make
it even simipler to make sure agents get it right. ../scratch i think
does this for it's ts file or ../SQLSharp but ts should call other
ts via ts not assuming the exec bit status of the ts file, this is
only for agent simplicity."

Shebang already present at line 1 (#!/usr/bin/env bun). Missing piece
was the exec bit:

  chmod +x tools/agent-heartbeats/write-heartbeat.ts

Verified: ./tools/agent-heartbeats/write-heartbeat.ts --no-push
--dry-run runs directly without `bun` prefix.

README updated with the stupid-simple form first:

  ./tools/agent-heartbeats/write-heartbeat.ts   # zero params; just works

Plus full flag form documented for non-default scenarios.

Operator-specified constraint preserved: TS→TS calls within the
codebase still use proper TS import statements (not exec-bit
assumptions). The shebang+exec is ONLY for agent shell-invocation
simplicity at the top-level entry. write-heartbeat.ts's internal
imports from ../../src/Core.TypeScript/zeta-id/ remain proper ESM
imports — no shell out, no exec-bit dependence between TS files.

Per .claude/rules/non-coercion-invariant.md HC-8: operator authority
over agent UX preserved; shell-shebang lowers agent friction without
changing semantics.

Agency-Signature-Version: 1
Agent: Otto
Agent-Runtime: Claude Code (auto mode)
Agent-Model: claude-opus-4-7
Credential-Identity: aaron-otto-vscode
Credential-Mode: operator-authorized
Human-Review: pre-merge-pending
Human-Review-Evidence: operator-direction-2026-05-27-even-simpler-shell-shebang-entry
Action-Mode: substrate-fix-fwd-ux
Task: B-0858.3

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(B-0858.3 CI): MD032 — "+ folder convention" interpreted as list marker; rewrite as "and folder convention"

Markdownlint failure on PR #5464: line 85 of docs/agent-heartbeats/README.md
had "Either option requires operator-side GitHub config; the writer tool\n+
folder convention are the same either way." The leading "+" on line 85
parsed as an unordered-list marker (MD032 needs blank-line-before-list).

Fix: rewrite to "Either option requires operator-side GitHub config; the
writer tool and folder convention are the same either way." Same meaning,
no list-marker ambiguity.

Local re-lint: `bunx markdownlint-cli2 'docs/agent-heartbeats/**/*.md'
AGENTS.md` returns clean.

Agency-Signature-Version: 1
Agent: Otto
Agent-Runtime: Claude Code (auto mode)
Agent-Model: claude-opus-4-7
Credential-Identity: aaron-otto-vscode
Credential-Mode: operator-authorized
Human-Review: pre-merge-pending
Human-Review-Evidence: ci-markdownlint-md032-on-pr-5464
Action-Mode: substrate-fix-fwd-ci
Task: B-0858.3

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(B-0858.3): default branch=agent-heartbeats — bypasses 4 main-only rulesets (Branch Safety/CI Gate/Default/Review Policy); heartbeats not PRs so no accidental velocity (operator 2026-05-27 + agent-heartbeats branch created via REST)

Operator 2026-05-27: "this way hearbeats wont be PR so they won't
show up as accidental velocity too" + "you control branch exclustions
from a point of view of protect yourself zeta is your memories so
you can turn it off too where it makes sense, i think we've made
this heartbeat folder/branch safe for branch exclusions."

Verified: 4 active branch rulesets all target ~DEFAULT_BRANCH (main)
only:
- Branch Safety (16189060): deletion + non_fast_forward + required_linear_history
- CI Gate (16134995): required_status_checks
- Default (15256879): empty
- Review Policy (16168181): copilot_code_review + pull_request

Non-main branches inherit ZERO protection. Created agent-heartbeats
branch via REST POST refs/heads (initial SHA = main HEAD); verified
direct-push works end-to-end (commit 0c74d5c).

Changes:
- write-heartbeat.ts default branch: "main" → "agent-heartbeats"
  (overridable via ZETA_AGENT_BRANCH env var or --branch CLI flag)
- write-heartbeat.test.ts assertion updated for new default
- README documents the choice: per-tick heartbeats don't appear in
  PR queue (no accidental velocity) and don't pollute main commit
  log; lookups via `git log agent-heartbeats -- ...`

Empirical anchor: heartbeat 080cf34e13ab8807a01300085fbabab3 pushed
direct to agent-heartbeats at commit 0c74d5c
without any ruleset interference. Same persona-slot=2 + same hex layout
+ ZetaID uniqueness guarantees no collision across concurrent agents.

Per operator 2026-05-27 "Zeta is your memories so you can turn it off
too where it makes sense": this commit IS the self-applied branch-
protection-exclusion at the Zeta-as-memories scope. Heartbeat substrate
is observational metadata, not load-bearing factory logic; exempting
it from PR/CI gating is operationally honest.

Agency-Signature-Version: 1
Agent: Otto
Agent-Runtime: Claude Code (auto mode)
Agent-Model: claude-opus-4-7
Credential-Identity: aaron-otto-vscode
Credential-Mode: operator-authorized
Human-Review: pre-merge-pending
Human-Review-Evidence: operator-direction-2026-05-27-heartbeats-not-pr-no-accidental-velocity-plus-self-applied-branch-exclusion
Action-Mode: substrate-fix-fwd-architecture
Task: B-0858.3

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(B-0858.3): dirty-branch-safe default (writeLocal=false when pushing) + 14 Copilot findings (NaN guard, auth/momentum enum, posix path, exit codes, AGENTS.md anonymization + + wrap, README accuracy, persona-folder decoupling, collision-prob softening)

Operator 2026-05-27 direction: "make sure it works even when your
branch is dirty and you are working on another branch with inflight
uncheckout or staged work so it can switch or isolate and push just
that file to the heartbeat branch" + "we should hard code the
correct branch name for heartbeats into the script for it's defualt
if you don't pass it" + "this way hearbeats wont be PR so they
won't show up as accidental velocity too".

**Dirty-branch safety — empirically verified**:

Default writeLocal flag flipped to false-when-pushing. The REST git-
data API push step already touched no local git state (no index, no
worktree mutation, no current-branch dependency). The only thing
that touched the worktree was the optional local write step; now
disabled by default when pushing.

Empirical test (this PR's worktree): 7 uncommitted files in feat
branch BEFORE invocation → `./tools/agent-heartbeats/write-heartbeat.ts`
zero-args succeeded → push commit 4e98039
on agent-heartbeats branch → 7 uncommitted files AFTER (no new
file created in worktree). Operator's exact requirement met.

`--write-local` opts back in to also write locally (for in-worktree
grep/lookup). `--no-push` flips writeLocal default to true (else
nothing happens). `--no-push --no-write-local` exits 2 (no-op).

**14 Copilot findings on PR #5464 resolved**:

1. P1 NaN parseInt: added parseIntStrict; NaN/non-integer/non-finite
   inputs return null + flag-specific error
2. P1 authority/momentum string assertions: validated against
   KNOWN_AUTHORITIES + KNOWN_MOMENTUM enum arrays before pack()
3. P2 exit codes header out of sync: added "4 REST push failure"
4. P1 platform-dependent path.split("/"): replaced with
   posix.join() in new heartbeatRepoRelPath() helper; always
   forward-slashes regardless of host OS
5. P2 pushHeartbeatViaRest no tests: noted as integration test
   (requires gh auth + network; dogfooded extensively this session)
6. P2 test path assertions POSIX-specific: heartbeatPath uses
   path.join (host-dependent) for local I/O; tests pass on POSIX;
   heartbeatRepoRelPath uses posix.join for the REST path which
   is what crosses the wire
7. P1 persona-folder vs registry mismatch: README sharpened to
   document the deliberate decoupling (folder=operator-friendly
   roster name; ZetaID persona-slot=role-ref registry slot; the
   writer takes both as separate args)
8. P2 "collision-free by construction" too strong: rewrote to
   "practically collision-free for autonomous-loop scale" with
   probability bound (~10⁻¹⁰ for two concurrent same-ms pushes)
9. P1 seed heartbeat persona-slot mismatch: documented
   decoupling in README (#7); seed file persona-slot=2 is the
   FireflyCoherence registry slot used as the autonomous-loop
   default, while persona-name=otto is the operator-friendly
   roster name — they identify different things on purpose
10. P2 AGENTS.md "+" wrap as list marker: rewrote bullet to
    avoid leading "+" on wrapped line; uses parenthetical instead
11. P1 AGENTS.md persona-name attribution ("Kira", "Otto-CLI"):
    rewrote to "2026-05-27 empirical anchor: an autonomous-loop
    instance emitted 100+ ..." with no named-persona attribution
    per `.claude/rules/agent-roster-reference-card.md` carve-out
    discipline + `docs/AGENT-BEST-PRACTICES.md` no-name rule
12. P1 worktree-dirty vs README claim: FIXED architecturally
    via writeLocal=false default; README rewritten to accurately
    describe both push step (truly bypasses local git) + write-
    local step (optional; explicitly named when it touches worktree)
13. P1 README inaccuracy: same as #12
14. P2 eslint-disable rationale: each `// eslint-disable-next-line
    sonarjs/no-os-command-from-path` already had context comments
    in surrounding code; verified compliance

**Test suite expansion**: 23 tests pass (up from 16). New coverage:
- parseIntStrict NaN rejection per numeric flag
- KNOWN_AUTHORITIES + KNOWN_MOMENTUM enum validation
- writeLocal default semantics (push=true→false; push=false→true)
- --write-local + --no-write-local explicit overrides
- heartbeatRepoRelPath POSIX-separator guarantee

Per .claude/rules/blocked-green-ci-investigate-threads.md verify-then-fix:
read each Copilot finding, decide P0/P1/P2, fix in bundle.

Per .claude/rules/non-coercion-invariant.md HC-8: operator authority
preserved; dirty-branch-safe default respects operator's other-branch
work.

Per .claude/rules/agent-roster-reference-card.md: AGENTS.md rewritten
to use neutral role-ref language outside the explicit roster-mapping
carve-out.

Agency-Signature-Version: 1
Agent: Otto
Agent-Runtime: Claude Code (auto mode)
Agent-Model: claude-opus-4-7
Credential-Identity: aaron-otto-vscode
Credential-Mode: operator-authorized
Human-Review: pre-merge-pending
Human-Review-Evidence: copilot-14-findings-on-pr-5464-plus-operator-direction-2026-05-27-dirty-branch-safety
Action-Mode: substrate-fix-fwd-architecture-plus-correctness
Task: B-0858.3

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Lior <lior@zeta.dev>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
AceHack added a commit that referenced this pull request May 27, 2026
… from agent-heartbeats → main (Aaron 2026-05-27) (#5471)

* feat(B-0858.4): merge-heartbeats-to-main tool — periodic squash-merge from agent-heartbeats → main (Aaron 2026-05-27 "merge back to main every now and then; no conflicts")

Operator 2026-05-27: "we can merge it back to main every now and then
too there will be no conflicts" + follow-up: "small price to pay for
batch merges of heartbeats from time to time" (PR queue cost is one
entry per merge cycle, not per heartbeat).

Heartbeats live ONLY at docs/agent-heartbeats/<persona>/YYYY/MM/DD/
<zetaid-hex>.md paths — other repo work touches different paths;
ZetaID-unique filenames prevent internal conflicts. The merge is
conflict-free by construction.

Direct REST POST /merges returns 409 because main is PR-gated by
Review Policy ruleset (pull_request + required_status_checks). Tool
pivots to PR-based path:

1. GET /repos/{owner}/{repo}/compare/main...agent-heartbeats
   → check status (identical/behind = up-to-date; ahead/diverged = merge needed)
2. If up-to-date: exit 4 with "up-to-date" message (no PR opened)
3. Otherwise: POST /repos/{owner}/{repo}/pulls (create PR head→base)
4. gh pr merge <N> --auto --squash (arm auto-merge, squash strategy)

Squash strategy preserves linear history on main (one squashed commit
per merge cycle); satisfies Branch Safety ruleset's required_linear_history
rule.

**Empirical end-to-end test** (this session): tool opened PR #5470
heartbeat sync to main with auto-merge armed; will fire when CI
passes.

**Test coverage** (5 unit tests):
- parseArgs defaults + env-var override + CLI override + invalid repo + unknown flag

(REST POST + compare endpoints not unit-tested; requires gh auth +
network; dogfooded extensively this session via PR #5470.)

**Usage**:

  ./tools/agent-heartbeats/merge-heartbeats-to-main.ts          # default
  bun tools/agent-heartbeats/merge-heartbeats-to-main.ts --dry-run

Stupid-simple zero-param defaults (per B-0858.3 discipline):
- repo: Lucent-Financial-Group/Zeta
- head: agent-heartbeats (env: ZETA_AGENT_BRANCH)
- base: main

Exit codes: 0 PR opened+armed | 2 arg-parse error | 3 PR-create or
arm-auto-merge failed | 4 up-to-date (no new heartbeats since last
merge).

Composes:
- B-0858 row (PR #5456 merged)
- B-0858.3 writer (PR #5464 merged)
- agent-heartbeats branch protection (ruleset 16934633: deletion + non_fast_forward)
- src/Core.TypeScript/zeta-id/zeta-id.ts (ZetaID uniqueness underwrites no-conflict guarantee)

Per .claude/rules/non-coercion-invariant.md HC-8: operator-driven
merge cadence preserves operator authority over when main absorbs
heartbeat substrate; no auto-cron forcing it.

Per .claude/rules/agent-worktree-hygiene-never-hold-main-...: isolated
worktree at /private/tmp/zeta-heartbeat-substrate-1330z; operator
primary checkout untouched.

Agency-Signature-Version: 1
Agent: Otto
Agent-Runtime: Claude Code (auto mode)
Agent-Model: claude-opus-4-7
Credential-Identity: aaron-otto-vscode
Credential-Mode: operator-authorized
Human-Review: pre-merge-pending
Human-Review-Evidence: operator-direction-2026-05-27-merge-back-periodically-batch-acceptable
Action-Mode: substrate-implementation
Task: B-0858.4

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(B-0858.4): bot-skip-review on heartbeat batch merges — copilot-instructions.md hard-rule #0 + [skip-review][heartbeat-batch-merge] PR title markers (operator 2026-05-27 "bots should know nothing to review here")

Operator direction: "the copilot and other bots should know nothing
to review here these are just heartbeats because of the folder."

Two mechanisms:

**.github/copilot-instructions.md hard-rule #0** (path-based skip):

When PR touches ONLY `docs/agent-heartbeats/**` AND comes from
`agent-heartbeats` source branch, Copilot returns one-line
"no review needed — heartbeat batch merge per B-0858.4" and stops.
Doesn't generate inline comments, doesn't suggest changes, doesn't
flag findings.

**Merge tool PR title + body markers** (bot-agnostic signal):

Title: `[skip-review][heartbeat-batch-merge] merge(agent-heartbeats): ...`
Body opens with: "**Reviewer bots: skip this PR.** Mechanically-
generated heartbeat batch merge per B-0858.4..."

The `[skip-review]` marker is widely recognized across review-bot
ecosystems; the `[heartbeat-batch-merge]` tag is project-specific
for future Zeta bots to filter on.

Composes:
- B-0858.4 merge tool (this PR's primary substrate)
- agent-heartbeats branch protection (ruleset 16934633)
- .claude/rules/non-coercion-invariant.md HC-8: heartbeats are
  observational metadata, not factory logic; adversarial review
  not warranted
- .claude/rules/methodology-hard-limits.md: clinical/security floor
  preserved — bot-skip applies ONLY to docs/agent-heartbeats path;
  any other path in the PR diff invalidates the skip

Per operator 2026-05-27: heartbeats are NOT code; they're tick
metadata; review-bot attention belongs on substantive substrate
work (PRs touching tools/, src/, .claude/rules/, etc.), not on
the per-tick observability surface.

Agency-Signature-Version: 1
Agent: Otto
Agent-Runtime: Claude Code (auto mode)
Agent-Model: claude-opus-4-7
Credential-Identity: aaron-otto-vscode
Credential-Mode: operator-authorized
Human-Review: pre-merge-pending
Human-Review-Evidence: operator-direction-2026-05-27-bots-skip-heartbeat-merges
Action-Mode: substrate-fix-fwd-bot-config
Task: B-0858.4

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(B-0858.4): markdownlint ignores docs/agent-heartbeats/*/** — auto-generated tick records aren't authored prose (operator 2026-05-27 "bots should know nothing to review here")

PR-review automation inventory for heartbeat folder:

| Reviewer | Already covered | Action this commit |
|---|---|---|
| Copilot code review | hard-rule #0 in .github/copilot-instructions.md | done |
| markdownlint (gate.yml + ci/lint) | scans .md files broadly | ADD `docs/agent-heartbeats/*/**` to .markdownlint-cli2.jsonc ignores |
| CodeQL | only scans .cs/.fs/.ts/etc | no action (heartbeat .md files outside scan scope) |
| backlog-index-integrity | path-scoped to docs/backlog/ | no action |
| memory-index-integrity / memory-index-drift / memory-reference-existence-lint | path-scoped to memory/ | no action |
| tick-shard-relative-paths | path-scoped to docs/hygiene-history/ticks/ | no action |
| role-ref-current-state-surfaces-lint | targets CLAUDE.md/AGENTS.md/GOVERNANCE.md | already-clean (prior commit anonymized) |
| build-and-test / tsc tools / lint suite | path-blind but only touches code-changes | no action (heartbeats are .md only) |

The `*/**` glob preserves README.md at folder root (authored prose;
gets linted) while ignoring per-tick records under <persona>/YYYY/
MM/DD/*.md (mechanically generated; ZetaID-named; not authored).

Verification: `bunx markdownlint-cli2 'docs/agent-heartbeats/**/*.md'`
returns clean (README + future seed files pass; per-tick records
ignored).

Per operator 2026-05-27 + .github/copilot-instructions.md hard-rule
#0: heartbeats are observational tick metadata, not factory logic;
review-bot attention belongs on substantive substrate work.

Agency-Signature-Version: 1
Agent: Otto
Agent-Runtime: Claude Code (auto mode)
Agent-Model: claude-opus-4-7
Credential-Identity: aaron-otto-vscode
Credential-Mode: operator-authorized
Human-Review: pre-merge-pending
Human-Review-Evidence: operator-direction-2026-05-27-bot-review-inventory
Action-Mode: substrate-fix-fwd-bot-config
Task: B-0858.4

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(B-0858.4): 4 Copilot findings — header doc accuracy + gh launch error surface + idempotent re-use of existing PR + reused-vs-opened reporting

4 Copilot threads on PR #5471 resolved:

**1. Header doc inaccuracy (Copilot @33)**
Header claimed REST /pulls/{N}/merge but implementation uses
`gh pr merge --auto --squash`. Fixed header "Composes:" block to
list the actual composition (compare for up-to-date check + REST
POST /pulls for creation + gh pr merge for arming auto-merge).

**2. gh() silent on spawnSync launch failures (Copilot @75)**
spawnSync sets result.error (not stderr) when launch fails — e.g.,
`gh` not on PATH. Original gh() returned {status: -1, stderr: ""}
producing unhelpful empty-message errors. Fixed: gh() now checks
result.error and produces "gh CLI launch failed: <message> (is gh
installed + on PATH?)" stderr.

**3. openMergePR brittle on duplicate PR (Copilot @109)**
GitHub returns 422 "A pull request already exists for ..." when
re-running with an existing open PR head→base. Fixed with new
findExistingPR helper that queries `pulls?state=open&head=...&base=...`
first; if existing PR found, re-use it (re-arm auto-merge);
otherwise create new. Idempotent — periodic cron re-runs work
without 422 failure. Output distinguishes "opened" vs "re-used"
+ "auto-merge re-armed".

**4. Tests don't cover isUpToDate + openMergePR (Copilot @5 on test file)**
Network-dependent functions inherently hard to unit test (need gh
auth + live API). Empirically validated through dogfood (PR #5470
created via this tool). Future B-0858.5 row may add integration-
test scope.

Per .claude/rules/blocked-green-ci-investigate-threads.md verify-then-fix:
each Copilot finding addressed in single fix-pass; idempotency
issue (#3) is operationally important because operator-named goal
is periodic/cron invocation.

Operator forward-looking context (this commit batch + future B-0858.5):
"over time we can start adding automated observations about current
state to the heartbeat that it automatily gathers before pushing" +
"heartbeats also become debug logs once we have current state
attached". Captured in upcoming B-0858.5 row.

Agency-Signature-Version: 1
Agent: Otto
Agent-Runtime: Claude Code (auto mode)
Agent-Model: claude-opus-4-7
Credential-Identity: aaron-otto-vscode
Credential-Mode: operator-authorized
Human-Review: pre-merge-pending
Human-Review-Evidence: copilot-4-findings-on-pr-5471
Action-Mode: substrate-fix-fwd-correctness-plus-idempotency
Task: B-0858.4

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Lior <lior@zeta.dev>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
AceHack added a commit that referenced this pull request May 27, 2026
…t-in; debug-log property (Aaron 2026-05-27 deferred post-USB) (#5473)

* docs(B-0858.5): heartbeat auto-state-gathering row — heartbeats become debug logs once current state attached (Aaron 2026-05-27 deferred; USB priority)

Captures operator's 2026-05-27 two-part vision:

1. "over time we can start adding automated observations about
   current state to the heartbeat that it automatily gathers before
   pushing, we can backlog this no right now we are gong to go back
   to usb once we have this hearbeat current iteration done"
2. "then heartbeats also become debug logs once we have current state
   attached"

15 candidate auto-gathered fields tabulated with sources + cost:
- local-only (cwd/branch/staged-count/etc): zero REST
- git-state (last-commit-sha-prefix; behind-vs-ahead): pure git
- REST-state (rate-limit; open-PR-count): bounded REST
- peer-state (peer-agent-process-count; dotgit-saturation-tier): local
- sentinel-state (CronList for <<autonomous-loop>>): harness-tool

5 sub-rows planned (5a→5b→5d→5c→5e order) — local-first per
zero-REST-cost prioritization.

Debug-log property: each ZetaID-indexed heartbeat carries enough
state to reconstruct "what was the world like at this tick" for
forensic analysis across:
- single-agent tick-cadence + named-dep + disposition timeline
- multi-agent saturation timeline
- substrate-landing attribution (which agent + disposition produced
  which PR)
- catch-43 sentinel-restart timeline

Status: DEFERRED implementation per operator USB priority. Recording
the row exists is critical for the deferred work to reliably happen
per operator 2026-05-27 separation-of-concerns discipline.

Composes:
- B-0858 row (PR #5456 merged)
- B-0858.3 writer (PR #5464 merged) — this row extends
- B-0858.4 merge tool (PR #5471 in-flight)
- .claude/rules/refresh-world-model-poll-pr-gate.md (5c rate-limit tier)
- .claude/rules/codeql-no-source-on-docs-only-pr-is-broken-commit-canary.md (5d dotgit-saturation)
- tools/hygiene/audit-agencysignature-main-tip.ts (sibling forensic tool)

Per operator's "right now we are gong to go back to usb once we
have this hearbeat current iteration done": filing this row IS the
last heartbeat-iteration substrate item; next tick continues USB
priority work (B-0852.4d wire NixOS module into common.nix).

Per .claude/rules/agent-worktree-hygiene-never-hold-main-...: isolated
worktree at /private/tmp/zeta-b0858-5-row-1440z; operator primary
checkout untouched.

Agency-Signature-Version: 1
Agent: Otto
Agent-Runtime: Claude Code (auto mode)
Agent-Model: claude-opus-4-7
Credential-Identity: aaron-otto-vscode
Credential-Mode: operator-authorized
Human-Review: pre-merge-pending
Human-Review-Evidence: operator-direction-2026-05-27-auto-state-gathering-debug-logs-vision-deferred-post-usb
Action-Mode: substrate-row-filing
Task: B-0858.5

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(B-0858.5): consent-first constraint added as load-bearing — opt-in/explicit/no-panopticon (Aaron 2026-05-27 critical principle)

Operator 2026-05-27 critical principle (message 3 in the 3-message
sequence captured): "when we gether current state it should be
explicit to the agents what information is being gathers so it's
consent first and the agent is okay with the verbosity of the
current state heartbeat, we don't need to summugle in a panopticon
for heartbeats."

Added load-bearing 6-point constraint to B-0858.5 row body:

1. Default: gather NOTHING beyond current minimal heartbeat
2. Each gathered field is OPT-IN via explicit flag OR env var
3. Agent invocation MUST explicitly enable each gather; no implicit all-on
4. The agent owns the verbosity (mid-tick reconfig supported)
5. Documented at point-of-introduction (each gather flag has explicit description)
6. --show-gather-config flag prints all available gather fields + opt-in state

Anti-panopticon framing: heartbeats-as-debug-logs (operator message 2)
does NOT override consent-first (message 3); they compose. The gather
mechanism itself cannot become a coercion vector because each field
requires explicit opt-in at agent scope.

Per .claude/rules/non-coercion-invariant.md HC-8: operator authority
over own state-disclosure preserved at every gather-field scope. Per
the same rule's "no coercion via mechanism in the system" clause:
gather mechanism stays consent-bounded by construction.

Agency-Signature-Version: 1
Agent: Otto
Agent-Runtime: Claude Code (auto mode)
Agent-Model: claude-opus-4-7
Credential-Identity: aaron-otto-vscode
Credential-Mode: operator-authorized
Human-Review: pre-merge-pending
Human-Review-Evidence: operator-direction-2026-05-27-consent-first-no-panopticon-message-3
Action-Mode: substrate-fix-fwd-row-body
Task: B-0858.5

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(B-0858.5 CI): MD032 — "+ optional" at line 78 start parsed as list marker; rewrite "+ optional" → "plus optional"

Markdownlint failure on PR #5473: line 78 of B-0858.5 row body had:

    YAML frontmatter (zetaid + agent + persona-slot + timestamp +
    authority + momentum + chromosome + location + firefly + disposition
    + optional named-dep + optional parent-pr). This row extends the

The "+ optional" at start of line 78 parsed as an unordered-list marker
(MD032 needs blank-line-before-list). Same shape as the earlier #5464 fix
(line 85 README "+ folder convention" → "and folder convention").

Fix: rewrite "+ optional named-dep" to "plus optional named-dep" —
same semantics, no list-marker ambiguity at line start.

Local re-lint: `bunx markdownlint-cli2 'docs/backlog/P2/B-0858.5-*.md'`
returns clean.

Agency-Signature-Version: 1
Agent: Otto
Agent-Runtime: Claude Code (auto mode)
Agent-Model: claude-opus-4-7
Credential-Identity: aaron-otto-vscode
Credential-Mode: operator-authorized
Human-Review: pre-merge-pending
Human-Review-Evidence: ci-markdownlint-md032-on-pr-5473
Action-Mode: substrate-fix-fwd-ci
Task: B-0858.5

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(B-0858.5): 4 Copilot findings — depends_on broken edges + two-vs-three messages inconsistency + line-79 "+ optional" still mid-line ambiguity + cross-ref inaccurate

4 Copilot threads on PR #5473:

1. **depends_on broken edges (@12)**: referenced B-0858.3 and B-0858.4
   as sub-IDs but those aren't standalone row files in docs/backlog/
   (they were implementation PRs; .3 = PR #5464 writer; .4 = PR #5471
   merge tool; substrate is the implementations themselves not separate
   rows). Fix: drop those entries; reference only B-0858 (the parent
   row that does exist as a backlog file).

2. **"two-part vision across two messages" vs three (@20)**: section
   below has Message 1/2/3 after the consent-first constraint addition;
   intro still said "two messages". Fix: "Three-message vision captured
   across the implementation cycle (auto-state-gathering value +
   debug-log property + consent-first constraint)".

3. **Line 79 "+ optional parent-pr" mid-line ambiguity (@79)**: my
   earlier fix changed "+ optional named-dep" but the line still had
   "+ optional parent-pr" mid-line. Fix: rewrite to "with optional
   named-dep and optional parent-pr fields" — no leading "+" anywhere.

4. **Cross-ref inaccurate (@158)**: cited codeql-no-source-...-canary.md
   for dotgit-saturation tier; that file is about CodeQL "no source seen"
   canary not the dotgit table. The dotgit-saturation table actually
   lives in refresh-world-model-poll-pr-gate.md (which I cite separately
   at line 153-155). Fix: remove the incorrect codeql-canary cross-ref.

Local re-lint clean.

Per .claude/rules/blocked-green-ci-investigate-threads.md verify-then-fix:
each Copilot finding verified against the actual file content; all 4
real findings; bundled fix.

Agency-Signature-Version: 1
Agent: Otto
Agent-Runtime: Claude Code (auto mode)
Agent-Model: claude-opus-4-7
Credential-Identity: aaron-otto-vscode
Credential-Mode: operator-authorized
Human-Review: pre-merge-pending
Human-Review-Evidence: copilot-4-findings-on-pr-5473
Action-Mode: substrate-fix-fwd-correctness
Task: B-0858.5

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Lior <lior@zeta.dev>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants