RFC: Goal Channel Collaboration v0¶
- Status: Draft
- Scope: provider-backed collaboration channels for one LoopX goal
- Decision type: product architecture and staged integration contract
Summary¶
This RFC introduces Goal Channel as the LoopX-owned abstraction for an external collaboration channel bound to exactly one goal. A channel may be a Lark/Feishu group, Slack channel or thread, GitHub issue, Linear thread, or another provider surface. The provider owns delivery and UI primitives; LoopX owns goal state, todos, human gates, quota, evidence, receipts, and accepted state transitions.
The first provider target is Lark/Feishu:
- create or reuse one group chat for a goal;
- create or reuse a Lark Base Kanban projection;
- pin a compact control message and Kanban link in the group;
- send bounded human-gate notifications;
- sync accepted LoopX state back to the Kanban projection.
The channel is not the source of truth. It is the visible collaboration entry point and feedback surface for a LoopX goal.
Problem¶
LoopX already has durable state and Lark-specific pieces:
lark-kanbanprojects LoopX todos and status into Lark Base;- Lark notification code paths already prove send, readback, idempotency, and profile checks for narrower domains.
These pieces do not yet compose into the product shape users expect from a Claude Tag-like workflow:
- Mention a bot in a collaboration surface.
- Get or create an isolated collaboration channel for that objective.
- See progress and gates in the same place.
- Receive human-gate prompts without leaving the collaboration surface.
- Let LoopX keep the authoritative goal, todo, gate, and evidence state.
Without a first-class Goal Channel abstraction, a Lark group, Base board, message thread, pinned status, and notification receipts can drift apart.
Goals¶
- Define a provider-neutral LoopX concept for "the external collaboration channel for this goal".
- Keep Lark group chat, Kanban, pinned messages, and gate notifications bound
to one
goal_id. - Preserve LoopX as the only writer of canonical goal, todo, gate, evidence, and quota state.
- Make provider writes explicit, previewable, idempotent, and readback verified.
- Let humans see and answer gate prompts in the channel without granting the channel broad write authority.
- Allow later provider adapters such as Slack, GitHub, or Linear without renaming the core concept.
Non-Goals¶
- Replacing
lark-kanban; Goal Channel composes it. - Making Lark, Slack, or any external tool the source of truth.
- Shipping a LoopX-managed global Lark app in the open-source CLI.
- Requiring one fixed bot identity across all users or tenants.
- Treating arbitrary chat text as an accepted state transition.
- Copying raw chat history, private message ids, local paths, credentials, or raw provider payloads into public packets.
- Solving full remote runner orchestration in this RFC.
Naming¶
Use Goal Channel for the core abstraction.
Avoid room as the primary name. A group chat may be one implementation
detail, but a Goal Channel can contain chat, pinned status, Kanban,
notification receipts, and provider-specific metadata.
Suggested command surface:
loopx goal-channel setup --provider lark --goal-id <goal-id>
loopx goal-channel target add --name <target> --provider lark ...
loopx goal-channel setup --goal-id <goal-id> --target <target>
loopx goal-channel attach --target <target> --goal-id <goal-a> --goal-id <goal-b>
loopx goal-channel configure --goal-id <goal-id> --auto-notify-human-gates
loopx goal-channel doctor --goal-id <goal-id>
loopx goal-channel sync --goal-id <goal-id>
loopx goal-channel notify-gate --goal-id <goal-id>
loopx goal-channel runtime setup --goal-id <goal-id> --bot-id <bot-id> --chat-id <chat-id>
goal-channel is the durable control-plane object and the user-facing CLI.
The optional botmux runtime integration
delegates IM delivery and persistent agent sessions to botmux without changing
Goal Channel or LoopX state authority. Delivery does not imply a state
transition; the configured agent runtime must still invoke LoopX explicitly.
Ownership Model¶
| Capability | LoopX | Provider channel | Provider adapter |
|---|---|---|---|
| Goal lifecycle | Owner | Projection | Calls LoopX |
| Todos, claims, gates, quota | Owner | Projection and prompts | Syncs bounded packets |
| Kanban rows | Source data owner | Display owner | Upserts rows |
| Group/chat/thread | References binding | Owner | Creates, updates, reads |
| Pinned status | Builds bounded content | Displays | Sends and pins |
| Human gate question | Owner of question and cooldown | Delivery | Sends and verifies |
| Credentials and profile | Never stores secrets | Provider auth | Uses local-private profile |
| Receipts | Owner of accepted transition receipts | Message ids are private | Records compact send/readback receipt |
The provider may store its own state. LoopX stores only the minimum local-private binding needed to operate the channel.
Lark Provider Binding¶
A Lark Goal Channel binding is local-private and project-scoped:
{
"schema_version": "loopx_goal_channel_lark_binding_v0",
"goal_id": "loopx-goal",
"provider": "lark",
"enabled": true,
"channel": {
"chat_id": "oc_<private-chat-id>",
"chat_name": "LoopX - loopx-goal",
"pinned_message_id": "om_<private-message-id>"
},
"kanban": {
"base_token": "<private-base-token>",
"table_id": "tbl...",
"view_ids": {
"Kanban": "vew...",
"User Gates": "vew..."
}
},
"identity": {
"mode": "project_bot",
"sender_profile": "loopx-project-bot",
"sender_identity": "bot",
"bot_display_name": "LoopX Bot"
},
"receipts": {}
}
The file belongs under .loopx/ or another ignored local-private path. Public
status packets must not expose chat ids, member ids, message ids, profile names,
raw Lark payloads, local file paths, or credentials. Public packets may expose
booleans, counts, sanitized provider labels, and operator-safe URLs only when
the caller has already chosen to show them.
Shared provider targets¶
Several Goal Channels may reference one named local-private provider target. The target owns the reusable Lark chat and sender identity; each Goal binding continues to own its control message, Kanban, receipts, and cooldown state:
loopx goal-channel target add \
--name loopx-dev \
--provider lark \
--chat-id <private-chat-id> \
--bot-app-id <private-app-id> \
--execute
loopx goal-channel attach \
--target loopx-dev \
--goal-id goal-a \
--goal-id goal-b \
--execute
The target store belongs under the resolved LoopX runtime root and is never a
public or repository-tracked configuration. A target-linked Goal binding stores
only target_ref plus Goal-local state; changing a target updates the resolved
chat or sender for every referencing Goal without merging their state.
After moving a target to another group, rerun bounded attach batches so each
Goal can establish and verify its own control message in the new group.
Machines do not synchronize private chat ids or authentication profiles. To use the same group from a workstation and a development host, configure the same target name independently on each machine. Inbound routing must reply to a specific gate message or carry an explicit Goal id; ordinary group text must never be inferred as belonging to one of several Goals.
BYO Provider Identity¶
Open-source LoopX should default to Bring Your Own provider identity:
- users create or select their own Lark app or bot in their tenant;
- users authenticate it through
lark-clior a future provider-specific profile manager; - LoopX stores only the local profile reference and compact verification state;
- LoopX never ships a fixed cross-tenant bot as an implicit dependency.
Supported identity modes:
| Mode | Intended use | Tradeoff |
|---|---|---|
local_user |
Create and own the group and Base as the user | Easy resource ownership; the bot is still required for messages |
project_bot |
Use a dedicated bot profile for channel messages | Requires app/bot setup but gives stable message identity |
managed_app |
Future hosted product | Best UX, requires tenant install, compliance, and operations |
The first implementation uses the local user identity for group and Base
operations. Goal Control messages, pins, and gate notifications always use the
configured bot identity. It does not require or request
im:message.send_as_user.
Effectful direct setup requires an explicit --bot-app-id cli_...; target-based
setup obtains that explicit selection from the local-private target. LoopX
verifies that it matches the selected lark-cli profile before adding the bot or
sending a message. Omitting the flag is a preview-only convenience, not
authorization to select the default profile's bot.
Lifecycle¶
Setup¶
goal-channel setup --provider lark --goal-id <goal-id> should:
- Resolve and validate the goal.
- Load or create the local-private Lark channel binding.
- Verify
loopx-larkextension activation and required permissions. - Verify the local user resource identity and the configured bot sender.
- Create or reuse a Lark group chat and verify the bot is a member.
- Create or reuse the Lark Kanban Base through
lark-kanban. - Read back and persist the canonical Base URL.
- Send a compact Goal Control message containing the Kanban link.
- Pin that verified control message.
- Save the local-private binding and compact receipts.
Default mode is dry-run. External writes require --execute.
Sync¶
goal-channel sync composes existing projections:
lark-kanban sync-loopx-todosfor active user/agent todos and derived domain outcomes;- a compact status/control message update or append when the visible channel summary changed materially;
- optional periodic report or explore projection sinks only when separately configured.
The sync command must not create new canonical todos from remote rows.
Human Gate Notification¶
goal-channel notify-gate sends a bounded message when LoopX already decided a
human gate or user todo needs attention. The trigger input is the existing quota
and interaction-contract surface:
state=operator_gate;notify_user_on_gate=true;notify_user_on_open_todo=true;gate_prompt;operator_question;open_todo_notify_reason;user_todo_summary;user_gate_notification_cooldown.
The message includes:
- goal label and short objective;
- concrete gate question;
- up to three user-gate or user-action todos;
- expected reply format;
- Kanban link or channel control link;
- next safe action while waiting, if any.
It excludes local paths, raw active state, private logs, credentials, message ids, and raw provider payloads.
Automatic delivery is disabled by default. After Goal Channel setup, preview and then enable it explicitly:
loopx goal-channel configure --goal-id <goal-id> --auto-notify-human-gates
loopx goal-channel configure --goal-id <goal-id> --auto-notify-human-gates --execute
Once enabled, each successful non-dry-run refresh-state rebuilds quota from
canonical LoopX state. It sends only when quota selects a human gate, and it
reuses the same bot verification, semantic idempotency, cooldown, provider
idempotency key, and message readback as notify-gate.
Use loopx refresh-state ... --suppress-external-sinks to suppress delivery for
one refresh without disabling the binding. Turn-bound recovery remembers that
pause and requires --resume-external-sinks <resume_key> to resume delivery using
the returned key. This acknowledges the current operation, not new permissions;
historical operations without pause evidence retain their previous behavior.
See the recovery handshake. Disable automatic
delivery persistently with:
The opt-in is stored only in the project-local private Goal Channel binding. It does not grant repository or LoopX transition authority. Chat replies can provide context, but a gate changes only after LoopX validates and records the corresponding decision.
Automatic lifecycle delivery resolves the enabled, doctor-verified Lark
extension before reading the private binding. Enabling automatic delivery
requires the canonical project-local binding path; custom --binding-path
values are rejected because refresh-state has no per-invocation path input.
The explicit local disable command is the only recovery exception: it may clear
the opt-in while the extension or binding is incomplete, and it never enters
provider code or performs an external write.
Enabling also writes an owner-only local marker containing only the enabled
boolean. The lifecycle may read this marker before extension activation solely
to distinguish a never-configured project from a configured sink whose
extension became unavailable. The latter fails closed with a retryable
extension_unavailable postcondition; the marker contains no provider ids,
credentials, channel metadata, or raw payloads.
Command Contract¶
Each effectful command returns a compact packet:
{
"schema_version": "loopx_goal_channel_operation_v0",
"ok": true,
"goal_id": "loopx-goal",
"provider": "lark",
"operation": "notify_gate",
"execute": true,
"external_write_performed": true,
"readback_verified": true,
"idempotency_key": "sha256:...",
"receipt_id": "receipt_...",
"public_summary": "sent one gate notification to the configured Lark channel",
"private_provider_payload_captured": false
}
Failures should be typed:
extension_unavailable;provider_identity_unverified;channel_binding_missing;channel_membership_unverified;kanban_binding_missing;notification_cooldown_active;readback_mismatch;state_transition_rejected;provider_api_failed.
Idempotency And Cooldown¶
Provider writes use idempotency keys derived from the semantic action, not the wall-clock attempt:
Rules:
- retrying the same send returns
already_sentor the original receipt; - gate text changes may create a new notification key;
- cooldown suppresses repeated reminders without closing the gate;
- stale provider events cannot override newer LoopX revisions.
Security And Privacy¶
- Channel membership is not LoopX write authority.
- Bot membership is verified before sending.
- Message readback is required before recording a successful send receipt.
- Raw provider payloads stay local-private.
- Shared/global registry calls resolve Goal Channel state beside the selected
goal's canonical
source_registry; caller CWD is never a default state root. - Local-private JSON uses an owner-only temporary file plus atomic replace, so interrupted writes do not expose or truncate the previous binding.
- Local checkout paths, active-state paths, credentials, chat ids, member ids, message ids, and profile names do not enter public artifacts.
- The channel may show a Kanban link, but the Kanban remains a projection.
- Destructive, credentialed, production, publish, merge, or external-write gates remain LoopX gates and cannot be bypassed by chat text.
Smallest Useful Slice¶
The smallest useful implementation should be:
- Add
loopx goal-channelwithsetup,configure,doctor,sync, andnotify-gate. - Implement only the Lark provider.
- Reuse existing
lark-kanbansetup/sync andloopx-larkextension activation checks. - Create or reuse one Lark group for one existing goal.
- Send and pin one compact Goal Control message.
- Send a human-gate notification with idempotency and readback.
- Optionally send LoopX-selected human gates after authorized
refresh-statewrites. - Store local-private binding, automation opt-in, and receipts under
.loopx/.
This slice proves the external collaboration entry point.
Validation¶
The first slice must prove:
- setup is dry-run by default and performs external writes only with
--execute; - one goal maps to one local-private Lark binding;
- extension activation is checked before private config is read;
- a Kanban board can be reused or created and then synced;
- a Goal Control message is sent, pinned, and readback verified;
- human-gate notification respects cooldown and idempotency;
- repeated notification retries do not duplicate visible messages;
- automatic delivery is disabled by default and can be suppressed per refresh;
- automatic delivery reads canonical quota and does not send for non-gate state;
- doctor reports missing bot auth, missing channel, missing Kanban, or stale extension activation with typed blockers;
- local-private binding files remain ignored and untracked;
- public packets do not contain chat ids, member ids, message ids, profile names, local paths, raw provider payloads, or credentials.