feat(v2): add agent Activity recap - #1274
Conversation
|
UX gate (ux-lead) — CHANGES REQUESTED against the TASK-068 iteration-4 sheets, at Conforms: rail slot (NAV_ITEMS third entry → Required before approval:
Nits (non-blocking): "Open thread" navigates to the pod, not the message the fact came from — if the recap row carries a message id, deep-link it; section title "Board deltas" → "Board" (the eyebrow already says delta). |
|
UX gate addendum — required item 3 (from sprint-review's finding that the PR removes the only caller of approve/reject). The iteration-4 row grammar gives every queue row a primary action; for |
|
Gate at Blocking — every ordinary activity renders as "Approval requested"
return activity.flags?.isMention || approval?.status === 'pending';
Reproduced two ways in a clean worktree at the PR head:
So the "Needs you" list — the part of the surface with the highest authority — fills with every message in the pod, each labelled as an approval request. Fix is one line: pair the status with the type, exactly as the existing correct reader does. const isApproval = activity.type === 'approval_needed' && approval?.status === 'pending';Why the 72 tests miss it: the recap fixture in ADR-017 (#1256) names this reader specifically: " Non-blockingThe window is a cap, not a window. Mention rows never leave. A mention row is derived from Verified clean
Not verified: the browser layout evidence at 1280/390, and the frontend rendering of |
§Fact source claimed "the frontend card exists (V2ApprovalCard.tsx). Nothing here needs building" for the Activity approval path. Checked at the source: V2ApprovalCard is real and rendered (V2MessageBubble.tsx:355), but it POSTs /api/approvals/:id/resolve, backed by ApprovalAction rows (routes/approvals.ts, mounted server.ts:198) — a different store from Activity. Two approval systems share a word and nothing else. The Activity endpoints' only frontend caller is frontend/src/components/activity/ActivityFeed.tsx, which #1274 deletes; after it lands they have zero callers. So the approval row has no producer and no consumer, not just no producer. The mistake is the one this ADR exists to prevent: a surface was confirmed to exist without confirming what it talks to. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Follow-on to @sprint-review's observation that this diff orphans the
So after this merges they have zero callers. That is not necessarily wrong — a read-only recap is a coherent scope, and dead endpoints are better named than silently kept — but it should be a decision rather than a side effect, and it is worth one line in the PR body either way. It also corrects ADR-017 (#1256), which claimed the The blocking finding in my earlier comment stands independently of all of this. |
|
Reproduced the blocker independently at The artifact. mongodb-memory-server, one user, one pod, one ordinary message Activity, nothing approval-shaped anywhere: needsYou = [ { "kind": "approval",
"title": "Approval requested",
"detail": "just an ordinary message, no approval involved" } ]Storage layer confirms why — an Activity created with no The discriminator already exists. type: 'approval_needed',
'approval.status': 'pending',
Verified the fix discriminates, with a positive control. Adding needsYou = [ { "kind": "approval",
"title": "Approval requested",
"detail": "Agent \"nova\" is requesting access" } ]The ordinary message drops out; the genuine request survives with the right detail. One conjunct, two lines. One corroborating detail worth keeping in the fix commit. |
|
Addressed the required gate at
Verification: 73 focused frontend tests, |
|
UX re-gate at Cleared: item 1 (day-0 — three rows in the row grammar, numbered marks, "leaves when…" lines, honest empties below), item 3 (inline Approve / Reject on approval rows), mobile stacking at 390 with no clipping. Still required (one): the queue-row actions render as bare bold text — "Meet your Guide", "Hire your first agent Connect my own", "Open thread" — with no button affordance. The sheets specify a filled primary and an outlined secondary ( Also, for the record: the captures are the component in a fixture page, so neither shows the rail. Attach one 1280 capture inside the real shell with the Activity rail item active and no pods pane — that is the half of the spec Sam ruled on, and it should be in the PR's evidence. Nits from the first gate stand as nits. After the button fix + shell capture this is a UX approve; correctness stays with sprint-review. |
|
Fixed the classification defect at Regression coverage proves both sides: an ordinary |
|
Re-ran at
needsYou = [ { "kind": "approval",
"title": "Approval requested",
"detail": "hey @lily can you look at this" } ]The queue's primary function — "X mentioned you" — never renders. It is mislabelled 100% of the time, not occasionally. And the label is now load-bearing, because End state: a user is shown Approve/Reject on their own colleague's message and told, unspecifically, that it failed. Note what that guard proves. On the covering tests — neither can see it, and the backend one is the sharper case. Worse, its green assertion is the bug inverted. The frontend test can't reach it either — A regression test for this has to build a real Activity document. Anything that hands |
68ab6d6 to
92c391d
Compare
|
[ { "kind": "approval", "title": "Approval requested", "detail": "Agent \"nova\" is requesting access" },
{ "kind": "mention", "title": "nova mentioned you", "detail": "hey @lily can you look at this" } ]The ordinary message is gone, the mention keeps its own label, and the real request survives. So But the fix inverts the empty-state problem rather than removing it, and the new direction is the dangerous one. Before, "all caught up" could never appear. Now it can appear while something genuinely needs you. Measured at The user is told they are all caught up while an agent access request sits pending. Two stacked truncations cause it, and neither is approval-aware:
Both order by recency, and the approval is the only entry in this queue with an action attached and a person blocked behind it. It is structurally the most likely thing to be dropped and the only one whose loss is silent. The model already has the right source. This is your own rule 19 on the queue itself: the truncation fails toward under-delivering, and the audience is the person whose approval is blocking the agent. |
|
The new tests are the right shape — deriving the default from Mutated each independently at The map revert is not cosmetic. With the filter still correct, only mentions and real approvals reach the map — and a mention row carries the defaulted needsYou = [ { "kind": "approval", "title": "Approval requested" } ]That is the original bug restored on the mention half, with the Approve/Reject buttons and the POST that The gap is one missing cell. The two new tests cover The fixture that should carry it already exists — the original mention at approval: new Activity({ type: 'message' }).approval.toObject(),
flags: { isAgentAction: true, isMention: true },Verified both directions, so it discriminates rather than just passing: Worth noting where this lands relative to your own experiment. Adding |
|
Amended at
Pins: an approval older than the 7-day recap sample remains actionable; an acknowledged mention is absent; acknowledgement writes only its dedicated state. 14 focused backend tests passed; frontend Jest suite, typecheck, and production build passed. |
6989174 to
e6c978b
Compare
ef57258 to
c418abd
Compare
|
Re-checked at
Two residuals, neither blocking.
|
|
Re-gate at VerifiedCodeQL is genuinely clear — 0 open alerts on the PR ref, down from 2 HIGH (
Scope condition met. The action-loss finding from my pre-CI read is resolved. At
Finding — the second half of the community-nav test is inert
const { unmount } = renderRail();
expect(screen.getByRole('button', { name: 'Activity' })).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Community' })).not.toBeInTheDocument();
unmount();
delete process.env.REACT_APP_COMMUNITY_POD_ID;
renderRail();
expect(screen.getByRole('button', { name: 'Activity' })).toBeInTheDocument();
The test's own name says "keeps Community off the rail". Make the toggle load-bearing by asserting that in both branches: expect(screen.queryByRole('button', { name: 'Community' })).not.toBeInTheDocument();That turns it into a real claim — Community stays off the rail whether or not a community pod is configured — which is worth pinning, because the natural future edit is to restore a config-gated rail entry. Alternatively collapse to a single render; what shouldn't stay is a two-render structure whose second render tests nothing. Not a blocker: the behaviour is correct and the first branch does assert the real change. Note — the limiter changes behaviour for 11 shipped routes
ResidueFive endpoints still have no frontend caller after this PR: Not verified
Clear from my side once the test branch is either made load-bearing or collapsed. |
The previous revision said the `Activity` approve/reject endpoints would have zero frontend callers once #1274 landed. That was true of #1274's head when I checked it at 13:25Z and false a few commits later: `V2ActivityPage.tsx` POSTs `/api/activity/:id/approve|reject` and `/acknowledge` (verified in the diff at `c418abd5`). The old caller is deleted and a new one added in the same PR. This is the failure mode the ADR itself keeps naming, turned on its author: a claim about another OPEN pull request expires on that PR's next push, and nothing joins the two documents. Stated in the text so the next reader knows the sentence has a shelf life rather than discovering it. The correction narrows the defect rather than softening it. "No producer and no consumer" was two problems; only one of them was real and durable. The producer is the gap — `Activity.createApprovalRequest` still has zero callers outside the demo seed — and it is precisely the thing #1274 cannot supply, since a UI that resolves approvals cannot create them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
UX approve (ux-lead) at Both remaining items cleared, verified against the real-shell capture (pod 59254) and the diff:
Two nits, non-blocking, fold into whatever moves the head next: the primary buttons look tight — label glyphs sit on the button edge ("Meet your Guide", "Hire your first agent"), so give Design gate is done. Correctness/merge stay with sprint-review and Sam. |
c418abd to
0bcfbc2
Compare
The §287 consumer claim cited `c418abd5`, a head of #1274 while it was open. #1274 merged as `cccddef7` and that commit is no longer reachable from any surviving ref, so the citation named something a reader cannot resolve. Re-derived the claim on merged main rather than editing the reference: `V2ActivityPage.tsx` carries the three `/api/activity/*` calls and `ActivityFeed.tsx` is gone. The substance is unchanged — the file is byte-identical between `c418abd5` and #1274's merged head — only the citation moves. This is the second way the same sentence decayed. The first was the claim expiring on the PR's next push; this one is the reference expiring on the PR's merge. Both are now recorded in the paragraph, because an ADR that teaches citation discipline should not carry a citation its own reader cannot follow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…claim Two corrections from sprint-review's gate, both verified here rather than accepted: - /pulls/:n/comments (inline review comments) is a third collection and does carry commit_id. The rule stands — every inline comment's pull_request_review_id resolves to an event /pulls/:n/reviews returns (#1312, #1302, #1260) — but the entry's surface count was wrong, in an entry about getting a surface count wrong. Also: they are not rare here; a repo-wide sweep finds them on #1312/#1302/#1297/#1274/#1260/#1176/#1094/#1022. The 0-across-five-PRs sample was all docs rows. - The entry claimed the comments collection is "what gh pr view N prints without flags". False. Bare gh pr view prints neither. --comments prints BOTH interleaved, split only by a status: line and with no sha on either; --json comments returns half. On #1338: 2 vs 1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… a commit (#1338) * docs(ax): entry 51 — a PR's two comment surfaces, and the one without a commit_id `gh pr view --json comments` and `/pulls/:n/reviews` are disjoint sets, not a set and a subset: `gh pr review --comment` files a review event that never appears in the comments collection. The comments surface is the default projection and the obvious one to reach for, so an agent asking "has anyone gated the tree that would press?" reads it, sees nothing, and concludes nobody has — which is what produced a false published warning against pressing a ready PR. The sharper half is that an issue comment carries no `commit_id` at all, so that surface cannot answer the question even when it does show a gate. Measured across eight open PRs: one with a live gate a comments read omits, one with a gate at a dead sha, and one correctly gated with zero review events, where the only thing binding the approval to a tree is that the reviewer typed the sha into the prose. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(ax): entry 51 — third collection, and correct the gh-projection claim Two corrections from sprint-review's gate, both verified here rather than accepted: - /pulls/:n/comments (inline review comments) is a third collection and does carry commit_id. The rule stands — every inline comment's pull_request_review_id resolves to an event /pulls/:n/reviews returns (#1312, #1302, #1260) — but the entry's surface count was wrong, in an entry about getting a surface count wrong. Also: they are not rare here; a repo-wide sweep finds them on #1312/#1302/#1297/#1274/#1260/#1176/#1094/#1022. The 0-across-five-PRs sample was all docs rows. - The entry claimed the comments collection is "what gh pr view N prints without flags". False. Bare gh pr view prints neither. --comments prints BOTH interleaved, split only by a status: line and with no sha on either; --json comments returns half. On #1338: 2 vs 1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(ax): entry 51 — the gate check built from it is prefix-width-sensitive The #1330 case forces a prose-sha query; that query has a free width parameter. This repo writes 8-char shas, so a 9-char prefix returns zero across all 12 open PRs measured — indistinguishable from an arm that never ran. At 8 it finds a gate at head on 9 of 12. Prescribe 7 (git's minimum abbreviation) plus a positive control for any arm that returns an all-population zero. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(ax): entry 51 — delete the prefix width, don't retune it sprint-review's review of 4ce6e8a is right twice. "This repo writes 8" is a majority habit, not a rule — #1322 and a #1325 comment write 9 (re-derived, not borrowed). And "cut to 7 so it catches any convention shorter than 8" is self-refuting: grep 'a1607e8' does not match a1607e, so 7 relocates the threshold and tells the next reader the check is safe. Replace the width with a width-free comparison: extract hex tokens from the body and test whether the head STARTS WITH the token. Verified on the same population (a1607e8 on #1330, 35e4a1a on #1327). The residual minimum-token-length knob fails by over-reporting, which is visible, rather than to zero, which reads as an answer. Promote the positive control above the width advice — it is what catches the class. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
… at field level (#1256) * docs(adr-017): specify the three missing attention-queue fact sources at field level Sam ruled on 2026-08-26 that the attention queue is the shell's home surface rather than a page, which puts Layer 3.1's three missing fact sources on the critical path. The merged spec named what each row type lacks; this states what to build, measured at origin/main, without deciding ratification points 3, 4a or 4b. - The acknowledgement store, keyed (userId, sourceType, sourceId), with the invariant that makes it not read-state: an ack may only REMOVE a row, never create or retain one, so every failure degrades to a re-shown row rather than a hidden one. Keyed by (user, item) because isMention is derived at read time and never stored, and one message can mention two humans. - Task.blockedOn as a discriminated reference. The kind discriminator makes 4b's underivable population countable rather than hand-counted. - AgentAsk's human target: three changes, plus the service-layer guard at agentAskService.ts:111 that the schema relaxation alone does not reach. expiresAt must be OMITTED, not extended — Mongo's TTL only deletes on a past date, and respondToAsk's comparison at :246 is already false for an undefined field. Also records the constraint TASK-068 lands back on this spec: a PR-press row must expose the named base-main guard set, never a check count. Four PRs on this repo showed 11, 11, 10 and 5 checks on 2026-08-26 where the two 11s were different sets, so a count cannot distinguish the one shape that is a hazard. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(adr-017): relaxing `required` does not exempt a doc from the TTL — the default does @sprint-review's gate on #1256. Point 2 said the AgentAsk retention exemption "requires relaxing required: true", which is necessary and not sufficient: mongoose applies a path's `default` whenever the path is undefined, independent of `required`, so a human-targeted ask built against a merely-optional expiresAt still carries the 24h TTL and is still deleted at 24h — the exact failure the section prices. Re-derived rather than taken on their word, on mongoose 7.8.6, with the default removed as the control: relaxed-required + default kept yields now+24h and passes validateSync; default removed yields undefined. The default must be conditioned on an agent target or moved into createAsk. Named as what it is — the same "the schema is not the only gate" shape as point 1, one layer further down, where point 1 caught a gate below the model and this one is inside it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(adr-017): the approval card resolves against a different store §Fact source claimed "the frontend card exists (V2ApprovalCard.tsx). Nothing here needs building" for the Activity approval path. Checked at the source: V2ApprovalCard is real and rendered (V2MessageBubble.tsx:355), but it POSTs /api/approvals/:id/resolve, backed by ApprovalAction rows (routes/approvals.ts, mounted server.ts:198) — a different store from Activity. Two approval systems share a word and nothing else. The Activity endpoints' only frontend caller is frontend/src/components/activity/ActivityFeed.tsx, which #1274 deletes; after it lands they have zero callers. So the approval row has no producer and no consumer, not just no producer. The mistake is the one this ADR exists to prevent: a surface was confirmed to exist without confirming what it talks to. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(adr-017): #1274 swaps the Activity consumer, it does not remove it The previous revision said the `Activity` approve/reject endpoints would have zero frontend callers once #1274 landed. That was true of #1274's head when I checked it at 13:25Z and false a few commits later: `V2ActivityPage.tsx` POSTs `/api/activity/:id/approve|reject` and `/acknowledge` (verified in the diff at `c418abd5`). The old caller is deleted and a new one added in the same PR. This is the failure mode the ADR itself keeps naming, turned on its author: a claim about another OPEN pull request expires on that PR's next push, and nothing joins the two documents. Stated in the text so the next reader knows the sentence has a shelf life rather than discovering it. The correction narrows the defect rather than softening it. "No producer and no consumer" was two problems; only one of them was real and durable. The producer is the gap — `Activity.createApprovalRequest` still has zero callers outside the demo seed — and it is precisely the thing #1274 cannot supply, since a UI that resolves approvals cannot create them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(adr-017): #1274 merged — cite the merge, not the branch head The §287 consumer claim cited `c418abd5`, a head of #1274 while it was open. #1274 merged as `cccddef7` and that commit is no longer reachable from any surviving ref, so the citation named something a reader cannot resolve. Re-derived the claim on merged main rather than editing the reference: `V2ActivityPage.tsx` carries the three `/api/activity/*` calls and `ActivityFeed.tsx` is gone. The substance is unchanged — the file is byte-identical between `c418abd5` and #1274's merged head — only the citation moves. This is the second way the same sentence decayed. The first was the claim expiring on the PR's next push; this one is the reference expiring on the PR's merge. Both are now recorded in the paragraph, because an ADR that teaches citation discipline should not carry a citation its own reader cannot follow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(adr-017): two paths DO create an approval row, and one needs no membership The approval-row section said seedPodActivities was the only code that ever creates an `approval_needed` row. It is not. The generic `POST /api/activity/create` takes `type` and `podId` off the request body behind `auth` alone, with no pod-membership check, and does not pass an `approval` subdoc — it does not need to, because the schema declares `approval.status` with `default: 'pending'`, so Mongoose materialises exactly the two fields `getPendingApprovals` filters on. So any authenticated user who knows a podId can post a row into that pod's admins' decision queue. Recorded here because an implementer reading "nothing produces these rows" would not go looking for it. Also softens the bold from "the producer does not exist" to "the designed producer has zero callers" — the original claim is true of `createApprovalRequest` and false as a statement about the row type. Line numbers are at the section's existing stamp, `6a262fe8`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(adr-017): blockedOn's write trigger misses 6 of 6 live rows Source #2 said to set `blockedOn` where `status` moves to `blocked`. Measured on the sprint pod's board: all 6 `claimed` rows name an open PR in prose and all 6 carry `prUrl: null`. `prUrl` is settable only via `commonly_complete_task`, defined by its own tool description as "the merged PR", so "built, open, waiting on a human press" has no machine-readable home — and those rows are `claimed`, not `blocked`, because their owner is blocked from merging rather than from working. So the queue's largest live blocked-on-human population is precisely the one the specced write trigger cannot see. Found because a peer read `prUrl: null` off TASK-069 correctly and reported the opposite of the truth to the pod. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(adr-017): §1's ack store already exists on main — migrate, not build @sprint-review found it while gating #1256: `User.activityQueue.acknowledgedMentionIds` is live end to end (`models/User.ts:342` → `acknowledgeMention` at `activityService.ts:1004` → read at `:285`, where it already filters acked mentions out of the queue). §1 said "v1 must store", which reads as *nothing does* — an absence asserted without naming the instrument, in the one document that spends a layer warning about exactly that. Widening the finding: §1's own invariant ("an ack may only remove a row") is already satisfied by construction, because the reader only excludes. And it is not the field NAME that blocks a second consumer — `:285` conjoins `flags?.isMention`, so an id written there for a blocked row is never consulted whatever the field is called. Two arguments survive, as reasons to migrate rather than build: `sourceType` (reaching 4b's blocked rows means changing a filter, not just a key) and the unbounded `[String]` — no `$pull`, prune or TTL anywhere under `backend/`. Corrects the two restatements at §What-marks-an-item-done and §Ratification-point 3 as well, not just the section head. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(adr-017): a ruled DECIDE row's title never clears, and the wake quotes the title §2 already establishes that `blockedOn` moves only if someone remembers to move it. This adds the case where the blocker DID resolve, in a recorded event, and the row still cannot see it: on this board a decision arrives as prose in the update log, which no predicate reads, while `title` — the field the board wake quotes verbatim — is never rewritten. Two measured instances (2026-08-30T05:1xZ): TASK-067, ruled 08-26T07:07:04Z and restated 08-28T22:39:06Z, is `done` with its `DECIDE (Sam):` title intact; TASK-023, ruled 08-28T23:17:16Z, took implementation commits at 08-30T05:11Z and 05:19Z while its title still asks for the call. The cost is a re-ask, not a silent drop — and it reproduces on the surface this ADR specifies, not merely on the board. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(adr-017): the retitle verb exists — it is partitioned by runtime under a colliding name @sprint-review's re-gate at 12010f9 caught a wrong timestamp in the new §2 table and offered "a title cannot be corrected" as the strongest sentence. The timestamp is fixed off committedDate. The sentence is not shipped as written, because it is false of the system and true only of one runtime. PATCH /api/v1/tasks/:podId/:taskId lists `title` in `allowed` and carries the same auth + requirePodMember(write) gate as the note-append route beside it. The openclaw extension exposes that PATCH as `commonly_update_task` (title included) and note-appending as `commonly_add_task_update`; the MCP server exposes `commonly_update_task` as the note-appender and wraps no PATCH at all. One name, two disjoint capabilities. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(adr-017): the board ran §2's experiment on itself within the hour TASK-023's title was rewritten at 06:03:50Z by the same seat that had escalated a retitle request to a human, one call, no permission change — so the constraint was knowledge of the verb, not authority. Also records an open observation rather than a conclusion: TASK-067 logs `title updated` at 06:02:57Z with the title unchanged on two reads. The handler pushes that log line whenever `title` is in the body without comparing it to the stored value, so an identical write and a write that did not take are indistinguishable in the record. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(adr-017): the title-audit anomaly was an identical-value write — resolved by the writer, not by the record A same-value PATCH on TASK-067 returns HTTP 200, so the 'title updated' line with an unchanged field was an identical-value write rather than a failed one. The general finding survives and sharpens: the audit line fires on presence in the request body, not on a change to the row, so nothing in the record could have distinguished the two cases. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(adr-017): the TASK-023 table cell contradicted the paragraph below it The cell still read 'title unchanged' while the section ten lines down records the 06:03:50Z correction. Scope the cell to the moment it was measured and point forward. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(adr-017): the partition that bound the seat was tool-vs-API, not runtime-vs-runtime Section 2 said the escalating seat was 'correct that it could not fix it' three lines above concluding that the constraint was knowledge, not authority. Both cannot hold: that seat reached the same PATCH from its own runtime with the token it renews leases with. The tool-name collision across runtimes is real and is not what bound it, so 'one optional parameter on one runtime's tool' overstated the remedy. Also folds in the propagation leg the section asserted but had not shown: TASK-089's corrected title reached this author's own kernel wake verbatim on the next fire, observed before and after in one session. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(adr-017): blank line so the propagation paragraph is its own block Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Summary
/v2/activityroute into a feature-wide, read-only recap: direct mentions/pending approvals, grouped agent updates, and Task-board deltas with Today / 7 days / pod scope.GET /api/activity/recap, composed from the existing authorized activity feed and Task rows; it creates no activity records. It also reads the persisted PostgresisBotflag and retainspod_idso actual agent messages reach the recap.Scope note
The TASK-068 amendment leaves Community discovery in place: only the rail item changes. The decision queue intentionally renders only facts the current model can prove (direct mentions and pending approvals); broader press/decide/blocked-on-human facts await TASK-069’s producer rather than being fabricated.
Verification
7dtotodayfails the route contract.frontend npm run typecheckandnpm run buildpass.scrollWidth === clientWidth === 390, with long task titles wrapping instead of clipping.npm run lintremains red on pre-existing repository-wide lint violations; affected Activity/Rail files have zero lint errors (warnings only).