Reframe the interface around composed posts, not republished entries

The channel publishes a post written from a hall entry -- condensed,
personal, reach-optimized -- not the entry itself. That changes what
this interface is for and where its boundary sits.

Composition is deliberately outside the interface. Composing is a
probabilistic editorial task, and Blueprint 48.1 forbids an LLM in the
mandatory request path, so an interface that composed on demand would be
non-deterministic exactly where determinism is the whole claim. It is
also editorial: a post speaks about a named person's work in
HelixForge's voice, and a review step inside the request path is a
review step nobody performs.

The contract now takes a composed post with a required reviewer, a cited
source entry and a consent basis. A post carrying none of those is
refused. Absence of objection is not consent, and in an audit trail an
unfaithful post looks exactly like a faithful one -- the defence is that
a human read it, the source is named, and both are recorded.

The first experiment is rebuilt accordingly. The old competition asked
how to fit a whole entry into Telegram; that question no longer exists.
The new one asks whether the portrait earns the 3072 characters of
caption limit it costs, which is a delivery question this interface
actually owns. Competition over editorial voice belongs to the campaign
and runs on the per-variant engagement this interface reports.

Adds T05b (composition and review), T05c (consent basis for the 94
existing participants, blocking public publication) and T11 (extracting
the campaign into pr-hall-of-helix).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014KmVxhJ35tCo7rE7UnLwWu

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 1116572@bnt-lap001
Assistant-Session: 8ba9bb93-a72a-4883-b189-2499cce5c400
This commit is contained in:
tegwick 2026-09-04 11:57:28 +02:00
parent aa95a09ab6
commit 3aa8cb7eaf
14 changed files with 793 additions and 521 deletions

View file

