feat: cross-harness observability hooks — the dashboard now mirrors terminal activity on every harness (#251)#252
Conversation
New `rstack-agents observe`: reads a Claude Code PostToolUse/Stop/SessionEnd hook payload on stdin (or --event-type/--tool/--summary flags), resolves the active run via RSTACK_RUN_ID/latest (the same helpers guard uses), and appends a normalized event to that run's events.jsonl in the EXACT shape Pi writes (type/tool/input/summary/ts) plus a source label — so the Business Hub renders harness activity on Claude Code / Tau / Operator identically to a Pi run. Hard rules (observability, not a gate): never blocks (always exit 0), never throws out of the handler (best-effort), value-safe (truncate ~1200 chars + redact secret paths, inline credentials, and content fields), no active run = silent no-op (never creates run state), fast (one locked atomic append). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nd) (#251) CLAUDE_CODE_HOOKS now installs the observability writer alongside the guard: PostToolUse (Bash|Write|Edit) plus Stop and SessionEnd all call `rstack-agents observe --source claude-code`, so ordinary terminal edits reach the Business Hub within one poll cycle — the terminal and dashboard finally agree on Claude Code, not just Pi. Pins the new hook shape in the init test, updates docs/integrations/claude-code.md (hook block, the 'what remains Pi-only' note now excludes event logging, an Observability section with the observe contract + verification), and the init report/next-steps text. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tau exposes a real post-execution `tool_result` hook, so the adapter now
registers @tau.on("tool_result") and feeds `rstack-agents observe --source tau`
(normalized tool_result event, same shape Pi writes) — Tau terminal activity
finally shows in the Business Hub. The existing pre-execution tool_call guard
hook also emits a tool_call INTENT event before the verdict, so activity is
visible even when a call is blocked.
Both are best-effort: subprocesses that always exit 0, never awaited for a
verdict, every error swallowed. Guard enforcement is unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
claude-code: verifies a PostToolUse/Stop/SessionEnd hook invokes `rstack-agents observe` (WARN with a paste-in fix if absent — additive, never FAIL). tau: verifies the adapter file emits observe events on its tool hooks. Mirrors the guard-hook check pattern; every problem prints its fix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
buildActivityFeed already renders harness tool_call/tool_result events via the minute-bucketed burst rollup (source-agnostic), so observe-written events show up with no Pi-only assumptions. This adds an honest source label: a burst dominated by a non-Pi source reads '... (via claude-code)' and carries data.source. Pi events (no source field) stay labeled implicitly as before. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…251) Covers: PostToolUse→tool_result shape+source, PreToolUse→tool_call intent, no active run = silent no-op (nothing created), secret values never written verbatim (AWS key / password field / huge content field), malformed + empty stdin exit 0 without throwing, --run-id / RSTACK_RUN_ID targeting, flag-driven events, and observe-written events flowing through getRunsForRoot + buildActivityFeed (burst names the source). Plus unit coverage for normalizeObservation + sanitizeInput. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
|
Warning Review limit reached
Next review available in: 49 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughAdds a new ChangesCross-harness observability
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Harness as Claude Code / Tau
participant Hook as PostToolUse Hook
participant ObserveCLI as rstack-agents observe
participant Normalize as normalizeObservation
participant EventsFile as events.jsonl
participant Dashboard
Harness->>Hook: tool call/result payload
Hook->>ObserveCLI: pipe JSON via stdin
ObserveCLI->>Normalize: parse & sanitize payload
Normalize-->>ObserveCLI: normalized event or null
alt active run exists
ObserveCLI->>EventsFile: append event with source label
else no active run
ObserveCLI-->>Hook: silent no-op
end
ObserveCLI-->>Hook: exit code 0
Dashboard->>EventsFile: poll events.jsonl
EventsFile-->>Dashboard: normalized events
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
From the #252 adversarial review: PostToolBatch fires parallel observe subprocesses at one run; assert every append lands as valid JSON, none lost, all exit 0 under lock contention. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adversarial security review — PASSED, no blockersIndependent attack-first review focused on the two launch-killer classes for a hook that runs in every user session. Zero BLOCKING/HIGH findings. Session disruption (highest priority) — HELD: every path in Secret leakage — HELD: redaction runs before truncation; secret-looking keys dropped, content/body/new_string collapsed to length notes, inline credential patterns (AWS keys, GitHub/Slack tokens, JWTs, bearer, password=/token:) replaced with Integrity — HELD: events are JSON-encoded (crafted tool names/ Follow-through: the one reviewer recommendation worth acting on — a concurrency test for parallel bursts (PostToolBatch) — was added ( Reviewer verdict: solid, strictly additive, no risk to enforcement or user sessions. Ready to merge. 🤖 Generated with Claude Code |
There was a problem hiding this comment.
🧹 Nitpick comments (3)
src/observability/dashboard/state/feed.js (1)
10-38: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valuePer-source burst tracking and dominant-source labeling logic is correct.
The dual-map approach (
toolBursts+burstSources) is sound — both are populated in the same loop soburstSources[min]is guaranteed non-empty whencount >= 3. Edge cases (missingts, non-string/emptysource) are properly guarded.One minor inconsistency:
dataon line 38 is set to{ source: dominant }even whendominant === 'pi', but thevialabel on line 29 is only added for non-pi sources. This means pi-dominant bursts getdata: { source: 'pi' }without a corresponding human-readable label. Consider aligning thedatacondition with theviacondition for consistency.♻️ Optional alignment for data field condition
- data: dominant ? { source: dominant } : undefined, + data: via ? { source: dominant } : undefined,🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/observability/dashboard/state/feed.js` around lines 10 - 38, The burst feed labeling in feed.js is inconsistent because the `data` payload on the `activityFeed.push` entry still includes `source: dominant` even when the dominant source is `pi`, while the human-readable `via` suffix is only shown for non-pi sources. Update the `activityFeed.push` logic in the tool-burst aggregation block so the `data` field follows the same condition as `via`, using the `dominant` source only when it is a real harness source and omitting it for `pi`.src/commands/observe.js (1)
119-119: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winContent-key matcher is substring-based and over-redacts common keys.
/(content|contents|body|data|...)/imatches any key containing the substring, so benign fields likemetadata,database, orformDataget their values collapsed to[N chars omitted]. Since the point of this writer is dashboard fidelity, consider anchoring to whole-key names to avoid silently dropping useful, non-secret input.♻️ Anchor to exact key names
- if (/(content|contents|body|data|new_string|old_string)/i.test(key)) { + if (/^(content|contents|body|data|new_string|old_string)$/i.test(key)) { out[key] = typeof value === 'string' ? `[${value.length} chars omitted]` : REDACTED; continue; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/commands/observe.js` at line 119, The key matcher in the observe writer is too broad because the regex in the redaction check matches substrings, causing non-sensitive fields like metadata or database to be collapsed. Update the condition in observe.js to match only exact key names in the content-key path, using the existing key variable in the same redaction block, so only intended fields are redacted and dashboard fidelity is preserved.src/integrations/tau/rstack_sdlc.py (1)
366-389: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winConsider making
_emit_observationnon-blocking to avoid adding latency to every tool call.
await proc.communicate(...)blocks the caller until the observe subprocess finishes. In thetool_callhook this adds a full subprocess round-trip to the critical path before the guard even runs, and intool_resultit delays delivering the result back to the user. With no timeout, a slownpxinvocation can hang a Tau session indefinitely.Since the observe result is never needed, fire-and-forget via
asyncio.create_task()would remove this from the critical path while preserving the best-effort contract. Adding a timeout as a safety net would also help.♻️ Suggested non-blocking emission
async def _emit_observation(payload: dict, cwd: str) -> None: """Feed one observability event to `rstack-agents observe` (`#251`). ... """ npx = shutil.which("npx") if npx is None: return try: + data = json.dumps(payload).encode("utf-8") proc = await asyncio.create_subprocess_exec( npx, "--yes", "rstack-agents", "observe", "--source", "tau", "--project", cwd, stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.DEVNULL, env=os.environ, cwd=cwd, ) - await proc.communicate(json.dumps(payload).encode("utf-8")) + proc.stdin.write(data) + await proc.stdin.drain() + proc.stdin.close() + # Reap the process in the background — prevents zombies without blocking the caller. + asyncio.create_task(proc.wait()) except Exception: pass # observability is additive — never let it break a sessionThen in the
tool_callandtool_resulthooks, replaceawait _emit_observation(...)withasyncio.create_task(_emit_observation(...))to avoid blocking the hook entirely. Since_emit_observationalready swallows all exceptions, the fire-and-forget task is safe.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/integrations/tau/rstack_sdlc.py` around lines 366 - 389, The _emit_observation path is still blocking the caller because it awaits proc.communicate, which can add latency or hang the Tau session. Update _emit_observation in rstack_sdlc.py to be truly best-effort by making the hook fire-and-forget with asyncio.create_task when called from the tool_call and tool_result hooks, and add a timeout or other safety guard around the subprocess wait so slow npx observe invocations cannot block the critical path. Keep the existing exception swallowing behavior so observability remains non-disruptive.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@src/commands/observe.js`:
- Line 119: The key matcher in the observe writer is too broad because the regex
in the redaction check matches substrings, causing non-sensitive fields like
metadata or database to be collapsed. Update the condition in observe.js to
match only exact key names in the content-key path, using the existing key
variable in the same redaction block, so only intended fields are redacted and
dashboard fidelity is preserved.
In `@src/integrations/tau/rstack_sdlc.py`:
- Around line 366-389: The _emit_observation path is still blocking the caller
because it awaits proc.communicate, which can add latency or hang the Tau
session. Update _emit_observation in rstack_sdlc.py to be truly best-effort by
making the hook fire-and-forget with asyncio.create_task when called from the
tool_call and tool_result hooks, and add a timeout or other safety guard around
the subprocess wait so slow npx observe invocations cannot block the critical
path. Keep the existing exception swallowing behavior so observability remains
non-disruptive.
In `@src/observability/dashboard/state/feed.js`:
- Around line 10-38: The burst feed labeling in feed.js is inconsistent because
the `data` payload on the `activityFeed.push` entry still includes `source:
dominant` even when the dominant source is `pi`, while the human-readable `via`
suffix is only shown for non-pi sources. Update the `activityFeed.push` logic in
the tool-burst aggregation block so the `data` field follows the same condition
as `via`, using the `dominant` source only when it is a real harness source and
omitting it for `pi`.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 55b498a5-0013-4277-bd21-d12775252f9d
📒 Files selected for processing (10)
bin/rstack-agents.jsdocs/integrations/claude-code.mdsrc/commands/doctor.jssrc/commands/observe.jssrc/integrations/init.jssrc/integrations/tau/rstack_sdlc.pysrc/observability/dashboard/state/feed.jstests/doctor.test.jstests/integrations-init.test.jstests/observe-cli.test.js
Closes #251. Root cause of the reported terminal↔dashboard mismatch: the Business Hub derives everything from
events.jsonl, but only Pi's native extension wrote tool events — the guard on Claude Code/Tau/Operator is read-only, so the dashboard was blind to ordinary terminal work on 3 of 4 harnesses.Six bisected commits:
rstack-agents observe(src/commands/observe.js): framework-neutral observability writer. Reads PostToolUse/Stop/SessionEnd hook JSON on stdin (or flags), resolves the active run (silent no-op when none — nothing written, exit 0), appends Pi-shaped events (tool_call/tool_result/session_shutdown+sourcelabel) toevents.jsonlunder lock.observe --source claude-code, alongside the existing guard; pinned-shape test + claude-code.md updated.@tau.on("tool_result")(real post-execution event, verified against Tau 0.6.1) emittingsource:"tau"events + atool_callintent on the pre-exec hook so blocked calls still appear. Guard enforcement unchanged.sourcesurfaced on tool-call activity.Session-safety invariants (this runs inside users' live sessions):
.rstack/runsis never created by observation.[redacted], ~1200-char truncation matching Pi. Tested with real credential patterns + a 50KB payload.Gates: typecheck, 847/847 tests, lint, validate (196), security-audit, diff-check, Tau AST — all green.
Adversarial review of the session-safety + redaction surface follows on this PR before merge.
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
observecommand to record tool activity and session events.Bug Fixes
observeis best-effort and will not interrupt a session, even on errors or missing run state.Documentation