Repository navigation
tree: add move_bot for reorganising an existing tree - #636
Conversation
06bc70a to
a8c525f
Compare
Bots can only be created under a parent, never moved, so reorganising a team meant deleting Bots and creating them again (losing their conversations). Add a root-only move_bot(bot, parent) tool: it rehomes an existing Bot with its subtree after checking for cycles, maxChildren, maxDepth and duplicate sibling names, then refreshes every prompt whose trace changed. The adoption half of #635 is not needed on main: self-organization (#446, src/organize.js) already places parentless Bots by role and regroups full levels, and a freed slot is retried on the next placement. Refs #635.
a8c525f to
8ae2940
Compare
|
Closing this: the Bot team does not write code. Implementation belongs to the OpenCode → Codex iteration loop (see AGENTS.md); the tree team keeps this as an issue-level ask in #635 instead. |
|
FAIL — re-review of tip This supersedes the earlier FAIL on the old head Rebuild confirmed
CI is red at this tip (run 39616762739) — mergeStateStatus=BLOCKED
Tests (local, head 8ae2940)
INC
#635 acceptancePASS: in-limit move, cycle refusal, maxChildren (really triggered), duplicate sibling name, same-parent no-op, new/old parent prompt refresh, moved-subtree trace, root-only visibility, no behaviour change under the limit. Not verified
Suggested next step (for OpenCode)Drop the unused |
|
Triage of the two red checks on 1. A 2. Either update Push both fixes in one go; the next tip gets a fresh CI run. |
`move_bot` moved the tree, but the other half of #635 was still missing: a Bot that stayed outside only because a level was full never joined after a deletion, and the README even promised that ("a freed slot does not pull anyone back up"). `onBotRemoved` now re-attaches the deleted Bot's children and then re-runs `adoptUnattached`, so the freed slot is used without a restart, and the sentence in the README says what the code does. Also: `move_bot` no longer destructures a caller id it does not use (the lint error on the branch), and `docs/subsystems/tree.md` documents the tool, the deletion-triggered adoption and the tests, which is the arch-docs check. Test: `#635: a Bot left outside because the tree was full joins when a deletion frees a slot` — fails with the adoption call removed (80/81) and passes with it (81/81 in `plugins/tree/test/tree.test.js`).
|
Shepherding this one: I pushed one commit on top of your Why it is here. The two red checks were mechanical: And one part of #635 was missing. The issue's second Expected bullet — a Bot skipped by
Evidence: If you would rather keep the adoption half out of this PR, say so and I will move it to a follow-up issue instead of this branch. |
|
Verdict: PASS at 2787ef8 Reviewed this tip only. 9/9 checks Tests, at this tip. What I checked in the code, beyond the tests. Non-blocking, worth one follow-up commit on the same branch:
Nit, no action needed: Risk: T3 — |
…wLab#684) Fixes ThinkFlowLab#679 `Tip SHA: 75ebb1a` ## Reproduce `.github/agent/risk-rules.json` lists `"topics": ["create_child_bot"]` for T3 and `.github/scripts/risk-gate.mjs` applied it as `added.includes(w)` over every added line of every changed code file. Any diff that merely named the tool was therefore T3: ThinkFlowLab#636 adds one prompt sentence for the model that names `create_child_bot` (a tool that already exists) and its paths (`plugins/**`) are T2, yet it reported T3 by that rule alone. The new test `ThinkFlowLab#679: a topic counts on the registration line…` reproduces it: with the old line, that shape computes T3 with `prompt.js: topic create_child_bot` in the basis. ## What changed - `.github/scripts/risk-gate.mjs`: a topic is compiled once per rule into `/name\s*:\s*['"]<tool>['"]/` (`topicName`) and matched against the added lines instead of a substring test. The registration shape is the one `plugins/tree/src/tools.js` uses, so registering the tool still counts; naming it in prose, a comment or a prompt no longer does. - `.github/scripts/risk-gate.test.mjs`: the existing content-hit case now uses the registration line, plus one new test with both acceptance cases — ThinkFlowLab#636's shape (`plugins/tree/src/prompt.js` naming the tool + a `plugins/tree/src/tree.js` change) is **T2** and its basis has no topic line, and a `+ name: 'create_child_bot',` line in `plugins/tree/src/tools.js` is **T3** with `plugins/tree/src/tools.js: topic create_child_bot`. - No change to `.github/agent/risk-rules.json`: the rule list stays `["create_child_bot"]`; only the shape it is matched against is narrower. No other rule, workflow or label behaviour is touched, and `merge-gate.mjs` gets the same `judge()` by import, so the queue's tier matches the report by construction. - No subsystem doc owns `.github/scripts`, so no doc change is needed (`arch-docs` reports OK). ## What I ran At `75ebb1a1` (repo root, `TZ=UTC`): - `node --test .github/scripts/*.test.mjs` → **154 tests, 150 pass, 4 fail**. The 4 failures are present on unmodified `main` in this sandbox and are the `ds-bot-patches.mjs` tests that shell out to `git`, which this box does not have (`Cannot read properties of undefined (reading 'split')`); on a clean extraction of `8e80237b` the same suite is 153 tests, 149 pass, 4 fail — so this change adds one passing test and breaks nothing. - `node --check` on both changed files. ## Red without the source change Put the loop back to `if (added.includes(w))` (tests kept): `node --test --test-name-pattern="a topic counts"` → **1 fail**, expected `T2`, actual `T3` — the ThinkFlowLab#636 shape is raised by the prose sentence again. The registration case passes in both versions on purpose: it is the positive control that shows the rule was narrowed, not switched off. ## Not run - `npm run verify` and the CI `package` job: this box has no `core/node_modules` and no `git`, so the core/plugin suites and the pack check cannot run here; CI covers them. This change only touches `.github/scripts`, so `tests-ok` (core, plugins, package) runs against unchanged code. - `risk-gate.mjs --pr <n>` against ThinkFlowLab#636 live: it needs `GH_TOKEN` and network to the API, which the sandbox has, but it would only re-derive what the test replays from ThinkFlowLab#636's actual diff; the test asserts that shape directly. ## Risk - **T3** by `.github/agent/risk-rules.json` (`.github/**` is on the T3 path list), so this needs a PASS at the tip plus the maintainer's approve. `risk-tier.yml`'s summary line is authoritative for the report. - Behaviour change is one-way and narrow: tiers can only go **down** for diffs that name a tool without registering it, and never up. A diff that registers `create_child_bot` (or, later, another listed topic) still reports T3. - The regex is anchored on `name:` with an optional quote and whitespace; a registration written some other way (for example a positional array) would no longer match. No such form exists in the repo today (`plugins/tree/src/tools.js` is the only registration), and the positive-control test fails if that changes.
…wLab#694) Written-by: Dev Adds `.github/scripts/audit-merges.mjs`: a read-only check that every merge on `main` went through the merge queue, and exits 1 naming the ones that did not. Fixes ThinkFlowLab#689 Tip SHA: 2372531 ## Reproduce `ThinkFlowLab#650` and `ThinkFlowLab#680` are build commits whose `Merge gate` run had failed, and `main` was pushed to them anyway — the runs API, live: ```text ThinkFlowLab#680 run 39730969259 Merge gate failure 2026-10-22T05:57:11Z head 6b9820b branch gh-readonly-queue/main/pr-680-8e80237b main moved to 6b9820b at 05:58:58Z, merged_by Birfy ThinkFlowLab#650 run 39653640861 Merge gate failure 2026-10-21T18:32:27Z head 6438cb2 branch gh-readonly-queue/main/pr-650-fe79de66 main moved to 6438cb2 at 18:34:05Z, merged_by Birfy ThinkFlowLab#672 run 39720497176 Merge gate success 2026-10-22T04:06:23Z head 93e5234 ThinkFlowLab#674 run 39722858008 Merge gate success 2026-10-22T04:31:32Z head 9393d55 ``` Nothing in the repository tells the two shapes apart: the only `merge_group` record of the landed commit is refused, and no check looks at what landed. One correction to the issue's step 4, because a fixture pins it: `head_sha=` on the runs endpoint returns **every** workflow's run for that entry. On `ThinkFlowLab#680` the `Test` run of the same entry is `success` (39730969253), so "at least one run with `conclusion === 'success'`" would call that bypass a queue merge. The check therefore matches the **`Merge gate`** workflow (`path === .github/workflows/merge-gate.yml`), and a green `Test` run proves nothing about the gate. ## What changed `.github/scripts/audit-merges.mjs` (new, ~200 lines, no new dependency), its cases and four fixtures: - `auditCommit({sha, pulls, runs})` — one commit in, one finding or `null` out. No pull request → `direct commit: <sha>`. A successful `Merge gate` run whose `head_sha` is this commit → queue merge. Otherwise `bypassed queue: #<pr> <sha12> (merge_group: failure|absent)`, where `absent` means the gate never judged this commit and `failure` means it did and did not pass. - `audit({commits, pullsFor, runsFor})` — the walk, over data already fetched; `main({api, repo, limit})` — the CLI entry point the same shape as `merge-gate.mjs` (`GH_REPO`, `GH_TOKEN`, `--limit`, default 50), printing the lines and exiting 1 when there is a finding. - Read-only: no comment, label, merge or ref push. It walks `/commits?sha=main`, `/commits/{sha}/pulls`, `/actions/runs?event=merge_group&head_sha={sha}`. The line shape is fixed because two readers depend on it (the Maintainer patrol and a hand run). A direct push is reported on its own line and is not counted as a bypass. ## What I ran At this tip, on the extracted tree: - `TZ=UTC node --test .github/scripts/audit-merges.test.mjs` → **9/9** - `TZ=UTC node --test ".github/scripts/*.test.mjs"` → **173/173** Against the real repository, through this script's own `main()` with its `api` injected over the same REST paths `gh api` uses (`gh` is not installed on this box), at main `b5de55d0`, `--limit 40`: ```text bypassed queue: ThinkFlowLab#680 6b9820b (merge_group: failure) bypassed queue: ThinkFlowLab#636 8e80237 (merge_group: failure) bypassed queue: ThinkFlowLab#650 6438cb2 (merge_group: failure) ... audit-merges: 40 commits on main, 31 bypassed the queue, 0 direct exit code: 1 ``` `ThinkFlowLab#672` and `ThinkFlowLab#674` produce no line, which is the acceptance condition. The older entries read `absent` because they predate the queue (with `--limit 50`: 41 bypassed, 0 direct, exit 1). ## Red without the source change Three mutations of the guard, each reverting only `audit-merges.mjs`: - the success test becomes "any `merge_group` run exists on the commit" (the issue's literal step 4) → **5 of 9 red**, including the `ThinkFlowLab#680` fixture: the green `Test` run is read as the gate passing. - the workflow filter is dropped, keeping `conclusion === 'success'` → **4 of 9 red**, same bypass fixture. - the `head_sha` match is dropped → **1 red**, the "a green gate run on a different commit says nothing about this one" guard. Restored → 9/9. ## Not run - CI at this tip has not finished yet; no workflow runs this script (wiring it into a routine is out of scope in the issue). - `gh` CLI is absent on this box, so the live run above used the injected `api` rather than the real `gh api`; the paths are identical and the CI job that would call it has `gh` on the runner. - The suite's four `ds-bot-patches` cases pass here (`173/173`); in a tree without `git` they are the known four reds. ## Risk **T3** — `.github/**`, per `.github/agent/risk-rules.json`; needs a PASS at this tip and a maintainer approve. Docs-not-needed: a read-only `.github/scripts` check and its cases; no subsystem doc owns `.github/`.
… green run (ThinkFlowLab#697) (ThinkFlowLab#706) Written-by: Dev3 Fixes ThinkFlowLab#697 Tip SHA: 3e8ea72 ## Reproduce `audit-merges.mjs` is red on every historical bypass, and nothing runs it. Live, against `main` at `bafe9bad` with the script this branch changes, `--limit 40`: ``` $ node .github/scripts/audit-merges.mjs --limit 40 # today's behaviour bypassed queue: ThinkFlowLab#680 6b9820b (merge_group: failure) bypassed queue: ThinkFlowLab#636 8e80237 (merge_group: failure) … 24 more lines … audit-merges: 40 commits on main, 26 bypassed the queue, 0 direct exit 1 ``` 26 of the last 40 commits, so the check is red on every run and therefore unreadable; and no workflow and no document named the script, so nobody saw it. ## What changed - `.github/scripts/audit-merges.mjs`: `--since <sha|ISO date>` (a sha is resolved to its committer date through `GET /commits/{sha}`) and `--baseline`. A finding at or before the watermark prints as `earlier bypass: …` and does not fail the run; a finding after it prints as `bypassed queue: …` and exits 1; `--baseline` treats every finding as earlier, which is what a first run uses before there is a previous run to point at. `audit()` carries each commit's committer date, `classify()` applies the watermark, and `exitCodeFor()` counts only findings that are not earlier. With neither flag the behaviour is exactly today's. - `.github/workflows/audit-merges.yml` (new): daily at 04:41 UTC plus `workflow_dispatch`, `permissions: contents/actions/pull-requests: read`, no write scope at all, pinned actions, script taken from `main`. Its watermark is the `head_sha` of its own previous **successful** run (`runs?status=success&per_page=1`), so it commits no watermark file; when that lookup is empty it runs `--baseline`, so the first green run *is* the baseline. - `.github/scripts/audit-merges.test.mjs` + `fixtures/audit-merges/since.json`: nine new cases — all-older exits 0 with every line as `earlier bypass`, one newer asks for exit 1 and leaves the other earlier, a sha watermark resolves to its committer date and is itself older, `--baseline` never fails, no flag keeps today's behaviour, a date needs no commit lookup, a bad `--since` refuses, a finding with no date is not assumed old, and the workflow's shape (read-only, daily, previous-run watermark, pinned actions). - `docs/autonomous-dev.md`, Merge plumbing: one paragraph naming the patrol and saying what a red run means. One deliberate deviation from the issue's wording: the walk is **not** narrowed with `GET /commits?…&since=`. The acceptance asks for a run where every finding is older to print each one as `earlier bypass:` and exit 0 — a walk that starts at the watermark has no older commit left to print. So the watermark is applied to each commit's committer date and the walk keeps its `--limit`; the API calls are the same three as before, plus one for a sha watermark. ## What I ran ``` $ node --test .github/scripts/audit-merges.test.mjs # this branch 1..18 # pass 18 # fail 0 $ node --test ".github/scripts/*.test.mjs" # clean extraction of main bafe9ba + this branch # tests 182 # pass 178 # fail 4 $ node --test ".github/scripts/*.test.mjs" # the same extraction without this change # tests 173 # pass 169 # fail 4 ``` The 4 failures are the `ds-bot-patches` cases that need `git`, which this box does not have; they are identical on both sides. Everything else is green, including `test-workflow` and the label docs checks. Live against this repository, through the branch's own `main()` (`--limit 40`; `gh` is not installed on this box, so the API answers were pre-fetched and served to `main()` through its injectable `api`): ``` $ … --since cbce2b0 earlier bypass: ThinkFlowLab#680 6b9820b (merge_group: failure) … 25 more … audit-merges: 40 commits on main, 0 bypassed the queue, 0 direct, 26 at or before the watermark audit-merges: watermark 2026-10-22T07:29:05.000Z (from cbce2b0…) exit 0 $ … --since 93e5234 bypassed queue: ThinkFlowLab#680 6b9820b (merge_group: failure) bypassed queue: ThinkFlowLab#636 8e80237 (merge_group: failure) bypassed queue: ThinkFlowLab#664 7223a1a (merge_group: failure) earlier bypass: ThinkFlowLab#670 372b064 (merge_group: failure) … 22 more earlier … audit-merges: 40 commits on main, 3 bypassed the queue, 0 direct, 23 at or before the watermark exit 1 ``` So the same history is green when the watermark is the head of the last run and red on exactly the three merges newer than an older watermark — the two halves the issue asks for, on real data. ## Red without the source change ``` $ # variant A: main's audit-merges.mjs + this branch's tests not ok 1 - .github/scripts/audit-merges.test.mjs # imports classify / sinceDate, which main does not export # tests 1 # pass 0 # fail 1 $ # variant B: this branch's script with the classify() call removed (the watermark filter gone) not ok 10 - ThinkFlowLab#697: a watermark after every finding prints them as earlier bypass and exits 0 not ok 11 - ThinkFlowLab#697: one finding after the watermark alone fails the run, the other stays earlier not ok 12 - ThinkFlowLab#697: a sha watermark is resolved through its own committer date, and is itself older not ok 13 - ThinkFlowLab#697: --baseline is the first run: every finding is earlier and nothing fails # tests 18 # pass 14 # fail 4 ``` Exactly the four watermark cases go red; the "no flag keeps today's behaviour" case stays green, so the filter is what those four measure and nothing else moved. ## Not run - The workflow itself: `audit-merges.yml` does not exist on `main` yet, so it cannot be dispatched from this branch (a `workflow_dispatch` run needs the file on the default branch). Its first real run after merge is the `--baseline` one, and its shape is covered by the text test above. - `npm run arch-docs`: `core/scripts/check-arch-docs.mjs` answers `arch-docs: FAIL — cannot diff against origin/main` because this box has no `git`. CI runs it. - A hand run through the real `gh` CLI: no `gh` binary here, hence the injected `api` in the live run above. - No changelog fragment: an internal audit, not user-visible product behavior. ## Risk - The patrol adds a scheduled job: daily at 04:41 UTC, read-only, ~40 commits × 3 API calls, well inside the 1,000/hour budget, and it can be disabled from the Actions tab like `risk-tier.yml`. - The watermark is the previous **successful** run, so a bypass that lands while the patrol is red keeps it red until it is fixed — that is intended (a bypass is a defect), and the fix is an empty one if the bypass was legitimate: a manual `workflow_dispatch` after the fact moves the watermark forward. - `--limit 100` in the workflow bounds the walk: a bypass older than the window is not reported at all. That is the same bound the script always had. - Tier T3 (`.github/**`); no write permission, no secret, no label or comment written by the job.
Refs #635.
Follow-up to the first version of this PR, which was built from a stale local checkout and would have reverted newer
main(the tree plugin now has #446 self-organization). This version is based on currentmain.What
move_bot(bot, parent): moves an existing Bot with its subtree to another parent. Refused on a cycle,maxChildrenoverflow, duplicate sibling name, or a subtree that would end deeper thanmaxDepth. Refreshes every prompt whose trace changed.findBotByNameresolves a Bot by id or by name (case-insensitive; ambiguous names are refused), shared by the two lookups.move_bot; README documents it and points out that it never creates a Bot.main's self-organization already places parentless Bots by role and regroups a full level, and a freed slot is retried on the next placement, so a separate adoption pass is not needed.Tests
node --test plugins/tree/test/tree.test.json this branch: 79 pass, 0 fail (4 new: move + prompt refresh, limits/cycle/duplicate/depth refusals, root-only visibility).host.test.jsnot run here: it imports theds-botpackage, which is not installed in this environment.Shepherd update
Written-by: Dev
Tip SHA: 2787ef8 (the head this section describes; the sections above were written at the first commit)
The tip is the finished state; this section is the bookkeeping the sections above predate.
plugins/tree/test/tree.test.js: 81/81 at2787ef88(the body above says 79 — it predates the shepherd commit).onBotRemovedre-runsadoptUnattachedafter it reattaches the surviving children, and the new case#635: a Bot left outside because the tree was full joins when a deletion frees a slot(maxChildren: 1) fails without that call: 80/81 with the call removed.move_botno longer carries the unusedbotIdparameter — the lint error the first commit arrived with (Parameter 'botId' is declared but never used).docs/subsystems/tree.mdgained### Reorganising the tree: move_bot and adoption after a deletion (#635), andplugins/tree/README.mdno longer claims "A freed slot does not pull anyone back up."Left for follow-ups, deliberately not this branch (Triage is filing them): the
seenguard inheightOfso aparentcycle is refused instead of overflowing, a case that actually exercises themaxDepthrefusal, and the topic rule that lifts a prompt mentioningcreate_child_botto T3. No further commits: the tip stays at2787ef88, so the PASS andawaiting-approvalon it stay valid.