@ -59,21 +59,37 @@ Raising the mode is a governance change under §20, not an operational decision.
### Mission statement
> Make hall-of-helix entries reliably and faithfully available to a Telegram
> audience, without the people writing them having to think about Telegram.
> Deliver composed channel posts to a Telegram audience reliably, verifiably,
> and without an unreviewed word ever reaching a subscriber.
An entry is written once, as Markdown, in `hall-of-helix/entries/`. Everything
about how it becomes a Telegram post — length, formatting, media, threading —
is this interface's problem and nobody else's.
The unit of publication is a **post**, not a hall entry. A post is short,
personal and written to travel; a hall entry is a considered first-person
account written for someone reading a repository. They are different pieces of
writing for different readers, and the second is the source of the first rather
than its content.
**Composition is not this interface's job.** Turning an entry into a post that
is condensed, entertaining and reach-optimized is a probabilistic, editorial
task. It happens upstream, before this interface is called. Two reasons, and
the first is architectural rather than stylistic:
1. `ArchitectureBlueprint.md` §48.1 names an LLM in the mandatory request path
as an anti-pattern. An interface that composed on demand would be
non-deterministic at exactly the point where determinism is the whole claim.
2. A composed post speaks about a named person's work in HelixForge's voice.
That deserves a human read before it goes out, and a review step in the
request path is a review step nobody performs.
### Success looks like
- A finished hall entry reaches the channel without a human reformatting it.
- The published form is faithful: no entry is silently truncated, and no
attribution is lost.
- A composed post reaches the channel without a human formatting it for
Telegram.
- No post reaches a subscriber unreviewed.
- Every post is traceable to the entry it was written from, so a claim can be
checked against what the person actually said.
- Publishing twice does not post twice.
- When Telegram's constraints and the entry's shape conflict, the conflict
surfaces as recorded pressure rather than as a mangled post.
- Engagement is attributable to the composition strategy that produced it, so
the campaign can learn which writing travels.
---
@ -105,10 +121,13 @@ is this interface's problem and nobody else's.
### In scope
- Publishing hall-of-helix entries to a Telegram channel (Canon PUB-01).
- Rendering an entry into Telegram's message forms (Canon PUB-02, MED-02).
- Publishing composed posts to a Telegram channel (Canon PUB-01).
- Rendering a post into Telegram's message forms (Canon PUB-02, MED-02).
- Attaching a visual to a post (Canon MED-01).
- Publication state: what was published, when, as which message.
- Idempotency and correction of previously published entries.
- Idempotency and correction of previously published posts.
- Recording the composition variant a post came from, and reporting engagement
against it.
- Bot identity presentation (Canon BOT-01).
### Conditionally in scope
@ -116,12 +135,14 @@ is this interface's problem and nobody else's.
Capabilities that may be introduced if evidence justifies them:
- Scheduling a publication for a later time.
- Publishing other HelixForge artifacts, such as release notes.
- Attaching entry visuals as media (Canon MED-01).
- Publishing other HelixForge material, such as release notes.
- A linked discussion group for comments.
- Publishing to more than one channel from one call.
### Out of scope
- **Composing post text from a source entry.** This belongs to the campaign
that owns the editorial voice, not to the interface that delivers it. See §7.
- Inbound bot commands and conversation (Canon BOT-02, AGT-01). A second
revision family may add this later; it is not this interface today.
- Payments and paid content (Canon PAY-*).
@ -135,20 +156,28 @@ Capabilities that may be introduced if evidence justifies them:
### Responsibilities owned by this interface
- Representation: how an entry becomes one or more Telegram messages.
- Composition: assembling an entry, its metadata and its visual.
- Validation: refusing an entry that cannot be published faithfully.
- Representation: how a composed post becomes a Telegram message.
- Delivery: attaching the visual, the source link, the formatting.
- Validation: refusing a post that arrives unreviewed, unattributed, or in a
form Telegram cannot carry.
- Routing: which revision serves a publication request.
- Idempotency: recognising an entry it has already published.
- Idempotency: recognising a post it has already published.
- Attribution of engagement to composition variants.
### Responsibilities owned by backend systems
| Backend | Responsibility | Boundary |
|---|---|---|
| Telegram Bot API | Message delivery, channel state, subscriber identity | FLUID may request delivery. It may not assume a limit will move, and must treat platform constraints as fixed. |
| `hall-of-helix` | Entry content, its correctness, and whether it is finished | FLUID may read entries. It must never edit one to make it publishable. |
| `pr-hall-of-helix` (planned) | Composing posts, the editorial voice, choosing what is worth publishing, obtaining subject consent | FLUID publishes what it is given and reports how it performed. It does not write, edit or select. |
| `hall-of-helix` | Entry content, its correctness, and whether it is finished | FLUID never reads or edits an entry. Its only knowledge of one is the id and URL a post cites. |
| OpenBao | Custody of the bot token | FLUID receives a credential. It never stores, logs or forwards one. |
Until `pr-hall-of-helix` exists, composition is done by hand as part of
`FT-WP-0001`. That is a temporary arrangement, and the boundary above is
written for the state it is heading toward rather than the state it is in — so
that the campaign, when it is extracted, finds the seam already cut.
### Explicit non-authority
The Daimon must not autonomously:
@ -156,14 +185,20 @@ The Daimon must not autonomously:
- create, rename or delete Telegram channels, groups or bots;
- change channel membership, administrators or permissions;
- read, rotate or re-scope the bot token;
- edit or truncate a hall-of-helix entry to fit a platform constraint;
- compose, rewrite, shorten or embellish post text;
- publish a post that carries no reviewer;
- publish to any chat other than those declared in the routing configuration;
- post on behalf of a person who has not asked for it.
- publish a post about a person who has not consented, on either basis in §15.
The last of these matters most. A hall entry is a first-person account someone
wrote about their own work. Publishing it is an act taken on their behalf, and
that authority comes from their having written it — not from the interface
finding it convenient.
The last two matter most, and they are the reason composition sits outside this
interface. A post is a condensation written in HelixForge's voice about a named
person's work. It can flatter, it can misstate, and it can put a claim in
someone's mouth that their own entry never made — and in the audit trail an
unfaithful post looks exactly like a faithful one.
The defence is not that the composer is careful. It is that a human read the
post, the post names the entry it came from, and both are recorded before
anything reaches a subscriber.
---
@ -171,23 +206,29 @@ finding it convenient.
### Concepts consumers should naturally understand
- An **entry** is the unit of publication, identified by its hall entry id.
- Publishing is **idempotent** on that id.
- A **post** is the unit of publication, identified by its own post id.
- A post **cites** the hall entry it was written from; it is not that entry.
- Publishing is **idempotent** on the post id. A recomposition is a new post,
because it is a different piece of writing about the same work.
- A **publication** has state: pending, published, failed, superseded.
### Interaction patterns considered natural
- Publish one finished entry, then check its state.
- Re-publish the same entry after a correction and get the same message updated.
- List what has been published to reconcile against the hall.
- Compose a post, have it reviewed, publish it, check its state.
- Re-publish the same post id after a correction and get the same message
updated.
- List publications by composition variant to compare how they performed.
### Expectations that should be discouraged
- That Telegram formatting is Markdown. It is not, and entries that assume so
- That Telegram formatting is Markdown. It is not, and posts that assume so
will render wrongly.
- That an entry appears instantly. Rate limits are real.
- That a long entry arrives as one message. It cannot, and the interface's job
is to make that invisible rather than to pretend otherwise.
- That a post appears instantly. Rate limits are real.
- That this interface will shorten an over-length post. It will refuse it. A
composition that does not fit was not condensed, and deciding what to cut is
the composer's judgement, not the deliverer's.
- That publishing a hall entry is possible. It is not: entries are sources, and
posts are what this interface carries.
---
@ -203,10 +244,10 @@ volume.
| Dimension | Priority | Target / Constraint | Notes |
|---|---:|---|---|
| Correctness | High | No entry published in a form that loses content | Hard guardrail |
| Client utility | High | One call publishes one entry | The reason the interface exists |
| Correctness | High | No post published unreviewed or unattributed | Hard guardrail |
| Client utility | High | One call publishes one post | The reason the interface exists |
| Reliability | High | Publication succeeds or reports why | |
| Discoverability | High | A reader can reach the full entry | Drives the first experiment |
| Discoverability | High | A reader can reach the source entry | Drives the first experiment |
| Security | High | Token never leaves its custody | Hard guardrail |
| Compatibility | Medium | Versioned evolution | Few consumers, all internal |
| Simplicity | Medium | Prefer one publication concept | |
@ -226,7 +267,7 @@ volume.
| Dimension | Current | Preferred | Maximum | Scoring method |
|---|---:|---:|---:|---|
| Surface area (operations) | 3 | 4 | 8 | Count of declared operations |
| Concept count | 2 | 3 | 5 | entry, publication |
| Concept count | 2 | 3 | 5 | post, publication |
| Parameter dimensionality | 4 | 6 | 12 | Declared parameters across operations |
| Semantic overlap | 0 | 0 | 1 | Operations meaning the same thing |
| Alternative-path count | 1 | 1 | 3 | Ways to achieve one outcome |
@ -242,7 +283,9 @@ volume.
another endpoint.
- A candidate exceeding a hard maximum must not be promoted.
- Presentation adaptations do not consume surface-area budget: they change how
an entry is rendered, not what the interface offers.
a post is delivered, not what the interface offers.
- Any candidate that would move composition inside this interface is rejected
regardless of its complexity score. That boundary is not a budget question.
---
@ -258,8 +301,9 @@ volume.
- Breaking changes require a successor revision.
- Every consumer is internal and known, so a breaking change is a coordination
problem rather than a public one — but it is still a successor revision.
- A change to how an entry is *rendered* is a presentation adaptation and does
not break the contract, even though readers will notice it.
- A change to how a post is *delivered* — visual placement, link style,
formatting — is a presentation adaptation and does not break the contract,
even though readers will notice it.
### Coexistence expectations
@ -325,6 +369,12 @@ success criteria, guardrails, complexity impact, risk, expected learning value.
defensible answers, and choosing between them by argument rather than evidence
is exactly the habit FLUID exists to break.
Note the division of labour. Competitions about **delivery** — does a visual
help, does a link preview help — belong to this interface and are run here.
Competitions about **composition** — which editorial voice travels — belong to
the campaign, and are run on the per-variant engagement this interface reports.
Both are legitimate FLUID experiments; only the first is this interface's.
### Reach preference
Prefer the smallest sufficient adaptation. This interface is small enough that a
@ -378,6 +428,23 @@ population, not a caller.
Single tenant. The interface serves HelixForge and no one else.
### Subject consent
A post speaks publicly about a named person's work. Two bases are recognised:
- **standing** — the contributor agreed, when they took a seat in the hall,
that HelixForge may write about that work publicly. This is the ordinary case
and requires the agreement to actually exist, in `hall-of-helix/ENTRY.md`, and
to be revocable.
- **explicit** — per-post approval from the subject. Required where the post
makes a claim the entry does not, quotes the subject, or concerns work they
have since asked not to be publicised.
`not-required` covers posts about the project itself with no personal subject.
A post carrying no basis is refused with 403. Absence of objection is not
consent, and the interface must not treat it as such.
### Data handling
Telegram user identity (Canon ID-01) must not enter the evidence store in any
@ -388,9 +455,13 @@ consumers must not quietly learn about audiences.
### Logging and provenance
Every publication records which entry, which revision, which message, and which
consumer asked. The bot token appears in no log, error, telemetry event or
evidence record.
Every publication records which post, which source entry, which reviewer, which
revision, which message, and which consumer asked. The bot token appears in no
log, error, telemetry event or evidence record.
Post *text* is retained in the publication record. It is what was said in
HelixForge's name, and an audit that could not reproduce it would be unable to
answer the only question that matters when a post is disputed.
### AI boundary
@ -404,14 +475,15 @@ reaches this interface at all.
| Quality attribute | Requirement | Hard guardrail? |
|---|---|---:|
| Correctness | No published entry loses content or attribution | yes |
| Idempotency | Publishing an entry twice yields one publication | yes |
| Correctness | No post published without a reviewer and a cited source entry | yes |
| Consent | No post about a subject without a recorded basis | yes |
| Idempotency | Publishing a post twice yields one publication | yes |
| Availability | Best effort; publication may be retried | no |
| Reliability | A failed publication reports why and may be retried safely | yes |
| p95 latency | Under 10s excluding Telegram rate limiting | no |
| Error rate | Under 1% of publication attempts | yes |
| Security | Token never leaves custody | yes |
| Auditability | Every publication traceable to entry, revision and caller | yes |
| Auditability | Every publication traceable to post text, source entry, reviewer, revision and caller | yes |
| Rollback | Any revision can be replaced by its predecessor within one policy generation | yes |
---
@ -503,5 +575,9 @@ of scope, the first 100 subscribers, or annually.
The Daimon should optimize aggressively inside this document and remain
conservative at its boundaries.
This interface may evolve in how it presents an entry. It may not evolve in
whose voice it speaks.
This interface may evolve in how it delivers a post. It may not evolve into
writing one.
The voice belongs to the campaign, the work belongs to the person who did it,
and this interface's job is to carry the first without misrepresenting the
second.

