Chainlink
chainlink is a CLI issue tracker (Rust binary). Each repo / workspace has a
.chainlink/ directory holding issues with id, title, labels, parent/child
relations, and comments. Everything is local — no GitHub, no server.
The five-whys skill uses chainlink as its storage backend (see
five-whys/CHAINLINK_USAGE.md for the RCA-specific commands). This skill is
the general-purpose CLI reference for everything else: todos, bug tracking,
follow-ups, parking-lot items, anything that needs to persist between turns.
Contract
Trigger: User mentions a task, bug, open question, or multi-step plan that needs
to outlive the current session. Mimir notices work that deserves to be tracked across
heartbeats or that needs chainlink issue ready-driven pickup in a future session.
Requires: A cwd with a .chainlink/ directory (or ancestor) — the agent's home
(where operator-tracked issues live), not the mimir source tree. The chainlink
binary is on PATH.
Guarantees:
- Issues created with enough context for future-mimir to act on: title + description with handle (what to do), success path, failure path.
- Subissue trees have dependency edges (
chainlink issue block) where ordering matters. - Multi-heartbeat work has a parent issue with subissues decomposed before implementation
starts (planning-heartbeat-first discipline from
memory/core/50-heartbeat-patterns.md).
Does not: Sync to GitHub; send notifications; auto-close on PR merge (stale-open sweep is the mechanism, run weekly); initialize a new tracker without operator instruction.
Where to operate
Chainlink commands need a .chainlink/ directory in the cwd or an ancestor.
The operator's home repo or a working directory will have one. If you get
No .chainlink directory found, you're in the wrong place — check the
operator's repo root for .chainlink/, or ask before initializing one.
To initialize a fresh tracker (only when the operator says so):
chainlink init
Verifying availability
chainlink --version
If the command isn't found, the binary isn't installed in this environment — report that to the operator rather than guessing.
Day-to-day patterns
Create a todo
chainlink issue create \
--label todo \
--label "for-jason" \
--priority medium \
"Re-check the simhi multi-session regression after the sim_threshold tune"
# Returns: Created issue #42
--label / -l is repeatable. Pick labels that match an existing scheme — query
chainlink label list first if you're not sure what's in use.
Attach a description on creation with --description / -d:
chainlink issue create "Add timeout to dispatch loop" \
--description "Currently unbounded; need a 30s ceiling with retry." \
--priority high \
--label todo
For non-trivial descriptions (with §, parens, smart quotes, or multi-line),
use a heredoc — bare --description "..." breaks on shell-quoting (see
Failure modes below):
DESC=$(cat <<'EOF'
Multi-line description with §1.2 and (parens) and
"smart quotes" all preserved.
EOF
)
chainlink issue create "<title>" --description "$DESC" --priority medium --label todo
Priority is low | medium | high | critical (default: medium). Treat it
as load-bearing — filter on it later via --priority high to focus.
Add notes / context
chainlink issue comment 42 "Initial run: 71% multi-session at 0.80 sim."
Comment takes a positional <TEXT> (no --body flag). For non-trivial bodies
with special characters, use the same heredoc pattern as --description above.
List open work
# All open (default)
chainlink issue list
# Filter by status (flag is --status, NOT --state)
chainlink issue list --status all # open + closed
chainlink issue list --status closed
# Filter by label / priority
chainlink issue list --label todo
chainlink issue list --priority high
chainlink issue list --label todo --label for-jason
# JSON for scripting
chainlink issue list --label todo --json
Pick what to work on next
# Unblocked issues only — primary "what's actually actionable?" command
chainlink issue ready
# Tool suggests next from heuristics
chainlink issue next
# Text search across issues — useful before creating to avoid duplicates
chainlink issue search "dispatch timeout"
ready correctly filters out blocked items AND closed-issue blockers (an issue
unblocks once its blocker closes). Prefer ready over list when you're asking
"what should I pick up now?"
Look at one issue
chainlink issue show 42 # human-readable, shows Blocked-by / Blocking edges
chainlink issue show 42 --json # structured (for chaining with jq)
# Show the whole hierarchy (NOT a single issue's subtree — tree takes no positional ID)
chainlink issue tree
chainlink issue tree --status open
Note: issue tree does NOT accept a positional ID — it always shows the
whole hierarchy filtered by --status / --quiet. For a single issue's
children/relations, use issue show <id> (its Blocked-by / Blocking edges
show the immediate relationships).
show retains historical edges (an issue still lists a closed blocker in
its "Blocked by" output), while ready filters them — see Failure modes
for when this matters.
Mark progress
chainlink issue label 42 in-progress
chainlink issue unlabel 42 todo
chainlink issue close 42 # finished
Note: close does NOT accept --comment or -m — if you want to leave a
closing note, run chainlink issue comment 42 "..." first, then chainlink issue close 42 separately.
Subissues for breakdowns
When a todo is too big or has dependencies:
chainlink issue subissue 42 \
--label todo \
--description "Acceptance: alert fires on threshold cross + matching test." \
"Sketch the alert flow"
chainlink issue subissue 42 \
--label todo \
--description "Acceptance: dispatch path delivers payload + ack." \
"Wire it through to the dispatch path"
# Closing the parent doesn't auto-close children — close them as you go.
Each subissue should have a non-empty --description capturing acceptance
criteria — that's how a future session (or another agent) can pick up the
work without re-reading the parent. See "Subissue decomposition pattern"
below for the full workflow.
Block / unblock relations
chainlink issue block 17 42 # 17 is blocked by 42 (don't work on 17 yet)
chainlink issue unblock 17 42
The live CLI argument order is chainlink issue block <ID> <BLOCKER>:
<ID> is the issue that should wait, and <BLOCKER> is the prerequisite.
Once issue 42 closes, issue 17 automatically appears in chainlink issue ready. issue show 17 still lists 42 in its "Blocked by" output as
historical record — trust ready for "is this actionable?", trust
show/tree for "what was this once depending on?"
When to create a chainlink issue
Create one when:
- The user mentioned something they want you to remember beyond the current conversation ("don't let me forget to call the dentist", "track down that flaky test").
- You discovered a bug or rough edge worth fixing later but not now.
- A reflection / heartbeat surfaced a follow-up that needs a real owner.
- A 5-Whys analysis produced an action item (use the
rca+action-itemlabels — seefive-whys/CHAINLINK_USAGE.md). - A task is too big for one session and benefits from subissue decomposition (see pattern below).
Don't create one when:
- It's a within-turn working note → use the Plan tool or just hold it in context.
- It's a memory / preference about the user → store in saga via
memory_store, not chainlink. - The user is just venting and didn't ask you to track anything.
Subissue decomposition pattern
The canonical shape for any "this is too big for one session" task — two-phase work split between a planning session and one or more implementing sessions.
Phase 1 — planning session
- Create parent with a description summarizing the goal and overall acceptance criteria.
- Decompose into subissues. Each subissue gets a non-empty
--descriptionwith its own acceptance criteria — sized so a future session can pick it up without re-reading the parent. - Add dependency edges via
chainlink issue block <blocked> <blocker>when<blocked>must wait for<blocker>to complete. - Mark parent in-progress via
chainlink issue label <parent> in-progress.
Acceptance for the planning session: parent has subissues, every subissue has a non-empty description with acceptance criteria, dependency graph committed. Half-decomposed parents are worse than not starting — if planning runs out before decomposition finishes, comment on the parent and resume next session.
Phase 2 — implementing sessions
chainlink issue ready # find unblocked subissues
chainlink issue show <subissue> # read its acceptance criteria
# ... do the work ...
chainlink issue close <subissue>
Pick the highest-priority unblocked subissue. Close on completion. The next
session repeats ready to find what's actionable now.
Phase 3 — closing the parent
Once all subissues are closed, do any integration work (tests across the decomposition, docs, the final PR if applicable), then close the parent.
Querying open work
End-of-session is a good time to surface what's open. Prefer ready for
actionable work; use list when you need the full picture including blocked
items:
# What's actionable RIGHT NOW (unblocked)?
chainlink issue ready
# What's currently labeled todo / for-jason?
chainlink issue list --label todo --label for-jason --json \
| jq '.[] | "#\(.id): \(.title)"'
# Stale issues (no comment in N days) — useful for librarian sweeps
chainlink issue list --json | jq '.[] | select(.updated_at < "2026-04-04")'
If the operator asks "what should I work on?" or "what did we have open?",
chainlink issue ready is the right starting point — not your memory,
and not list (which includes blocked items the operator can't actually
start on).
Five-Whys integration
Five-whys reuses chainlink for tree storage. The labels are:
| Label | Meaning |
|---|---|
rca |
Part of a root cause analysis |
5-whys |
Root issue of a 5 Whys tree |
bedrock |
Leaf node — root cause found |
action-item |
Concrete fix derived from bedrock |
external-boundary |
Root cause outside your control |
accepted-tradeoff |
Known intentional choice — not a bug |
See five-whys/CHAINLINK_USAGE.md for the full tree-construction flow,
falsification cascades, and example.
Conventions
- Labels: prefer existing labels over inventing new ones.
chainlink label listshows what's in use. - Titles: imperative for todos ("Add timeout to dispatch loop"), past- tense / observational for records ("Discord typing-indicator regression fixed in commit 2ea9863").
- Descriptions: every subissue gets one; non-trivial parents get one. Empty description = future session can't pick up the work.
- Priority: load-bearing. Default
medium; reservehighfor "blocks shippable work" andcriticalfor "service-affecting now". - Don't close issues you didn't open without an explicit operator OK — closing destroys the open-work signal.
- Don't bulk-create. Each chainlink issue should earn its keep. A pile of micro-issues is harder to navigate than five well-titled ones.
Writing descriptions future-you can act on
Heartbeat picks up issues by reading descriptions cold. Prose ("look into
the rate-limit thing later") is unanchored — a future heartbeat reads it,
shrugs, and moves on. Descriptions with handles ("investigate why
oauth_quota_anomalous has fired 5×/day on
discord-100000000000000002 since 2026-05-03") are actionable on first
read. This applies equally to -d/--description on create and to the
acceptance-criteria bullets on subissue descriptions (see "Subissue
decomposition pattern" above) — same four questions, same handles.
A good description answers four questions:
- What's the trigger, named with handles? Specific event names,
file paths, commit hashes, dollar thresholds, dedupe-key shapes —
anything a future heartbeat can grep for. Not "saga thing went
wrong" but "
saga_synthesis_skipped_boundaryfired 5×/24h on synthesis turns since PR #32 (2026-05-06)". - Why does this matter? The reason it crossed the bar from noise to signal. "Atoms aren't landing → silent memory loss" is a reason; "felt off" is not.
- What's the success path? The concrete next step if the
investigation finds something. "If the cause is the new dedup
window, the fix is bumping the floor in
_pre_message_hook." - What's the failure path? Likely modes if the cause isn't what
you expect. "If it's not the dedup window, check the consolidator —
PR #32 also touched
_partition_turns."
The handles matter most. With handles, future-you can grep
events.jsonl, query saga, or git log and find every record tied to
the issue. Without handles, the issue is unanchored and gets deferred
forever.
Idempotency: design for the boundary firing twice
Chainlink issues can be picked up by overlapping heartbeats. Pollers can emit duplicate events when a cursor wasn't saved before a crash. Subagent completion notifications can arrive after a retry. Synthesis turns can fire twice on the same session boundary. Design actions so they're safe to run twice.
The wrong instinct is "I just got here, I should do the thing." The right instinct is "I just got here, has the thing already been done?"
General tactics:
- Tag durable artifacts with a unique key. PR titles, commit messages, and chainlink issue titles are good unique keys — grep before acting.
- Update cursors atomically and after the side effect. Save the
poller cursor only once the events have been emitted; never
save-then-emit (a crash between would save the cursor but lose the
event). See
pollers/SKILL.mdfor the cursor pattern. - Use mkdir / O_CREAT-style claims for cross-process work. If a marker file or directory exists, another instance is already working — back off rather than collide.
- Treat
send_messageand similar with care. The harness circuit-breaks near-duplicate sends within a turn — but design as if it won't; the breaker is a safety net, not a correctness guarantee.
Mimir-specific idempotency surfaces
Several primitives have built-in idempotency you can lean on — plus a couple that look idempotent but aren't:
memory_storeis unique-key idempotent. Same content twice → one atom (deduplicated at the atom layer). Synth turns firing twice on the same boundary don't create two atoms.applied_proposals.jsonlis the legacy proposal-replay guard. Before re-applying a historicalstate/proposed-changes.mdentry, check whether its id is already instate/applied_proposals.jsonl— if so, it landed already and re-applying is a foot-gun. Protected memory/prompt proposals now go throughopen_proposal/submit_proposalPRs instead.chainlink issue createis append-only, not idempotent. Re-runningissue create "Same title"produces a new issue with a fresh id every time. Alwayschainlink issue search "<title-or-handle>"before creating — this is the most common chainlink foot-gun.send_messagehas harness-level dedup within a turn only. Across turns the breaker doesn't help; rely on unique keys in the message itself if the same content might be generated twice.gh pr createis not idempotent. It happily makes a second PR against the same branch. Alwaysgh pr list --head <branch>first.
Failure modes
No .chainlink directory found→ wrong cwd; navigate to the operator's repo or initialize viachainlink initif explicitly authorized.chainlink: command not found→ binary missing from PATH. Surface to operator; don't try to install yourself.unexpected argument '--state'(or similar--state/--body/-mrejection) → flag-name surprises vs gh-style intuition. The CLI is its own tool, not a gh clone. Always checkchainlink <subcommand> --helpwhen a flag fails — it's exhaustive. The mimir-tracked surprises list lives atmemory/issues/chainlink-cli-flag-surprises.md(close has no--commentor-m; list uses--statusnot--state; comment takes positional<TEXT>not--body; create takes--description / -dnot--body-file; tree takes no positional ID).- Comment / description bodies break on
§, parens, or smart quotes → shell-quoting bites. Use the heredoc-to-tmpfile pattern shown under "Create a todo" above. Reference:memory/issues/chainlink-comment-shell-quoting.md. issue showsays "Blocked by #N" but #N is closed → that's historical record. Useissue readyto determine actionability;showretains edges for audit. Reference:memory/issues/chainlink-ready-vs-show.md.- Subissue depth gets unwieldy past ~3 levels — flatten or split into separate trees if you find yourself going deeper.