feat(r5d): office cycle-scorecard consumer (event-driven, construct-from-correlationId) - #30
Merged
Merged
Conversation
…nstruct-from-correlationId) Consumes trading-lab R5c-lab contract (lab#180). ScorecardFollower reacts to cycle.scorecard.built, bootstraps correlationId, constructs canonical /v1 path (branded), fetches text/markdown, publishes to operator chat. Single-flight + coalesced wake-up + TTL-safe + exactly-once + bounded tombstone. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…plete() helper, stop-safety, env surface, honest exactly-once/drift wording Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…h + deferred), missing state-machine tests, activeByTask guard, terminal-first complete(), testable fan-out, Bearer/MIME asserts, deploy passthrough as separate Lab PR Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ersal (defense in depth) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…mmaryLinks.scorecardUrl Deviates from task-1-brief's dot-guard: single %2E percent-encoding of a "."/".." correlationId does not block new URL() path-collapse under the WHATWG URL spec (%2e is decoded and still treated as a dot segment). Fixed by percent-encoding the "%" too in that branch so the literal %2E text survives normalization undecoded; verified with new URL(...).pathname. Real Lab correlationIds are UUID-shaped, so this branch never fires in practice. Details in .superpowers/sdd/task-1-report.md.
…d (brief's %2E was collapsible) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…d/transient/permanent Self-contained never-throws fetch for the cycle scorecard (text/markdown), mirroring getCompletionSummary's degrade-to-null contract. Consumes the branded ValidatedScorecardPath from Task 1; MIME is normalized (charset param stripped) before matching text/markdown.
… wake-up, TTL-safe, stop-safe Task 3 of R5d: reacts to cycle.scorecard.built agent events, bootstraps correlationId from the anchor task, fetches scorecard markdown via the canonical path, and publishes an operator_assistant_message. Single-flight via pendingByTask/activeByTask/doneByTask guards, coalesced wake-up during an in-flight fetch, TTL-during-fetch deferral, and a terminal-state-first complete() so a throwing publish subscriber can never strand a reg. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…SCORECARD_* env surface Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…flag-gated, independent of downstream watcher) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… OfficeEvent cast (final-review cleanups)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
R5d — Office Cycle-Scorecard Consumer
Makes the operator see the Lab cycle scorecard. When a Lab research cycle closes, Lab persists a
CycleScorecardand emits acycle.scorecard.builtagent event (R5c-lab, trdlabs/lab#180). Office now reacts to that event, fetches the scorecard Markdown from Lab, and posts it into the operator conversation. Behind an off-by-default flag.Upstream contract:
docs/superpowers/specs/2026-07-15-r5d-office-cycle-scorecard-consumer-design.mdreferences lab R5c-lab spec + PR #180.Why event-driven, construct-from-correlationId
The scorecard is a cycle-level artifact that lands at cycle close — much later than the run-cycle task completes. Lab writes the row before emitting the event, so reacting to
cycle.scorecard.builtgives a near-zero 404 window and needs no guessing of cycle duration. Office constructs the canonical/v1/cycles/${encodeURIComponent(correlationId)}/scorecard?format=markdownpath from acorrelationIdobtained through the trusted agent-event stream — it never follows a DTO-supplied URL (security invariant). The publishedLabSummaryLinks.scorecardUrlis declared for contract fidelity but not trusted at runtime. Because the follow anchor can be a chainedstrategy.onboardtask, it's namedanchorTaskId.What ships (5 code commits)
ValidatedScorecardPath+buildScorecardPath—getScorecardMarkdownaccepts only the branded type, so no unvalidated string reachesfetch. Guards against a./..correlationId collapsing the path via URL normalization (%252Edouble-encoding — WHATWG URL treats even%2e%2eas a dot segment).TradingLabHttpClient.getScorecardMarkdown— never throws; returnsok | not_found | transient | permanent(sendsAccept: text/markdown+ bearer, normalized MIME check).ScorecardFollower— event-driven state machine: bootstrapcorrelationIdfrom the anchor's agent events, recovery probe, single-flight per correlation, coalesced wake-up (an event during a fetch is never dropped), TTL/fetch race-free (TTL mid-fetch defers viaexpired, never publishes failure alongside an in-flight success),complete()records terminal state before a best-effort publish (a throwing subscriber can't strand a reg),pendingByTask ∪ activeByTask ∪ doneByTaskregister guard, bounded tombstones, transient-exhausted → idle (TTL is the backstop), permanent & bootstrap-exhaustion →unavailableonce, and stop-safety. Publishes the sameoperator_assistant_messageshape asDownstreamBacktestWatcher.OPERATOR_CYCLE_SCORECARD(off by default, trading-lab mode only, independent ofOPERATOR_DOWNSTREAM_BACKTESTS) +OFFICE_SCORECARD_*guards +apps/server/.env.example.makeRunCycleFanouthelper fansonRunCycleTaskto both the backtest watcher and the follower; the follower is constructed independently of the downstream watcher and stopped in shutdown.Guarantees & non-goals
importBoundarytest green).Verification
npm run typecheck -w @trading-office/serverclean;npm test -w @trading-office/server— 37 files / 263 tests pass (18 new state-machine tests exercise coalesced wake-up during a real in-flight fetch, TTL-during-fetch, terminal-first-then-throwing-publish, stop-during-bootstrap/fetch; deferred promises + microtask flush for deterministic timing).%2Edot-guard was WHATWG-collapsible — was caught empirically by the implementer and fixed to%252E.Rollout (order)
OPERATOR_CYCLE_SCORECARD/OFFICE_SCORECARD_*through to Office inlab/docker-compose.demo.yml(deploy config; deliberately not in this PR since it's a lab-repo change).🤖 Generated with Claude Code