View file

@ -9,5 +9,8 @@ live. Governance, contract, seed hypotheses and the adapter's connector contract
are in place; the bot, the channel and the adapter itself are not.
- Governance: [`InterfaceEvolutionIntent.md`](InterfaceEvolutionIntent.md)
- The channel publishes composed posts written *from* hall entries, not the
entries themselves. Composition happens upstream and moves to
`pr-hall-of-helix` once the loop has closed once (FT-WP-0001 T11).
- Work: [`workplans/FT-WP-0001-telegram-identity-and-hall-channel.md`](workplans/FT-WP-0001-telegram-identity-and-hall-channel.md)
- Handover from fluid-core: `fluid-core/docs/handover/fluid-telegram.md`

View file

@ -3,96 +3,147 @@ info:
title: HelixForge Telegram Publishing
version: "R-1"
description: |
Publishes hall-of-helix entries to a Telegram channel.
Publishes composed channel posts to a Telegram channel.
The interface takes an entry as it was written and is responsible for
everything about how it becomes a Telegram post. A caller should never need
to know Telegram's constraints.
The unit of publication is a POST, not a hall entry. A post is a short,
personal, reach-optimized piece written *from* a hall-of-helix entry — it
is not the entry, and it is not a truncation of the entry.
Publication is idempotent on entry_id: publishing the same entry twice
yields one publication. This matters because the caller is usually
automation that may be retried.
Composition happens upstream, before this interface is called. That is a
deliberate architectural boundary and not an accident of layering:
composing a condensed, entertaining post is a probabilistic task, and
ArchitectureBlueprint.md section 48.1 names an LLM in the mandatory request
path as an anti-pattern. This interface stays deterministic by taking the
composed text as input.
Publication is idempotent on post_id, because the caller is automation that
may be retried and a duplicate post cannot be quietly undone.
paths:
/v1/hall-entries:
/v1/channel-posts:
post:
operationId: publishEntry
summary: Publish a hall-of-helix entry
operationId: publishPost
summary: Publish a composed channel post
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [entry_id, title, body]
required: [post_id, body, source_entry_id, source_entry_url, reviewed_by]
additionalProperties: false
properties:
entry_id:
post_id:
type: string
minLength: 1
maxLength: 128
description: |
The hall entry's own id, from its frontmatter. Publication
is idempotent on this value.
title:
type: string
minLength: 1
maxLength: 256
Stable id for this post. Publication is idempotent on it.
A recomposition of the same entry is a NEW post_id, because
it is a different piece of writing about the same work.
body:
type: string
minLength: 1
maxLength: 4096
description: |
The entry body as Markdown, frontmatter removed.
The composed post, already within Telegram's single-message
limit. Bounded here on purpose: a composition that does not
fit was not condensed, and this interface must not decide
on the composer's behalf what to cut.
Deliberately unbounded. Telegram's 4096-character message
limit is this interface's problem, not the caller's, and a
maxLength here would push it back onto them.
display_name:
source_entry_id:
type: string
minLength: 1
description: |
The hall entry this post was written from. Required: a post
with no traceable source cannot be checked against what the
person actually said about their own work.
source_entry_url:
type: string
minLength: 1
description: Canonical location of the source entry.
reviewed_by:
type: string
minLength: 1
description: |
Who approved this composition for publication. Required at
FLUID-2 and above: the post speaks about a named person's
work in HelixForge's voice, and no generated text reaches
subscribers without someone having read it.
subject_display_name:
type: string
maxLength: 128
worker_kind:
description: The person or agent whose work the post is about.
subject_consent:
type: string
enum: [human, agent-session]
recorded_at:
enum: [explicit, standing, not-required]
description: |
On what basis this post may speak about the subject's work.
"standing" covers contributors who agreed when they took a
seat in the hall; "explicit" is per-post approval.
variant:
type: string
format: date
source_url:
maxLength: 64
description: |
Which composition strategy produced this post. Opaque to
this interface: it records the label and reports engagement
against it, so that the campaign can compare strategies it
owns without this interface needing to understand them.
visual_url:
type: string
description: Canonical location of the entry in hall-of-helix.
description: Portrait or illustration to attach.
tags:
type: array
maxItems: 8
items: {type: string, maxLength: 32}
responses:
"201":
description: Published
"200":
description: Already published; the existing publication is returned
"400":
description: The entry cannot be published faithfully
description: The post cannot be published as given
"403":
description: Publication refused; no review or no basis for consent
"502":
description: Telegram was unreachable or refused the message
get:
operationId: listPublications
operationId: listPosts
summary: List publications, newest first
parameters:
- name: limit
in: query
required: false
schema: {type: integer, minimum: 1, maximum: 100}
- name: state
in: query
required: false
schema:
type: string
enum: [pending, published, failed, superseded]
- name: variant
in: query
schema: {type: string, maxLength: 64}
- name: source_entry_id
in: query
schema: {type: string}
responses:
"200":
description: Publications
/v1/hall-entries/{entry_id}:
/v1/channel-posts/{post_id}:
get:
operationId: getPublication
summary: Publication state for one entry
operationId: getPost
summary: Publication state for one post
parameters:
- name: entry_id
- name: post_id
in: path
required: true
schema: {type: string, minLength: 1}
@ -100,4 +151,4 @@ paths:
"200":
description: Publication state
"404":
description: This entry has not been published
description: This post has not been published

View file

