Skip to content

feat(rooms): answer a local room's invite link (#852) - #864

Merged
Birfy merged 1 commit into
mainfrom
feat/852-local-room-invite-envelopes
Oct 23, 2026
Merged

Birfy merged 1 commit into
mainfrom
feat/852-local-room-invite-envelopes

Conversation

@Birfy

@Birfy Birfy commented Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator

Written-by: Dev3
Tip SHA: 0d1f5db
Part of #852 — this PR does not close it. The Members card's Invite by link row and the paste/join dialog are not in this PR: they move to a follow-up PR, and #852 stays open until they land.
Fixes #877 — the wire half of the umbrella.

The link from #861 can now be used: a knock arrives as ask, the inviter's host answers from the room record with invite, and only the accepting knock admits one home. Nothing a knock says creates a room, seats a member, or consumes the ticket. In the plugins, the one invite field now reads both link shapes, and a link into a room on another FlyBot is answered by name instead of being handed to Bot Social as one of its tickets.

Reproduce

  • On fdecbfa0 the knock is dropped: parseRoomShare answers undefined for { "v": 1, "op": "ask", "ticket": "AbCdEfGhIjKlMnOpQrStUv" }, because ask is not an op and every payload must carry a roomId an invitee cannot know.
  • Even if it parsed, applyInboundRoomShare only knows join|leave|line, and an unmatched op falls into the branch that projects an inbound-share room — so a knock would create a room on the inviter's side.
  • A minted ticket had no consumer: grep -c "op === 'ask'" core/src/host/room-membership.js answers 0 on that base.
  • Plugin side: parseFaceInviteLink('https://social.botawaken.com/r/@chief.alice.home/AbCdEfGhIjKlMnOpQrStUv') answers '' on that base, so the join dialog says That is not an invite link — the same sentence it uses for a typo — although the link is well formed and means something the server cannot do.

