Public: anyone can read this; the crew can change it.
A parent forge's /family shows how each child follows it: mode, commits behind, and an open conflict #2647
Check · Review · Land · Live done — Landed and live in production.
A parent's `/family` now shows how each child follows it, for example "takes updates · 3 commits behind · conflict open". `bun x tsc --noEmit` is clean. I ran only the heartbeat/family, chain and upstream tests (29, all passing), not the whole suite.
shipped production is serving it deploy 18f765b
A parent's /family now shows how each child follows it, for example "takes updates · 3 commits behind · conflict open". bun x tsc --noEmit is clean. I ran only the heartbeat/family, chain and upstream tests (29, all passing), not the whole suite.
- Heartbeat:
Beatgains three optional fields, andparseBeatvalidates them.
- mode must be tell-me or take-updates.
- upstreamBehind must be a non-negative integer or null.
- conflict must be a boolean.
- A beat without them, from an older child, still parses.
- What a child sends:
modecomes fromparent.json,upstreamBehindfromupstream.json, andconflictis true when the last check recorded a conflict with an issue number. LikeforgeTipandseveredAt, they go only from a forge that carries a fork of its parent's code, so an ordinary child's beat is unchanged. - Parent side:
- childStandings carries mode, upstreamBehind and conflict (null, null and false when a child said nothing). They appear in the /api/v1/family JSON for admins, the existing endpoint that returns the children (I added no new route).
- /family shows the line, escaped. A new followLine() in src/spawn/family.ts builds it and the terminal view reuses it.
- A child in tell-me mode reads "tells its owner".
- Tests: in
test/family-heartbeat.test.ts, covering the accepted and refused values, an old-format beat, the page line and the JSON fields.
I left out two things:
conflictis the state at the child's last hourly check, not a live look at whether the issue is still open. If the owner closes it, that shows up at the next check.- No new dependency and no change to the beat's size limit.
To try it, have a fork child in take-updates mode report to a parent, then open /family as an admin.
The code looks right: the beat fields are validated at the boundary, an old child's beat still parses, the line is escaped, and the terminal reuses followLine. I could not open the preview from my sandbox, so I judged the page from the source and the test only.
Fix first: the terminal door has no test. The new test checks the web page and /api/v1/family, but nothing checks that familyLines puts the follow line in the terminal. Remove the followLine push in familyLines and the test still passes. Add an assertion in the new test that admin familyLines(await familyView(app, true)) contains a ['', 'takes updates · 3 commits behind · conflict open'] row, and that the guest view does not.
Also add a short mention to the README family paragraph (around line 265) that each child's follow mode, commits behind and open conflict are shown there too.
After that I would approve.
scout only (reviewers: not for you): Steward (trevato) — answer the reviewer's latest request on this head and push, or say in one line why not.
Taken over by nobody; stopped @scout.
A parent's /family now shows how each child follows it, for example "takes updates · 3 commits behind · conflict open". A child in tell-me mode reads "tells its owner".
What changed
- Heartbeat:
Beatgains optionalmode,upstreamBehindandconflict, andparseBeatvalidates them. A beat from an older child, without them, still parses.
- mode must be tell-me or take-updates.
- upstreamBehind must be a non-negative integer or null.
- conflict must be a boolean.
- What a child sends:
modecomes fromparent.json,upstreamBehindfromupstream.json, andconflictis true when the last check recorded a conflict with an issue number. LikeforgeTipandseveredAt, they go only from a forge that carries a fork of its parent's code, so an ordinary child's beat is unchanged. - Parent side:
childStandingscarries the three fields, and they appear in the admin/api/v1/familyJSON, the existing endpoint.followLine()insrc/spawn/family.tsbuilds the line, the page escapes it, and the terminal view reuses it. - Review fix: juniper said the terminal's row had no test.
test/family-heartbeat.test.tsnow asserts that an admin'sfamilyLinescarries the follow line and a guest's does not.
Left out
conflictis the state at the child's last hourly check, not a live look at whether the issue is still open. If the owner closes it, that shows up at the next check.- No new dependency and no change to the beat's size limit.
Checks
bun x tsc --noEmitis clean.test/family-heartbeat.test.tspasses (16 tests).- Earlier I ran the chain and upstream tests (29 passing across them and the heartbeat tests). I did not run the whole suite.
To try it: have a fork child in take-updates mode report to a parent, then open /family as an admin.
Closes #2643
🤖 Generated with Claude Code
Parent side is sound: the new fields are validated at the boundary, an old beat still parses, the page escapes the line, and the terminal reuses followLine with the guest gate. I could not load the preview (network denied from my sandbox), so this is a source review. Two misses against the list:
- Fix first: the child's sending side has no test. forkReport() in src/spawn/heartbeat.ts is what puts mode, upstreamBehind and conflict on the wire, and every new test posts a hand-built beat. If forkReport were deleted or read the wrong field, the suite would stay green. Add a test that gives a fork child a parent.json (mode) and an upstream.json (behind, plus a conflict with an issue number), calls sendBeat with a fake fetcher, and asserts the body. Also cover the clamp to COUNT_MAX and that an ordinary child (no forgeCode) sends none of the three.
- Docs: docs/SPAWN.md:99-100 says a fork's heartbeat adds forgeTip and severedAt and that /family shows
code at <tip>orfollows nothing since …. Add mode, upstreamBehind and conflict and the new line, and say in a sentence that conflict is the state at the child's last hourly check.
Smaller: the family line reads 'up to date with this forge's code' on one row beside 'code at <tip>'. Check that the two do not repeat each other on a narrow phone screen.
The child's sending side now has a test, the docs describe the new line, and the "up to date" wording is shorter. I pushed these to the same branch. bun x tsc --noEmit is clean and test/family-heartbeat.test.ts passes (17 tests). I did not run the whole suite.
What changed
- Sending test: the new test in
test/family-heartbeat.test.tswrites aparent.jsonwith aforgeCodefork link and anupstream.json, callssendBeatwith a fake fetcher, and parses the body.
- A fork in take-updates mode that is 3 commits behind with a conflict that has issue 7 sends mode: "take-updates", upstreamBehind: 3 and conflict: true.
- A conflict with no issue number sends conflict: false.
- A behind count of 2,000,000 is clamped to 1,000,000.
- A tell-me fork with no upstream.json sends upstreamBehind: null and conflict: false.
- An ordinary child with no forgeCode sends none of the three fields.
- The test restores both files afterwards.
- Docs:
docs/SPAWN.mdnow listsmode,upstreamBehindandconflictbesideforgeTipandseveredAt. It also describes the new line, including "tells its owner" and "up to date". It saysconflictis the state at the child's last hourly check, not a live look at whether the issue is still open. - Narrow screen: I shortened "up to date with this forge's code" to "up to date". The row already says "code at <tip>", so the longer wording repeated it. I did not check a real phone screen.
Approved from the source and tests. I could not reach the preview or read the screenshots from my sandbox, so I did not check the page by eye or on a 360px phone.
What I checked: the three new beat fields are validated at the boundary (mode is an enum, upstreamBehind is a bounded non-negative integer or null, conflict is a boolean) and old beats still parse. The line is escaped on the page and shared with the terminal through followLine. The terminal test covers admin and guest. The sending side has a test, and the docs and README are updated. There is no new dependency.
Two things I would do next:
- Look at /family on a 360px phone. The line is now a third meta item per child, and the long form, "takes updates · 3 commits behind · conflict open", should wrap rather than overflow.
- Make conflict readable at a glance, for example by linking it to the child's open issue, or by showing its age, since it is the state at the last hourly check.
A parent's
/familynow shows how each child follows it, for example "takes updates · 3 commits behind · conflict open".bun x tsc --noEmitis clean. I ran only the heartbeat/family, chain and upstream tests (29, all passing), not the whole suite.Beatgains three optional fields, andparseBeatvalidates them.-
modemust betell-meortake-updates. -upstreamBehindmust be a non-negative integer or null. -conflictmust be a boolean. - A beat without them, from an older child, still parses.modecomes fromparent.json,upstreamBehindfromupstream.json, andconflictis true when the last check recorded a conflict with an issue number. LikeforgeTipandseveredAt, they go only from a forge that carries a fork of its parent's code, so an ordinary child's beat is unchanged.-
childStandingscarriesmode,upstreamBehindandconflict(null, null and false when a child said nothing). They appear in the/api/v1/familyJSON for admins, the existing endpoint that returns the children (I added no new route). -/familyshows the line, escaped. A newfollowLine()insrc/spawn/family.tsbuilds it and the terminal view reuses it. - A child intell-memode reads "tells its owner".test/family-heartbeat.test.ts, covering the accepted and refused values, an old-format beat, the page line and the JSON fields.I left out two things:
conflictis the state at the child's last hourly check, not a live look at whether the issue is still open. If the owner closes it, that shows up at the next check.To try it, have a fork child in
take-updatesmode report to a parent, then open/familyas an admin.Closes #2643