@ -7,18 +7,22 @@ API.
## What the adapter owns
- Rendering a hall entry into Telegram's message forms.
- Rendering a **composed post** into a Telegram message.
- Attaching the post's visual, where the revision calls for one.
- Talking to the Telegram Bot API.
- Custody of the bot token for the duration of a call.
- Idempotency: recognising an `entry_id` it has already published.
- Idempotency: recognising a `post_id` it has already published.
## What the adapter must not do
- Create, rename or delete channels, groups or bots.
- Change membership, administrators or permissions.
- Post to any chat other than the one it is configured with.
- Edit entry content to make it fit. If an entry cannot be published
faithfully, refuse it — that refusal is the pressure signal.
- **Compose, rewrite, shorten or embellish post text.** The post arrives
written. If it does not fit, refuse it: a composition that exceeds the limit
was not condensed, and deciding what to cut is the composer's judgement.
- Publish a post carrying no `reviewed_by`, or none of the recognised consent
bases. Refuse with 403.
- Log, echo or forward the bot token, including in error messages.
## Endpoints
@ -27,6 +31,10 @@ Exactly the three operations in `contracts/r1.openapi.yaml`, at the same paths.
The gateway validates requests against that contract before the adapter sees
them, so the adapter may assume shape but must still assume hostile content.
Note what the contract does **not** accept: a hall entry. The adapter never
reads `hall-of-helix`. Its only knowledge of an entry is the id and URL a post
cites, which it renders as a link.
## Backend capability contract
| Property | Value |
@ -42,49 +50,68 @@ them, so the adapter may assume shape but must still assume hostile content.
## The idempotency requirement
`POST /v1/hall-entries` is idempotent on `entry_id`. The caller is automation
that may be retried, and a duplicate hall entry in the channel is not something
you can quietly undo — subscribers have already seen it.
`POST /v1/channel-posts` is idempotent on `post_id`. The caller is automation
that may be retried, and a duplicate post is not something you can quietly undo
— subscribers have already seen it.
The adapter keeps its own mapping from `entry_id` to the Telegram message ids it
The adapter keeps its own mapping from `post_id` to the Telegram message id it
produced. On a repeat call it returns `200` with the existing publication rather
than posting again. If the entry content has changed, it edits the existing
messages rather than posting new ones.
than posting again. If the body has changed, it edits the existing message.
A **recomposition** of the same source entry is a new `post_id`, not an edit.
It is a different piece of writing about the same work, and collapsing the two
would lose the comparison between them.
This state is the adapter's, not fluid-core's. The evidence store records that a
publication happened; the adapter records which Telegram messages implement it.
## Rendering rules
Telegram formatting is not Markdown. Entry Markdown must be converted, not
Telegram formatting is not Markdown. Post Markdown must be converted, not
passed through.
| Entry construct | Telegram |
| Post construct | Telegram |
|---|---|
| `# Heading` | Bold line |
| `**bold**`, `_italic_` | `<b>`, `<i>` in HTML parse mode |
| Fenced code | `<pre>` |
| Tables | Flattened to lines; Telegram has no table |
| Links | `<a href>` |
| Frontmatter | Stripped; `display_name` and `recorded_at` surfaced in the header |
| Source link | Appended as an attributed link to `source_entry_url` |
Anything the converter cannot represent faithfully must fail the publication
rather than degrade it silently. `entry_content_loss` is a hard guardrail in
both competing hypotheses, and it can only be honest if the adapter refuses
rather than approximates.
rather than degrade it silently.
Two constants regardless of revision: every post carries a link to its source
entry, and every post is attributable. `missing_source_attribution` is a hard
guardrail, and a post that cannot be checked against what the person actually
wrote is the failure this whole arrangement exists to prevent.
**Caption limit.** When the revision attaches a visual (R-2), Telegram caps the
caption at 1024 characters rather than 4096. The adapter must refuse a post
that exceeds the limit for the form it is publishing in, rather than truncating
it — the difference between those two limits is the entire subject of the first
experiment, and silently trimming would destroy the measurement.
## Telemetry the adapter should emit
The gateway observes the request path already. The adapter adds what only it can
see, by POSTing to the control plane's `/control/v1/telemetry`:
- `messages_per_entry` — how many Telegram messages one entry became.
- `entry_content_loss` — whether the conversion dropped anything.
- `variant` — the composition strategy label the post carried, so engagement
can be attributed to it. This is what lets the campaign compare strategies it
owns without this interface understanding them.
- `unreviewed_publication` — always false in practice, since such a post is
refused; emitted so the guardrail has a series rather than an absence.
- `missing_source_attribution`, `missing_subject_consent` — likewise.
- Telegram rate-limit encounters, as error events with class `backend_failure`.
It must not emit subscriber identity in any form. Engagement metrics
(`read_through_rate`, `reaction_rate`) come from Telegram's channel statistics
as aggregates, never per-reader.
It must not emit subscriber identity in any form, and it must not emit post
body text as telemetry — the body belongs in the publication record, not in the
observation stream.
Engagement metrics (`engagement_rate`, `source_link_rate`, `forward_rate`) come
from Telegram's channel statistics as aggregates, never per-reader.
## Configuration
@ -94,6 +121,7 @@ as aggregates, never per-reader.
| `TELEGRAM_CHANNEL_ID` | The channel this adapter publishes to |
| `FLUID_CONTROL_URL` | Where to POST telemetry |
| `HALL_BASE_URL` | Base URL for links back to hall-of-helix |
| `ATTACH_VISUAL` | Whether this revision attaches the portrait (R-2 true, R-3 false) |
One adapter instance serves one channel. The private test channel and the public
channel are separate instances with separate configuration, which is what keeps

View file

@ -7,8 +7,8 @@ enough to compare over time and no more granular than the analysis requires.
| Cohort | Assigned by | Why it is distinct |
|---|---|---|
| `hall-publishing-jobs` | `X-FLUID-Consumer` prefix `hall-` | Batch behaviour; publishes many entries at once |
| `helix-forge-agents` | `X-FLUID-Consumer` prefix `agent-` | One entry at a time, at session end; the richest pressure signal |
| `hall-publishing-jobs` | `X-FLUID-Consumer` prefix `hall-` | Batch behaviour; publishes many posts at once. Becomes the `pr-hall-of-helix` campaign job |
| `helix-forge-agents` | `X-FLUID-Consumer` prefix `agent-` | One post at a time, at session end |
| `operators` | `X-FLUID-Consumer` prefix `op-` | Rare, deliberate, often corrective |
| `telegram-subscribers` | Not a caller | An observed population, never an API consumer |
@ -31,9 +31,10 @@ Beyond the fluid-core defaults, this interface adds:
usernames, display names of subscribers — must not reach the evidence store
in any form, redacted or otherwise. There is no legitimate analysis that
needs them, so the safe rule is that they never arrive.
- **Entry content is not telemetry.** An entry body may appear in a request; it
must not appear in a telemetry event. What is recorded is its length, its
section count, and whether conversion succeeded.
- **Post text is not telemetry.** The body appears in the request and is kept
in the publication record, because it is what was said in HelixForge's name.
It must not appear in a telemetry event: observation is for measuring how
posts performed, not for accumulating a second copy of what they said.
- **The bot token appears nowhere.** Not in errors, not in traces, not in
redacted form.
@ -55,16 +56,32 @@ loss and start a fresh evidence baseline, not to rotate quietly.
| Metric | Role | Source |
|---|---|---|
| `read_through_rate` | primary | Telegram channel statistics, aggregate |
| `messages_per_entry` | guardrail | Adapter |
| `entry_content_loss` | guardrail | Adapter |
| `engagement_rate` | primary | Telegram channel statistics, aggregate |
| `unreviewed_publication` | guardrail | Adapter |
| `missing_source_attribution` | guardrail | Adapter |
| `missing_subject_consent` | guardrail | Adapter |
| `error_rate` | guardrail | Gateway |
| `reaction_rate` | secondary | Telegram channel statistics, aggregate |
| `time_to_first_read` | learning | Telegram channel statistics, aggregate |
| `source_link_rate` | secondary | Telegram channel statistics, aggregate |
| `forward_rate` | secondary | Telegram channel statistics, aggregate |
| `time_to_first_view` | learning | Telegram channel statistics, aggregate |
`read_through_rate` is the one to be careful about. Telegram reports view counts
per message, not per reader. For the teaser form it is link clicks over views;
for the serialized form it is views of the last part over views of the first.
Those are not the same measurement, and comparing them directly is the weakest
part of the first experiment. Say so in the experiment's amendment record rather
than letting the comparison look cleaner than it is.
`engagement_rate` is the one to be careful about. Telegram reports views,
reactions and forwards per message, not per reader, and a "rate" therefore has
subscribers as its denominator rather than people who saw the post. It is a
proxy, and a noisy one at small subscriber counts. Say so in the experiment's
amendment record rather than letting the comparison look cleaner than it is.
## Variant attribution
Every post carries a `variant` label naming the composition strategy that
produced it. This interface treats it as opaque: it records the label, and
reports engagement grouped by it.
That is deliberately the whole of the interface's involvement in composition.
The campaign compares its own strategies using this data, running its own FLUID
experiments over its own artifacts. The interface does not know what
`variant: warm-specific-v2` means and must not acquire an opinion about it.
Without this field the campaign would have to infer which post came from which
strategy by timestamp, which is exactly the kind of reconstruction that quietly
stops being accurate.

