Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions skills/list-hygiene-judge/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
---
name: list-hygiene-judge
description: Decide and durably record evidence-bound contact consent transitions for engagement decay and hard bounces. Use when an operator needs to re-permission, suppress, or stop for human review before any campaign send.
registry_owner: ArgonautWorks
---

# List Hygiene Judge

Use this skill between contact evidence collection and outbound delivery. It
turns fresh engagement, bounce, and consent evidence into one conservative
consent-state decision, records an allowed transition with compare-and-set
semantics, and reads the contact projection back before reporting success.

This is not a sender. A later `send-as` run must independently read the recorded
consent state at send time and gate delivery. Never treat this skill's output as
send authority, a campaign proposal, or proof that a message was delivered.

## Operating model

1. Read the contact projection through the exact provider-neutral
`data.read_projection` operation owned by the canonical `data-store`
contract. Bind `data_source_ref`, `resource`, and `aggregate_id` to the
contact's event stream. The read establishes the durable version used by the
decision.
2. Admit the supplied engagement and consent evidence only when its status is
`read`, its `evidence_version` equals `expected_version`, and the durable
projection has that same version. Missing, unreadable, ambiguous, or stale
evidence stops without an append.
3. Honor an active unsubscribe marker as terminal for automation. Route the
case to a human list-hygiene reviewer; never re-permission it automatically.
4. Suppress when verified `hard_bounces` is greater than zero and policy names
`suppress`. Otherwise, re-permission only when `recency_days` exceeds the
declared decay threshold and no unsubscribe marker exists.
5. Append exactly one `list_hygiene.consent_transitioned` event with the
caller's stable `idempotency_key` and `expected_version`. The data-store owns
compare-and-set enforcement and idempotent replay.
6. Read the projection back. Report a recorded transition only when the new
version and event type match the plan. A stop returns `human_review` with no
event and confirms that the projection version did not change.

## Evidence, authority, and finality

The evidence inputs are bounded observations, not permission to send. Do not
invent opens, clicks, bounces, recency, consent markers, or versions. A caller
that cannot supply them must repair the upstream evidence read rather than fill
defaults.

The runner requests only `runx:data:read` and `runx:data:append` through the
canonical data operations used by `data-store`. The allowed append is
intentionally ungated: it records contact policy state, not external delivery. The stable
`idempotency_key` makes an unchanged retry return the already-recorded version;
a competing write produces a version conflict and must be retried only after a
fresh read and new decision.

A sealed result proves the governed read, decision, optional append, and
readback. It does not prove that a campaign was sent. `downstream_send.status`
therefore remains `not_run` on every result.

## Decisions and recovery

- `re_permission`: engagement is older than the declared threshold, there is
no hard bounce, and no active unsubscribe marker exists. One transition is
recorded.
- `suppress`: at least one hard bounce is present and policy explicitly selects
suppression. One transition is recorded.
- `human_review`: evidence is missing, unreadable, ambiguous, stale, versioned
differently from the contact projection, protected by an unsubscribe marker,
or does not justify an automated transition. No event is appended.

On a version conflict, read the contact again and make a new decision. Reuse an
idempotency key only for the identical intended transition. If bounce recovery
or unsubscribe history is ambiguous, keep the case in the human review lane;
do not weaken the stop condition to make the run pass.

## Inputs and result

Provide the logical data source, resource, contact `aggregate_id`, current
`expected_version`, stable `idempotency_key`, engagement counts and recency,
the bounce policy, and the current consent state with evidence status/version.

The result contains:

- `decision`: `re_permission`, `suppress`, or `human_review`, with a reason;
- `recorded_transition`: readback-bound contact, version, event, projection,
and idempotency evidence, plus whether a write occurred;
- `escalation`: the human lane when automation stopped;
- `downstream_send`: an explicit `not_run` handoff reminder for `send-as`.

For example, a subscribed contact at version 3 with no hard bounces and
`recency_days: 121` under a 90-day threshold may be moved to `re_permission`
with `expected_version: 3`. The result is final only after readback reports
version 4 and the consent-transition event.

## Agent rules

- Read the projection before deciding and read it again after any append.
- Prefer suppression over engagement recency when verified hard-bounce evidence
exists.
- Never re-permission an active unsubscribe marker.
- Never append on missing, unreadable, ambiguous, or stale evidence.
- Never dispatch, send, mint authority, or claim provider delivery.
- Return the sealed receipt with the result so the next operator can verify the
decision without private context.
261 changes: 261 additions & 0 deletions skills/list-hygiene-judge/X.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
skill: list-hygiene-judge
version: "0.1.0"

catalog:
kind: graph
audience: public
visibility: public
role: context
execution: execute
completion: runtime_receipt
requires_adapter: true
approval: none

