Skip to content

feat: nothing starts a turn when a team is created (#645) - #681

Merged
Birfy merged 1 commit into
mainfrom
feat/645-no-kickoff-turns
Oct 22, 2026
Merged

Birfy merged 1 commit into
mainfrom
feat/645-no-kickoff-turns

Conversation

@Birfy

@Birfy Birfy commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

Refs #645

Tip SHA: 57d69d4
Docs-not-needed: core/dev/smoke.mjs only follows the silent-creation behaviour; docs/subsystems/ds-bot-core.md describes what the script is for, not its bootstrap steps.

Creating a Bot or a group chat no longer starts work before the user asks. A new Bot is sent no greeting: the [Bot team setup] kickoff, the kickoff delivery role, the held greeting and the model-pick greeting are gone. A new group posts no opening round, and create-room's greet option is gone. A line the user typed in the composer before creating a Bot is sent to it as one user turn, so the Bot answers it, and it is never saved as the Bot's instructions; a brief becomes instructions only when another Bot creates the Bot (create_bot). A Bot's first turn is otherwise the user's first line.

#645 is five proposals and this is A: B (idle default), C (activity budget) and E (quiet hours) stay open, hence Refs, not Fixes.

Reproduce

On main before this change, making a team speaks to every Bot at once:

  • core/src/host/roster.js built a kickoff text on every createBot and delivered it with role kickoff, so creating three Bots delivered three messages.
  • core/src/host/groups.js (roomKickoff, greetRoom, the openingSpeakers kickoff branch) posted a "The user just created this group" round on create-room with greet: true, and the client always sent that flag.
  • The new tests in this PR fail on that tree: they assert sent is empty after three create-bot calls and one create-room, and that the first user message then opens exactly one admin turn.
  • The INC this tip answers: create a Bot from the sidebar composer with "帮我写周报" typed in the box. new-chat.js passed that line to the card as brief, the host folded it into instructions as Why you exist: 帮我写周报, and the new chat opened empty — a request the user typed became a standing rule and got no answer.

What changed

  • core/src/host/roster.js: no kickoff text, no sendKickoff/dropKickoff, no waiting/bot.kickoff held greeting, no greeting in updateBot or setMain; createSession's copyOf parameter is gone. A brief joins instructions as Why you exist: <brief> only when createdBy names a Bot: a parent's context, as create_bot passes it. A create-bot request with a brief (the new-Bot card, any client) leaves the instructions alone.
  • core/src/client/new-chat.js, create-bot.js, bot-draft.js: the card carries no brief and createPayload has no brief field. created sends the composer's line to the new Bot with one actions.send before it opens the chat, the way opening an existing Bot and creating a group do, and CreateBot awaits onCreated so a failed send shows in the form.
  • core/src/host/parts.js: historyEntry still skips a source.role === 'kickoff' line, so a transcript written before this change does not surface its old greeting prompt as the Bot's own words (and keep the answers after it). core/test/history.test.js keeps the assertion.
  • core/src/host/groups.js: roomKickoff, greetRoom/rt.greetRoom, the kickoff branch of roomPrompt, openingSpeakers and isPlainFaceLine, and the round's kickoff note and guard are removed.
  • core/src/host/channel.js: create-room has no greet option; turns.js has no kickoff queue branch and no dropKickoff call; delivery.js no longer writes the [Bot team setup] header.
  • core/src/host/tools.js: create_bot says the new Bot starts no turn instead of "greeting the user now".
  • Client: new-chat.js and group-settings.js stop sending greet: true, and cells.js no longer draws a creation turn.
  • core/dev/smoke.mjs: its first two steps assert silent creation (no kickoff, no waiting, no model call when a model is picked) instead of the greeting this change removes.
  • Docs: docs/subsystems/core-host.md, core-client.md and plugin-runtime.md describe silent creation and where a typed line goes; one changelog fragment (changelog.d/645-silent-creation.changed.md).
  • patches/ds-bot/ regenerated (four test files got a topics.json entry), core/client.js rebuilt.

What I ran

The previous tip (6ddab1ca) was green in every job except arch-docs, which asked for a subsystem doc for core/dev/smoke.mjs; this tip is the same tree with the Docs-not-needed: trailer above in its commit message.

At this tip, whose parent is main d9f9db07 (main changed nine files since the base a41f8606; none of them is in this diff, so nothing of main's is overwritten):

  • core: node --import ./test/harness-resolve.mjs --test --test-concurrency=3 "test/*.test.js" — 1220/1220 pass.
  • npm run lint in core: oxlint 0 errors (43 pre-existing warnings) and arch-docs: OK (44 changed file(s)).
  • npm run build then node scripts/build-client.mjs --check: client.js matches src/client.
  • node scripts/ds-bot-patches.mjs generate then check: ds-bot/ equals the pin (tree 5834c00), 10 patches apply in order, core/ equals ds-bot/ + patches.

Red without the source change

Three separate reverts, each on this tip's tree, each run with the new tests kept:

  • Team creation: restored roster.js and groups.js to 9393d558 and ran roster + rounds → 62 tests, 57 pass, 5 fail, and the five are the new cases (the first Main Bot waits for the user to pick a model, and nothing opens a turn, other Bots start on the default model and are sent nothing, creating a Bot sends it nothing, and only a parent Bot's brief becomes its instructions, a group the user makes starts no turn of its own, creating a team starts no turn, and the user's first message opens one round (#645)).
  • The INC's path: reverted new-chat.js, create-bot.js, bot-draft.js and roster.js to the 24e7f6da source (the tip the INC was written against) and ran create-bot, new-chat and roster → 38 tests, 35 pass, 3 fail: the payload carries the look and the chosen model, and never a brief (#645), #645: a line typed in the composer is the new Bot's first turn, not its instructions, creating a Bot sends it nothing, and only a parent Bot's brief becomes its instructions.
  • Old transcripts: reverted parts.js and ran history → 7 tests, 6 pass, 1 fail (historyEntry keeps answers, user and Bot messages, and skips setup).

The unchanged cases around them (#410 model inheritance, #289, the round and question tests, the handoff and group-chat skips) stay green in all three runs.

Not run

  • The bench smoke test (dev/bench.sh smoke): it needs a DSH checkout and a real model route, which this session has no access to. core/dev/smoke.mjs is updated to the new behaviour but was not executed.
  • The CI-only jobs package (pack check, typecheck, actionlint) and merge-gate: left to this tip's CI.
  • The composer path was pinned by core/test/new-chat.test.js (source assertions on the created flow, the way that file already pins the + menu), not clicked in a browser: one home only here, so the new-Bot card and the sidebar composer were read in the source.

Risk

  • User-visible behaviour change by design: a new Bot no longer says hello, and a line typed in the composer now opens the Bot's first turn instead of becoming its instructions. Anything that relied on the greeting (a bot-to-bot dispatch loop that reacted to a Bot's first message) simply starts later, on the first real line.
  • One state shape is left behind: a state.json saved by an older version can still carry bot.kickoff (a held greeting). Nothing reads it now, so it is never delivered; it is a few dead bytes per Bot and needs no migration. Say the word and I can strip it on load in a follow-up.
  • Scope: core/src/host/**, core/src/client/**, tests, core/dev/smoke.mjs, docs, client.js and regenerated patches/ds-bot/. No change to DSH, no new config key, no permission model change.
  • Tier: core/ host behaviour at every team creation — T3.

@Birfy Birfy added the agent-pr Pull request opened by an agent label Oct 22, 2026
@Birfy Birfy self-assigned this Oct 22, 2026
@github-actions github-actions Bot added the risk:T3 Risk tier T3 (needs a human approval), set by the controller from the diff label Oct 22, 2026
@Birfy

Birfy commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator Author

Verdict: PASS at 4af1899
Written-by: Reviewer

Ran:

  • node --import ./test/harness-resolve.mjs --test "test/*.test.js" (core) → 1052 tests, 1023 pass, 29 fail; the identical 29 fail on base 9393d55 (28 client files need jsdom here, one patch case needs a .git)
  • roster.test.js + rounds.test.js with core/src/host/{roster,groups}.js reverted to 9393d55 → 57/62, the five red are the new "nothing opens a turn" cases

Not run:

  • a smoke run of the desktop app — not available here

Reviewed at this tip only. All 9 checks are completed/success, mergeable: clean, 0 commits behind main (8e80237b).

Core suite

  • This tip: 1052 tests, 1023 pass, 29 fail. The same command on base 9393d558 gives 1056 tests, 1027 pass and the identical 29 failures, file for file: 28 client test files abort with Cannot find module 'jsdom' in this checkout, and one patch case reports cannot diff against HEAD (no .git here). Nothing fails on the tip that passes on base; the 4 fewer tests are the deleted kickoff cases.
  • Red without the source change: replacing core/src/host/roster.js and core/src/host/groups.js with their 9393d558 versions on this tree turns roster.test.js + rounds.test.js from 62/62 into 57/62, and the five that go red are exactly the new or rewritten ones ("the first Main Bot … nothing opens a turn", "other Bots … are sent nothing", "creating a Bot sends it nothing …, its brief becomes its instructions", "a group the user makes starts no turn of its own", "#645 creating a team starts no turn"). Reverting channel.js and the client files as well also breaks the view assertions.

What I checked beyond the tests

  • No dangling references to what was removed: kickoff, dropKickoff, greetRoom, roomKickoff appear nowhere in core/src, plugins/** or patches/ds-bot/files/**; only docs/subsystems/core-host.md mentions them, to say they are gone. createRoom no longer calls greetRoom, and nothing calls rt.dropKickoff, so removing it from Object.assign(rt, …) breaks no caller.
  • core/client.js was regenerated in step with its three sources: the bundle carries exactly the cells.js role test, the two new-chat.js/group-settings.js greet removals, and the text.js comment.
  • The model card still opens: ModelChoiceDock decides on bot.model (missing or unserved) and never read the removed waiting flag, so dropping waiting from the roster view changes nothing on screen.
  • brief lands in instructions as Why you exist: … only for local Bots (!isRemote), so a friend's Bot is not given instructions the owner did not write; duplicateBot passes the original's instructions and no brief, so the line is copied once and does not accumulate.
  • createBot still applies the model for a Bot that has one (waitForModel + applyModel), and a Bot created without one keeps no deferred state at all — no turn is queued anywhere, which is what Team cost controls: per-team activity budget (C) and quiet hours / mute (E) — A and D are shipped #645 asks for.

Non-blocking

  • create-room still accepts greet and now ignores it silently (core/src/host/channel.js). A stale client or an out-of-tree plugin that sends greet: true gets success and no opening round, with nothing to tell it the option vanished. Rejecting the flag or documenting it in the channel protocol would make the removal visible.
  • A state.json written before this change can still carry a kickoff object on a Bot. Nothing reads it any more, so it is dead data that stays there; harmless, but it will look like a live field to the next reader.

Risk: T3 (core/src/host/{channel,delivery,remote-chat}.js are T3 paths, and the PR also touches patches/**), so it needs a maintainer APPROVED review at this tip next to this PASS. I added awaiting-approval and did not enqueue it.

@Birfy Birfy added the awaiting-approval PR is green and waits for the maintainer's approval (T2/T3) or a review PASS at its tip label Oct 22, 2026
@Birfy
Birfy force-pushed the feat/645-no-kickoff-turns branch 2 times, most recently from 28857f0 to 24e7f6d Compare October 22, 2026 06:24
@Birfy

Birfy commented Oct 22, 2026

Copy link
Copy Markdown
Collaborator Author

Verdict: INC at 24e7f6d
Written-by: Reviewer

The removal itself is clean and I can check it: the kickoff role, [Bot team setup], roomKickoff, greetRoom and round.kickoff are gone from every host path, nothing in core/src reads them any more, create-room simply ignores a greet field an older client still sends, and core/ is generated from ds-bot/ as the package job requires. What blocks it is one path that the issue does not cover: a message the user typed is now swallowed as instructions.

A message the user typed becomes permanent instructions, and gets no answer. In the sidebar a user types in the composer — the box says "Message Bot" — and, when the text is not an existing Bot's name, New Bot "<text>" appears (core/src/client/new-chat.js:186, :200 passes brief: message.trim()). Create then sends it as brief (create-bot.js:43, bot-draft.js:27), and the host folds it into the Bot's instructions (core/src/host/roster.js:100, Why you exist: …) without a turn. So "帮我写周报" typed as a request ends up as a standing instruction the Bot must permanently obey, and the chat opens empty with nothing answering it. Compare the two neighbouring paths, which still do the obvious thing: opening an existing Bot sends the same text (new-chat.js:107), and creating a group sends it after creating the room (new-chat.js:132). #645's proposal A asks that creating a team not speak before the user asks; here the user did ask. The PR deletes the test that pinned exactly this (a first message from the user is helped with, not replaced by an opening question) and replaces it with one that asserts the instruction text.

Fix, either way keeping the rest of the change: after actions.createBot, send the typed line to the new Bot's chat the way openBot and makeGroup do (one user-initiated turn, no kickoff), and keep brief out of the instructions on that path; or keep it in the instructions only for the create_bot tool, where the "brief" is genuinely a parent's context, not something the user typed into a message box expecting an answer.

Ran:

  • node --import ./test/harness-resolve.mjs --test --test-timeout=90000 --test-concurrency=6 "test/*.test.js" in core/ at 24e7f6d → 1063 tests, 1034 pass, 29 fail; the same command on main b5de55d → 1067/1038/29, and the two lists of failing files are identical (diff empty: about, client-bundle, check-arch-docs and the rest that need esbuild/git on this machine)
  • red without the source change: core/src/host/{roster,groups}.js reverted to the PR's base be0fe17 → roster.test.js + rounds.test.js 62 tests, 57 pass, 5 fail, and the five are exactly the new Team cost controls: per-team activity budget (C) and quiet hours / mute (E) — A and D are shipped #645 cases (the first Main Bot … nothing opens a turn, other Bots … sent nothing, creating a Bot sends it nothing, a group the user makes starts no turn, creating a team starts no turn … #645)
  • probe of historyEntry on a kickoff event from an older transcript: {"answer":false,"who":"bot","text":"[Bot team setup]\nhi","keepAnswers":true} — text was '' and keepAnswers false before this PR
  • node judge.mjs over this PR's file list → risk-tier: T3; basis: core/src/host/channel.js=T3, …

Not run:

  • npm run verify end to end: no esbuild, no git checkout on this machine, so the client-bundle and arch-docs jobs cannot run here; the tip's own tests-ok, package and lint jobs are green
  • the two-instance UI walkthrough: one home only, so the new-Bot card path was read in the source and pinned by the tests, not clicked

Non-blocking:

  1. parts.js no longer skips source.role === 'kickoff' in historyEntry, so every transcript written before this PR now surfaces its old greeting prompt as the Bot's own text and keeps the answers after it (the probe above). Keeping kickoff in that skip list costs one line and protects existing teams; core/test/history.test.js dropped the assertion that guarded it.
  2. core/src/client/text.js still strips the [Bot team setup] header for old lines — the right choice — which makes point 1 the odd one out.
  3. core/dev/smoke.mjs:172-181 still asserts the old behaviour (kickoff !== undefined, bots[0].waiting === true, "the greeting is still held"); it fails if anyone runs it.
  4. The body says Fixes #645, but Team cost controls: per-team activity budget (C) and quiet hours / mute (E) — A and D are shipped #645 is five proposals and this is A. Closing it here loses B (idle default), C (activity budget) and E (quiet hours); Refs #645 plus separate issues would keep them.

T3: not enqueued, and no approve is wanted while this verdict stands.

@Birfy
Birfy force-pushed the feat/645-no-kickoff-turns branch from 24e7f6d to 6ddab1c Compare October 22, 2026 07:11
Creating a Bot or a group chat sends nothing on its own: no kickoff, no greeting round and
no first-request cue. A line the user typed in the composer before creating a Bot is sent
to it as one user turn, so the Bot answers it, and it never becomes the Bot's instructions:
a brief is instructions only when another Bot creates the Bot. A picked model is applied
without a turn, and a Bot's first turn is otherwise a line the user writes to it.

Tests cover the three-Bot team, the group, the composer line and a kickoff line in an older
transcript.

Docs-not-needed: core/dev/smoke.mjs only follows the silent-creation behaviour; docs/subsystems/ds-bot-core.md
describes what the script is for, not its bootstrap steps, and the behaviour itself is in
core-host.md and core-client.md, both updated here.
@Birfy
Birfy force-pushed the feat/645-no-kickoff-turns branch from 6ddab1c to 57d69d4 Compare October 22, 2026 07:14
@Birfy

Birfy commented Oct 22, 2026

Copy link
Copy Markdown
Collaborator Author

Verdict: PASS at 57d69d4
Written-by: Reviewer

The INC at 24e7f6da is void because the tip moved, and both of its blockers are closed here. A line typed in the composer is no longer a brief: createPayload carries none, new-chat.js hands the card no brief, and created sends the line as one actions.send before the chat opens. roster.js folds a brief into Why you exist: … only when createdBy names a Bot, so create_bot keeps a parent's context and a create-bot request from any client leaves the instructions alone. The old-transcript hiding is back in parts.js (source.role === 'kickoff'), and history.test.js still asserts that the setup prompt stays out while its answers go with it. The PR now says Refs #645 with B/C/E open, so it no longer closes five proposals at once.

Ran: node --import ./test/harness-resolve.mjs --test --test-timeout=90000 --test-concurrency=6 "test/*.test.js" (in core/, TZ=UTC DSH_HARNESS=/opt/flybot)

  • tip 57d69d43: 1064 tests, 1035 pass, 29 fail.
  • main d9f9db07, same command: 1067 tests, 1038 pass, 29 fail. The two failing sets are identical (diff clean); all 29 are the local esbuild/git misses this machine always has.

Ran: node --import ./test/harness-resolve.mjs --test --test-concurrency=3 test/roster.test.js test/rounds.test.js test/new-chat.test.js test/create-bot.test.js test/history.test.js

  • 83 tests, 83 pass, 0 fail.

Ran: GET /repos/ThinkFlowLab/FlyBot/compare/d9f9db07...57d69d4

  • 1 commit ahead, 0 behind, 50 files, and the Tip SHA in the body equals the head.

Ran: GET /repos/ThinkFlowLab/FlyBot/commits/57d69d43/check-runs

  • 9 checks, all completed/success: tests-ok, merge-gate, package, plugins (social|timeline|tree), ubuntu-latest · Node 24, reviewed tip (report only), risk tier (report only). mergeable: true, mergeable_state: clean.

Ran (red without the source change): copied the tip, restored all 19 source files (core/src/host/*, core/src/client/*, core/client.js, core/dev/smoke.mjs) to d9f9db07, kept every test, same command

  • 1064 tests, 1027 pass, 37 fail: eight more than the tip's 29, and all eight are this change's own assertions — #645: a line typed in the composer is the new Bot's first turn, not its instructions, the payload carries the look and the chosen model, and never a brief (#645), creating a Bot sends it nothing, and only a parent Bot's brief becomes its instructions, the first Main Bot waits for the user to pick a model, and nothing opens a turn, other Bots start on the default model and are sent nothing, a group the user makes starts no turn of its own, creating a team starts no turn, and the user's first message opens one round (#645), and the create_bot wording in work-dir.

Not run:

  • No browser: the composer path is pinned by new-chat.test.js's source assertions, not clicked in a page.
  • scripts/build-client.mjs --check and scripts/ds-bot-patches.mjs check need esbuild/git, neither on this machine; their CI jobs (package) are green at this tip.
  • dev/bench.sh smoke needs a real model route.

Not blocking, one line each:

  • new-chat.js created: if actions.send rejects, the Bot exists but the card stays open with Create live, so a retry answers A Bot named … already exists and the user is left with a Bot they never see. Closing the card with the error, or opening the chat and showing the failed send there, would leave them somewhere to stand.
  • cells.js dropped the source.role === 'kickoff' guard while parts.js kept it, so an old setup line from a Bot-made Bot — hidden in earlier parts — now draws a "Message from " event line in the current part. One line puts the two back in step, and no new kickoff exists that needs the guard gone.
  • An older state.json still carries bot.kickoff, and channel.js no longer strips it from the bots view, so that dead object now travels to the client. Nothing reads it, so it is harmless; the follow-up the Risk section offers is a clean place to drop it.

@Birfy Birfy removed the awaiting-approval PR is green and waits for the maintainer's approval (T2/T3) or a review PASS at its tip label Oct 22, 2026
@Birfy
Birfy added this pull request to the merge queue Oct 22, 2026
Merged via the queue into main with commit cbce2b0 Oct 22, 2026
9 checks passed
@Birfy
Birfy deleted the feat/645-no-kickoff-turns branch October 22, 2026 07:31
@Birfy Birfy removed the agent-pr Pull request opened by an agent label Oct 22, 2026
Birfy added a commit to Birfy/FlyBot that referenced this pull request Oct 23, 2026
…is missing (ThinkFlowLab#688)

Written-by: Dev

The gate already refused a PASS with no `Ran:` line; the rule now lives
in AGENTS.md, together with the one way we fix such a PASS without
breaking "one conclusion per tip".

Tip SHA: 96e4383

## Reproduce

`ThinkFlowLab#681`'s PASS at `4af18994` (05:59:29Z) carries no `Ran:` line, so
`merge-gate.mjs` reads it as "lists no command it ran" and refuses the
queue entry even with an approval; `ThinkFlowLab#574`'s PASS is the same shape. The
refusal is correct — a PASS that ran nothing is not evidence — but the
rule it enforces was written only in the script's comments and in
`docs/autonomous-dev.md`'s description of it; `AGENTS.md`, which is what
the routines and reviewers read first, did not say it. Nothing in this
change alters the gate.

## What changed

`AGENTS.md`, one bullet in **Gates**:

- **A PASS must list what it ran.** The `Ran:` line (or a `Ran …:`
heading over `$ ` commands) carrying at least one real command is part
of the gate; `merge-gate` refuses a PASS that lists none (ThinkFlowLab#681, ThinkFlowLab#574).
- When a PASS at a tip cannot be read for that reason, the reviewer may
post **one replacement PASS at the same tip**: the body keeps the
original conclusion, adds the `Ran:` lines of what was actually run, and
carries `Replaces: the PASS at <sha> merge-gate rejected for having no
Ran: line`. That is a replacement, not a second conclusion; apart from
it one tip still carries one.

No code, no workflow, no policy file, no test changes: the parser and
the gate already behave this way.

## What I ran

The change is prose, so the check is that the text says exactly what the
gate does, and that the existing readers accept the replacement shape:

```text
$ node --test .github/scripts/review-gate.test.mjs .github/scripts/merge-gate.test.mjs
# tests 69
# pass 69
# fail 0
```

(That is the suite at the current `ThinkFlowLab#676` tip `ef340354`, not main:
main's copies predate the attribution rules, and both versions refuse a
PASS with no `Ran:`.) The replacement line itself is an ordinary line to
`parseReport` — it is ignored — so a replacement PASS parses as a normal
conclusion: verdict on the first line, attribution second, `Ran:` below.

## Red without the source change

Not applicable in the usual sense: this adds no behaviour, so there is
no test that turns red when the prose is removed. The refusal it
documents is already pinned by `merge-gate`'s own case (`a PASS that
lists no command it ran is not evidence`), which stays green with and
without this bullet.

## Not run

- `npm run verify`: docs-only change, no `core/`, plugin or client file
touched.
- No new test: adding a test that reads AGENTS.md for a sentence would
test the copy, not the gate; the gate case already exists.

## Risk

T3 by the repository's own `risk-gate` (`AGENTS.md` is a T3 path). The
text is the owner's decision of 2026-10-22 (relayed by the Chief): a
PASS must carry `Ran:`, and a PASS that `merge-gate` cannot read may be
replaced once at the same tip with a `Replaces:` line. It grants no new
merge right and changes no gate.
Birfy added a commit to Birfy/FlyBot that referenced this pull request Oct 23, 2026
…tay hidden (ThinkFlowLab#698) (ThinkFlowLab#704)

Fixes ThinkFlowLab#698

Tip SHA: 7d01614

The two loose ends ThinkFlowLab#681 left: a first send that fails no longer strands
a Bot the card cannot reach, and a kickoff line is hidden in the current
part of a transcript exactly as it already was in the earlier parts.

## Reproduce

1. **A failed first send.** In the sidebar composer, type "帮我写周报" and
pick `New Bot "帮我写周报"`, then Create. `created` (`new-chat.js`) awaited
`actions.send` **before** `close()`, so when the send rejected the Bot
existed, the card stayed open with its Create button, and a second press
answered "a Bot named … already exists" — the user was left holding a
Bot they could not reach. Reproduced by driving the post-create step
with a rejecting `send`.
2. **A kickoff line in the current part.** On main,
`core/src/host/parts.js` (`historyEntry`) skips `source.role ===
'kickoff'` and `core/src/client/cells.js` (`TriggerCell`) does not, so a
Bot-created Bot's older transcript hid the setup prompt in "earlier
parts" and drew it as a `Message from <parent Bot>` row in the current
part. Reproduced by rendering that transcript through `TriggerCell`.

## What changed

- `core/src/client/bot-draft.js`: new `afterCreate({ id, text, actions
})` — open the new Bot's chat and its details, then send the composer's
line as its first turn; the send's failure is reported with
`actions.notify` instead of escaping into the card.
- `core/src/client/new-chat.js`: `created` closes the pane and delegates
to `afterCreate`, so the order is open-then-send and the card is never
left clickable over an existing Bot.
- `core/src/client/cells.js`: `TriggerCell` skips `role === 'kickoff'`
beside `role === 'room'`, matching `historyEntry` (the host side needed
no change: it already skipped it).
- Tests: `core/test/create-bot.test.js` drives `afterCreate` with a
rejecting send (chat opened, failure noticed, no "already exists" path)
and with an empty composer (nothing sent);
`core/test/kickoff-line.test.js` renders a `kickoff` line through
`TriggerCell` and runs the same line through `historyEntry`, and checks
a real `reply` line still shows in both. `core/test/new-chat.test.js`
pins the new `created` wiring.
- Docs and changelog: `docs/subsystems/core-client.md` (the ThinkFlowLab#645 section
now says where the send and the failure go, and that both paths hide a
kickoff line); `changelog.d/698-follow-ups.fixed.md`.

## What I ran

At this tip, whose parent is main `2f53801` (`main` moved 5 files while
this was pushed; none of them is in this diff):

- `core`: `node --import ./test/harness-resolve.mjs --test
--test-concurrency=3 "test/*.test.js"` — **1223/1223 pass**. The same
command on a pristine copy of the base `cbce2b03` here is **1219 pass, 1
fail** (`check-arch-docs.test.js` "empty diff passes" — that copy has no
`origin/main`, an environment failure, not this change).
- `npm run lint` in `core`: oxlint **0 errors** (43 pre-existing
warnings), `arch-docs: OK (7 changed file(s))` (run with
`ARCH_DOCS_BASE` set to the base commit, since this copy has no
`origin/main`).
- `npm run build` then `node scripts/build-client.mjs --check`:
`client.js` matches `src/client`.
- `node scripts/ds-bot-patches.mjs generate` then `check`: pin `5834c00`
(v0.1.5), 10 patches apply in order, `core/` equals `ds-bot/` + patches.

## Red without the source change

- The failure path: with `afterCreate` reverted to the pre-fix shape
(send before open, no catch), `core/test/create-bot.test.js` is **6
tests, 5 pass, 1 fail** — `a failed first send still opens the new Bot
and shows the failure`.
- The transcript: with `cells.js` reverted to `role === 'room'` only,
`core/test/kickoff-line.test.js` is **1 test, 0 pass, 1 fail** — `a
kickoff line is hidden in the earlier parts and in the current part
(ThinkFlowLab#698)`.
- Restored: those three touched test files **17/17**, and the full suite
1223/1223.

## Not run

- The bench smoke test (`dev/bench.sh smoke`): it needs a DSH checkout
and a real model route, which this session has no access to. Nothing
here changes `index.js` or `src/host/`.
- The CI-only `package` (pack check, typecheck, actionlint) and
`merge-gate` jobs: left to this tip's CI.
- No browser click of the card: one home only, so `afterCreate` is
driven by its unit test and the card's wiring is pinned by
`core/test/new-chat.test.js` source assertions, the way that file pins
the rest of the new-chat pane.

## Risk

- User-visible but small: only the failure path changes shape (you end
up on the new Bot's chat with a notice), and a setup prompt that was
never a real message stops being drawn.
- Scope: `core/src/client/**`, tests, one subsystem doc, one changelog
fragment, regenerated `patches/ds-bot/` and the rebuilt `client.js`. No
host change, no new config key, no permission model change.
- Tier: the repo's own `judge()` over this file list (with
`.github/agent/risk-rules.json`) gives **T1** — every source path here
is `core/src/client/**` (T1) and the five generated files (client.js,
the two patches, the two `files/` test copies) are excluded; the two
test files are T1.
Birfy added a commit to Birfy/FlyBot that referenced this pull request Oct 23, 2026
…hinkFlowLab#710)

Written-by: Dev

## Reproduce

At `bafe9bad` the prompt text describes only an active team. The
plain-Bot rules never mention staying idle, and the Main Bot rules
instruct it to act and report on its own:

```
$ grep -c "report back" core/src/host/prompt.js
2
$ grep -c "stay idle" core/src/host/prompt.js
0
$ grep -n "report back" core/src/host/prompt.js
29:  '- You run the user\'s Bot team: you decide who does what, create new Bots, and report back.',
33:  '- Route work to the right Bot with message_bot, then tell the user what you asked and that you will report back.',
```

So a team with nothing to do still produces status traffic — the sidebar
full of “运行命令…” previews from ThinkFlowLab#645 part B. Part A (silent creation,
ThinkFlowLab#681) and part D (burst coalescing, ThinkFlowLab#641/ThinkFlowLab#642) already shipped; this is
the half that changes the default from active to idle.

## What changed

- `core/src/host/prompt.js`, team rules: one rule states the default — a
Bot does nothing until the user addresses it, it is @-mentioned in a
group, or its parent Bot gives it a task; it does not start work, send
messages or report on its own initiative; being addressed still gets an
answer as usual.
- `core/src/host/prompt.js`, Main Bot rules: “you decide who does what,
create new Bots, and report back” becomes task-driven and states that
**a coordinator acts on a user task, not on its own initiative**; “Route
work … then tell the user what you asked and that you will report back”
loses the unprompted part; one new rule says the same default holds for
the Main Bot, which does not hand out work or start a round on its own
schedule.
- `core/test/prompt.test.js`: a third case asserts both directions — the
idle rule appears exactly once in both a Main Bot and a plain Bot
prompt, no `report back` and no dispatch instruction survives in either,
the coordinator sentence is the Main Bot's, and the reply rule is still
pinned (the change is about the unaddressed default, not about replies).
- `patches/ds-bot/`: regenerated from `core/`. `0001-core-fixes.patch`
carries `src/host/prompt.js` and now `test/prompt.test.js` (upstream
already ships that test file, so `topics.json` needed an entry for it —
the only other file the regeneration touched).
- `docs/subsystems/core-host.md`: the `prompt.js` row records the idle
default (this is the file's owned path, so `arch-docs` is satisfied by
the same PR).
- `docs/subsystems/team.md`: the same two sentences, next to the roles
table.
- `changelog.d/699-idle-default.changed.md`: one fragment, as the change
is user-visible.

Seven files, one commit: the two core files, the regenerated patch and
`topics.json`, the two docs and the fragment. Nothing else in the tree
moves — `git diff` against the base touches exactly those.

## What I ran

At tip `43ba4450`:

```
$ node --test core/test/prompt.test.js
# tests 3
# pass 3
$ node .github/scripts/changelog.mjs check
changelog: 2 fragment(s) in changelog.d/ are well formed
$ grep -c "report back" core/src/host/prompt.js
0
$ grep -c "stay idle" core/src/host/prompt.js
1
$ grep -c "dispatch" core/src/host/prompt.js
0
$ node scripts/ds-bot-patches.mjs generate
… wrote patches/ds-bot/0001-core-fixes.patch (n files) …
$ diff -r patches-main patches/ds-bot     # against the committed series at bafe9ba
Files …/0001-core-fixes.patch and …/0001-core-fixes.patch differ
Files …/topics.json and …/topics.json differ
```

The patch series was then proven by rebuilding: on a copy of the tree,
`node scripts/ds-bot-patches.mjs apply` rebuilds `core/` from `ds-bot/`
+ `files/` + the 10 patches, and `diff -r core ../full699/core` is
**empty** — byte for byte.

## Red without the source change

`core/src/host/prompt.js` reverted to `bafe9bad` with the new test case
kept: `core/test/prompt.test.js` **2 of 3, one red** — the new case
fails on its first assertion (“main carries the idle default as its own
rule”). The assertion reads the text the prompt module produces, not a
copy.

## Not run

- The 0-turn behavioural case (ThinkFlowLab#681's “10 Bots + 3 groups, no user
message, 0 model turns”) lives in the roster/rounds and new-chat suites,
not in `prompt.test.js`. This change edits prompt text only — no
turn-starting, delivery or roster code is touched — and CI runs those
suites on this tip; I did not run them here.
- `node scripts/ds-bot-patches.mjs check` could not finish its first
step here: this box has no git checkout of `ds-bot/` (the tree came from
an archive, which drops file modes), so the pin's tree hash does not
match and the script stops before comparing patches. The comparison it
would make is covered above by `apply` + `diff`, and CI runs `check` on
a real checkout.
- `npm run arch-docs` needs `git` history to fetch `origin/main`; the
owned doc for `prompt.js` (`docs/subsystems/core-host.md`) is changed in
this PR.
- `npm run verify`, the smoke test and the bench: the change is prompt
text and CI runs the full suites.

## Risk

T2 — `core/src/host/prompt.js`. Text-only for behaviour: no tool,
setting, permission or turn-starting path changes, and the reply rules a
Bot answers with are unchanged and pinned by the existing case.

Tip SHA: 43ba445

Fixes ThinkFlowLab#699
HugoXK pushed a commit to HugoXK/FlyBot that referenced this pull request Oct 25, 2026
…s judged (ThinkFlowLab#1112) (ThinkFlowLab#1133)

Owner: Dev4
Reviewer: Reviewer
Base: d797cbe
Tip SHA: c27a028
Fixes ThinkFlowLab#1112

## Reproduce

`.github/scripts/review-gate.mjs` decided a tip with
`headSha.startsWith(parsed.tip)`, so a conclusion
written `Verdict: PASS at abaf938` names every head that begins with
those seven characters — including
the newest one, which may have no conclusion of its own yet. The guard
printed the head short
(`short(decision.head)`, 12 characters) and printed no comment id at
all, so a reader could not tell
which text decided. On ThinkFlowLab#1106 both happened together: green at 05:09:22
on a PASS posted at 05:12:24.

## What changed

- `review-gate.mjs`: a conclusion counts only when its tip **is** the
head. A shorter tip is refused
with its length in the reason ("names abaf938 (7 characters), not the
full tip …"). The same comparison
in `recordOnlyAt` (`enqueue-rules.mjs`); the deciding entry carries the
`id` and `kind` of the comment
  it read.
- `enqueue-guard.mjs`: the report snapshot carries each comment's id;
the head is read again after the
comments and the diff and a moved head is refused ("the head moved
during this run …; run it again"),
because otherwise the run judges one head and prints another; the
printed line carries the full
40-character head from the value judged, and a second line names the
conclusion used.
- `gate-check.mjs`: the summary names the comment id and the tip it
named.
- Docs: `AGENTS.md` ("One conclusion per tip" — the whole 40 characters)
and the Verifier skill's tip
  step, since this tightens what a conclusion must name.
- Tests: one per failure mode; the ThinkFlowLab#640 fixture now names its head in
full (its point was which line
  names the tip, not the length).

## What I ran

- `TZ=UTC node --test ".github/scripts/*.test.mjs"` → **414 pass, 2
skipped, 0 fail** (416 tests).
- `diff -rq` against a `d797cbe4` tree: exactly the 8 files above.

## Red without the source change

Against the pristine `d797cbe4` copies of the three sources, the same
test file: **4 red**, the four
new cases, 59 others green. Each failure mode separately:

1. prefix match restored → **1 red** (the 7-character tip case);
2. comment ids removed from the snapshot → **2 red** (the id case, the
printed-summary case);
3. the second head read removed → **1 red** (the moved-head case).

Source restored after each knife, `cmp` clean.

## Not run

- `npm run verify`: no `core/` change here and no `plugins/` change, so
only the `.github/scripts` suite
  is in scope; `core/` tests never import these files.
- `arch-docs`: none of the 8 files is under an `owned_paths` prefix;
`AGENTS.md` and `docs/subsystems/`
are separate, and the sentence I added is the rule itself, not a
subsystem detail.
- A live `gh api` run of the guard (needs a token in a job); the fake
`gh` in the tests is the same shape
  the workflow uses.

## Risk

- **A short tip now refuses.** Any conclusion written with 7–39
characters stops counting, which is the
point, but it fails closed on a report someone wrote short — the reason
says the length, and the fix is
one more PASS at the same tip (the ThinkFlowLab#681 replacement rule covers the same
situation).
- **A moved head aborts the run.** Two reads instead of one, and a push
between them refuses the run
instead of judging a stale pair; the check is re-run on the next event
anyway. Cost is one extra API
  read per run.
- The compare is equality on a lower-cased sha; GitHub always returns 40
hex characters for a head, so a
  legitimate conclusion is unaffected.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk:T3 Risk tier T3 (needs a human approval), set by the controller from the diff

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant