Ground the repo docs and move presence provisioning to a declared spec

Align AGENTS.md with the FT-WP- prefix the hub already registered, and
rewrite INTENT.md and SCOPE.md, which were still the generated stubs and
carried none of the composed-post framing the interface is built around.

Fix two artifact inconsistencies: R-1's retry comment still described
POST /v1/hall-entries idempotent on entry_id, from before the reframe to
composed posts; and the seed pressure record listed only one of the two
hypotheses that cite it. Rename it to match its id.

Replace the manual T01-T03 route with FT-WP-0002. 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), driven from a declared
presence spec owned by the campaign. One bounded human bootstrap remains
and docs/provisioning.md says why it cannot be removed.

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

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 1361245@bnt-lap001
Assistant-Session: b3b428ef-f3e6-4688-b091-01f71461d66a
This commit is contained in:
tegwick 2026-09-04 19:21:08 +02:00
parent 3dae4ae74c
commit 5a785b9d9f
11 changed files with 670 additions and 46 deletions

View file

@ -0,0 +1,44 @@
# Worked example of a declared presence.
#
# The real instance belongs in pr-hall-of-helix/presence/telegram.yaml — this
# copy exists so the schema has something to be read against, and so the
# provisioner's tests have a fixture. Keep it in sync with the schema, not with
# the campaign.
presence:
schema_version: "0.1"
campaign: "hall-of-helix"
interface: "helix-forge-telegram-publishing"
bot:
name: "HelixForge"
username_preference:
- "HelixForgeBot"
- "HelixForgePublishBot"
- "HelixForgeHallBot"
about: >-
Notes from the Hall of Helix — the people and sessions building HelixForge.
description: >-
HelixForge turns intent into structure, structure into capability, and
capability into lasting progress. This channel carries short accounts of
the work and the people doing it, each linking back to the entry it was
written from.
avatar: "presence/assets/helixforge-avatar.png"
channels:
test:
title: "HelixForge — test"
description: "Private rendering checks. Nothing here is published."
visibility: private
admin_rights: [post_messages]
public:
title: "Hall of Helix"
description: >-
Short notes on the work going into HelixForge, and the people doing it.
visibility: public
username_preference:
- "hallofhelix"
- "halloftheHelix"
admin_rights: [post_messages]
linked_discussion_group: false

View file

@ -0,0 +1,114 @@
# Declared presence — schema for a campaign's Telegram surface.
#
# The instance lives in the campaign repo (pr-hall-of-helix/presence/telegram.yaml).
# This file is the contract it is validated against, and lives here because the
# provisioner is here. See docs/provisioning.md.
$schema: "https://json-schema.org/draft/2020-12/schema"
$id: "fluid-telegram/presence/telegram.schema.yaml"
title: "Telegram declared presence"
type: object
required: [presence]
additionalProperties: false
properties:
presence:
type: object
required: [schema_version, campaign, interface, bot, channels]
additionalProperties: false
properties:
schema_version: { const: "0.1" }
campaign:
type: string
description: >
Campaign slug. Names the resolved-state file and the OpenBao subtree,
so it must be stable for the life of the presence.
pattern: "^[a-z0-9][a-z0-9-]*$"
interface:
type: string
description: The FLUID interface that will publish through this presence.
const: "helix-forge-telegram-publishing"
bot:
type: object
required: [name, about, description]
additionalProperties: false
properties:
name:
type: string
maxLength: 64
description: Display name. What a reader encountering it cold sees first.
username_preference:
type: array
description: >
Candidate usernames in order. BotFather requires a globally unique
name ending in "bot" or "_bot", and the first choice is often taken.
Declaring fallbacks keeps apply from stopping on a collision; the
one actually issued is recorded in the resolved state.
items: { type: string, pattern: "^[A-Za-z][A-Za-z0-9_]{3,30}([Bb]ot|_bot)$" }
minItems: 1
about:
type: string
maxLength: 120
description: BotFather /setabouttext — shown on the profile before a chat starts.
description:
type: string
maxLength: 512
description: BotFather /setdescription — shown in the empty chat.
avatar:
type: string
description: >
Repo-relative path to a square image in the campaign repo.
Provisioning is content-addressed on its digest, so replacing the
file is what triggers an update.
channels:
type: object
required: [test, public]
additionalProperties: false
description: >
Exactly two. The test channel is created first and is where every
verification runs; nothing reaches the public channel until a person
has looked at a rendering in the test channel (Canon PUB-01).
properties:
test: { $ref: "#/$defs/channel" }
public: { $ref: "#/$defs/channel" }
linked_discussion_group:
type: boolean
default: false
description: >
Whether the public channel has a linked group for comments. Off by
default: comments are an inbound surface, and this interface is
outbound only. Turning it on is a scope change, not a setting.
$defs:
channel:
type: object
required: [title, visibility]
additionalProperties: false
properties:
title:
type: string
maxLength: 128
description:
type: string
maxLength: 255
visibility:
enum: [private, public]
username_preference:
type: array
description: Public channels only. Ordered candidates; a private channel must not declare one.
items: { type: string, pattern: "^[A-Za-z][A-Za-z0-9_]{4,31}$" }
admin_rights:
type: array
description: >
Rights granted to the bot. The provisioner clamps this: post_messages
is the only permitted value, and any other entry fails validation
rather than being silently dropped. Declared explicitly so that a
reader of the spec can see the boundary rather than infer it.
items: { const: post_messages }
default: [post_messages]
maxItems: 1