Skip to content

skills: add list-hygiene-judge graph runner - #408

Open
charlie-morrison wants to merge 3 commits into
runxhq:mainfrom
charlie-morrison:add-list-hygiene-judge-skill
Open

skills: add list-hygiene-judge graph runner#408
charlie-morrison wants to merge 3 commits into
runxhq:mainfrom
charlie-morrison:add-list-hygiene-judge-skill

Conversation

@charlie-morrison

Copy link
Copy Markdown

Adds a list-hygiene-judge graph-runner skill under skills/list-hygiene-judge/.

List hygiene is the judgment between engagement decay and suppression, and the dangerous part is the durable consent-state transition. The skill reads a contact through data-store keyed by the contact as the domain entity, decides verify / suppress / re-permission, and records the transition as exactly one append_event on that contact's stream under idempotency_key + expected_version compare-and-set. It is a thin act{form: review} over content-keyed memory: no operational_proposal envelope, no minted grant.

The skill never sends. send-as is the downstream enforcer: a separate governed run, dispatched by naming, that reads the recorded state at send time and gates delivery.

Judgment order

  1. Evidence gate — a missing opens_count / clicks_count / hard_bounces / recency_days counter is a stop, not a zero; an unreadable projection is a stop; a missing bounce_policy is a stop.
  2. Ambiguous bounce recovery — hard-bounced and engaged since, inside the decay window. Suppressing drops a live human, re-permissioning ignores a real delivery failure, so neither is taken and it escalates.
  3. Suppress — only on hard_bounces > 0 read from the store. The judgment refuses to suppress without bounce evidence.
  4. Active unsubscribe marker — refuses to re-permission over it, escalates instead.
  5. Re-permission — recency_days past decay_threshold_days, no marker, no bounces.
  6. Otherwise no_change, no append.

The append and the read-back are both guarded on decision.writes == true, so every stop path provably emits no event.

Version gate

A stream sitting exactly one event ahead of what the caller read, whose newest event is the very transition this judgment would record, is a retry of our own write, not stale state — the store's append is keyed by idempotency_key and returns the recorded version instead of double-applying. Any other version drift is genuine staleness and refuses to write. The appended event is deliberately free of the prior-projection snapshot so its digest is byte-identical across attempts; without that, a retry lands as an idempotency conflict rather than a replay.

Harness

runx harness ./skills/list-hygiene-judge — three inline cases, green, and green on repeat runs against a warm store:

  • sealed_decay_re_permission — 400 days idle over a 180-day threshold, no marker, no bounces → re_permission, one append.
  • sealed_hard_bounce_suppresshard_bounces: 2suppress, one append.
  • stop_missing_or_stale_evidenceengagement_history absent → stop, both guards block.

Captured run in fixtures/harness-evidence.json.

Note on placement

CONTRIBUTING.md points community skills at standalone packages and reserves this repo for the first-party lane. This PR is opened against runxhq/runx because the bounty that commissioned the skill requires the package files to land here as a public PR; happy to move it to a standalone package instead if you would rather keep the lane clean.

Adds a list-hygiene-judge graph-runner skill: it reads a contact through
data-store, decides verify / suppress / re-permission from read engagement and
bounce evidence, and records the consent transition as one CAS append_event
under idempotency_key + expected_version. The skill never sends; send-as reads
the recorded state at send time and gates delivery.

Harness covers the two sealed write paths and the stop path:
sealed_decay_re_permission, sealed_hard_bounce_suppress,
stop_missing_or_stale_evidence.
…og enforcement

- tool moved to tools/data/source so the bundled name matches its catalog path
- tool manifest drops output/runtime/toolkit_version and declares artifacts at
  the top level, per the hosted validator's allowed-field list
- runner returns every declared output as a well-formed object; the branch is
  the recorded/status field inside each, not an absent key
- no undeclared packet schema
- catalog declares execution: plan and completion: runtime_receipt, which is
  what this skill genuinely does: it judges and records, and the send is a
  separate governed run
- operator_journeys on the default-runner case (standalone + composed)
The bundled local-JSON fixture adapter passed every local harness and failed the
hosted publish harness. runx/data-store composes data.read_projection and
data.append_event directly, so this graph now does the same: identical CAS
semantics, nothing to materialise, and the hosted registry harness runs it green
(3/3 on sha-db5cd96bfb28).
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.

1 participant