policy:
side_effects: data_state_transition
sends_messages: denied
mints_authority: denied
persists_state: conditional
secrets_required: false

harness:
cases:
- name: sealed_decay_re_permission
runner: judge
inputs:
data_source_ref: local://list-hygiene-judge/decay
resource: contact_consent_events
aggregate_id: contact:decay
expected_version: 0
idempotency_key: contact:decay:consent:v1
engagement_history: { opens_count: 0, clicks_count: 0, hard_bounces: 0, recency_days: 121 }
bounce_policy: { hard_bounce_action: suppress, decay_threshold_days: 90 }
current_consent_state: { state: subscribed, active_unsubscribe_marker: false, evidence_status: read, evidence_version: 0 }
expect:
status: sealed
steps: [read_contact, decide, append_transition, readback, finalize]
step_outputs:
append_transition:
subset:
data_operation_result:
data: { operation: append_event, status: committed, aggregate_id: "contact:decay" }
finalize:
subset:
list_hygiene_result:
data:
decision: { state: re_permission }
recorded_transition: { recorded: true, state: re_permission, aggregate_id: "contact:decay", after_version: 1 }
downstream_send: { skill: send-as, status: not_run }
receipt: { schema: runx.receipt.v1 }

- name: sealed_hard_bounce_suppress
runner: judge
inputs:
data_source_ref: local://list-hygiene-judge/hard-bounce
resource: contact_consent_events
aggregate_id: contact:hard-bounce
expected_version: 0
idempotency_key: contact:hard-bounce:consent:v1
engagement_history: { opens_count: 2, clicks_count: 1, hard_bounces: 1, recency_days: 12 }
bounce_policy: { hard_bounce_action: suppress, decay_threshold_days: 90 }
current_consent_state: { state: subscribed, active_unsubscribe_marker: false, evidence_status: read, evidence_version: 0 }
expect:
status: sealed
steps: [read_contact, decide, append_transition, readback, finalize]
step_outputs:
append_transition:
subset:
data_operation_result:
data: { operation: append_event, status: committed, aggregate_id: "contact:hard-bounce" }
finalize:
subset:
list_hygiene_result:
data:
decision: { state: suppress }
recorded_transition: { recorded: true, state: suppress, aggregate_id: "contact:hard-bounce", after_version: 1 }
downstream_send: { skill: send-as, status: not_run }
receipt: { schema: runx.receipt.v1 }

- name: stop_missing_or_stale_evidence
runner: judge
inputs:
data_source_ref: local://list-hygiene-judge/stale
resource: contact_consent_events
aggregate_id: contact:stale
expected_version: 1
idempotency_key: contact:stale:consent:v2
engagement_history: { opens_count: 0, clicks_count: 0, hard_bounces: 0, recency_days: 180 }
bounce_policy: { hard_bounce_action: suppress, decay_threshold_days: 90 }
current_consent_state: { state: subscribed, active_unsubscribe_marker: false, evidence_status: stale, evidence_version: 0 }
expect:
status: sealed
steps: [read_contact, decide, readback, finalize]
step_outputs:
finalize:
subset:
list_hygiene_result:
data:
decision: { state: human_review }
recorded_transition: { recorded: false, state: human_review, aggregate_id: "contact:stale", after_version: 0 }
escalation: { lane: "human:list-hygiene-reviewer", status: required_before_any_write }
receipt: { schema: runx.receipt.v1 }

- name: stop_active_unsubscribe_marker
runner: judge
inputs:
data_source_ref: local://list-hygiene-judge/unsubscribed
resource: contact_consent_events
aggregate_id: contact:unsubscribed
expected_version: 0
idempotency_key: contact:unsubscribed:consent:v1
engagement_history: { opens_count: 0, clicks_count: 0, hard_bounces: 0, recency_days: 180 }
bounce_policy: { hard_bounce_action: suppress, decay_threshold_days: 90 }
current_consent_state: { state: unsubscribed, active_unsubscribe_marker: true, evidence_status: read, evidence_version: 0 }
expect:
status: sealed
steps: [read_contact, decide, readback, finalize]
step_outputs:
finalize:
subset:
list_hygiene_result:
data:
decision: { state: human_review }
recorded_transition: { recorded: false, state: human_review, aggregate_id: "contact:unsubscribed", after_version: 0 }
receipt: { schema: runx.receipt.v1 }

- name: reject_negative_engagement_metrics
runner: judge
inputs:
data_source_ref: local://list-hygiene-judge/invalid-metrics
resource: contact_consent_events
aggregate_id: contact:invalid-metrics
expected_version: 0
idempotency_key: contact:invalid-metrics:consent:v1
engagement_history: { opens_count: 0, clicks_count: 0, hard_bounces: -1, recency_days: 10 }
bounce_policy: { hard_bounce_action: suppress, decay_threshold_days: 90 }
current_consent_state: { state: subscribed, active_unsubscribe_marker: false, evidence_status: read, evidence_version: 0 }
expect:
status: failure

