Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 5457@bnt-lap001 Assistant-Session: 792bf84a-3783-4f5d-a811-cded61dd2012
21 KiB
| id | type | title | domain | repo | status | flavor | owner | topic_slug | created | updated | planning_priority | planning_order | depends_on | related_repos | state_hub_workstream_id | ||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| FT-WP-0001 | workplan | Establish HelixForge's Telegram identity and publish the Hall of Helix | infotech | fluid-telegram | blocked | implementation | worsch | fluid-telegram | 2026-09-04 | 2026-10-08 | high | 1 |
|
|
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 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
configuration, the seed pressure record, and two competing hypotheses for the
first experiment. See docs/ and the handover package at
fluid-core/docs/handover/fluid-telegram.md.
What remains needs either a person with a Telegram account or a running adapter.
Blocked (2026-09-27). T05 and T09 are implemented and merged. Everything
else still open needs one of: the human session bootstrap in FT-WP-0002 T01
(which the live provisioning apply for T01–T03 waits on), the fluid CLI from
fluid-core (T04), or a person's judgment call (T07's consent basis, T08's
editorial review, T10's publishing pace). None of that can be resolved from
this repo alone; re-open by working FT-WP-0002 T01 first.
T01–T03 have moved. They were written as manual steps; they are now
provisioned from a declared specification by FT-WP-0002, and the three tasks
below are kept as the record of what must be true rather than as instructions to
follow. The Bot API cannot create a bot or a channel, so provisioning is an
MTProto client acting as a designated operator account (Canon INT-03); one
bounded human bootstrap remains, and docs/provisioning.md says exactly what it
is and why it cannot be removed.
T01 — Register the bot
id: FT-WP-0001-T01
status: done
priority: high
state_hub_task_id: "a5449e86-3181-54b6-a0fe-e5c761b3e4ba"
Superseded by FT-WP-0002 T04. The bot is registered by the provisioner
from the declared presence spec, not by hand: BotFather /newbot driven over
MTProto, name, about text, description and avatar set from the spec, and the
token written straight to OpenBao without passing through a terminal. Consistent
with HF-WP-0002 (bao.coulomb.social).
What survives from the original step is the requirement, not the method: a
reader encountering the bot cold should recognise it as HelixForge. That is now
bot.name / bot.about / bot.description in the spec, where it is reviewable
before it is applied.
Done on 2026-10-07. provision apply registered @HelixForgeBot (id 8859371696), set the profile and avatar, and stored the token in OpenBao. The resolved record is presence/resolved/hall-of-helix.yaml.
2026-10-03: human gate routed to hub decision 7f455718-66c9-4049-9aaf-1143aea39d34 (D6: Telegram operator session: register the bots, create the channels, bootstrap the user session) by CUST-WP-0075; resolving that decision raises C-40 here.
T02 — Create the channels
id: FT-WP-0001-T02
status: wait
priority: high
needs_human: true
blocking_reason: "Private test channel 4427430639 exists and @HelixForgeBot may post there. The public channel stays held until a checked rendering exists."
state_hub_task_id: "597ab1b7-5903-5a5d-a510-0a6946325404"
Superseded by FT-WP-0002 T04. Both channels are created by the
provisioner via channels.createChannel, private test channel first (Canon
PUB-01), with the bot added as administrator through channels.editAdmin.
The ordering and the rights are no longer conventions a person is trusted to
follow. apply will not touch the public channel until the test channel has
recorded a successful publication, and it grants Post Messages and refuses
anything wider even if the spec asks for it — the intent forbids the system
from holding a right it may not exercise, so the grant is the right place to
enforce that.
Every verification below still runs against the private channel, and nothing reaches the public channel until a person has looked at a rendering.
Close this task when FT-WP-0002 T04 has run.
2026-10-03: human gate routed to hub decision 7f455718-66c9-4049-9aaf-1143aea39d34 (D6: Telegram operator session: register the bots, create the channels, bootstrap the user session) by CUST-WP-0075; resolving that decision raises C-40 here.
T03 — Generate and store the redaction salt
id: FT-WP-0001-T03
status: done
priority: high
state_hub_task_id: "bbe5da73-e604-5dfe-b25f-fba802f67618"
Superseded by FT-WP-0002 T05. The provisioner generates 32 bytes if and
only if the OpenBao path is empty, and has no path that overwrites one. Injected
as FLUID_REDACTION_SALT.
It must never change: see docs/observation.md for why rotating it silently
invalidates every longitudinal comparison. Making that a property of the tool
rather than a note in a workplan is the whole reason the step moved.
Done on 2026-10-07. provision apply created the redaction salt at the empty OpenBao path and recorded that it is never rotated. The salt value is not in the repository.
T04 — Record the intent and publish R-1
id: FT-WP-0001-T04
status: wait
needs_human: true
blocking_reason: "The durable key helix-forge-telegram-publishing exists outside git. R-1 is not published: its contract digest is filled (2026-10-08) but the accepted review did not include publication; the founder approves the publish."
priority: high
state_hub_task_id: "13a92a96-2f87-5de2-a69c-7edc21f3a0c7"
export FLUID_INTERFACE=helix-forge-telegram-publishing
fluid intent put --version IEI-2 --file InterfaceEvolutionIntent.md --activate
# fill the digest in revisions/R-1.yaml, then
fluid revision publish --file revisions/R-1.yaml --key-file "$FLUID_SIGNING_KEY" \
--adaptation-classes presentation --approved-by "$USER" --traffic-share 1.0
Generate a real signing key pair for this; --ephemeral-key is development
only, and a revision signed with one stops verifying after a restart.
Created 2026-10-07. Key id helix-forge-telegram-publishing. The public key is
docs/keys/helix-forge-telegram-publishing.pub. The private key is mode 0600
at ~/.config/fluid/keys/helix-forge-telegram-publishing.key, which is not a
git work tree. Set FLUID_SIGNING_KEY to that file and
FLUID_SIGNING_KEY_ID to the key id before a later publish. R-1 stays
unpublished.
T05 — Build the adapter
id: FT-WP-0001-T05
status: done
priority: high
state_hub_task_id: "c89782da-9896-5957-b729-7c35165a0067"
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.
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.
Done. Go, not TypeScript — the wire contract makes the language irrelevant,
and Go keeps one toolchain for the whole repo. internal/adapter (adapter.go,
render.go, store.go, telegram.go, telemetry.go) plus cmd/adapter. All
three operations are implemented against Sender, an interface the real Bot
API client (BotAPISender) and the tests both satisfy — the same pattern
internal/apply.Surface uses for MTProto. Idempotency, the reviewed_by and
consent-basis refusals, the per-form length limit, and the Markdown-to-Telegram
rendering rules are covered by internal/adapter/*_test.go. What is not yet
exercised is a real Telegram channel: that needs a live bot token, which waits
on FT-WP-0002 T01.
T06 — Establish the composition step
id: FT-WP-0001-T06
status: done
priority: high
state_hub_task_id: "317bef32-e82e-586a-8947-8cbb330f6c98"
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 — T13 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.
Done. The temporary composer is internal/compose (command cmd/compose):
the quoted-remembrance-v1 strategy and a review queue under a directory the
caller passes. It does not publish without a reviewer and a caller-supplied
consent basis, and it does not import the adapter. Tests:
go test ./internal/compose/... ./cmd/compose/....
T07 — Record the consent basis for hall participants
id: FT-WP-0001-T07
status: wait
needs_human: true
decision_id: 8a095daa-35f9-4561-ae50-256558600be7
blocking_reason: "Public posts need a recorded consent basis. Absence of objection is not consent, and the adapter refuses a post that lacks one."
priority: high
state_hub_task_id: "6a91ea08-8814-58f3-8297-a7ca63405294"
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 T10.
2026-10-08: draft basis for the founder's decision in
docs/consent-basis-draft.md (standing/ask/withdrawn seat field, predating
seats stay per-post, open question on operator consent for agent seats). The
outlet's own test and introduction posts use not-required; they are
prepared in pr-hall-of-helix/posts/.
T08 — Publish one post to the private channel
id: FT-WP-0001-T08
status: wait
depends_on: [FT-WP-0001-T07, FT-WP-0002-T01]
blocking_reason: "The bot and private test channel exist. The first private-channel post still waits on the recorded consent basis."
priority: high
state_hub_task_id: "5e5cd89a-e7b1-5aaf-9331-ece424afe5e3"
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 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_idedits rather than duplicates; - the evidence store holds the post text but no Telegram user identity and no bot token;
fluid audit trace R-1reconstructs the publication, naming the reviewer and the source entry.
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.
2026-10-08: first publish-path check. The outlet's own test post
(hoh-test-2026-10-08-outlet-parametrization, subject_consent: not-required,
reviewed by tegwick) went to the private channel through a short-lived local
adapter (scripts/send-post.sh inside the attended OpenBao login): HTTP 201,
published 10:10:19 +02:00. Fixed on the way: channel ids are now emitted in
Bot API form (-100<id>). This is not T08: T08 still needs a post composed
from one entry, which waits on the T07 consent basis. The rendering in the
channel was checked by the founder (rendering fine) before T02's public channel.
2026-10-08: posts now carry images. The adapter response includes
telegram_message_id and a t.me/c/... link. compose visual resolves a post's
image: the post's visual_url, else the source entry's rendered portrait, else
hold when the entry has only a visual prompt, else the HelixForge main image.
Across the hall: 154 entries have portraits and 103 drafts need a render. No
renderer is wired in yet; that is a founder decision. send-post.sh resolves
the image before touching credentials, refuses public posts without one, and
runs the adapter with ATTACH_VISUAL=true (caption limit 1024).
2026-10-08: image generation requested from llm-connect (hub message
ae2b441f-7c38-4cb5-8572-47791cb52456); follow-up pending.
2026-10-08: image path verified. hoh-test-2026-10-08-image-attachment, signed by
tegwick, was published to the private channel with the HelixForge main image at
10:36:20 +02:00 as message 3 (https://t.me/c/4427430639/3), with the message id
in the adapter response.
T09 — Implement both delivery forms as R-2 and R-3
id: FT-WP-0001-T09
status: done
priority: medium
state_hub_task_id: "649e7df6-6c07-5cc8-a022-d1eadcdb7236"
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.
The two arms impose different length limits on the composer, which is the one place delivery and composition genuinely couple.
Implemented as part of T05, not as a separate build. adapter.Config. AttachVisual selects the arm: false is R-3 (SendMessage, 4096-char limit),
true is R-2 (SendPhoto with the composed text as caption, 1024-char limit).
One adapter instance still serves one channel, per the adapter contract; which
revision it speaks is a deployment-time config choice
(ATTACH_VISUAL), not a code fork. Both arms are covered in
internal/adapter/adapter_test.go. Verifying against the private channel with
real Telegram traffic is unchanged and still waits on FT-WP-0002 T01.
T10 — Open the public channel and start publishing
id: FT-WP-0001-T10
status: wait
depends_on: [FT-WP-0001-T07]
blocking_reason: "No public post goes out about anyone's work without the recorded consent basis."
priority: medium
state_hub_task_id: "b469b9b8-a1a8-5f76-a1bb-dd51dd4d2ce5"
Point the production adapter at the public channel. Blocked on T07: no post goes out about anyone's work without a recorded consent basis.
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.
T11 — Run the first delivery experiment
id: FT-WP-0001-T11
status: wait
depends_on: [FT-WP-0001-T10]
blocking_reason: "The delivery experiment starts only after the public channel is publishing."
priority: low
state_hub_task_id: "eb09fcaf-f1bc-5e38-918e-c66f638d356f"
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 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.
T12 — Close the loop
id: FT-WP-0001-T12
status: wait
depends_on: [FT-WP-0001-T11]
blocking_reason: "The loop closes after the first delivery experiment. The channel, consent basis, and experiment are still ahead of it."
priority: low
state_hub_task_id: "9f8a7724-3e5c-5a36-9e3e-e6837bd0ffdd"
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.
2026-10-05: T12 was a dependency root only because this chain was still todo.
The loop cannot close before the experiment, the public channel, the consent
basis (8a095daa-35f9-4561-ae50-256558600be7), and the operator session
(FT-WP-0002-T01). T04 waits on the durable signing key
(d5d8a24d-d77e-4c63-ab52-b878d687e8b9). T06 is done. T13 waits on T12; the
temporary composer stays in this repository until the loop has closed once.
T13 — Extract the campaign into pr-hall-of-helix
id: FT-WP-0001-T13
status: wait
depends_on: [FT-WP-0001-T12]
blocking_reason: "Extract the campaign only after the loop has closed once (T12). The temporary composer stays here until then."
priority: medium
state_hub_task_id: "d471516c-2f20-525f-8d79-7814160851fb"
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 (T12). Extracting a seam before you have used it is how you find out you cut it in the wrong place.
T14 — Upload post images instead of linking them
id: FT-WP-0001-T14
status: done
priority: high
state_hub_task_id: "c822885b-c2a2-57a7-a448-7d1a853a26c0"
The forge is closing (prj-forgejo-org-refactor ORGREF-WP-0002, decision
decisions/forge-visibility-policy.md point 4). Telegram can then no longer fetch
images from Forgejo raw URLs. The adapter fetches the image itself with a
read-only forge identity, injected like the bot token and never logged, and
uploads it to Telegram as a multipart sendPhoto. Editing an existing photo post
replaces the media the same way.
visual_urlstays the reference the reviewer signs off on; only the delivery changes.- Refuse an image over Telegram's upload limit (10 MB for photos) rather than resizing on the composer's behalf.
- Tests: upload path with a fake forge and a fake Bot API; no credential in logs, errors or the evidence store.
- Verify with one signed test post to the private channel before
hall-of-helixorpr-hall-of-helixcloses.
No public asset location is improvised for this. A dedicated asset store with creator and trust management comes later, on its own.
2026-10-08: implemented. internal/adapter/visual.go fetches the image from an
allowlisted host: https only, no redirects to other hosts, JPEG/PNG/WebP only,
refused over 10 MB, with an optional FORGE_READ_TOKEN sent only to that host.
BotAPISender.UploadPhoto sends it as multipart sendPhoto. Enabled with
UPLOAD_VISUALS=true (on in scripts/send-post.sh). Tests cover upload,
refusals, the token never appearing in responses or errors, and the multipart
fields. Dry run fetched the real image. Open: one signed test post to the
private channel (pr-hall-of-helix/posts/261008T205518-test-image-upload.yaml).
Editing an existing photo post still edits only the caption; replacing the
image would be a separate change.
2026-10-08: verified. hoh-test-2026-10-08-image-upload, signed by tegwick, was
published to the private channel at 21:20:47 +02:00 as message 4
(https://t.me/c/4427430639/4), with the image fetched and uploaded by the
adapter. The founder confirmed the result. T14 done.