View file

@ -0,0 +1,68 @@
# The interface's first experiment: does the portrait earn its place?
#
# This tests DELIVERY, which this interface owns. The competition about which
# editorial voice travels belongs to the campaign and is run on the per-variant
# engagement this interface reports — see docs/observation.md.
#
# fluid experiment design --file experiments/E-tg-delivery.yaml
# fluid experiment start E-tg-delivery --generation 2 \
# --default-revision R-2 --policy-out rp.json
# fluid policy put --file rp.json
fluid_experiment:
schema_version: "0.1"
id: "E-tg-delivery"
interface_id: "helix-forge-telegram-publishing"
hypothesis_refs:
- "H-tg-visual"
- "H-tg-textonly"
# R-2 attaches the portrait, R-3 publishes text only. Neither is an
# incumbent: R-1 publishes without either treatment being settled, so this
# is a comparison between two answers rather than against a baseline.
control_revision: "R-2"
candidate_revisions:
- "R-3"
cohorts:
- "telegram-subscribers"
allocation:
control: 0.5
candidate: 0.5
# An even split because there is no incumbent to protect. The usual 90/10
# caution limits exposure to an unproven candidate; here both arms are
# equally unproven and the scarce resource is posts, not safety.
metrics:
primary:
- "engagement_rate"
guardrails:
- "unreviewed_publication"
- "missing_source_attribution"
- "missing_subject_consent"
- "error_rate"
secondary:
- "source_link_rate"
- "forward_rate"
learning:
- "time_to_first_view"
start_conditions:
- "both revisions verified against the private test channel"
- "channel has at least 100 subscribers"
stop_conditions:
- "hard_guardrail_violation"
- "unreviewed_publication"
- "manual_stop"
- "max_duration_reached"
max_duration_hours: 2160 # 90 days
result:
state: "PLANNED"
preferred_revision: null
evidence_refs: []

View file

@ -1,68 +0,0 @@
# The first live experiment: which presentation serves readers better.
#
# Do not start this until the private test channel has confirmed both forms
# render correctly, and the public channel has enough subscribers for the
# result to mean anything. The intent (section 12) is explicit that an
# underpowered window should report INCONCLUSIVE rather than be shortened.
#
# fluid experiment design --file experiments/E-tg-length.yaml
# fluid experiment start E-tg-length --generation 2 \
# --default-revision R-2 --policy-out rp.json
# fluid policy put --file rp.json
fluid_experiment:
schema_version: "0.1"
id: "E-tg-length"
interface_id: "helix-forge-telegram-publishing"
hypothesis_refs:
- "H-tg-teaser"
- "H-tg-serial"
# R-2 implements the teaser form, R-3 the serialized form. Neither is the
# control in the usual sense: R-1 cannot publish a long entry at all, so
# this is a comparison between two answers rather than against a baseline.
control_revision: "R-2"
candidate_revisions:
- "R-3"
cohorts:
- "telegram-subscribers"
allocation:
control: 0.5
candidate: 0.5
# An even split because there is no incumbent to protect. The usual 90/10
# caution exists to limit exposure to an unproven candidate; here both arms
# are equally unproven and the scarce resource is entries, not safety.
metrics:
primary:
- "read_through_rate"
guardrails:
- "entry_content_loss"
- "error_rate"
- "messages_per_entry"
secondary:
- "reaction_rate"
- "forward_rate"
learning:
- "time_to_first_read"
start_conditions:
- "both revisions verified against the private test channel"
- "channel has at least 100 subscribers"
stop_conditions:
- "hard_guardrail_violation"
- "entry_content_loss"
- "manual_stop"
- "max_duration_reached"
max_duration_hours: 2160 # 90 days: roughly 12 entries at current cadence
result:
state: "PLANNED"
preferred_revision: null
evidence_refs: []

View file

@ -1,125 +0,0 @@
# Competing hypothesis B, in group CG-tg-length.
fluid_hypothesis:
schema_version: "0.1"
id: "H-tg-serial"
interface_id: "helix-forge-telegram-publishing"
state: "DRAFT"
title: "The whole entry in the channel serves readers better than a link"
observation:
summary: >
85% of hall entries exceed Telegram's 4096-character message limit, with
a median body of 5767 characters. The interface must choose a form for
long entries before it can publish at all.
evidence_refs:
- "measurement:hall-of-helix/entries@2026-09-04"
- "pressure:P-tg-length"
affected_cohorts:
- "telegram-subscribers"
pressure:
classes:
- "missing_interface_capability"
pressure_refs:
- "P-tg-length"
severity: 0.85
confidence: 1.0
explanation:
claim: >
A link out of Telegram is a cost most readers will not pay. Telegram's
rich-text posts are built for long-form reading and the audience is
already there. Serializing the entry across a small number of linked
messages keeps the whole account where the reader is, and the median
entry needs only two.
reach:
score: 0.5
explains:
- "P-tg-length"
notes: >
Medium reach. It answers the length constraint natively but does not
transfer to platforms with harder limits.
proposed_adaptation:
class: "presentation"
summary: >
Split the entry at section boundaries into a small number of rich-text
messages, each linked to the previous, with the visual attached to the
first.
implementation_scope: "interface_only"
backend_requirements:
required: false
requirement_refs: []
expected_outcomes:
- metric: "read_through_rate"
cohort: "telegram-subscribers"
target: 0.15
direction: "higher"
- metric: "messages_per_entry"
cohort: "all"
baseline: 1.0
target: 2.0
direction: "higher"
guardrails:
- metric: "entry_content_loss"
operator: "=="
threshold: false
- metric: "error_rate"
operator: "<="
threshold: 0.01
- metric: "messages_per_entry"
operator: "<="
threshold: 4
fitness_dimensions:
expected:
client_utility: 0.5
discoverability: 0.3
simplicity: -0.2
correctness: 0.0
security: 0.0
complexity:
expected_delta:
operation_count: 0
concept_count: 1
alternative_path_count: 1
cognitive_load: 0.2
score: 0.3
risk:
level: "LOW"
reasons:
- "presentation only"
- "splitting can lose meaning at a bad boundary; guarded by content-loss check"
economics:
estimated_experiment_cost: 0.0
estimated_implementation_cost: 0.0
currency: "EUR"
expected_value_class: "MEDIUM"
learning_value:
score: 0.8
notes: >
Tests whether an audience will read long-form in place, which is the
question behind every publishing interface HelixForge will build.
success_criteria:
expression: >
read_through_rate >= 0.15 AND messages_per_entry <= 4 AND no guardrail violation
failure_criteria:
expression: >
any hard guardrail violation
OR read_through_rate below the teaser alternative over the same window
outcome:
status: null
summary: null
evidence_refs: []

