Repository navigation
Conversation
The worker used the default 3 second internal fetch budget, but Django gives the gateway 5 seconds and maps a gateway timeout to a 503 for the worker to retry. The worker aborted first, discarded late answers, and sent the same classification again. callInternalApi now accepts an optional per-attempt timeoutMs. The classify step passes 7 seconds. Other callers keep the default. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Generated-By: PostHog Desktop Task-Id: 8cb2ba00-a783-4633-a0ce-424140c553eb
The AI usage report groups workflow AI spend by the team_id event property. The classify call sent only hog_flow_id, so any billable Jev generation grouped under team 0. The call now sends team_id with the other labels, as the Anthropic gateway client does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Generated-By: PostHog Desktop Task-Id: 8cb2ba00-a783-4633-a0ce-424140c553eb
The classification endpoint refuses more than 16 categories or a description over 500 characters with a 400, so such a step failed on every run. A strict workflow save now runs the endpoint's own categories serializer on the step inputs. The mocked test run checks the same limits, so the test panel no longer reports success for inputs a live run rejects. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Generated-By: PostHog Desktop Task-Id: 8cb2ba00-a783-4633-a0ce-424140c553eb
The classification endpoint mapped a gateway rejection to 502, which the worker retries, so a permanent rejection reached the gateway three times. A gateway 5xx also got the rejection message that blames the step inputs. A rejection now returns 422, which the worker does not retry, and a gateway 5xx returns 503 with the busy message. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Generated-By: PostHog Desktop Task-Id: 8cb2ba00-a783-4633-a0ce-424140c553eb
The classification endpoint forwarded any context size to the gateway. It now rejects a context over 65,536 characters of JSON with a 400 that names the limit, the same state limit the ml_inference decide API uses for the same model. The worker checks the same limit, so a mocked test run fails on the same input as a live run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Generated-By: PostHog Desktop Task-Id: 8cb2ba00-a783-4633-a0ce-424140c553eb
The step becomes a native AI decision action instead of a function template, so the Django route, its JWT purpose, audience, and secret take the ai_decision name. The template, its bytecode fixture, the postHogClassify async function, the callInternalApi timeout option, the flag-gated template entry, and the frontend flag and toolbar node are removed. Co-authored-by: Harley Alexander <harley.a@posthog.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The decide route now asks the hosted decision model through ml_inference decide_when_available, with privacy mode on, ai_product "workflows", the invocation as trace id, and the workflow and step as labels. The question goes only into the instructions and the rendered context only into the state. Before the model call the route checks, in order: the workflows-ai-decision flag for the organization, AI data processing approval, AI credits, an 8 KiB cap on the compact UTF-8 state, and a per-team then global token bucket. The answer is an envelope: 200 succeeded with every answer's probability, 200 failed with a terminal code and run-log message, 429 with Retry-After for admission or gateway throttling, and 503 when the gateway or Redis is unavailable. Logs carry codes only, never the state or a gateway body. Co-authored-by: Harley Alexander <harley.a@posthog.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ai_decision joins the supported action types. A save validates its config with the same serializer the decide route uses, so a step that saves cannot fail at run time on its own question, options, or thresholds. Only inputs.context is templated: it compiles through a fixed server-side inputs schema, and the question and options stay plain text that the executor never renders. The branch slot count is one edge per answer plus Unsure when it is on, and a strict save requires every one of those edges. A new ai_decision is rejected while the workflows-ai-decision flag is off for the organization, and one an active flow already holds keeps saving. The test invocation request takes an optional mock_answer and passes it to the CDP for the mocked walk. Co-authored-by: Harley Alexander <harley.a@posthog.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…onfigured get_client raises ImproperlyConfigured, not RedisError, when no Redis URL is set, so the admission check now answers 503 for both instead of erroring. Co-authored-by: Harley Alexander <harley.a@posthog.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- A stored ai_decision no longer grandfathers a flag-gated template saved under the same action id, and the reverse: each gate keeps its own set of stored action ids. - A gateway 200 the facade cannot read, and a pick_one answer whose probabilities do not match the options or fall outside 0 to 1, fail as model_refused. Any other 4xx fails as gateway_unavailable. Only 5xx and an unreachable gateway answer 503, so a retry never repays for a failure that repeats. - A flag evaluation error answers 503 at run time instead of failing the person for good. Saves still fail closed. - The team gets its admission token back when the global bucket denies the request. - A rendered context may hold null values and lone surrogates. - answer_type is required, so options sent without it are not saved as a yes or no step. - 503 logs carry the cause, throttling logs at debug, and the admission budgets are checked at startup. Co-authored-by: Harley Alexander <harley.a@posthog.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
posthog-foss ships without ee, so the billing import now sits inside the guarded lookup and a missing module fails open like any other lookup error, instead of a 500. Co-authored-by: Harley Alexander <harley.a@posthog.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A local pnpm install reordered two importer entries, and the previous commit picked it up. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- A lone surrogate in the rendered context becomes U+FFFD before the size check and the gateway call, which could not encode it and answered 500. - A flag the SDK cannot evaluate (definitions not loaded) answers 503 at run time instead of failing the person for good. - The global bucket's Redis failure no longer triggers a refund on the default Redis timeouts. - The route goes through the workflows facade, and the decision contracts move into it, so presentation imports only presentation and facade modules. The shared config serializer moves to its own validation module. - products.workflows declares its facade-only dependency on products.ml_inference. - Throttling carries its source into a route outcome counter. - Tests prove the gates spend no admission token, the flag rejects a second decision on an active flow, and a builder draft saves before its outputs are wired. Co-authored-by: Harley Alexander <harley.a@posthog.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- A state nested deeper than the decision contract reads fails as model_refused before admission, instead of a 500 whose log quoted the state. The call contract keeps the state out of its repr. - The decision contracts carry team_id instead of a Team model, and the service loads the team, as the products architecture asks of a facade. - A strict save checks AI decision edges only when it sends the graph, so an unrelated programmatic edit of an unwired builder draft still saves. - Admission spreads Retry-After over the bucket's refill window, so a throttled batch does not retry all at once. - Terminal failures carry a log-only cause, organization-level codes log at debug, and gateway_unavailable no longer blames the customer's deployment. - Tests prove the dedicated signing key, the credit lookup key, that a throttled request never reaches the model, the global bucket's Redis failure, and that a flag the SDK cannot evaluate blocks a save. Co-authored-by: Harley Alexander <harley.a@posthog.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- A strict save requires an AI decision's 'continue' edge unless on_error is abort, because a failed decision follows it and the runtime stops without it. - The /graph endpoint reports AI decision edge errors only through validate_graph, together with the rest of the graph's errors. - A gateway 429 spreads Retry-After so a throttled batch does not retry at once. - Failure causes that hit every decision on the deployment (gateway status, region without decisions) log at warning; per-organization causes stay at debug. - Tests prove the per-team bucket spends no global token, the state never reaches a log on the unreadable-state path, and grandfathering holds when the flag cannot be evaluated. Co-authored-by: Harley Alexander <harley.a@posthog.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Malformed pick_one options give an AI decision no answer slots instead of a 500 from the graph checks. - The team-token refund on a global denial uses the short-timeout admission Redis client; token_bucket.refund takes an optional client like consume. - Failure causes are a typed set with the gateway status in its own field; only causes that fail every decision (gateway_unavailable, a region without decisions, an answer that does not fit its question) log at warning. - The route declares its 400 response and says 503 carries no Retry-After; the settings comment states the worker bound the right way round. - Tests use an explicit failure-edge switch, cover abort-on-error without the failure edge, and prove an unreadable state spends no admission token. Co-authored-by: Harley Alexander <harley.a@posthog.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…qa round - The failure reasons get one named Literal that the route's log rule takes, and the option bound has one name in the validation module. - The log-leak test renders logged exceptions before it looks for the state, and a programmatic save that sends only edges is shown to need every answer edge. Co-authored-by: Harley Alexander <harley.a@posthog.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A strict save accepted an answer or failure edge that pointed at a missing action, which strands the person at run time just like a missing edge. Co-authored-by: Harley Alexander <harley.a@posthog.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… logs Flag and Redis outages answer 503 before admission, so a batch run repeats them for every person; they log at debug and the outcome counter carries their rate. A test proves that activating an unwired builder draft still needs every decision edge. Co-authored-by: Harley Alexander <harley.a@posthog.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… tests Tests now fail when the refund ignores its short-timeout client, when a deployment-wide decision failure logs at debug, or when a strict save rejects a decision for a reason other than its missing 'continue' edge. The ai_decision action validation runs each serializer once. Co-authored-by: Harley Alexander <harley.a@posthog.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e in tests The billing-missing case swapped sys.modules before the first request, so running it first evicted the urlconf imports: it failed with a 500 and the next test patched a fresh facade copy. The config docstring now promises only what the serializer enforces, and the context check takes an object. Co-authored-by: Harley Alexander <harley.a@posthog.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A gateway 200 the facade cannot read fails every decision on the deployment, like an answer that does not fit its question, so it gets its own reason and logs at warning. Tests now cover a token minted for another team, the config defaults a save stores, and a blank context field name. Co-authored-by: Harley Alexander <harley.a@posthog.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…acade The facade imported the ai decision service at module level, and django.setup() loads the facade, so every process imported the openai and anthropic SDKs at boot and the startup import budget guards failed. The facade now imports the service when a decision runs. Tests cover the log level of every cause, the Retry-After bounds, and a lenient draft that keeps an incomplete config. Co-authored-by: Harley Alexander <harley.a@posthog.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A strict save keeps every config key, so a stored decision that carried a flag-gated template_id landed in the template grandfathering set, and a later edit could turn that action id into the gated template without its flag. Co-authored-by: Harley Alexander <harley.a@posthog.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…to publish A stage_draft save on an active flow that sends only actions or only edges was checked against the other half of the live graph, not the draft's, so a fully wired draft got a false 400. Publish re-validates the whole draft strictly, so the early check now waits for it. The AI decision test helpers take flag_enabled explicitly. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…s-only half The early check condition now lives in _sends_a_graph_to_check_early. A test case removes a decision through an actions-only stage and an edges-only stage, so the edges-only half of the staged skip is proven. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Tests now cover a context template that fails to compile, debug logging for a disabled flag and a gateway 402, the team Retry-After floor under a frozen clock, and no Retry-After on any 503. The early edge check comment names activation, and the flag test reuses the second-decision helpers. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Move helper functions and methods so each definition appears before its first call, per the repo's Python style rule. Pure moves. Add a test that a stored non-decision step does not let an ai_decision saved under its id through the flag gate. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…_refused The outcome counter labeled a failed decision by its code only, so a gateway that refuses every request looked the same as bad customer input. Failed outcomes now also carry their failure reason. The model_refused message no longer tells the customer to edit the question when the gateway sent an answer it could not read. Drop a Redis flush in the decide route tests that the base test case already does on teardown. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…n skip a person The runtime follows a step's continue edge for a person its filters skip, whatever on_error says. A strict save let an ai_decision with filters and on_error abort through without that edge, so a skipped person's run failed with "No next action found". The abort waiver now applies only to a step without filters. Tests also prove that a signed-in user without a service token cannot call the decide route, and that the outcome counter labels every outcome. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
An oversized state was repaired for lone surrogates before the size check, so the reject path copied it several times before admission. Measure first with surrogatepass, which gives the same size because a lone surrogate and its U+FFFD replacement both take 3 bytes, and repair only a state that fits. The ai_decision config help text now says the continue edge is also taken when the step's filters skip a person, and when it may be left out. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… edge docstring Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
An ai_decision step now renders its context, asks the workflows decide route once with a scoped token, and follows the answer's branch edge. A busy route reschedules the person within the spec's bounds without a pause and resume pair in the run log. Test runs mock the answer, or ask once and stop on a busy route. One cases table, read by the Django serializer tests and the Node handler tests, keeps the runtime's config checks equal to a strict save. Co-authored-by: Harley Alexander <harley.a@posthog.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…butors The graph schema reference describes the ai_decision node, its edge indexes, its result, and a worked example. The MCP create and test-run tools name the node, its AI credit use and mock_answer. CONTRIBUTING covers a native step that calls Django and reschedules. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Merging to
After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here |
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configuration
📒 Files selected for processing (13)
Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 7 remain after this review. 📝 WalkthroughWalkthroughThe change adds Priority: ➖ Normal Merge Risk: ⚪ Minimal · up to No actionable issue remains from this review. The AI-decision change is mergeable after normal checks and the stated deployment prerequisites are met. Security Architecture ReviewSecurity architecture risk: 🔵 Low · up to Team-scoped credentials, data-processing approval and resource limits constrain the new execution path. The main remaining concern is repeated paid work after an interrupted response; access across teams was not demonstrated. Retained concerns
Security review detailsSecurity Blast Radius
Security Findings and Attack Paths
Trust Boundaries and Controls
Resilience and Maintainability Implications
Hardening Proposals
🚥 Pre-merge checks | ✅ 1✅ Passed checks (1 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Note
Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.
🟡 Other comments (1)
products/workflows/skills/building-workflows/references/graph-schema.md-118-118 (1)
118-118: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winRemove
ai_decisionauthoring instructions from the shared MCP skill.This skill ships to every MCP client, but strict save validation can reject new
ai_decisionactions whenworkflows-ai-decisionis disabled. The warning does not make the action available to every user.conditional_branchsupports deterministic property filters, so it is not a general fallback for an AI decision.
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository: PostHog/posthog/.coderabbit.yaml
- Review profile: QUIET
- Plan: Enterprise
- Run ID:
1ac8d7c6-bddf-4ca9-adbc-dd5348493878
⛔ Files ignored due to path filters (4)
products/workflows/frontend/generated/api.schemas.tsis excluded by!**/generated/**products/workflows/frontend/generated/api.zod.tsis excluded by!**/generated/**services/mcp/src/generated/workflows/api.tsis excluded by!**/generated/**services/mcp/src/tools/generated/workflows.tsis excluded by!**/generated/**
📒 Files selected for processing (43)
nodejs/src/cdp/cdp-api.tsnodejs/src/cdp/cdp-services.tsnodejs/src/cdp/config.tsnodejs/src/cdp/schema/hogflow.tsnodejs/src/cdp/services/hogflows/actions/action.interface.tsnodejs/src/cdp/services/hogflows/actions/ai_decision/answer-selection.test.tsnodejs/src/cdp/services/hogflows/actions/ai_decision/answer-selection.tsnodejs/src/cdp/services/hogflows/actions/ai_decision/client.tsnodejs/src/cdp/services/hogflows/actions/ai_decision/config.tsnodejs/src/cdp/services/hogflows/actions/ai_decision/handler.tsnodejs/src/cdp/services/hogflows/hogflow-executor.service.test.tsnodejs/src/cdp/services/hogflows/hogflow-executor.service.tsnodejs/src/cdp/services/hogflows/hogflow-functions.service.tsnodejs/src/cdp/types.tsnodejs/src/cdp/utils/jwt-utils.tsposthog/jwt.pyposthog/settings/data_stores.pyposthog/settings/web.pyposthog/test/test_token_bucket.pyposthog/token_bucket.pyproducts/workflows/CONTRIBUTING.mdproducts/workflows/backend/facade/api.pyproducts/workflows/backend/facade/contracts.pyproducts/workflows/backend/facade/enums.pyproducts/workflows/backend/facade/service_jwt.pyproducts/workflows/backend/models/hog_flow/hog_flow.pyproducts/workflows/backend/presentation/views/ai_decision_validation.pyproducts/workflows/backend/presentation/views/graph_validation.pyproducts/workflows/backend/presentation/views/hog_flow.pyproducts/workflows/backend/presentation/views/workflow_ai_decisions.pyproducts/workflows/backend/routes.pyproducts/workflows/backend/service_jwt.pyproducts/workflows/backend/services/ai_decision.pyproducts/workflows/backend/tests/api/ai_decision_config_cases.jsonproducts/workflows/backend/tests/api/test_graph_validation.pyproducts/workflows/backend/tests/api/test_hog_flow.pyproducts/workflows/backend/tests/api/test_workflow_ai_decisions_api.pyproducts/workflows/mcp/tools.yamlproducts/workflows/skills/building-workflows/references/graph-schema.mdservices/mcp/schema/generated-tool-definitions.jsonservices/mcp/schema/tool-definitions-all.jsonservices/mcp/src/api/generated.tstach.toml
Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 11 remain after this review.
- A reply whose body breaks after the status line now reschedules as an unavailable route instead of failing the step. - A 503 Retry-After is capped at 60 s, like a 429. - A yes or no answer needs a valid probability for both yes and no. - The runtime rejects null characters and lone surrogates in the step's text, as the Django serializer does; the shared cases table covers both. - The run log no longer prints P(yes) next to a "no" answer, and shows one decimal so 79.5% does not read as 80%. - A mocked answer the step does not have lists the step's answers. - Node CI runs the parity tests when only the shared cases table changes. - The graph schema reference says what probability holds, and what to do when the organization does not have the step. - Tests: the test-run API forwards mock_answer and mock_async_functions, the 8,192 byte boundary in live and mocked runs, and a mocked result in an output variable. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Problem
ai_decision.feat/workflows-ai-decision-routeon the fork). Review the top two commits only; the rest belong to feat(workflows): add the ai_decision action type and its decide route #111612.Changes
ai_decisionstep now goes down the edge of the model's answer. The answer, its probability, every probability, and the model go to mapped output variables.AI decision answered "yes". Probabilities: yes 62%, no 38%. Model: jevk5-fp8-0.2.It never contains the rendered context.yes_threshold, else Noyes_threshold, No at or belowno_threshold, else Unsuremin_pick_probability, else the top optioncontinue), or ends the run withon_error: abort. The run log names the failure code and the route's message.Retry-After, capped at 60 s, plus up to 25% jitterthrottledRetry-Afterwhen sent, capped at 60 s, else 2, 4, 8, 16, 32 sgateway_unavailablestate_too_largewithout a call to Django.mock_answer, or the first answer. The result carries the rendered context and its size for the panel.mock_async_functions: false): one call. A 429 or 503 ends the test with "The AI service is busy. Try again in a moment."ai_decisionsection and worked example in the building-workflows graph schema reference, the node type, credit use andmock_answerin the MCPworkflows-createandworkflows-test-runtools, and a CONTRIBUTING section on a native step that calls Django.ci-nodejs.ymlpath filters for the cases table, the regenerated MCP tool definitions, atestRunoption and atestOutputresult field on the action handler interface, and the client wiring incdp-services.ts.Design calls a reviewer may question:
ml_inferencefacade's 5 s timeout applies per httpx phase, so Django can wait 5 s to connect and 5 s to read. A worker that gives up first retries a decision the gateway may still bill.routingOnlyReschedule. That flag already suppresses the pause and resume pair for queue routing. A second flag would duplicate the three executor sites that read it.{answer, mocked: true}. It has no probabilities, so nothing in a mock reads like a real answer. The rendered context goes to the test panel only, so a mapped variable cannot hit the 5 KB variable cap in a test that a live run would pass.unsurewins over the Unsure output when it is the mocked answer. Rejecting that name belongs to the save validation.Note
Rollout: keep
workflows-ai-decisionoff until this PR deploys, and provisionWORKFLOW_AI_DECISION_JWT_SECRETfor the CDP worker. Without the secret the step fails closed asgateway_unavailable.How did you test this code?
Test seams (spec seams 3, 4, and 5):
answer-selection.test.ts,it.eachat threshold boundaries).internalFetchstubbed (describe('ai_decision')inhogflow-executor.service.test.ts).ai_decision_config_cases.json), run by the Node handler tests and byTestAIDecisionConfigCasesintest_workflow_ai_decisions_api.py.Test rationale:
p * 100(0.29 * 100 falls under 29), a band edge on the wrong side, and a tie that goes to the later option. No answer selection existed before.on_errorpath, a reschedule outside its bounds, a pause and resume log pair, a call made for an oversized context, a scoped token without the team or workflow claim, and a test run that reschedules. No native step called Django before, so no existing executor test covered this path.cdp-api.serial.test.tscatch a test-run API that stops forwardingmock_answerormock_async_functionsto the handler.p * 100, and counting UTF-16 units each fail one test.Red, green, and the local gate
Action type 'ai_decision' not supported;selectAnswerthrew).ai_decisiontests pass (73), and the Django cases table passes (38).src/cdp/services/hogflows/folder andcdp-api.test.tspass except one email round-trip test, which needs a local SMTP server on port 1025 that this devbox does not run. That test does not touch this change.pnpm typescript:checkinnodejs/reports no errors in changed files. It reports missing@posthog/replay-anonymizertypes, a Rust-built package this devbox has not built.hogli ci:preflight --strictexited 0. Repo-wideuv run mypy --cache-fine-grained .found no issues.Not checked: a live gateway call, and a run on a deployed CDP worker. Fork CI is held for maintainer approval.
Release status
Automatic notifications
Docs update
The building-workflows graph schema reference, the MCP tool descriptions, and
products/workflows/CONTRIBUTING.mdcover the step in this PR.🤖 Agent context
Autonomy: Human-driven (agent-assisted)
Agent: Claude Code,
claude-opus-5-5(tests, review, docs, refactor of the handler); Codex, GPT-6.1 Sol (first implementation of the handler, client, and config parse from a written brief)/tdd,/writing-tests,/implementing-mcp-tools,/writing-user-facing-copy,/writing-code-comments,/writing-pr-descriptions, thenqa-swarm.classify.ts, credited to Harley Alexander with aCo-authored-bytrailer.🤖 Generated with Claude Code