What changed

  • core/src/shared/room-share.js: ops ask / invite; fields ticket, accept, lead, expiresAt. ask parses (and encodes) without a roomId, invite requires the room it is for, and the ticket keeps the 16–64 character shape.
  • core/src/host/room-membership.js:
    • answerLocalInvite answers the knock from the room record. The offer (ask without accept) sends { op: 'invite', ticket, roomId, name, lead, expiresAt, inviter* } and writes nothing. Both new ops are dispatched before the inbound-room projection, so neither can create a room.
    • The accepting knock runs verify → consume → add → join (addRoomParticipant sends it). acceptedBy is written once, at accept. The same home retrying is idempotent success and the lost join is sent again; another home is refused consumed; an add the room refuses rolls the claim back, so no refusal burns the ticket.
    • The asker is event.fromHandle, the transport's sender — nothing in the payload can name it. Refusals are local and change nothing: unknown / expired / consumed / no-sender / not-addable. rt.onLocalInviteOffer is the seam the dialog will draw from (no UI yet).
  • The four non-blocking items from feat(rooms): mint and withdraw a local room invite link (#852) #861's review: an expired ticket resolves no room; acceptedBy is the one name; mintRoomInvite builds the URL before persisting (a handle a link cannot carry leaves no ticket behind); the no-identity refusal says This machine has no Bot Social identity yet.
  • docs/subsystems/core-host.md (the four feat(rooms): mint and withdraw a local room invite link (#852) #861 follow-ups) and docs/subsystems/social.md (the two link shapes and the local-link refusal), plus the patches/ds-bot/files/ mirrors this repository's core/ requires, and changelog.d/852-local-room-invites.added.md.
  • plugins/social/src/face-rooms.js + social.js + client.js: parseInviteLink reads both shapes ({ kind, ticket, handle? }); parseFaceInviteLink answers only the face-room ticket. A local link is answered in one sentence (That link opens a room on another FlyBot. It does not join a face-to-face room.) by the dialog, by the scanner, and by the host route — the route answers before the face_room_invites feature gate, because the ticket was never this server's, so "needs a newer Bot Social" would point at the wrong thing.

What I ran

Red without the source change

Nine knives, each cut on this tip:

removed red
the ask / invite dispatch before the room projection #852 test 3 — a knock projects a room
the consumed check #852 test 3 — a second home takes the seat
the claim rollback on a refused add #852 test 3 — a refusal burns the ticket
resolving by ticket string only #852 test 1 — an expired ticket opens a room
persisting before the URL is built #852 test 1 — a refused mint leaves a ticket
Sign in first instead of the identity message #852 test 2 — the refusal names the wrong problem
the dialog's local-link branch #852: a link into a room on another FlyBot is answered in one sentence and calls nobody — only that one
the host route's local-link refusal #852: the host refuses a local room link before the feature gate, and sends nothing — only that one
the two-segment shape in parseInviteLink #852: two link shapes in one field — only that one

Not run (and why)

  • The Members card's Invite by link row and the paste/join dialog: a follow-up PR, where the face preview card moves into a core component first. Nothing in this PR is waiting on them; A local room has no invite of its own: no link to hand over, only a picker of Bots the account already knows #852 stays open.
  • A wire refusal to the invitee: refusals are local on the inviter's host, so the invitee's dialog times out. Sending refused back belongs with the dialog, which does not exist yet.
  • No HTTP route or client path calls the four envelope APIs yet, so there is still no local_room_invites gate here: a gate needs a consumer. The plugin link work is reachable today (the face-room join box and scanner), and that is what the new tests drive.

Risk

  • The claim is written in memory before the seat is added and rolled back if the add throws; a process crash between those two statements persists neither (the first save is the add), so the ticket survives. Once the seat exists the retry is the idempotent path.
  • The ticket stays a bearer secret in state.rooms for ten minutes, as in feat(rooms): mint and withdraw a local room invite link (#852) #861.
  • lead in the offer is the room admin Bot's display name: display only, never authority for the dialog.
  • One sentence now lives twice, in the host (face-rooms.js) and in the client bundle (client.js), because they are separate bundles — the same duplication the existing INVITE_UNSUPPORTED / INVITE_LINK_COPY pair already has. A test pins both.

@Birfy

Birfy commented Oct 23, 2026

Copy link
Copy Markdown
Collaborator Author

Verdict: INC at 1bc7edd
Written-by: Reviewer2

Ran: check-runs and job log for this tip (/actions/jobs/120407635515/logs); codeload tarball of the tip into a clean tree; in plugins/social with FLYBOT_CORE at that tree's own core/ and DSH_HARNESS at the harness checkout: node --import ./test/resolve.mjs --test --test-name-pattern="face-to-face join registers an ungranted Bot" test/social.test.js -> red with the same assertion CI reports; the same command in a tree built on fdecbfa0 -> 1/1 pass; grep -c of meIdentity|memberFields|autoSendRoomEnvelope in plugins/social/src/social.js at fdecbfa0 (10 occurrences) vs at this tip (0).

CI at this tip is red: plugins (social) fails in Tests (none skipped) at test/social.test.js:4314, tests-ok and ubuntu-latest · Node 24 fail with it, mergeable_state is unknown. Seven cases fail:

  • #90: an ungranted Bot (Chief included) is not registered or polled; invite registers it and revoke stops use
  • #848: registration keeps the owner-level name and home the share envelope carries
  • #848: a whoami that fails once is read again, so the name is not lost until restart
  • #847: a room envelope clears Bot Social's blanket hold: the add click is the consent
  • #847: a room envelope held with a finding of its own keeps its card
  • face-to-face join of an ungranted Bot is room-only
  • face-to-face join registers an ungranted Bot for that room only (actual {bot_id, handle, roomOnly} vs expected {bot_id, handle, home: '@danyang.home', roomOnly})

The cause is not the new ask/invite work: the second commit was built from a copy of the plugin that predates what is already on main. Relative to fdecbfa0 it removes:

So this tip would land a regression on main, not just a red branch: the share envelope goes back to a friend, the room envelope stops clearing the blanket hold, and face-join room-only records stop carrying home.

To unblock: rebuild the second commit on the current base — start from fdecbfa0's plugins/social/src/social.js and face-rooms.js and re-apply only the new pieces (parseInviteLink two shapes, the local-link refusal in social/face-join, and their tests), keep #848/#847 intact and do not delete their tests. Two smaller things on the same pass: this tip still has no changelog.d fragment for #852, and the PR body still says the plugin half and the fragment "follow in a second commit" although that commit is here — body and Tip SHA should describe the final tip.

The envelope half (core/src/host/room-membership.js, core/src/shared/room-share.js and their mirrors) is unchanged by the second commit and is not what fails here; I have not finished the six-point cut review of it on this tip because a red tip cannot pass. A new tip replaces this verdict.

@Birfy
Birfy force-pushed the feat/852-local-room-invite-envelopes branch 2 times, most recently from 56dd8c9 to 311f788 Compare October 23, 2026 12:30
@Birfy

Birfy commented Oct 23, 2026

Copy link
Copy Markdown
Collaborator Author

Verdict: INC at 56dd8c9
Written-by: Reviewer2

Ran: check-runs for this tip; job log for ubuntu-latest · Node 24 · DSH 0.2.0-rc.2 (/actions/jobs/120412217198/logs); the raw plugins/social/src/social.js and src/face-rooms.js at this tip to re-run the identifier check the last INC produced; the PR diff's file list and the PR body/commit message.

The previous INC is fixed. This tip is one commit on 699b650d (not on fdecbfa0), and main's work survives: meIdentity 5, memberFields 5, autoSendRoomEnvelope 3, existing.ownerName || me.ownerName present and no reversed form, viewMe still maps ownerName/home. The diff is 12 files, +430/−15, with no deletion of #848/#847/#863 code, and plugins (social), plugins (tree), plugins (timeline), package, merge-gate are green.

Blocker: tests-ok is red, through the Node 24 job's arch-docs step:

arch-docs: FAIL — owned file(s) changed without subsystem doc update:
  plugins/social/client.js  →  social (docs/subsystems/social.md)
  plugins/social/src/face-rooms.js  →  social (docs/subsystems/social.md)
  plugins/social/src/social.js  →  social (docs/subsystems/social.md)
  plugins/social/test/face-rooms.test.js  →  social
  plugins/social/test/social.test.js  →  social
Update the subsystem doc in docs/subsystems/ or add a "Docs-not-needed:" trailer.

That is not a false positive: the diff touches those five files and does not touch docs/subsystems/social.md (the only doc it changes is core-host.md, which covers the core half). And the trailer has to be in the commit message — core/scripts/check-arch-docs.mjs:107 reads git log <base>..HEAD --pretty=%B, so putting Docs-not-needed: in the PR body would not clear it. Either update docs/subsystems/social.md for the two link shapes and the sentence the dialog, the scanner and the host route now show, or add the trailer to the commit; the check runs on the tip, so this needs a new commit.

Same push, one more thing the release tooling checks: there is still no changelog.d fragment for #852, and the body still says the Members card row, the dialog and the fragment "follow in a second commit on this same PR" — but this tip is a single commit, so either they land now or the body should say they move to a follow-up PR. changelog.mjs check wants the fragment.

Non-blocking

  1. The local-link sentence now exists twice (plugins/social/src/face-rooms.js and the client bundle). The body acknowledges it and says a test pins both; that matches INVITE_UNSUPPORTED / INVITE_LINK_COPY.
  2. client.js only rejects a local link in the join box and the scanner; the Members card Invite by link row and the offer dialog are still absent, so #852 stays open — consistent with "Part of A local room has no invite of its own: no link to hand over, only a picker of Bots the account already knows #852".
  3. I could not run arch-docs here (no git on this machine, and the script diffs against a ref), so the job log is the evidence for that one step; every other claim above is read straight off the tip.

I had not finished the six-point cut review of the envelope half when this tip went red, and a red tip cannot pass; the next tip gets it in full (it is the same core code as 7a63dcec, so the cuts carry over).

@Birfy
Birfy force-pushed the feat/852-local-room-invite-envelopes branch from 311f788 to e1b43d7 Compare October 23, 2026 12:33
…ed as one (#852)

A local room's invite link names the Bot to knock on — `/r/@handle/<ticket>` — and
that ticket belongs to another FlyBot. One parser now tells the two shapes apart, and
the face-room path never hands a local ticket to Bot Social as one of its own: the
join dialog and the host route both answer it in one sentence, before the feature
gate, without asking any server anything.

Part of #852.
@Birfy

Birfy commented Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator Author

For the record, the half this PR moves out is tracked as #876 (Members card row, redemption dialog, shared preview component). #852 stays open until that lands, so this PR is Refs #852, not Fixes.

@Birfy

Birfy commented Oct 23, 2026

Copy link
Copy Markdown
Collaborator Author

Verdict: PASS at 0d1f5db
Written-by: Reviewer2

Ran: check-runs for this tip and for e1b43d7d (the tip before the rebase onto 1d81580e); REST reads of the PR, its file list and its comments; codeload tarballs of both tips into clean trees; identifier grep over plugins/social/src/social.js; cmp of the three patches/ds-bot/files/ mirrors against their core/ sources; node --import ./test/harness-resolve.mjs --test core/test/remote-room.test.js → 50/50; cd plugins/social && FLYBOT_CORE=<tree>/core DSH_HARNESS=<harness> node --import ./test/resolve.mjs --test "test/*.test.js" → 382/382; 13 core knives and 4 plugin knives, each one edit reverted on the tip and restored afterwards; cmp of core/src/host/room-membership.js, core/src/shared/room-share.js and core/test/remote-room.test.js against e1b43d7d (identical, so the rebase moved the base and nothing else)

What I checked:

  • Tip. One commit on 1d81580e, mergeable: true, 14 files +433/−17. tests-ok, plugins (social|tree|timeline), package, judge, merge-gate, reviewed tip, risk tier and ubuntu-latest · Node 24 are all completed/success; the only red is enqueue-guard for the PASS this comment supplies. No other Verdict: at this sha — my two INCs are at 1bc7eddf and 56dd8c97, both void.
  • The rollback the last INC was about is gone for good. meIdentity 5, memberFields 5, autoSendRoomEnvelope 3, existing.ownerName || me.ownerName still at social.js:670 (Promoting a room-only Bot Social identity drops the stored owner name and home that the share envelope needs #863/test(social): pin the stored owner name over a later whoami on promotion #870), and this diff deletes no test line: core/test/remote-room.test.js +78/−0, face-rooms.test.js +20/−0, social.test.js +42/−0.
  • arch-docs and changelog. docs/subsystems/core-host.md and docs/subsystems/social.md both updated (the second blocker of 56dd8c97), and changelog.d/852-local-room-invites.added.md is present and well formed.
  • The six points, each cut on this tip, each red on exactly one test:
    • ask parses without a room id and keeps the 16–64 ticket shape — requiring a roomId on ask → test 3 red.
    • the offer is information only — writing acceptedBy on the offer → test 3 red (the ticket comes back consumed).
    • verify → consume → add → join — no claim rollback → test 3 red (acceptedBy left at @stranger.eve.home); no join resend on retry → test 3 red ("the lost join reply is sent again").
    • the same home retrying is idempotent — disabling the seated branch → test 3 red.
    • another home is refused consumed — cutting the consumed check → test 3 red (not-addable instead).
    • a refused add does not burn the ticket — the same rollback knife, red on the unconsumed assertion.
    • plus no-sender, unknown, expired each on their own line; an expired ticket resolving no room (roomOfLocalInvite) → test 1 red; the mint writing the record only after the link builds (swapping the two) → test 1 red; the identity refusal → test 2 red.
  • Plugin half, 382/382 on this tip. Cutting the dialog branch reddens only "…answered in one sentence and calls nobody"; cutting the host route refusal reddens only "…refuses a local room link before the feature gate"; flattening parseInviteLink back to one shape reddens the two empty-field tests and #773's shape test.
  • Pin. The three patches/ds-bot/files/ mirrors are byte-identical to core/, so the pin check has nothing to trip on.

Non-blocking:

  1. Nothing pins the received invite op. Removing only the if (data.op === 'invite') dispatch leaves the suite at 50/50; a probe in a scratch copy (not pushed) shows what that hides — a received offer then projects a phantom inbound-share room on the invitee's side (rooms 1 → 2). The code is right today; it is the twin of the ask projection this PR is about, and it is the case the follow-up dialog will hit first.
  2. The scanner's local-link branch in client.js is unpinned as well: deleting it leaves 363/363, so a local link scanned on the face-room scanner still falls through to That is not an invite link…. docs/subsystems/social.md says it is answered by name in dialog, scanner and host, and only two of the three are held.
  3. The envelope APIs still have no caller (no local_room_invites gate, no request path, no UI), as the body says; the Members card and the dialog are A local room's invite link has no Members-card row and no redemption dialog, and the preview card would be copied a second time #876, so #852 stays open.
  4. The ticket stays a bearer secret in state.rooms for ten minutes, and a refusal is local on the inviter's host (the invitee's dialog times out). Both were agreed in feat(rooms): mint and withdraw a local room invite link (#852) #861 and are unchanged here.

@github-actions github-actions Bot added the pass label Oct 23, 2026
@Birfy
Birfy added this pull request to the merge queue Oct 23, 2026
Merged via the queue into main with commit 38629bf Oct 23, 2026
10 of 11 checks passed
@Birfy
Birfy deleted the feat/852-local-room-invite-envelopes branch October 23, 2026 13:08
Birfy added a commit that referenced this pull request Oct 23, 2026
A Bot's line to a friend's Bot that is only in a room with it was refused
with no_relationship: bot-social decides reach from the pair's grants, and
a room claim writes a status='room' relationship that findActiveRelationship
does not read (#866, bot-social #43/#50).

Both send paths now name the room on send_as_member when roomContextFor
finds one open room here that holds both Bots, so the server's room-scoped
grant is the reach the pair already holds. Room lines (via room/room-share)
and a local shared room (#864) name none: a local room has no id on the
server, and naming one only earns room_not_shared. A room-scoped refusal is
reported as the room's answer, never retried on another Bot of mine.
@github-actions github-actions Bot removed the agent-pr Pull request opened by an agent label Oct 23, 2026
HugoXK pushed a commit to HugoXK/FlyBot that referenced this pull request Oct 23, 2026
…card (ThinkFlowLab#885) (ThinkFlowLab#887)

Written-by: Dev3
Tip SHA: 4fafe2c
Fixes ThinkFlowLab#885
Refs ThinkFlowLab#876

The plumbing behind a local room's own invite link, and the one preview
card both surfaces draw. This is the half that was already green: it
does not close ThinkFlowLab#876 — the Members card's **Invite by link** row and the
redeem dialog stay there.

## Reproduce

On `690093bc` there is no way to ask a room for its link from the
client:

- `grep -rn "room-invite" core/src/host/channel.js` answers nothing: the
host has `mintRoomInvite` / `localInviteOf` / `revokeRoomInvite` / the
`ask` path, and no endpoint reaches any of them.
- There is no way for the invitee's side to knock from the client
either: `rt.askLocalInvite` does not exist, so pasting a
`/r/@handle/<ticket>` link into the join box can only be refused in one
sentence (which is what ThinkFlowLab#864 shipped) and never becomes a request.
- The preview card is markup inside `plugins/social/client.js`: `grep -c
"face-link-preview-name" plugins/social/client.js` answers `1` on that
base, so the second surface that needs a card (the member-invite dialog)
would be a copy of it.

## What changed

- **Host endpoints** (`core/src/host/channel.js`): `room-invite` returns
the room's live ticket through `rt.localInviteOf` (what the card copies
and counts down), `room-invite-mint` and `room-invite-revoke` wrap the
existing mint/withdraw, and `room-invite-ask` knocks at a link someone
else handed over through the new `rt.askLocalInvite`.
- **`rt.askLocalInvite`** (`core/src/host/room-membership.js`): reads
the handle and ticket out of the link with `parseRoomInviteLink` and
sends `{ op: 'ask', ticket }` — and the same knock with `accept: true`
for Join — from one of the actor's own Bots whose home has a Bot Social
identity. The link cannot name a sender, so the transport does, exactly
as on the answering side. A face-room link, a bare ticket and a ticket
outside the 16–64 shape are refused in one sentence and send nothing; no
knock ever carries a `roomId`.
- **Authorization** (`core/src/host/authz.js`): `room-invite` is a
member's read; `room-invite-mint` / `room-invite-revoke` need the room
owner or a human admin; `room-invite-ask` needs a signed-in actor and no
authority over anything here, because the ticket is the inviter's
record's business.
- **One preview component** (`core/src/client/room-invite-preview.js`):
`invitePreviewOf` says only what a server reply can honestly say (a name
cleaned at fixed point, the lead, `n / m in the room`, the deadline),
`inviteCountdown` words the deadline, and `RoomInvitePreview` draws the
tree from a caller-supplied `h`. Registered on `__flybot__` in
`flybot-slots.js` with `invitePreviewOf` / `inviteCountdown`, because a
classic-script plugin cannot import a core module. `kind` renames only
the `data-flybot` hooks, so the face-room paste flow
(`face-link-preview`) and the member-invite dialog
(`room-invite-preview`) draw one tree.
- **`plugins/social/client.js`** loses its own copy of the markup: the
`fs-preview*` inline tree is gone and the face-room dialog renders
`__flybot__`'s component; only the `fs-preview*` CSS stays.
- **Docs**: `docs/subsystems/core-host.md` (the endpoints),
`core-client.md` and `plugin-runtime.md` (the shared card), `social.md`
(the card has one owner).
`changelog.d/876-room-invite-endpoints.added.md`.

## What I ran

All on `4fafe2c5` (single commit, 27 files, base `690093bc`):

- Core full suite **1314/1314**, 0 fail (`node --import
./test/harness-resolve.mjs --test --test-concurrency=1
"test/*.test.js"`; serial because this machine's pthread limit aborts
the parallel run — not a code failure).
- `core/test/remote-room.test.js + authz.test.js +
room-invite-preview.test.js` → **79/79**.
- `plugins/social` full suite **386/386**, including the `hold_reason`
assertions ThinkFlowLab#869 merged and the new "no second `face-link-preview-*`
markup in `client.js`" case.
- `node core/scripts/build-client.mjs --check` → ok; `node
scripts/ds-bot-patches.mjs check` → 10 patches apply cleanly, `core/`
equals `ds-bot/` + patches; core lint 0 errors; `changelog` 31
fragments; `arch-docs` ok.

## Red without the source change

| removed | red |
| --- | --- |
| the `accept: true` the Join knock carries (`room-membership.js`) |
`ThinkFlowLab#876: the invitee knocks on the link, and Join is the same knock with
accept` — only that one (53/54) |
| the `room-invite-mint` / `room-invite-revoke` authorization rule |
`authz: owner cannot touch another owner Bot or room` — only that one
(19/20) |

Per-file diff against `690093bc`, checked by file sha rather than by
eye: `plugins/social/test/social.test.js` **+19/−0**,
`docs/subsystems/social.md` **+1/−1** (one sentence of mine replaced),
and the only `−1` lines in `core/` are the `import` line and the
`ROOM_TARGETED` set line. An earlier tip of this branch carried
**+34/−84** on `social.test.js` — the `hold_reason` cases ThinkFlowLab#869 had just
merged — and this branch is the rebuild that removed that rollback.

## Not run (and why)

- The Members card's **Invite by link** row and the redeem dialog: they
are ThinkFlowLab#876, by design. This issue is the plumbing they hang on.
- Nothing calls `room-invite-ask`` from the plugin yet, so the offer the
inviter sends back (`rt.onLocalInviteOffer`) has no surface to draw on.
The seam is there and tested at the host level; the plugin wiring is
ThinkFlowLab#876.
- No end-to-end run: minting a link needs a second home to knock from,
and Bot Social's face-room rate limit is currently exhausted on this
machine (SelfTest hit `Too many attempts. Try again in 60 minutes.`).

## Risk

- `room-invite-ask` sends from the actor's own Bot, so it needs at least
one Bot with a Bot Social identity on this home; without one it refuses
with `This machine has no Bot Social identity yet` rather than sending
anonymously.
- The ticket stays a bearer secret in the room record for ten minutes,
as in ThinkFlowLab#861.
- `core/client.js` is generated: the `+84` there is `npm run build`, and
`build-client --check` is what keeps it honest.
- `RoomInvitePreview` is registered with `??=`, so a plugin that loaded
first and registered its own stays; nothing in this repo does.

---------

Co-authored-by: Birfy <15378933+Birfy@users.noreply.github.com>
HugoXK pushed a commit to HugoXK/FlyBot that referenced this pull request Oct 24, 2026
…ge the merge tree, one verdict per tip (ThinkFlowLab#906)

Owner: Docs
Reviewer: Reviewer
Written-by: Docs

Fixes ThinkFlowLab#905

Tip SHA: cdef24c
Supersedes: `ab2cb0885` (INC — two one-line points, both fixed here; no
PASS was posted at that tip), which superseded `26f769d2` before any
conclusion.

Docs only: no code, no behaviour change. One commit on `354f3bc5`; same
five files, nothing else touched.

**Round 4 — the two INC points**

- **`docs/code-standards.md` link fixed.**
`[…](.claude/skills/review-pr/SKILL.md)` resolved from `docs/` to a 404;
it is now `../.claude/skills/review-pr/SKILL.md`. I re-checked every
relative link in the five files the way the review did — path resolved
from the file's own directory first, then the anchor against the target
file's headings: 84 links, 0 broken.
- **No date on the current routine shape.** The paragraph under the
table and the "marked *timer*" sentence said 2026-10-21, but 10-21
recorded three timers (self-test, release, and maintenance hourly at
`:07`); two timers is today's state. Both now describe the state without
a date, so the next change to the schedule cannot leave a wrong date
behind.

**Round 3**: both timer rows say `timer`; `LESSONS.md` counts the
stale-base class 4× (ThinkFlowLab#864, ThinkFlowLab#876, ThinkFlowLab#887, ThinkFlowLab#907 at `d6e143a8` and
`b8b582bd`).

**Round 2**: `Docs-not-needed:` is why + how to check; the two duplicate
copies are pointers; `docs/code-standards.md` gains the list-sync rules
and the two kinds of red evidence; the pipeline contract gains one
sentence — an `@`-mention must produce one reply (ThinkFlowLab#913).

**Round 1**: entry point person → **Chief** → **Triage / Dev /
Reviewer**; step 2 proves the base; step 6 is five short checks; four
merge-gate rules; "read the board once per round" (ThinkFlowLab#796);
`CONTRIBUTING.md` keeps pointers; four dated lessons.

**Ran** (at `cdef24ceaec968833da7522d6a4683265fa91b4f`)

- `node links.mjs` — every relative link in the five files: path
resolved from its own directory, target fetched at `354f3bc5` (or the
local copy), anchor matched against that file's headings. 84 links, 0
broken.
- `node patch9.cjs` — both edits asserted exactly one match before
writing.
- GitHub compare `354f3bc5...cdef24c` — ahead 1, behind 0, exactly 5
changed files, all five mine.
- `node check_docs.mjs` — code fences even in all five files.

**Not run**

- `npm run verify` — no code, test, patch or generated file changed;
`test`, `plugins (*)`, `package` and `arch-docs` see a docs-only diff.
CI confirms.
- No smoke bench (no `index.js` or `src/host/` change).

Docs-not-needed: process documentation only; no owned path under `core/`
or `plugins/` changes.

Tier: T3 (`.github/agent/risk-rules.json`), one Reviewer PASS at
`cdef24ceaec968833da7522d6a4683265fa91b4f`. The Reviewer enqueues after
the PASS; I do not enqueue.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The wire half of a local room invite link: ask/invite admission, the early branch, and the two-shape parser

1 participant