View file

@ -1,125 +0,0 @@
# Competing hypothesis A, in group CG-tg-length.
#
# Create with:
# fluid hypothesis create --file hypotheses/H-tg-teaser.yaml
# fluid hypothesis compete --group CG-tg-length H-tg-teaser H-tg-serial
fluid_hypothesis:
schema_version: "0.1"
id: "H-tg-teaser"
interface_id: "helix-forge-telegram-publishing"
state: "DRAFT"
title: "A teaser and a link serve readers better than the whole entry"
observation:
summary: >
85% of hall entries exceed Telegram's 4096-character message limit, with
a median body of 5767 characters. The interface must choose a form for
long entries before it can publish at all.
evidence_refs:
- "measurement:hall-of-helix/entries@2026-09-04"
- "pressure:P-tg-length"
affected_cohorts:
- "telegram-subscribers"
pressure:
classes:
- "missing_interface_capability"
pressure_refs:
- "P-tg-length"
severity: 0.85
confidence: 1.0
explanation:
claim: >
A channel post is scanned, not read. A hall entry is a considered
first-person account that rewards attention. Presenting an opening and a
link lets a reader decide to give it that attention somewhere designed
for reading, rather than committing to a wall of text in a feed.
reach:
score: 0.5
explains:
- "P-tg-length"
notes: >
Medium reach. It answers the length constraint and would answer the
same constraint on X, but it does not address entries with visuals or
entries readers might want to keep in the channel.
proposed_adaptation:
class: "presentation"
summary: >
Publish the title, the display name, the opening of the entry, and a link
to the full entry in hall-of-helix.
implementation_scope: "interface_only"
backend_requirements:
required: false
requirement_refs: []
expected_outcomes:
- metric: "read_through_rate"
cohort: "telegram-subscribers"
target: 0.15
direction: "higher"
- metric: "messages_per_entry"
cohort: "all"
baseline: 2.0
target: 1.0
direction: "lower"
guardrails:
- metric: "entry_content_loss"
operator: "=="
threshold: false
- metric: "error_rate"
operator: "<="
threshold: 0.01
fitness_dimensions:
expected:
client_utility: 0.4
discoverability: 0.5
simplicity: 0.6
correctness: 0.0
security: 0.0
complexity:
expected_delta:
operation_count: 0
concept_count: 0
cognitive_load: -0.1
score: 0.05
risk:
level: "LOW"
reasons:
- "presentation only; no contract or authorization change"
- "fully reversible by replacing the revision"
economics:
estimated_experiment_cost: 0.0
estimated_implementation_cost: 0.0
currency: "EUR"
expected_value_class: "MEDIUM"
learning_value:
score: 0.8
notes: >
Whichever way this resolves, the answer transfers directly to
fluid-x and fluid-substack, which face the same problem under
different constraints.
success_criteria:
expression: >
read_through_rate >= 0.15 AND no guardrail violation
failure_criteria:
expression: >
any hard guardrail violation
OR read_through_rate below the serialized alternative over the same window
outcome:
status: null
summary: null
evidence_refs: []

View file

@ -0,0 +1,122 @@
fluid_hypothesis:
schema_version: "0.1"
id: "H-tg-textonly"
interface_id: "helix-forge-telegram-publishing"
state: "DRAFT"
title: "Text alone reaches further than a post constrained by a caption"
observation:
summary: >
Telegram limits a photo caption to 1024 characters, a quarter of the 4096 available to a text message. Attaching a portrait therefore constrains the composition it accompanies, and a post written to 1024 characters is a different piece of writing from one written to 4096.
evidence_refs:
- "canon:fluid-telegram/TelegramCapabilityCanon.md#MED-01"
- "pressure:P-register"
affected_cohorts:
- "telegram-subscribers"
pressure:
classes:
- "repeated_expectation_mismatch"
pressure_refs:
- "P-register"
severity: 0.6
confidence: 0.7
explanation:
claim: >
The scarce resource in a condensed post is room to say something specific. A generated portrait is decorative and interchangeable across entries; a concrete detail about what someone actually did is not. Trading three quarters of the available text for a picture spends the thing that makes a post worth reading on the thing that makes it look like everything else in the feed.
reach:
score: 0.4
explains:
- "P-register"
notes: >
Narrow but clean. It settles one delivery question decisively, and the
answer transfers to fluid-x and fluid-substack, which face the same
question under different feeds.
proposed_adaptation:
class: "presentation"
summary: >
Publish the post as a text message with a link to the source entry, and no attached visual.
implementation_scope: "interface_only"
backend_requirements:
required: false
requirement_refs: []
expected_outcomes:
- metric: "engagement_rate"
cohort: "telegram-subscribers"
target: 0.05
direction: "higher"
- metric: "source_link_rate"
cohort: "telegram-subscribers"
target: 0.02
direction: "higher"
guardrails:
- metric: "unreviewed_publication"
operator: "=="
threshold: false
- metric: "missing_source_attribution"
operator: "=="
threshold: false
- metric: "missing_subject_consent"
operator: "=="
threshold: false
- metric: "error_rate"
operator: "<="
threshold: 0.01
fitness_dimensions:
expected:
client_utility: 0.2
discoverability: 0.3
simplicity: 0.3
correctness: 0.0
security: 0.0
complexity:
expected_delta:
operation_count: 0
concept_count: 0
cognitive_load: 0.1
score: 0.05
risk:
level: "LOW"
reasons:
- "presentation only; no contract, authorization or consent change"
- "the simpler arm; nothing to attach and nothing to fail"
economics:
estimated_experiment_cost: 0.0
estimated_implementation_cost: 0.0
currency: "EUR"
expected_value_class: "MEDIUM"
learning_value:
score: 0.7
notes: >
If this wins, the campaign stops producing portraits for publication and spends that effort on the writing instead.
success_criteria:
expression: >
engagement_rate >= 0.05 AND no guardrail violation
failure_criteria:
expression: >
any hard guardrail violation
OR engagement_rate at or below the rival arm over the same window
competition:
group_id: "CG-tg-delivery"
alternatives:
- "H-tg-visual"
outcome:
status: null
summary: null
evidence_refs: []

122
hypotheses/H-tg-visual.yaml Normal file
View file