runners:
decide:
type: cli-tool
command: /usr/bin/env
args: [node, run.mjs, decide]
outputs:
decision_plan: object
inputs:
data_source_ref: { type: string, required: true, description: Logical contact data source. }
resource: { type: string, required: true, description: Contact consent resource. }
aggregate_id: { type: string, required: true, description: Contact stream key. }
expected_version: { type: number, required: true, description: Current compare-and-set version. }
idempotency_key: { type: string, required: true, description: Stable transition retry key. }
engagement_history: { type: json, required: true, description: Fresh engagement and bounce evidence. }
bounce_policy: { type: json, required: true, description: Hard-bounce and decay policy. }
current_consent_state: { type: json, required: true, description: Current consent and evidence freshness. }

finalize:
type: cli-tool
command: /usr/bin/env
args: [node, run.mjs, finalize]
outputs:
list_hygiene_result: object
artifacts:
named_emits:
list_hygiene_result: list_hygiene_result
inputs:
decision_plan: { type: json, required: true, description: Admitted list-hygiene decision plan. }
recorded_readback: { type: json, required: true, description: Data-store projection read after the optional append. }

judge:
default: true
type: graph
inputs:
data_source_ref:
type: string
required: true
description: Logical provider-neutral data source containing the contact stream.
resource:
type: string
required: true
description: Contact consent event-stream resource.
aggregate_id:
type: string
required: true
description: Contact entity and event-stream key.
expected_version:
type: number
required: true
description: Current contact projection version required for compare-and-set.
idempotency_key:
type: string
required: true
description: Stable retry identity for this exact intended consent transition.
engagement_history:
type: json
required: true
description: Fresh bounded engagement and bounce evidence for the contact.
bounce_policy:
type: json
required: true
description: Hard-bounce action and engagement-decay threshold.
current_consent_state:
type: json
required: true
description: Current consent and freshness evidence aligned to the durable version.
graph:
name: list-hygiene-judge
result_from: [finalize]
steps:
- id: read_contact
tool: data.read_projection
scopes: [runx:data:read]
inputs:
data_source_ref: $input.data_source_ref
resource: $input.resource
aggregate_id: $input.aggregate_id

- id: decide
inputs:
data_source_ref: $input.data_source_ref
resource: $input.resource
aggregate_id: $input.aggregate_id
expected_version: $input.expected_version
idempotency_key: $input.idempotency_key
engagement_history: $input.engagement_history
bounce_policy: $input.bounce_policy
current_consent_state: $input.current_consent_state
context:
contact_readback: read_contact.data_operation_result.data
skill: .
runner: decide

- id: append_transition
when:
field: decide.decision_plan.append_allowed
equals: true
tool: data.append_event
scopes: [runx:data:append]
inputs:
data_source_ref: $input.data_source_ref
resource: $input.resource
aggregate_id: $input.aggregate_id
expected_version: $input.expected_version
idempotency_key: $input.idempotency_key
context:
event: decide.decision_plan.event

- id: readback
tool: data.read_projection
scopes: [runx:data:read]
inputs:
data_source_ref: $input.data_source_ref
resource: $input.resource
aggregate_id: $input.aggregate_id

- id: finalize
context:
decision_plan: decide.decision_plan
recorded_readback: readback.data_operation_result.data
skill: .
runner: finalize
25 changes: 25 additions & 0 deletions skills/list-hygiene-judge/fixtures/decay-re-permission.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: list-hygiene-decay-re-permission
kind: skill
target: ..
runner: judge
inputs:
data_source_ref: local://list-hygiene-judge/fixture-decay
resource: contact_consent_events
aggregate_id: contact:fixture-decay
expected_version: 0
idempotency_key: contact:fixture-decay:consent:v1
engagement_history: { opens_count: 1, clicks_count: 0, hard_bounces: 0, recency_days: 121 }
bounce_policy: { hard_bounce_action: suppress, decay_threshold_days: 90 }
current_consent_state: { state: subscribed, active_unsubscribe_marker: false, evidence_status: read, evidence_version: 0 }
expect:
status: sealed
steps: [read_contact, decide, append_transition, readback, finalize]
step_outputs:
finalize:
subset:
list_hygiene_result:
data:
decision: { state: re_permission }
recorded_transition: { recorded: true, after_version: 1 }
receipt: { schema: runx.receipt.v1 }
metadata: { public_skill: list-hygiene-judge, source_case: decay-re-permission, source: skills-fixture }
Loading