issue_fix_reviewer_notification_sinks_v0¶
issue_fix_reviewer_notification_sinks_v0 extends the canonical GitHub
reviewer request with bounded secondary delivery. The first adapter sends an
idempotent Lark/Feishu group message that mentions the same repository-grounded
reviewer and then reads the message back. The sink does not select a different
reviewer and never replaces GitHub review state as the source of truth.
The result contract and adapter injection point are provider-neutral; the
public CLI currently exposes only the bounded lark_chat adapter.
Position In The Flow¶
The order is fixed:
reviewer-planranks candidates from repository-native evidence;reviewer-requestverifies author exclusion and establishes canonical GitHub coverage through a formal request or its permission-only comment fallback;- configured secondary sinks notify the same verified reviewer;
pr-lifecyclecontinues to derive review state from GitHub.
In no-write preview mode, a sink may validate its local configuration against the selected reviewer. In execute mode, a secondary send is skipped unless the canonical GitHub notification is already verified. A secondary failure is reported separately and does not erase a successful GitHub request.
Optional Reward Memory Gate¶
Reward Memory is a default-off, agent-scoped experiment. If the connected goal
enables it and the registered caller's experiment includes the exact
reviewer_artifact.summary surface with automatic_recall=true, reviewer
request planning runs the shared bounded hook and may produce a
read-only reviewer_artifact_reward_memory_preview even when secondary sinks
are absent or intentionally paused. Configured secondary notifications require
the same issue_fix_reviewer_artifact_reward_memory_application_v0 packet to
pass its notification gate before sending. The caller supplies its own
--agent-id, a concise model-authored Chinese --reviewer-summary, and compact
--reviewer-summary-reasoning; LoopX never infers or impersonates another peer.
The adapter reuses the provider-neutral Reward Memory core and the goal's local provider binding. OpenViking may back that binding, but it is not a protocol dependency or hard-coded repository. Before external delivery, the gate checks the exact surface, current PR identity and permalink, current-artifact verification, memory readback, attribution digests, application state, and a non-empty summary. A receipt for a different PR cannot be replayed.
Experiment resolution and read-authority construction happen before sink
routing. The normalized corpus supplies the read-authority kind, while the
normalized standing policy supplies authority_source_ref. A no-sink preview
may read the configured provider but never sends a notification, materializes a
notification lifecycle row, or performs an external write. Sink configuration
only changes whether the application receipt is mandatory for delivery. With
automatic recall disabled, this boundary performs zero provider calls.
This stricter behavior is bounded to the configured secondary effect. The
canonical GitHub review request or permission-only fallback executes first and
remains fail-open with respect to Reward Memory. Missing, stale, or unavailable
memory blocks only the secondary notification with
reward_memory_reviewer_artifact_unverified; it does not turn ordinary Issue
Fix work into a user gate or suppress the GitHub request.
The independent reviewer_notification.before_send surface may supply a
delivery policy immediately before a configured secondary adapter runs. It
performs one bounded automatic recall and accepts only a single distinct active
hard_policy whose content_summary is a compact JSON object with schema
issue_fix_reviewer_notification_delivery_policy_v0 and a valid
delivery_policy. The application gate verifies the exact surface, current PR
identity, current-artifact check, result readback, and non-empty memory
attribution. A passed receipt has precedence over the explicit sink policy;
otherwise the explicit sink policy remains the fallback. With neither source,
delivery is unrestricted. Provider failure, no match, invalid content, or
conflicting policies fail open and never become a user gate. The existing sink
still owns queue receipts, idempotency, external send, and readback.
Local-Private Input¶
--notification-sinks-json consumes
issue_fix_reviewer_notification_sinks_input_v0. The file is deliberately a
local capability packet rather than public issue-fix state:
{
"schema_version": "issue_fix_reviewer_notification_sinks_input_v0",
"receipts": [],
"delivery_policy": {
"timezone": "Asia/Shanghai",
"allowed_local_time": {"start": "09:00", "end": "21:00"},
"outside_window": "queue_without_send"
},
"sinks": [
{
"sink_kind": "lark_chat",
"sink_instance_key": "project-review-lane",
"identity_scope": "project_dedicated",
"reader_profile": "project-user-profile",
"reader_identity": "user",
"sender_profile": "project-review-bot-profile",
"sender_identity": "bot",
"bot_display_name": "Project Review Bot",
"destination_id": "<private-chat-id>",
"reviewer_identities": {
"@service-owner": {
"member_id": "<private-member-id>",
"display_name": "Service Owner"
}
}
}
]
}
The explicit reader/user binding verifies access to the approved destination.
The sender/bot binding independently verifies the dedicated bot identity and,
in that app's open_id namespace, verifies mapped reviewer membership before
performing send plus readback. Neither
binding depends on the machine's active/default Lark profile. The legacy
bot_profile field remains accepted for explicit manual configs, but a
goal-default config requires both bindings.
Execute mode verifies the configured reader's user credential before any
destination read or send. An unavailable or unverified user credential returns
the content-free blocker reviewer_notification_reader_auth_required and
performs zero external writes. This is an authentication gate for the existing
reader binding, not evidence that the reader and sender profiles should be
collapsed or rewritten. The operator restores the configured reader user login
with lark-cli auth login; LoopX keeps profile names and credential details out
of the public result.
Before sending, LoopX deduplicates one PR notification through three bounded
evidence layers: the durable PR-lifecycle receipt, an exact PR-link match in
the persisted configured_chat_all inbox, and a user-identity search of the
configured chat. The remote search is an evidence enhancement, not an action
authority boundary: if that user profile lacks search:message, LoopX records
permission_fallback and continues from durable receipt/inbox evidence rather
than projecting a user gate. A successful remote match is written back as the
same stable receipt; non-permission provider failures remain fail-closed.
delivery_policy is optional and provider-neutral. Its effective source order
is a verified reviewer_notification.before_send application, then this
explicit sink value, then the unrestricted default. When configured, execute
mode sends only while the current local time is inside the half-open
[start, end) window; overnight windows are supported. Outside the window,
LoopX performs no provider call and returns queued_until_window with a
compact issue_fix_reviewer_notification_queue_receipt_v1. The v1 receipt also
persists the public-safe summary and whether it came from a verified Reward
Memory artifact, so a later drain cannot regress a Chinese reviewer summary to
the raw PR title. Invalid timezone, time, or outside-window policy fails closed.
Preview remains read-only and is never converted into a queued execution. The
execute path uses the trusted invocation clock for this decision; the public
--generated-at artifact field cannot move a send into or out of the delivery
window.
The grouped state monitor drains due receipts with:
loopx issue-fix reviewer-notification-drain \
--goal-id <goal-id> \
--project <project> \
--execute \
--format json
This is a deliberate queue-schema cutover: existing
issue_fix_reviewer_notification_queue_receipt_v0 rows must be manually
migrated to v1 before enabling the grouped drain. The runtime does not retain a
v0 compatibility reader because a v0 row lacks the persisted Chinese summary
and therefore cannot satisfy the current reviewer-message contract. A detected
v0 row fails closed with reviewer_notification_queue_v1_migration_required;
it is never silently treated as an empty queue.
One bounded invocation scans every queued PR in the review-required state bucket; it does not create one continuous monitor per PR. Before each message, LoopX refreshes compact live GitHub state and cancels stale queues for closed, merged, draft, approved, or fully-covered reviewer sets. A send remains one PR per message (and at most one message per configured sink) and is complete only after semantic readback and receipt persistence. If only some queued reviewers already reviewed, the drain targets only the remaining reviewers. Temporary CI or branch-state changes keep the queue intact, and the drain always reuses the timezone and allowed local-time window frozen in the v1 receipt. A sink removed from current configuration cancels only its own stale receipt; other configured sinks for that PR continue independently.
Semantic history dedupe is sink-scoped. A single Lark sink may use the goal-level
feedback_inbox_config; multiple Lark sinks must each declare their own
feedback_inbox_config. If a multi-sink inbox cannot be attributed to one sink,
the drain fails closed and preserves the queue instead of suppressing another
chat's delivery. Bounded executions also report remaining_due_pr_count and
return partial_drain whenever verified or cancelled work coexists with due
rows held by a delivery window or left for the next --limit batch.
Profile names, destination_id, and member_id are execution inputs. They are
never copied into the result, domain state, todo, Kanban, PR, or public log.
The first contract requires a named, project-dedicated sender profile and expected
bot_display_name, verifies the live bot identity before every send, and
rejects shared/default or mismatched identities. This prevents a long-running
project employee from silently speaking as an unrelated application.
Identity mapping is advisory until both sides are verified: the GitHub handle
must come from the live author-excluding reviewer packet, and the messaging
member must be resolved in the approved destination. Missing or ambiguous
mapping produces reviewer_notification_identity_unresolved without sending.
Authority, Idempotency, And Verification¶
The same standing reviewer-notification authority that permits the canonical
request may permit an explicitly configured secondary sink. --execute is the
write assertion; preview never calls the provider.
Each logical (repository, PR, sink instance, reviewer set) produces a stable
sha256: idempotency key. The Lark adapter derives a provider-bounded key from
that digest without exposing it in the human-visible message. The message uses
public PR metadata to name the PR, linked issue when available, and a compact
summary of the fix. The full verified key is returned as a receipt. Callers
store only that compact receipt in existing issue-fix state and pass it back on
retry; a matching receipt returns already_notified without a provider call.
For connected goals, register only the repo-relative local-private pointer:
loopx configure-goal \
--goal-id example-goal \
--issue-fix-reviewer-notification-config \
.loopx/config/issue-fix/reviewer-notification-sinks.json \
--execute
Then reviewer-request --goal-id example-goal --project ... discovers the
config automatically. Execute mode loads the PR's existing lifecycle row or
auto-materializes it from a fresh compact GitHub lifecycle read before any
external notification, merges verified hashed receipts into the private
input, and writes new verified receipts or compact queued delivery metadata
back to that same row. A restart preserves a queued item; a later verified send
removes the matching queue entry while retaining the stable receipt. Retry
outside the window returns already_queued with the original queue receipt,
so neither the provider nor local state changes. Retry therefore remains
idempotent without a second ledger. The live reviewer route is authoritative
for unsent work: an existing GitHub-covered reviewer may be notified only as
that same reviewer, never replaced by a different mapped sink identity. Each
execute reconciliation atomically replaces the PR's unsent queue, cancelling
stale targets when coverage changes, a review completes, or no current sink
identity remains. Verified receipts are append-only and are never cancelled by
this queue replacement. A provider failure before any external write preserves
the matching current queue entry for retry; an unverified post-write result does
not requeue and risk a duplicate send. Goal boundary/status
projections expose only that the capability and pointer are configured; they
never expose the pointer value or profiles
(config_pointer_registered=true).
A zero exit status is insufficient. The adapter requires a message id from the
send response, fetches that message with the same dedicated bot profile, and
verifies both the id and PR URL. Results distinguish preview_ready,
queued_until_window, already_queued, sent_verified, already_notified,
sent_unverified, and gate_required.
Permission or group-membership errors become the concrete
lark_bot_group_access_required gate.
Dedicated Bot Setup¶
For a Lark sink, provision one app/bot identity for the project lane, grant only the scopes required for sending and group/member resolution, publish the app, and have the target group's owner or administrator install it. A local named CLI profiles select reader and sender credentials explicitly. The contract never falls back to the machine's default user or bot profile.
The setup gate should tell the owner exactly which missing invariant to repair:
- dedicated app/bot exists and has the intended visible name;
- bot capability is enabled and the version is published;
- send and approved chat/member-read scopes are granted;
- the bot is a member of the approved destination;
- each GitHub reviewer maps to one verified destination member.
Public-Safety Boundary¶
Every public result keeps these fields false:
private_destination_capturedprivate_member_ids_capturedprivate_bot_profile_capturedraw_provider_payload_captured
Only the public PR URL, reviewer handles, sink kind, status, compact blocker, and hashed receipts may leave the adapter. Credentials, raw member rosters, chat identifiers, message identifiers, provider errors, and local config paths remain private.
Validation¶
Run:
python3 examples/issue-fix-reviewer-notification-sink-smoke.py
python3 examples/issue-fix-reviewer-request-smoke.py
The provider-neutral fixture covers preview, dedicated-identity enforcement, author exclusion, identity-resolution gates, one send plus readback, stable receipt retry, permission classification, unverified writes, and public-safety redaction. It also covers normal and overnight delivery windows, zero-call queuing, and invalid-policy fail-closed behavior. The reviewer-request smoke proves the sink is a real post-canonical call site rather than a disconnected adapter and verifies queue persistence/removal across lifecycle restarts.