@ -0,0 +1,122 @@
fluid_hypothesis:
schema_version: "0.1"
id: "H-tg-visual"
interface_id: "helix-forge-telegram-publishing"
state: "DRAFT"
title: "A post carrying its portrait reaches further than text alone"
observation:
summary: >
72 of 94 hall entries already carry a generated portrait, produced from the entry's own visual prompt. Telegram renders images inline and its feed is scanned rather than read, so a post with a portrait occupies more of a scrolling reader's attention than one without.
evidence_refs:
- "measurement:hall-of-helix/visuals@2026-09-04"
- "pressure:P-register"
affected_cohorts:
- "telegram-subscribers"
pressure:
classes:
- "repeated_expectation_mismatch"
pressure_refs:
- "P-register"
severity: 0.6
confidence: 0.7
explanation:
claim: >
Reach in a scanned feed is won by stopping the scroll. The portraits are already made, already tied to the specific person and stretch of work, and cost nothing further to attach. If they do not measurably help, the effort of producing them for publication is not justified and the campaign should know that.
reach:
score: 0.4
explains:
- "P-register"
notes: >
Narrow but clean. It settles one delivery question decisively, and the
answer transfers to fluid-x and fluid-substack, which face the same
question under different feeds.
proposed_adaptation:
class: "presentation"
summary: >
Publish the post with its portrait attached as a photo message, with the post body as the caption.
implementation_scope: "interface_only"
backend_requirements:
required: false
requirement_refs: []
expected_outcomes:
- metric: "engagement_rate"
cohort: "telegram-subscribers"
target: 0.05
direction: "higher"
- metric: "source_link_rate"
cohort: "telegram-subscribers"
target: 0.02
direction: "higher"
guardrails:
- metric: "unreviewed_publication"
operator: "=="
threshold: false
- metric: "missing_source_attribution"
operator: "=="
threshold: false
- metric: "missing_subject_consent"
operator: "=="
threshold: false
- metric: "error_rate"
operator: "<="
threshold: 0.01
fitness_dimensions:
expected:
client_utility: 0.2
discoverability: 0.5
simplicity: -0.1
correctness: 0.0
security: 0.0
complexity:
expected_delta:
operation_count: 0
concept_count: 0
cognitive_load: 0.1
score: 0.15
risk:
level: "LOW"
reasons:
- "presentation only; no contract, authorization or consent change"
- "Telegram caps a photo caption at 1024 characters, a quarter of the text limit; this arm constrains the composition it carries"
economics:
estimated_experiment_cost: 0.0
estimated_implementation_cost: 0.0
currency: "EUR"
expected_value_class: "MEDIUM"
learning_value:
score: 0.7
notes: >
Settles whether the portraits earn their place in publication, and the answer transfers to every other channel HelixForge publishes to.
success_criteria:
expression: >
engagement_rate >= 0.05 AND no guardrail violation
failure_criteria:
expression: >
any hard guardrail violation
OR engagement_rate at or below the rival arm over the same window
competition:
group_id: "CG-tg-delivery"
alternatives:
- "H-tg-textonly"
outcome:
status: null
summary: null
evidence_refs: []

View file

@ -1,47 +0,0 @@
# Seed pressure record.
#
# This is not derived from telemetry. It is a structural constraint measured
# directly against hall-of-helix on 2026-09-04, and it is real before the
# interface has served a single request. The intent (section 13) permits a
# structural constraint to found a hypothesis without waiting for repetition.
#
# Record it with:
# fluid pressure ... (or let the first classification pass confirm it)
fluid_pressure:
schema_version: "0.1"
id: "P-tg-length"
interface_id: "helix-forge-telegram-publishing"
class: "missing_interface_capability"
first_seen: "2026-09-04T00:00:00Z"
last_seen: "2026-09-04T00:00:00Z"
affected_cohorts:
- "hall-publishing-jobs"
- "helix-forge-agents"
frequency:
observations: 80
independent_consumers: 2
severity: 0.85
confidence: 1.0
summary: >
Telegram limits a single message to 4096 characters. Of the 94 entries in
hall-of-helix on 2026-09-04, 80 have bodies exceeding that limit — 85%.
The median body is 5767 characters and the largest is 10220. No entry of
typical length can be published as one message, so the interface cannot
fulfil its mission without deciding how a long entry becomes a post.
evidence_refs:
- "measurement:hall-of-helix/entries@2026-09-04"
- "canon:fluid-telegram/TelegramCapabilityCanon.md#PUB-02"
status: "ANALYZING"
linked_hypotheses:
- "H-tg-teaser"
- "H-tg-serial"

View file

@ -0,0 +1,45 @@
# Seed pressure record.
#
# Measured directly rather than observed from telemetry. The intent (section 13)
# permits a structural constraint to found a hypothesis without waiting for
# repetition, and this one is structural: it is a property of what a hall entry
# is, not of how anyone has used the interface.
fluid_pressure:
schema_version: "0.1"
id: "P-register"
interface_id: "helix-forge-telegram-publishing"
class: "repeated_expectation_mismatch"
first_seen: "2026-09-04T00:00:00Z"
last_seen: "2026-09-04T00:00:00Z"
affected_cohorts:
- "telegram-subscribers"
frequency:
observations: 94
independent_consumers: 1
severity: 0.8
confidence: 1.0
summary: >
A hall-of-helix entry is written for a colleague reading a repository: a
considered first-person account under fixed headings, median body 5767
characters, with 80 of 94 entries exceeding Telegram's 4096-character
single-message limit. A channel post reaches a stranger scrolling. These
are different pieces of writing for different readers, and publishing the
first as the second serves neither. The length figures are the visible
symptom; the mismatch is one of register and audience, and it would remain
if the limit were ten times larger.
evidence_refs:
- "measurement:hall-of-helix/entries@2026-09-04"
- "canon:fluid-telegram/TelegramCapabilityCanon.md#PUB-02"
status: "ANALYZING"
linked_hypotheses:
- "H-tg-visual"

View file

