persona.agent fused Claude Code's name, version, and role into one string
(e.g. claude-code_2-1-233_agent), fragmenting it into ~96 distinct values in
analytics and making a simple "how many Claude Code sessions" query
impossible. Replace the hand-ported detect-agent signature table with the
upstream github.com/vercel/detect-agent module (13 -> 20 harnesses, also
fixes a live OpenCode detection bug), and split any version-shaped suffix
off the detected name into a new persona.agent_version field, canonicalising
the remaining name fragment against the module's own vocabulary rather than
a hand-written alias list. persona.agent now always reports "claude_code"
for Claude Code regardless of detection path.
Needs OSS/legal sign-off before merge: the upstream module ships no LICENSE
file (see PR description).
Summary
persona.agentfused Claude Code's name, version, and role into one string (claude-code_2-1-233_agent), fragmenting it into ~101 distinct values in analytics and making "how many Claude Code sessions" impossible to query. This splits name and version into two fields.detect-agentsignature table (cliv2/internal/persona/agent) with the upstreamgithub.com/vercel/detect-agentmodule — grows coverage 13 → 20 harnesses and fixes a live bug where plain-CLI OpenCode was undetected (our port matched the launcher's env var, not the one OpenCode itself sets).name[_/]version[_/role]), and canonicalises the remaining fragment againstdetect-agent's ownKnownAgentsvocabulary rather than a hand-written alias list.persona.agent_versionis written only when a version is found;persona.agentalways resolves toclaude_codefor Claude Code regardless of whether it was detected viaAI_AGENTor via environment signature.cursor-cli,codex, and every other already-clean harness are unaffected — no version-shaped suffix, so the regex doesn't match and the value passes through unchanged.Exact emitted string, flagging ahead of release per the spec: Claude Code will report as
claude_code(upstream's spelling), notclaude-code.Datadog: where the new field lands
Confirmed against live
analytics-servicelogs (service:analytics-service "analytics payload"). Today:After this change,
agent_versionis a new sibling key at the same nesting level, queryable as@analytics-service.interaction.extension.persona.agent_version:agent_versionis omitted from the payload entirely (not an empty string) whenever no version is parsed out.Blocker — needs OSS/legal sign-off before merge
github.com/vercel/detect-agenthas noLICENSEfile in its repo (confirmed via the GitHub API), despite tagged releases and external contributors. This needs sign-off before the dependency lands.Every third-party Go dependency in
cliv2ships its license text embedded in the CLI binary:cliv2/scripts/prepare_licenses.gorunsgo-licenses save ./...and writes oneLICENSE(orNOTICE/COPYING) file per module path undercliv2/internal/embedded/_data/licenses/, whichcliv2/internal/embedded/file.gopulls into the binary via//go:embed _dataand surfaces to end users as the OSS notices. This step runs as part ofmake configureincliv2/Makefile.Ran that script locally against this branch to see what it does with
detect-agent: it doesn't fail, it silently skips it. Exit 0, no warning naming the package, and nogithub.com/vercel/detect-agent/folder is created underinternal/embedded/_data/licenses/(confirmed viagit status— zero diff in that tree). So this isn't a build break that would get caught in CI; without this callout,detect-agent's code would ship inside the CLI binary with no bundled license text at all, and nothing in the existing pipeline would flag it.The script does have a manual-override path for cases
go-licensescan't auto-detect (github.com/davecgh/go-spew,github.com/alexbrainman/sspi,github.com/pmezard/go-difflib,go.devare each fetched from a hardcoded URL) — but those are cases where a license exists upstream andgo-licensesjust can't introspect it.detect-agenthas no license text anywhere to point that mechanism at, so that path doesn't apply here.For legal's context: this looks like a fixable oversight, not an unknown-license situation.
github.com/vercel/detect-agent'spackage.jsonexplicitly declares"license": "Apache-2.0"—go-licenses(and every other Go license tool) only reads an actualLICENSE/COPYING/NOTICEfile, so it can't see that declaration, and the repo has never committed one. Checked for an existing upstream issue/PR about it — there is none.If sign-off is refused (or upstream doesn't add a
LICENSE), the fallback is to keep the hand-port, apply the OpenCode fix directly, and implement the split/canonicalisation against a locally-maintained name list — the split logic itself is unaffected by that choice.Test plan
go test ./...passes incliv2andcliv2-privatego vet ./...andgolangci-lint runclean on changed packagesgo mod tidy -diffclean in bothcliv2andcliv2-privatecliv2/internal/persona/persona_test.go(TestReport_Agent) at the single seam where persona values are recorded onto analytics — extends the existing fake-analytics-recorder test rather than a new harness, usingt.Setenvper case (the upstream module reads the process environment directly, no injection point). Covers every real-world shape observed in production: fused underscore (both_agentand_harnessroles), legacy slash format normalising to the same bucket, bare signature detection, an already-clean harness, a fused identifier with no version-shaped segment, an identifier that fails to canonicalise, a redacted (***) identifier, and a version-shaped run with no trailing role.analytics-servicelogs: no harness other than Claude Code currently reports a version-shapedpersona.agentvalue at any real volume, confirming the ticket's Out-of-Scope call.persona.agent-value count and confirm Claude Code's ~101 buckets collapse to one, withpersona.agent_versionpopulated on those events.Ref: CLI-1771