Skip to content

Commit 699cd5d

Browse files
committed
connect: a boost.created without boosted_event_id is not a boost on the recording
A missing key said nothing about where the boost landed, but decoded as null and passed as a boost on the recording itself. Require the key, and read only an explicit null as the recording. Also qualify the skill's summary: only requests that ask for something are acknowledged.
1 parent b2f7937 commit 699cd5d

3 files changed

Lines changed: 17 additions & 11 deletions

File tree

‎internal/connector/admission/boost.go‎

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@ const eventBoostCreated = "boost.created"
1313

1414
// boostDetails is the part of boost.created's details admission reads. The
1515
// feed publishes boosted_event_id as null for a boost on the recording itself
16-
// and as the event's id for a boost on one of the recording's events.
16+
// and as the event's id for a boost on one of the recording's events. It is
17+
// kept raw so that a missing key, which says neither, is told from null.
1718
type boostDetails struct {
18-
BoostID int64 `json:"boost_id"`
19-
BoostedEventID *int64 `json:"boosted_event_id"`
19+
BoostID int64 `json:"boost_id"`
20+
BoostedEventID json.RawMessage `json:"boosted_event_id"`
2021
}
2122

2223
// opens reports whether the gate left trigger open.
@@ -38,14 +39,16 @@ func opens(gate GateResult, trigger Trigger) bool {
3839
// the agent's own comment, line or post. Whether the agent performed a given
3940
// event is not something this rule reads.
4041
//
41-
// Details without a boost id are not a boost.created Basecamp writes, and are
42-
// discarded as an invalid pointer: there is no boost to read.
42+
// Details without a boost id, or without boosted_event_id, are not a
43+
// boost.created Basecamp writes, and are discarded as an invalid pointer:
44+
// there is no boost to read, or nothing says whether it landed on the
45+
// recording. A missing key is never read as null.
4346
func boostOnRecording(ev Event) (int64, Reason) {
4447
var d boostDetails
45-
if len(ev.Details) == 0 || json.Unmarshal(ev.Details, &d) != nil || d.BoostID <= 0 {
48+
if len(ev.Details) == 0 || json.Unmarshal(ev.Details, &d) != nil || d.BoostID <= 0 || len(d.BoostedEventID) == 0 {
4649
return 0, ReasonInvalidPointer
4750
}
48-
if d.BoostedEventID != nil {
51+
if string(d.BoostedEventID) != "null" {
4952
return 0, ReasonNotAddressed
5053
}
5154
return d.BoostID, ""

‎internal/connector/admission/boost_test.go‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,9 @@ func TestABoostSettledFromItsPointerCostsNoRead(t *testing.T) {
267267
for name, details := range map[string]json.RawMessage{
268268
"without details": nil,
269269
"without a boost id": json.RawMessage(`{"boosted_event_id":null}`),
270-
"unreadable details": json.RawMessage(`{"boost_id":"x"`),
270+
// Not the same as null: nothing says the boost is on the recording.
271+
"without boosted_event_id": json.RawMessage(`{"boost_id":31337}`),
272+
"unreadable details": json.RawMessage(`{"boost_id":"x"`),
271273
} {
272274
t.Run(name, func(t *testing.T) {
273275
f := newFakeReads()

‎skills/basecamp-connect/SKILL.md‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ description: |
44
Drive your own Claude Code session from Basecamp. Starts the local connector
55
(basecamp connect), which listens to the account's event feed as a Basecamp
66
agent and prints one line per trusted request: a mention, an assignment, a
7-
comment on a thread the agent follows, a boost on something it wrote. This session acknowledges each one
8-
within seconds (a boost in its own words), picks the repo for the project,
9-
and hands the request to a background subagent that reads the context, does
7+
comment on a thread the agent follows, a boost on something it wrote. This
8+
session acknowledges each one that asks for something within seconds (a
9+
boost in its own words), picks the repo for the project, and hands the
10+
request to a background subagent that reads the context, does
1011
the work with your settings and tools, and replies as the agent. The session
1112
stays free to take the next request, and you can keep using it.
1213
Also connects an agent to this computer and manages its setup: the agent's

0 commit comments

Comments
 (0)