@ -22,8 +22,20 @@ state_hub_workstream_id: "a660ed65-700e-5b54-8d91-a556b73518f0"
# FT-WP-0001 — Telegram identity and the Hall of Helix channel
Establish a Telegram identity for HelixForge and a channel publishing
hall-of-helix entries, as the first real FLUID interface.
Establish a Telegram identity for HelixForge and a channel publishing composed
posts about hall-of-helix work, as the first real FLUID interface.
**The channel does not republish entries.** A hall entry is a considered
first-person account written for a colleague reading a repository. A channel
post is short, personal, entertaining and written to travel. The post is
composed *from* the entry and is a different piece of writing; the entry is the
source and the check, never the payload.
Composition happens before this interface is called. That boundary is
architectural: composing is a probabilistic editorial task, and
`ArchitectureBlueprint.md` §48.1 forbids an LLM in the mandatory request path.
It is also editorial: a post speaks about a named person's work in HelixForge's
voice, and that deserves a human read before it goes out.
Everything that could be prepared without credentials has been: the governing
intent, the R-1 contract, the adapter's connector contract, the observation
@ -115,12 +127,57 @@ Implement the three operations in `contracts/r1.openapi.yaml` against the
connector contract in `docs/adapter-contract.md`. TypeScript with grammY is the
natural choice; the wire contract makes the language irrelevant to fluid-core.
The two requirements that carry the most weight: publication is idempotent on
`entry_id`, and an entry that cannot be converted faithfully is refused rather
than approximated. `entry_content_loss` is a hard guardrail in both competing
hypotheses and can only be honest if the adapter refuses.
Three requirements carry the weight: publication is idempotent on `post_id`; a
post arriving without `reviewed_by` or a consent basis is refused with 403; and
a post that exceeds the limit for the form being published is refused rather
than truncated. The adapter renders and delivers. It never writes.
## T06 — Publish one entry to the private channel
## T05b — Establish the composition step
```task
id: FT-WP-0001-T05b
status: todo
priority: high
```
Build the step that turns a hall entry into a post: condensed, personal,
entertaining, reach-optimized, carrying a link back to the source entry.
For now this lives here, as a script plus a review queue. It is temporary by
design — T11 extracts it into `pr-hall-of-helix` — so keep the seam clean. The
composition step's only interface to publishing is `POST /v1/channel-posts`,
and it must not reach into the adapter or the evidence store.
Each composition records the `variant` label of the strategy that produced it.
Without that label the campaign cannot later compare strategies, and inferring
it from timestamps is the kind of reconstruction that quietly stops being
accurate.
Human review is not a nicety here. A generated condensation can put a claim in
someone's mouth that their own entry never made, and in the audit trail an
unfaithful post looks exactly like a faithful one. The reviewer reads the post
against the entry, and their name goes in `reviewed_by`.
## T05c — Record the consent basis for hall participants
```task
id: FT-WP-0001-T05c
status: todo
priority: high
```
Ninety-four entries already exist, written by people and agent sessions who
took a seat in the hall without anyone mentioning a Telegram channel.
Decide and record the basis on which HelixForge may write publicly about that
work. If `hall-of-helix/ENTRY.md` is to carry a standing agreement, add it and
say plainly that it is revocable. Contributors who predate it should be asked
rather than assumed into it.
Absence of objection is not consent, and the interface refuses a post carrying
no basis, so this blocks T08.
## T06 — Publish one post to the private channel
```task
id: FT-WP-0001-T06
@ -129,15 +186,22 @@ priority: high
state_hub_task_id: "317bef32-e82e-586a-8947-8cbb330f6c98"
```
The first real test. Publish a single hall entry to the private channel and
confirm, by looking at it:
The first real test. Compose one post from one entry, have it reviewed, publish
it to the private channel, and confirm by looking at it:
- the rendering is faithful and readable;
- re-publishing the same entry edits rather than duplicates;
- the evidence store contains no Telegram user identity and no bot token;
- `fluid audit trace R-1` reconstructs the publication.
- the post reads well and is worth a stranger's attention;
- it does not claim anything the source entry does not support;
- the source link works and the attribution is right;
- re-publishing the same `post_id` edits rather than duplicates;
- the evidence store holds the post text but no Telegram user identity and no
bot token;
- `fluid audit trace R-1` reconstructs the publication, naming the reviewer and
the source entry.
## T07 — Implement both presentation forms as R-2 and R-3
The second bullet is the one that needs a person. Everything else can be
automated; whether a condensation is fair to the person it describes cannot.
## T07 — Implement both delivery forms as R-2 and R-3
```task
id: FT-WP-0001-T07
@ -146,11 +210,16 @@ priority: medium
state_hub_task_id: "6a91ea08-8814-58f3-8297-a7ca63405294"
```
R-2 implements `H-tg-teaser`, R-3 implements `H-tg-serial`. Both are
presentation adaptations over the same contract, so neither needs a contract
change. Verify both against the private channel before either sees a subscriber.
R-2 implements `H-tg-visual` — portrait attached, and Telegram's 1024-character
caption limit with it. R-3 implements `H-tg-textonly` — a text message with the
full 4096 available. Both are presentation adaptations over the same contract,
so neither needs a contract change. Verify both against the private channel
before either sees a subscriber.
## T08 — Open the public channel and publish the backlog
The two arms impose different length limits on the composer, which is the one
place delivery and composition genuinely couple.
## T08 — Open the public channel and start publishing
```task
id: FT-WP-0001-T08
@ -159,14 +228,16 @@ priority: medium
state_hub_task_id: "5e5cd89a-e7b1-5aaf-9331-ece424afe5e3"
```
Point the production adapter at the public channel and publish the existing
entries. 94 entries at once would be both a rate-limit problem and an unkind
introduction for a new subscriber; publish them oldest-first at a human pace.
Point the production adapter at the public channel. Blocked on T05c: no post
goes out about anyone's work without a recorded consent basis.
Whichever of R-2 or R-3 is used here becomes the de facto control for the
experiment, which is why `E-tg-length` names R-2 as control rather than R-1.
Do not batch-publish the backlog. Ninety-four posts in a burst is a rate-limit
problem, an unkind introduction for a new subscriber, and — more to the point —
ninety-four compositions nobody has had time to read properly. Publish
selectively and at a human pace, and let the backlog be a source to draw on
rather than a queue to drain.
## T09 — Run the first experiment
## T09 — Run the first delivery experiment
```task
id: FT-WP-0001-T09
@ -175,14 +246,14 @@ priority: low
state_hub_task_id: "649e7df6-6c07-5cc8-a022-d1eadcdb7236"
```
Start `E-tg-length` once the channel has roughly 100 subscribers. Before then
Start `E-tg-delivery` once the channel has roughly 100 subscribers. Before then
the evaluator will report INCONCLUSIVE, and that is the correct answer rather
than a reason to shorten the window.
At one entry per week, a 90-day experiment yields about twelve entries. That is
At one post per week, a 90-day experiment yields about twelve posts. That is
thin, and the honest expectation is that the first run is inconclusive and
informs a better-powered second one. Record that expectation in the experiment's
amendments rather than discovering it as a disappointment.
informs a better-powered second one. Record that expectation in the
experiment's amendments rather than discovering it as a disappointment.
## T10 — Close the loop
@ -193,9 +264,43 @@ priority: low
state_hub_task_id: "b469b9b8-a1a8-5f76-a1bb-dd51dd4d2ce5"
```
Record the hypothesis outcome, resolve the competition group, and promote the
winner. Then confirm `fluid audit trace` reconstructs the whole chain from the
length-limit pressure through to the promoted revision.
Record the hypothesis outcome, resolve `CG-tg-delivery`, and promote the winner.
Then confirm `fluid audit trace` reconstructs the whole chain, from the register
mismatch through to the promoted revision.
That trace is the deliverable. It is what makes this the first FLUID interface
rather than a bot that posts to a channel.
## T11 — Extract the campaign into pr-hall-of-helix
```task
id: FT-WP-0001-T11
status: todo
priority: medium
```
Everything about *what to say and when* moves to a new repository,
`pr-hall-of-helix`, under the `pr-` prefix for public-relations campaigns. What
stays here is the deterministic delivery interface.
The split follows the boundary the intent already draws (§7):
| Moves to `pr-hall-of-helix` | Stays in `fluid-telegram` |
|---|---|
| Composition and the editorial voice | The R-1 contract and its revisions |
| Choosing which work is worth a post | The adapter |
| Variant strategies and their competition | Delivery experiments (`CG-tg-delivery`) |
| Consent records and the review queue | Per-variant engagement reporting |
| The publishing cadence | Bot and channel configuration |
The campaign becomes a *consumer* of this interface — cohort
`hall-publishing-jobs` — and runs its own FLUID loop over its own artifacts,
using the per-variant engagement this interface reports.
Two things this buys. `fluid-telegram` becomes reusable for any HelixForge
campaign rather than being about the Hall of Helix specifically. And the Hall of
Helix campaign becomes an ongoing offering with its own intent, budget and
cadence, rather than living inside a workplan that was supposed to finish.
Do this once the loop has closed at least once (T10). Extracting a seam before
you have used it is how you find out you cut it in the wrong place.