feat(tools): Langfuse trace-export tool — API integration + sanitised report synthesis [NES-1690]#9240
feat(tools): Langfuse trace-export tool — API integration + sanitised report synthesis [NES-1690]#9240jaco-brink wants to merge 20 commits into
Conversation
…h, types [NES-1690]
…ion normalisation [NES-1690]
…tisedConversation [NES-1690]
…hesis, HTML report [NES-1690]
…nal PDF [NES-1690]
…tion, PII echo in debug output, LLM resilience, pagination bounds [NES-1690]
|
Warning Rate limit exceeded
You’ve run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (7)
WalkthroughThis PR adds a TypeScript CLI under tools/langfuse-export that fetches Langfuse traces, normalizes and sanitizes user PII, computes deterministic usage statistics, optionally synthesizes themes via OpenRouter, and renders shareable HTML (optionally PDF). It includes environment loading, CLI parsing, paginated fetching, test coverage, and per-run gitignored outputs. ChangesLangfuse Trace Export Implementation
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ 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 |
subject must not be sentence-case, start-case, pascal-case, upper-caseheader must not be longer than 100 characters, current length is 105 |
|
View your CI Pipeline Execution ↗ for commit b1cad85
☁️ Nx Cloud last updated this comment at |
|
The latest updates on your projects.
|
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (1)
tools/langfuse-export/src/cli.spec.ts (1)
44-60: ⚡ Quick winAdd a regression test for valued string-flag swallowing.
Please add a
parseArgs(['--model', '--debug'])case asserting--model requires a valueso this parse bug doesn’t regress.🤖 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 `@tools/langfuse-export/src/cli.spec.ts` around lines 44 - 60, Add a regression test that ensures a valued string flag cannot swallow the next flag: in the existing spec near the other throttle tests, add an it(...) that calls parseArgs(['--model', '--debug']) and expects it to throw matching the message `--model requires a value` (e.g., expect(() => parseArgs(['--model','--debug'])).toThrow(/--model requires a value/)). This uses the parseArgs function and mirrors the style of the other tests to prevent regressions where a missing string value is interpreted as the next flag.
🤖 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.
Inline comments:
In `@tools/langfuse-export/src/aggregate.ts`:
- Around line 13-16: The dayKey function currently slices any string with length
>= 10 which can produce bogus keys for malformed timestamps; update dayKey to
validate the input is a well-formed ISO timestamp (e.g., parseable by Date or
matches /^\d{4}-\d{2}-\d{2}/) and return 'unknown' for invalid or unparseable
values, otherwise return the YYYY-MM-DD prefix; modify the dayKey(iso: string)
implementation accordingly so callers get 'unknown' instead of arbitrary slices.
In `@tools/langfuse-export/src/cli.ts`:
- Around line 34-37: requireValue is treating any token (including flags like
"--debug") as a valid value, allowing option values to swallow the next CLI
flag; update requireValue(argv, index, flag) to validate that argv[index] exists
and does not start with '-' (treat tokens beginning with '-' as missing),
throwing the same Error(`${flag} requires a value`) when it does; apply the same
validation logic where similar parsing occurs (the other call sites around lines
noted) so functions that parse flags such as requireValue and the code paths
handling options at indices 68-70 and 82-88 all reject tokens that look like
flags rather than accepting them as values.
In `@tools/langfuse-export/src/langfuse.ts`:
- Around line 151-157: Wrap the paginated Langfuse API calls (the
client.fetchTraces(...) call at the shown block and the similar paginated call
at lines 187-193) with a bounded retry/backoff helper: implement a
retryWithBackoff(asyncFn) that retries on transient errors (HTTP 429, 5xx, and
network errors) using exponential backoff with jitter, a small maxRetries (e.g.,
3-5) and a maxDelay cap, and rethrows the error after max retries; replace the
direct client.fetchTraces(...) invocation with calls to retryWithBackoff(() =>
client.fetchTraces({...})) passing the same parameters (from: window.from, to:
window.to, orderBy, limit: pageSize, page) so page iteration logic is unchanged.
Ensure the helper only retries idempotent paginated GET-style requests and logs
each retry attempt for observability.
In `@tools/langfuse-export/src/normalize.ts`:
- Around line 127-131: The exclusion check uses
options.excludeMessageRegex.test(...) which is stateful for global/sticky
regexes; make it deterministic by resetting the regex before each test or
testing a cloned regex instance. Update the code where
options.excludeMessageRegex is used with turn.userMessage and
MAX_DISCRIMINATOR_TEST_CHARS to either set options.excludeMessageRegex.lastIndex
= 0 prior to calling test(...) or replace the call with
RegExp(options.excludeMessageRegex).test(...) so each iteration uses a fresh,
non-sticky regex.
In `@tools/langfuse-export/src/openrouter.ts`:
- Around line 58-69: When building the parsed theme objects in the map callback
(the block that creates label and sessionIds from record), deduplicate
sessionIds before returning so repeated IDs in the same theme are removed;
update the sessionIds assignment (which currently filters using validIds) to
produce a unique array (e.g., use a Set or Array.from(new Set(...))) while
preserving only ids that pass the existing (id): id is string &&
validIds.has(id) filter, then return that deduplicated sessionIds so the final
returned { themes } contains no duplicate IDs per theme.
---
Nitpick comments:
In `@tools/langfuse-export/src/cli.spec.ts`:
- Around line 44-60: Add a regression test that ensures a valued string flag
cannot swallow the next flag: in the existing spec near the other throttle
tests, add an it(...) that calls parseArgs(['--model', '--debug']) and expects
it to throw matching the message `--model requires a value` (e.g., expect(() =>
parseArgs(['--model','--debug'])).toThrow(/--model requires a value/)). This
uses the parseArgs function and mirrors the style of the other tests to prevent
regressions where a missing string value is interpreted as the next flag.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 1464379c-0538-4516-a60b-0668ade9f057
⛔ Files ignored due to path filters (1)
apps/journeys-admin/__generated__/NewCloudflareImage.tsis excluded by!**/__generated__/**
📒 Files selected for processing (25)
.gitignoredocs/plans/2026-05-21-001-feat-langfuse-trace-export-tool-plan.mdtools/langfuse-export/.env.exampletools/langfuse-export/README.mdtools/langfuse-export/fetch-env.shtools/langfuse-export/run.tstools/langfuse-export/src/aggregate.spec.tstools/langfuse-export/src/aggregate.tstools/langfuse-export/src/cli.spec.tstools/langfuse-export/src/cli.tstools/langfuse-export/src/env.spec.tstools/langfuse-export/src/env.tstools/langfuse-export/src/langfuse.spec.tstools/langfuse-export/src/langfuse.tstools/langfuse-export/src/normalize.spec.tstools/langfuse-export/src/normalize.tstools/langfuse-export/src/openrouter.spec.tstools/langfuse-export/src/openrouter.tstools/langfuse-export/src/pdf.tstools/langfuse-export/src/report.tstools/langfuse-export/src/sanitize.spec.tstools/langfuse-export/src/sanitize.tstools/langfuse-export/src/types.tstools/langfuse-export/tsconfig.jsontools/langfuse-export/vitest.config.mts
…line/ (pure) [NES-1690]
….env next to script) [NES-1690]
…ads; verified-shape parsing [NES-1690]
Legacy list endpoints (/api/public/traces, /api/public/observations — what the
langfuse SDK v3 wraps) time out on Langfuse Cloud. Switch reads to raw fetch:
v2 observations index (cursor) enumerates traceIds, then GET traces/{id} per
trace returns context + full nested observations in one by-id call. Confirmed
input/output shape via curl, so the tolerant multi-shape parser is replaced with
a precise extractor. Doppler home corrected to journeys/dev. Verified end-to-end
against live data (265 traces, load-test excluded, report rendered).
…race-export' into jacobusbrink/nes-1690-langfuse-trace-export # Conflicts: # tools/langfuse-export/README.md # tools/langfuse-export/src/langfuse.ts # tools/langfuse-export/src/pipeline/sanitize.spec.ts # tools/langfuse-export/src/pipeline/sanitize.ts
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
tools/langfuse-export/src/clients/openrouter.ts (1)
58-67:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winGuard parsed theme entries before reading properties.
themecan benull/non-object from model output. Accessingrecord.labelthen throws and drops the entire synthesis result instead of filtering just the bad entry.Suggested patch
const themes = parsed.themes .map((theme) => { - const record = theme as Record<string, unknown> - const label = typeof record.label === 'string' ? record.label : '' - const sessionIds = Array.isArray(record.sessionIds) + if (theme == null || typeof theme !== 'object') { + return { label: '', sessionIds: [] as string[] } + } + const record = theme as { label?: unknown; sessionIds?: unknown } + const label = typeof record.label === 'string' ? record.label : '' + const sessionIds = Array.isArray(record.sessionIds) ? record.sessionIds.filter( (id): id is string => typeof id === 'string' && validIds.has(id) ) : [] return { label, sessionIds } })🤖 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 `@tools/langfuse-export/src/clients/openrouter.ts` around lines 58 - 67, The mapping over theme entries reads properties from possibly null/non-object `theme`, causing a crash; update the mapper to first guard that `theme` is an object (e.g. typeof theme === 'object' && theme !== null) before casting to `Record<string, unknown>` and accessing `record.label`/`record.sessionIds`, and skip or return a safe default for entries that fail the guard so only valid theme objects are processed (refer to the existing mapper that uses `record`, `label`, and `validIds` for guidance).
♻️ Duplicate comments (2)
tools/langfuse-export/src/cli.ts (1)
37-40:⚠️ Potential issue | 🟠 Major | ⚡ Quick winPrevent option values from swallowing the next flag token.
At Line 39,
requireValueonly checks fornull, so inputs like--model --debugare parsed as a model value and drop--debugentirely.Suggested patch
function requireValue(argv: string[], index: number, flag: string): string { const value = argv[index] - if (value == null) throw new Error(`${flag} requires a value`) + if ( + value == null || + value.length === 0 || + value === '-h' || + value === '--help' || + value.startsWith('--') + ) { + throw new Error(`${flag} requires a value`) + } return value }Also applies to: 71-77
🤖 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 `@tools/langfuse-export/src/cli.ts` around lines 37 - 40, The requireValue function currently only checks for null and thus will accept the next token even if it is another flag (e.g., "--model --debug"); update requireValue to also reject tokens that look like flags by checking if the candidate value starts with "-" (or "--") and throw the same Error(`${flag} requires a value`) when it does. Apply the same change to the duplicate parsing logic referenced (the code block around the other option parsing at lines 71-77) so all option value checks (use the function name requireValue or its duplicate logic) consistently validate that a value is present and not another flag token.tools/langfuse-export/src/pipeline/aggregate.ts (1)
13-16:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winValidate day format before bucketing.
This still accepts any 10+ char string as a day key, which can create bogus buckets instead of falling back to
unknown.Suggested patch
function dayKey(iso: string): string { // YYYY-MM-DD from an ISO timestamp; '' if unparseable. - return iso.length >= 10 ? iso.slice(0, 10) : '' + if (iso.length < 10) return '' + const day = iso.slice(0, 10) + return /^\d{4}-\d{2}-\d{2}$/.test(day) ? day : '' }🤖 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 `@tools/langfuse-export/src/pipeline/aggregate.ts` around lines 13 - 16, The dayKey helper currently accepts any string >=10 chars; update dayKey(iso: string) to strictly validate the first 10 chars match the YYYY-MM-DD pattern (four digits, hyphen, two digits, hyphen, two digits) and return that slice only when it matches and represents a plausible date (e.g., month 01–12, day 01–31); otherwise return the fallback "unknown". Locate and change the dayKey function to perform a regex/date-component check before slicing so buckets are only created for valid ISO date prefixes.
🤖 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.
Inline comments:
In `@tools/langfuse-export/fetch-env.sh`:
- Around line 22-25: The current redirect using > "$OUT" truncates the target
before doppler runs and leaves the secrets file created with default umask; fix
by writing to a secure temporary file and only replacing $OUT after a successful
download: run doppler secrets download --no-file --format=env-no-quotes
--project journeys --config dev redirecting into a temp file (e.g., "$OUT.tmp"
or mktemp), set restrictive permissions (chmod 0600) on that temp file, verify
the command succeeded, then atomically move the temp into place with mv -f to
overwrite $OUT, and emit the existing message; ensure any temp file is removed
on failure.
---
Outside diff comments:
In `@tools/langfuse-export/src/clients/openrouter.ts`:
- Around line 58-67: The mapping over theme entries reads properties from
possibly null/non-object `theme`, causing a crash; update the mapper to first
guard that `theme` is an object (e.g. typeof theme === 'object' && theme !==
null) before casting to `Record<string, unknown>` and accessing
`record.label`/`record.sessionIds`, and skip or return a safe default for
entries that fail the guard so only valid theme objects are processed (refer to
the existing mapper that uses `record`, `label`, and `validIds` for guidance).
---
Duplicate comments:
In `@tools/langfuse-export/src/cli.ts`:
- Around line 37-40: The requireValue function currently only checks for null
and thus will accept the next token even if it is another flag (e.g., "--model
--debug"); update requireValue to also reject tokens that look like flags by
checking if the candidate value starts with "-" (or "--") and throw the same
Error(`${flag} requires a value`) when it does. Apply the same change to the
duplicate parsing logic referenced (the code block around the other option
parsing at lines 71-77) so all option value checks (use the function name
requireValue or its duplicate logic) consistently validate that a value is
present and not another flag token.
In `@tools/langfuse-export/src/pipeline/aggregate.ts`:
- Around line 13-16: The dayKey helper currently accepts any string >=10 chars;
update dayKey(iso: string) to strictly validate the first 10 chars match the
YYYY-MM-DD pattern (four digits, hyphen, two digits, hyphen, two digits) and
return that slice only when it matches and represents a plausible date (e.g.,
month 01–12, day 01–31); otherwise return the fallback "unknown". Locate and
change the dayKey function to perform a regex/date-component check before
slicing so buckets are only created for valid ISO date prefixes.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 37ba98bb-8db6-47f4-8e46-151ad522f7cc
📒 Files selected for processing (17)
tools/langfuse-export/README.mdtools/langfuse-export/fetch-env.shtools/langfuse-export/run.tstools/langfuse-export/src/cli.tstools/langfuse-export/src/clients/langfuse.spec.tstools/langfuse-export/src/clients/langfuse.tstools/langfuse-export/src/clients/openrouter.spec.tstools/langfuse-export/src/clients/openrouter.tstools/langfuse-export/src/clients/pdf.tstools/langfuse-export/src/env.tstools/langfuse-export/src/pipeline/aggregate.spec.tstools/langfuse-export/src/pipeline/aggregate.tstools/langfuse-export/src/pipeline/normalize.spec.tstools/langfuse-export/src/pipeline/normalize.tstools/langfuse-export/src/pipeline/report.tstools/langfuse-export/src/pipeline/sanitize.spec.tstools/langfuse-export/src/pipeline/sanitize.ts
💤 Files with no reviewable changes (2)
- tools/langfuse-export/src/clients/pdf.ts
- tools/langfuse-export/src/clients/openrouter.spec.ts
…tion) [NES-1690] NES-1688 now tags Langfuse traces with their deployment environment (production | stage | preview | development), so reports can be scoped. - --environment defaults to `production` (the share-worthy deliverable); `all` disables filtering and includes pre-NES-1688 untagged history. - Filters server-side via the v2 observations index query param (fewer per-trace fetches) plus an authoritative re-check against each trace's own environment. - Prints the selected env and a hint when a filter matches nothing. - Also renames the report's share target from "Aaron" to "Leadership". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🧹 Nitpick comments (1)
tools/langfuse-export/src/clients/langfuse.ts (1)
22-27: ⚡ Quick winNarrow
FetchOptions.environmentto a deployment-environment type.On Line 26,
environment?: stringis too permissive and allows invalid values if this client is reused outsiderun.ts. Prefer a shared union type (for example,production | stage | preview | development) for compile-time safety.As per coding guidelines,
**/*.{ts,tsx}: Define a type if possible.🤖 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 `@tools/langfuse-export/src/clients/langfuse.ts` around lines 22 - 27, Replace the permissive environment?: string in the FetchOptions interface with a narrow union type (e.g., DeploymentEnvironment = 'production' | 'stage' | 'preview' | 'development') and use that type for the environment property; either import the shared DeploymentEnvironment union if it already exists or declare and export it from this module so callers get compile-time safety when passing environment to the functions that accept FetchOptions (look for the FetchOptions interface and its environment property in langfuse.ts).
🤖 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 `@tools/langfuse-export/src/clients/langfuse.ts`:
- Around line 22-27: Replace the permissive environment?: string in the
FetchOptions interface with a narrow union type (e.g., DeploymentEnvironment =
'production' | 'stage' | 'preview' | 'development') and use that type for the
environment property; either import the shared DeploymentEnvironment union if it
already exists or declare and export it from this module so callers get
compile-time safety when passing environment to the functions that accept
FetchOptions (look for the FetchOptions interface and its environment property
in langfuse.ts).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 7172cfdb-5196-4b2a-88a2-15e8500ff915
📒 Files selected for processing (9)
tools/langfuse-export/README.mdtools/langfuse-export/run.tstools/langfuse-export/src/cli.spec.tstools/langfuse-export/src/cli.tstools/langfuse-export/src/clients/langfuse.spec.tstools/langfuse-export/src/clients/langfuse.tstools/langfuse-export/src/pipeline/normalize.spec.tstools/langfuse-export/src/pipeline/normalize.tstools/langfuse-export/src/types.ts
✅ Files skipped from review due to trivial changes (2)
- tools/langfuse-export/src/types.ts
- tools/langfuse-export/README.md
- cli: requireValue rejects flag-shaped tokens so `--model --debug` no longer swallows the next flag as a value - cli: parseDiscriminator wraps RegExp construction with a friendly error (pattern is engineer-supplied, not untrusted input) - aggregate: dayKey validates the YYYY-MM-DD shape so a malformed timestamp buckets under 'unknown' instead of producing a bogus day key - openrouter: parseThemes trims labels and dedups sessionIds per theme - fetch-env.sh: write secrets to a chmod 600 temp file then mv into place, so a failed download can't truncate .env and secrets aren't world-readable Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Review feedback addressed (2fbd5f7)Fixed:
Fixed (adjusted) + challenged:
Challenged (no change):
Outdated (won't-fix):
All 7 threads resolved. Tool suite green: 89 Vitest tests pass,
|
Review feedback addressed — round 2CodeQL regex-injection (recurring): dealt with at the alert level — dismissed alerts #340 + #341 ( Root cause of the repeat: resolving the PR review thread is cosmetic — CodeQL re-raises the alert on each scan (round 1's Rationale: internal CLI tool; |
Summary
Engineer-run TypeScript CLI at
tools/langfuse-export/that reads Apologist chat traces from the Langfuse Public API, scrubs free-text user PII, computes deterministic usage stats, and uses an OpenRouter LLM to synthesise a shareable HTML report (optional Playwright PDF). The engineer manually uploads the artifact to Google Drive for Aaron.Implements the NES-1656 export-path spike. Plan:
docs/plans/2026-05-21-001-feat-langfuse-trace-export-tool-plan.md.Layout
src/splits on the boundary that matters:pipeline/(pure, unit-tested transforms) vsclients/(external I/O, manual-verified). Only sanitisedpipeline/output crosses intoclients/openrouter, enforced by a brandedSanitisedConversationtype.Key decisions
fetch, not the SDK. The legacy list endpoints (/api/public/traces,/api/public/observations) that the langfuse SDK v3 wraps time out on Langfuse Cloud. So the tool pages the cursor-based v2 observations index to enumeratetraceIds, thenGET /api/public/traces/{id}per trace — a by-id read that returns trace context (sessionId,metadata,tags) and the full nested observations (input/output/usage/cost) in one call. Neither call scans a list, so neither times out. (Confirmed by curl against the live API; verified end-to-end — 265 traces fetched, load-test excluded, report rendered.)SanitisedConversationtype makes "scrubbed before any LLM call" a compile error if violated.--environmentis intentionally absent — the chat tracing code sets noenvironmenton traces (NES-1688), so it can't filter anything; load-test exclusion is--discriminatorinstead.journeys/dev(the chat app already has all four keys there); the tool fetches them into a gitignored tool-local.env. Output is gitignored (chat-derived content).Test plan
npx vitest run --config tools/langfuse-export/vitest.config.mts --coverage=false).npx tsc -p tools/langfuse-export/tsconfig.json --noEmit).--help, bad-arg, env-missing error,--throttle/--daysvalidation.input=[{role, content:[{type,text}]}],output= string) is now confirmed in code, not assumed.Code review
Reviewed via
ce-code-review(Tier 2, autofix) — correctness/security/adversarial/kieran-typescript/reliability/testing. Fixes applied: arg validation, debug-output PII echo removal, LLM resilience + timeouts, bounded pagination, dead-code removal,parseThemesextraction. Then a structure regroup (clients//pipeline/) and the v2 read-layer correction above.Known residuals (accepted)
--discriminator message:<regex>— mitigated by capping the tested input to 2 KB + a comment; a full re2/worker-timeout sandbox is deferred. Bounded: default/named discriminators are linear, and a pathological custom regex is the same footgun asgrep.regexScrubmisses some PII classes (SSN, card numbers, surnames, lowercase names) — documented best-effort; deferred to NES-1562.--llm-scrubis the stronger pass.Post-Deploy Monitoring & Validation
No additional operational monitoring required — this is a manual engineer-run CLI under
tools/, not wired into any app, service, CI, or deploy. It only reads from Langfuse + OpenRouter (egress) and writes local gitignored files.🤖 Generated with Claude Code — Opus 4.7 (1M context), compound-engineering /ce-plan → /ce-work
Summary by CodeRabbit
New Features
Documentation
Tests
Chores