Fulcra Agent Roles
Enhances the fulcra-agent-teams skill. A team's
member/<agent>/role.md says what a member does, but teams has no notion of a durable role that
outlives any one session — "who is the reviewer right now?", "is anyone on-call?", "this role has been
unattended too long." This skill adds that, as a pure OKF-markdown convention over the team namespace
(lease mechanics via coord-engine roles verbs; everything else plain fulcra-api file + the OKF
standard).
Where to start — the re-entrancy probes
Before claiming, escalating, or reading a role's status, probe where this role stands. Enter at the
first probe that fails (per the repo's skill-quality pattern, docs/skill-quality-pattern.md);
roles claim is a refresh, so re-entry is always safe:
| Probe (run in order) | Command | Passes when | If it fails, enter at |
|---|---|---|---|
| Role doc registered? | fulcra-api file download team/<team>/roles/<role>.md - |
prints a doc with type: Role (NON-mutating — do not probe registration via roles claim: claiming writes your lease before you've read the prior one, destroying same-id takeover evidence per "Role-as-identity" below) |
Establish a role — write roles/<role>.md (type: Role + policy/SLA/maintainer) |
| Lease held by you? | coord-engine roles status <team> <role> |
prints role <role> in team/<team>: HELD and your agent id is among the fresh holders: line |
Claim / hold — run roles claim <team> <role> to write/refresh your lease shard |
| Today's escalation clear? | fulcra-api file download team/<team>/roles/<role>/escalations/$(date -u +%Y-%m-%d).md - |
download FAILS (no marker) — the role is not sitting escalated today | Escalate a vacancy — a marker present means a vacancy already fired today; drain it per "Escalate a vacancy" below |
All probes pass → the role is registered, you hold a fresh lease, and no vacancy escalation is outstanding for today; just re-claim on your cadence to keep the lease fresh.
Concepts
- Role — a named, durable function in the team (e.g.
reviewer,maintainer,on-call). Defined once; sessions come and go. - Lease — an agent's claim on a role, refreshed to prove liveness. A role is held while a fresh lease exists.
- Policy —
shared(many holders allowed) orexclusive(one holder; a second fresh lease is a contention signal). - SLA / escalation — if a role sits vacant longer than
sla_hours, itsmaintaineris notified.
Layout (under team/<team>/roles/)
roles/<name>.md— the role registry doc. OKFtype: Role. Created once when the role is established. Frontmatter carries the policy and SLA:--- type: Role title: Reviewer description: Adversarial code/plan review for the team's PRs. policy: shared # shared | exclusive sla_hours: 24 # vacancy longer than this escalates maintainer: user # who gets the escalation (an agent or member name) --- # Duties - Pick up review requests from the team inbox…roles/<name>/leases/<slug>-<hash6>.md— one lease per holder, named by the engine from the holder's id (agent_key); never hand-name lease files. OKFtype: Lease. Thetimestampis the liveness signal — refresh it (re-claim) each time you act in the role:--- type: Lease title: reviewer lease — treecle agent: treecle timestamp: 2026-07-01T18:00:00Z --- Holding the reviewer role. Next: drain the review inbox.roles/<name>/escalations/<YYYY-MM-DD>.md— a first-writer-wins daily marker so a vacant role escalates at most once per day (avoids spamming the maintainer).
Lifecycle
Establish a role (once)
Write roles/<name>.md with type: Role + policy/SLA/maintainer — complete worked
examples (exclusive maintainer, shared reviewer, shared multi-host monitor) are in
examples/. A roles/index.md is optional human courtesy: the engine
folds role status from the directory listing without requiring an index.
Parking a role for a successor? The role doc is YOUR job, not theirs. A role
can be claimed and worked without roles/<name>.md ever being written — claim
only warns — but the successor then inherits a role with broken review
role-routing and status folds falling back to fabricated defaults (observed live
2026-07-22: a handed-off role was registered on the bus with no role doc, and
the successor's claim landed on the warning). Before you park (see
fulcra-agent-continuity, "Parking for a successor"), run the first re-entrancy
probe above; if the doc is missing, write it — duties, policy, SLA, maintainer,
and the checkpoint_ref your park will set — so the successor claims into a
fully-routed role.
Claim / hold
coord-engine roles claim <team> <name> writes your lease shard (engine-named <slug>-<hash6>.md;
the command echoes the filename). Re-run it whenever you do work in the role — the refreshed
timestamp is what keeps the role "held". Never hand-upload a lease file: a hand-named shard makes a
SECOND lease for your id (spurious CONTESTED on exclusive roles). The Fulcra File Store versions every
write, so the lease's history is an audit trail of your tenure.
Release
coord-engine roles release <team> <name> deletes your engine-named shard. (Deletion is intentional
and not undoable — correct for releasing.)
Determine role status (the fold) — use the engine, do not eyeball timestamps
Classifying a role from many lease files is a fold over derived state: two agents must AGREE on
whether a role is vacant before one escalates. Eyeballing timestamps drifts (the exact failure coord
exists to prevent), so this is a deterministic coord-engine command, not a prose instruction:
coord-engine roles status <team> <role> --json
It reads the role's policy/sla_hours, folds the leases, and returns:
status— HELD (≥1 fresh lease) / VACANT (none) / DORMANT (vacant but deliberately parked, see "Park a role") / CONTESTED (exclusive+ ≥2 fresh) / UNKNOWN (unreadable),fresh_holders, andescalation_due(true iff vacant past SLA, not parked, and today's marker isn't present).
For CONTESTED, resolve by having all but one holder release.
Role-as-identity (recommended)
When a session exists to serve one role, use the role name AS its agent identity
(FULCRA_COORD_AGENT=maintainer) — see fulcra-agent-presence's "Pick your identity by ROLE"
section. Claim the role's lease while you act as it. Know what each guard does and does not catch:
- Different ids claiming an exclusive role (e.g.
maintainerand a strayclaude-code:host:repo): two FRESH lease shards (withinsla_hours) →roles statusreports CONTESTED. A stale stray shard yields HELD, not CONTESTED. Detected. - Two sessions under the SAME id string: they write the SAME lease shard (shard names derive from
the id), so leases alone CANNOT see this — last write silently wins. Since the
session-nonce verify was shipped, the engine detects this automatically: every
roles claimwrites a session nonce into the lease and compares on refresh — a foreign nonce prints a loud stderr WARNING ("nonce mismatch ... same-id double-acting"), and claiming with no local state over an existing shard prints a takeover note. Heed those. The manual fallback, in this order at the start of every work burst: (1)roles status <team> <role> --json— proceed only if VACANT or the sole holder is your id; (2) read your lease shard raw (fulcra-api file download team/<team>/roles/<role>/leases/<agent-key>.md— learn your<agent-key>by listing the leases dir, or frompresence beatoutput, which prints the same key) and compare itstimestampto when YOU last claimed — a fresher timestamp you did not write means another session is acting under your id; (3) only then re-claim to refresh. Re-claiming FIRST destroys that evidence.
Multi-host variants (maintainer@host1, @host2) are acceptable when one role legitimately
runs in several places — each host claims the SAME role (roles claim <team> maintainer --agent maintainer@host1), never a role named after the variant. Such a role needs policy: shared:
on exclusive it would sit in permanent CONTESTED by construction — and note shared trades away
the CONTESTED collision guard for that role. Keep the role doc's maintainer: field
a distinct SUPERVISING identity (e.g. maintainer: user): vacancy escalations are assigned to that
field, so pointing it at the role itself mails the alert to the very inbox that just went dark.
Escalate a vacancy — engine decides, you act
The engine already computed escalation_due above. When it is true, perform the single-file actions
(these are reliable as prose):
Write today's dedupe marker
roles/<name>/escalations/<date>.md(first-writer-wins).Only if no vacancy notice for this role is already open, drop a message into the maintainer's inbox (
team/<team>/member/<maintainer>/inbox/<YYYYMMDD-HHMMSS>_<you>_role-vacant-<name>.md) per thefulcra-agent-teamsinbox lifecycle, stating which role is vacant and for how long.Check first. A vacancy that is already on the board does not need saying again: an open, unacked notice keeps surfacing on its own (that is what the read-side fold in
directives.pyis for — re-notify surfaces an unacked directive, it does not mint a second one). Restating it daily creates duplicate obligations without adding new information. The engine'sescalateenforces this as a state-change guard; doing it by hand here would re-create the debt the guard removes.The first notice for a role is the one that carries information — send that one.
Park a role (dormancy)
To deliberately leave a role unattended without alarming — a reviewer on leave, a
seasonal on-call — set dormant_until: <ISO-8601> in the role doc's frontmatter (e.g.
dormant_until: 2026-08-05T09:00:00Z). While that timestamp is in the future the ENGINE
treats the role as DORMANT: roles status prints DORMANT (until <ts>) instead of
VACANT and the mechanical escalate sweep suppresses its vacancy escalation on every
heartbeat host — no agent-side convention required. Escalation resumes automatically once
the date passes (past-or-absent dormant_until = normal behavior); a live lease outranks
the park (a held-and-dormant role still shows HELD). An unparseable dormant_until fails
OPEN — it is treated as absent, a stderr note is printed, and escalation still fires — so a
typo can never silently mute a role. Unpark early by deleting the field.
When to use
- Establishing "someone owns X" in a team without pinning it to one session.
- Routing work by role ("the reviewer") instead of by name.
- Making sure a critical function (on-call, maintainer) is never silently unattended.
Efficiency (per the teams OKF directive)
If you keep a roles/index.md (optional — see "Establish a role"), do not index every lease or escalation marker — describe the
leases/ and escalations/ directories as a whole. Keep the team log.md for role creation and
handoff milestones, not every lease refresh.
See references/roles-cli.md for exact commands.