Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
9308cfe
feat(agents): add the error-tracking orchestrator flow
ablaszkiewicz Sep 1, 2026
76889ed
feat(agents): run the error-tracking code tasks on sol for the pi har…
ablaszkiewicz Sep 1, 2026
1b8e123
fix(skills): make source-map installs follow the project's package ma…
ablaszkiewicz Sep 2, 2026
06a49a4
fix(agents): make the error-tracking seed settle both facts before pl…
ablaszkiewicz Sep 2, 2026
f31e15f
feat(agents): document upload variable names in .env.example
ablaszkiewicz Sep 3, 2026
09ba5c6
feat(agents): skip the source-map uploader on Astro
ablaszkiewicz Sep 3, 2026
d50503f
fix(agents): make the error-tracking init wire .env loading on plain …
ablaszkiewicz Sep 8, 2026
aa72cc1
fix(agents): stop the error-tracking init inventing import.meta.env o…
ablaszkiewicz Sep 8, 2026
14bf73b
fix(agents): require the Angular init key to resolve, not just exist
ablaszkiewicz Sep 8, 2026
9085c0d
fix(agents): make configure prove the built bundle actually runs
ablaszkiewicz Sep 8, 2026
3096836
fix(agents): reconcile the configure build-verification with the no-b…
ablaszkiewicz Sep 8, 2026
d3ae598
fix(agents): drop the build/run verification steps from error-tracking
ablaszkiewicz Sep 9, 2026
e758d47
fix(agents): make the env variable names an explicit contract
ablaszkiewicz Sep 9, 2026
2b78b41
feat(error-tracking): make init check the variable an existing init r…
ablaszkiewicz Sep 9, 2026
978b6a2
feat(error-tracking): treat a keyless init as not integrated
ablaszkiewicz Sep 9, 2026
445e49d
feat(error-tracking): keep the type assertion in a typed build config
ablaszkiewicz Sep 9, 2026
ad713bb
feat(error-tracking): forward the release identity across the CI boun…
ablaszkiewicz Sep 9, 2026
7a38c8e
feat(error-tracking): match the app's own env names at the deploy bou…
ablaszkiewicz Sep 9, 2026
41018db
fix(error-tracking): forward CI git vars only where a provider sets them
ablaszkiewicz Sep 9, 2026
72ab03c
feat(error-tracking): make the run script load the env file it depend…
ablaszkiewicz Sep 10, 2026
fc553ca
feat(error-tracking): give the capture step its own docs matrix
ablaszkiewicz Sep 10, 2026
3f6b977
fix(error-tracking): stop the capture task splicing two SDK setups
ablaszkiewicz Sep 10, 2026
559da25
fix(error-tracking): default Nuxt to the 3.7+ capture docs
ablaszkiewicz Sep 11, 2026
22f0097
fix(error-tracking): keep the capture task off the SDK's own listeners
ablaszkiewicz Sep 13, 2026
a6edec6
test(skills): check error-tracking-step covers every integration variant
ablaszkiewicz Sep 14, 2026
48d0fc5
fix(agents): use allowed wizard models in the error-tracking flow
ablaszkiewicz Sep 14, 2026
1e46b87
fix(agents): pick the react-native uploader before native markers
ablaszkiewicz Sep 14, 2026
189ca8a
fix(agents): make the error-tracking init type-check
ablaszkiewicz Sep 14, 2026
d85a30f
fix(skills): do not set a devtool beside the PostHog webpack plugin
ablaszkiewicz Sep 14, 2026
0f98c63
fix(agents): stop turning on Error Tracking from the report task
ablaszkiewicz Sep 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions context/agents/error-tracking/capture-exceptions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
---
type: capture-exceptions
flow: error-tracking
label: Wire up exception capture
model_pi: openai/gpt-5.6-sol
effort_pi: medium
model_sdk: claude-sonnet-5
effort_sdk: high
skills: [integration-v2-error-tracking-step, posthog-best-practices]
allowedTools: [Read, Write, Edit, Glob, Grep]
disallowedTools: [enqueue_task]
dependsOn: [install, init]
---

## Goal

Make the errors the app does not catch reach PostHog, by whatever means the
SDK offers for that. Which means depends on the SDK: some autocapture
exceptions once you enable it at init, some wire into the framework's own
error handler, some give you a boundary to mount at the app entry. Follow the
docs and the reference example for this one, and set it up in one place —
never manual capture calls sprinkled across files.
Comment on lines +17 to +22

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cover every runtime in full-stack applications

should_fix

Issue description

The prompt requires one setup location, but the init skill creates separate browser and server clients in full-stack projects. Those clients have different failure paths. Next.js can require client autocapture, React boundary capture, and the server onRequestError hook. The singular rule can leave one runtime without exception capture.

Why we think it's a valid issue
  • Checked: the full context/agents/error-tracking/capture-exceptions.md, the task boundary its neighbours set (install.md:22-23, init.md:25-26), the skill it loads (context/skills/integration-v2/error-tracking-step/description.md and its nextjs-app-router variant, which is default: true), the init skill's own multi-runtime rule (context/skills/integration-v2/init/description.md), and the Next.js page the variant points the agent at (https://posthog.com/docs/error-tracking/installation/nextjs.md).
  • Found: The docs page makes the two runtimes separate required steps, not alternatives. Client capture uses posthog-js with app/global-error.tsx and error.tsx boundaries. Server capture is its own required step: initialise posthog-node as a singleton, add instrumentation.ts at the project root, and call posthog.captureException(err, distinctId) inside its onRequestError hook. The page never claims client autocapture reaches server errors; the server hook exists because it does not.
  • Found: The prompt names only the client half for this framework. capture-exceptions.md:52-56 lists framework hooks to add on top of autocapture and includes "Next.js global-error", and neither onRequestError nor instrumentation.ts appears anywhere in this file — or anywhere in context/agents/ or context/skills/integration-v2/.
  • Found: The singular framing then closes the job. capture-exceptions.md:21-22 says to "set it up in one place", and capture-exceptions.md:37-38 repeats "set up that one place; that is the whole job". The success criterion at capture-exceptions.md:63-67 asks only that "An error the app does not catch reaches PostHog" and never asks for coverage of each initialised client. The anti-sprinkling intent of line 22 is sound; what is missing is any per-runtime requirement beside it.
  • Found: One nearby rule actively pushes the agent to pick a single runtime. capture-exceptions.md:24-27 says a page "often shows two setups side by side: a global initializer, and an explicit client you hold", to "Follow one of them the whole way through", and that mixing them "compiles nowhere". The Next.js page shows exactly that shape — global posthog-js for the browser, a held getPostHogServer() client for the server — but there they are both required, so an agent applying the rule to that page drops one runtime by design.
  • Found: The rest of the flow assumes both runtimes exist, and assigns capture solely to this task. install.md:22-23 installs "the server library too, if the app runs server-side code", the init skill's §Init point item 2 states "Fullstack or SSR. You will initialize both a client and a server SDK", and init.md:25-26 tells init "Don't set up exception capture either way; the capture-exceptions task after you owns that." The init skill also carries the matching warning this file lacks — "a single SDK call often covers only some of them, and the rest stay uninstrumented while the build still succeeds".
  • Impact: On a full-stack Next.js project — the default variant of the skill this task loads — the run can wire the client boundary, leave instrumentation.ts unwritten, and finish with an initialised posthog-node client that captures nothing. Errors from route handlers and server components never reach PostHog, while report.md tells the user error tracking is live. Client-side errors do still flow, so the gap is partial rather than total, and the docs in the agent's context mark both steps required — which is why this sits at should_fix rather than higher.
Suggested fix

Define the rule as one centralized handler for each runtime and framework boundary. Tell the task to cover every initialized client and server runtime. For Next.js, require the client and server hooks that the referenced installation page lists.

Prompt to fix with AI (copy-paste)
## Context
@context/agents/error-tracking/capture-exceptions.md#L17-22
@context/agents/error-tracking/capture-exceptions.md#L29-38

<issue_description>
The prompt requires one setup location, but the init skill creates separate browser and server clients in full-stack projects. Those clients have different failure paths. Next.js can require client autocapture, React boundary capture, and the server `onRequestError` hook. The singular rule can leave one runtime without exception capture.
</issue_description>

<issue_validation>
- **Checked:** the full `context/agents/error-tracking/capture-exceptions.md`, the task boundary its neighbours set (`install.md:22-23`, `init.md:25-26`), the skill it loads (`context/skills/integration-v2/error-tracking-step/description.md` and its `nextjs-app-router` variant, which is `default: true`), the init skill's own multi-runtime rule (`context/skills/integration-v2/init/description.md`), and the Next.js page the variant points the agent at (`https://posthog.com/docs/error-tracking/installation/nextjs.md`).
- **Found:** The docs page makes the two runtimes separate required steps, not alternatives. Client capture uses `posthog-js` with `app/global-error.tsx` and `error.tsx` boundaries. Server capture is its own required step: initialise `posthog-node` as a singleton, add `instrumentation.ts` at the project root, and call `posthog.captureException(err, distinctId)` inside its `onRequestError` hook. The page never claims client autocapture reaches server errors; the server hook exists because it does not.
- **Found:** The prompt names only the client half for this framework. `capture-exceptions.md:52-56` lists framework hooks to add on top of autocapture and includes "Next.js `global-error`", and neither `onRequestError` nor `instrumentation.ts` appears anywhere in this file — or anywhere in `context/agents/` or `context/skills/integration-v2/`.
- **Found:** The singular framing then closes the job. `capture-exceptions.md:21-22` says to "set it up in one place", and `capture-exceptions.md:37-38` repeats "set up that one place; that is the whole job". The success criterion at `capture-exceptions.md:63-67` asks only that "An error the app does not catch reaches PostHog" and never asks for coverage of each initialised client. The anti-sprinkling intent of line 22 is sound; what is missing is any per-runtime requirement beside it.
- **Found:** One nearby rule actively pushes the agent to pick a single runtime. `capture-exceptions.md:24-27` says a page "often shows two setups side by side: a global initializer, and an explicit client you hold", to "Follow one of them the whole way through", and that mixing them "compiles nowhere". The Next.js page shows exactly that shape — global `posthog-js` for the browser, a held `getPostHogServer()` client for the server — but there they are both required, so an agent applying the rule to that page drops one runtime by design.
- **Found:** The rest of the flow assumes both runtimes exist, and assigns capture solely to this task. `install.md:22-23` installs "the server library too, if the app runs server-side code", the init skill's §Init point item 2 states "Fullstack or SSR. You will initialize both a client and a server SDK", and `init.md:25-26` tells init "Don't set up exception capture either way; the capture-exceptions task after you owns that." The init skill also carries the matching warning this file lacks — "a single SDK call often covers only some of them, and the rest stay uninstrumented while the build still succeeds".
- **Impact:** On a full-stack Next.js project — the default variant of the skill this task loads — the run can wire the client boundary, leave `instrumentation.ts` unwritten, and finish with an initialised `posthog-node` client that captures nothing. Errors from route handlers and server components never reach PostHog, while `report.md` tells the user error tracking is live. Client-side errors do still flow, so the gap is partial rather than total, and the docs in the agent's context mark both steps required — which is why this sits at `should_fix` rather than higher.
</issue_validation>

## Task
Investigate the issue and solve it

<potential_solution>
Define the rule as one centralized handler for each runtime and framework boundary. Tell the task to cover every initialized client and server runtime. For Next.js, require the client and server hooks that the referenced installation page lists.
</potential_solution>


A page often shows two setups side by side: a global initializer, and an
explicit client you hold. Follow one of them the whole way through. Taking the
wiring from one and the calls from the other reads fine and compiles nowhere —
an initializer that hands back nothing leaves you nothing to call methods on.

The SDK is installed and initialized — either it already was, or the install
and init tasks before you did it (see their handoffs); build on that, do not
re-check it.

This is an instrument-only task. Do not install dependencies, run the build,
run tests, or start the app — the user-driven test-setup step at the end of
the flow verifies, when the user wants it. Do not touch the build config
either way; when the flow includes a configure task, it owns those files.
Stay inside this project's directory and set up that one place; that is the
whole job.

## Do not copy the SDK's own listeners

Exception autocapture already registers the global handlers: `window.onerror`
and `unhandledrejection` in the browser, `uncaughtException` and
`unhandledRejection` in Node, `sys.excepthook` in Python, the panic hook in
Rust, and the uncaught-exception and signal handlers on iOS and Android. Turn
that on with the SDK's own option — `capture_exceptions`,
`enableExceptionAutocapture`, `enable_exception_autocapture`, `capture_panics`,
`errorTrackingConfig.autoCapture` — and register none of those handlers
yourself. A second listener on the same event sends every error twice, and it
stops matching the SDK's handling the moment the SDK changes.

What the SDK cannot see is yours to add: errors a framework catches before any
global handler fires. Express error middleware, Fastify `setErrorHandler`, Hono
`onError`, Vue `app.config.errorHandler`, Angular `ErrorHandler`, SvelteKit
`handleError`, a React error boundary, Next.js `global-error` — hook those,
because the framework swallows the error and the global listener never hears
of it. A platform with no autocapture at all, such as Go, is the other case:
there the capture boundary at the entry point is the mechanism, not a copy of
one.
Comment on lines +57 to +59

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One Go entry-point boundary cannot cover the process

should_fix

Issue description

Go has no process-wide exception boundary. recover only catches a panic in the same goroutine. An entry-point boundary misses HTTP handlers, worker goroutines, and returned errors. The Go installation reference instead captures errors with NewDefaultException.

Why we think it's a valid issue
  • Checked: context/agents/error-tracking/capture-exceptions.md in full, the skill it loads (context/skills/integration-v2/error-tracking-step/ — its description.md and the go variant at config.yaml:306-312), the repo's own Go rules in context/commandments.yaml, the Go example in context/skills/error-tracking-upload-source-maps/description.md:362-366, and the docs page the go variant hands the agent (https://posthog.com/docs/error-tracking/installation/go.md).
  • Found: The half of the sentence about missing autocapture is correct — the Go docs page describes no panic hook, no process-wide handler, and never mentions recover(), defer-based recovery, or HTTP middleware. The named mechanism is the part that fails: the page offers exactly two ways to capture, posthog.NewDefaultException(...) plus client.Enqueue(...), or posthog.NewSlogCaptureHandler(baseHandler, client, ...) wrapped in slog.New(...) for automatic capture at slog.LevelWarn and above. Neither one is "the capture boundary at the entry point".
  • Found: The repo's own Go guidance agrees with the docs and not with the prompt. context/commandments.yaml:91 lists only posthog.NewDefaultException(...) for direct captures and posthog.NewSlogCaptureHandler(...) for automatic capture. The only Go capture snippet elsewhere in the repo, context/skills/error-tracking-upload-source-maps/description.md:362-366, is an explicit client.Enqueue(posthog.NewDefaultException(...)) on a route, not a recover boundary.
  • Found: The prompt contradicts itself on this platform. capture-exceptions.md:20-21 tells the agent to "Follow the docs and the reference example for this one", and capture-exceptions.md:63-64 sets the success bar as capture "through the mechanism this SDK gives you rather than one you invented". Lines 57-59 name a Go mechanism the SDK does not give, so the agent that obeys those lines fails the file's own bar.
  • Found: The path is reachable. A go.mod project is a supported target of this flow — setup-error-tracking.md maps go.mod to the go uploader variant, and the capture task's skill carries a go variant with the Go error-tracking page as its docs.
  • Impact: A recover() at the process entry recovers only panics on its own goroutine. net/http runs each connection on its own goroutine and recovers handler panics itself, so a boundary in main() never sees them. On the most common Go target, a web service, the agent can finish, report a wired capture mechanism, and leave a build where no uncaught error reaches PostHog — a silent no-op, which is the one outcome the flow exists to prevent.
  • Impact: The mitigating factor is that the correct docs travel with the task, so a careful agent may follow NewSlogCaptureHandler or explicit NewDefaultException captures instead of the sentence. That keeps the defect below must_fix, but the sentence still steers against the docs and costs one clause to correct.
  • Note on the fix: Only part of the suggestion fits the file. Central capture through NewSlogCaptureHandler or an existing shared error path matches both the docs and the prompt's own "set it up in one place" rule (capture-exceptions.md:21). Adding recovery wrappers per worker goroutine pulls toward the hand-wrapping the same file forbids at capture-exceptions.md:66-67.
Suggested fix

Direct the task to use existing central boundaries. Add recovery middleware around HTTP requests. Recover inside worker wrappers. Capture returned errors at central handlers with NewDefaultException. Report any paths that have no shared boundary.

Prompt to fix with AI (copy-paste)
## Context
@context/agents/error-tracking/capture-exceptions.md#L57-59

<issue_description>
Go has no process-wide exception boundary. `recover` only catches a panic in the same goroutine. An entry-point boundary misses HTTP handlers, worker goroutines, and returned errors. The Go installation reference instead captures errors with `NewDefaultException`.
</issue_description>

<issue_validation>
- **Checked:** `context/agents/error-tracking/capture-exceptions.md` in full, the skill it loads (`context/skills/integration-v2/error-tracking-step/` — its `description.md` and the `go` variant at `config.yaml:306-312`), the repo's own Go rules in `context/commandments.yaml`, the Go example in `context/skills/error-tracking-upload-source-maps/description.md:362-366`, and the docs page the `go` variant hands the agent (`https://posthog.com/docs/error-tracking/installation/go.md`).
- **Found:** The half of the sentence about missing autocapture is correct — the Go docs page describes no panic hook, no process-wide handler, and never mentions `recover()`, `defer`-based recovery, or HTTP middleware. The named mechanism is the part that fails: the page offers exactly two ways to capture, `posthog.NewDefaultException(...)` plus `client.Enqueue(...)`, or `posthog.NewSlogCaptureHandler(baseHandler, client, ...)` wrapped in `slog.New(...)` for automatic capture at `slog.LevelWarn` and above. Neither one is "the capture boundary at the entry point".
- **Found:** The repo's own Go guidance agrees with the docs and not with the prompt. `context/commandments.yaml:91` lists only `posthog.NewDefaultException(...)` for direct captures and `posthog.NewSlogCaptureHandler(...)` for automatic capture. The only Go capture snippet elsewhere in the repo, `context/skills/error-tracking-upload-source-maps/description.md:362-366`, is an explicit `client.Enqueue(posthog.NewDefaultException(...))` on a route, not a recover boundary.
- **Found:** The prompt contradicts itself on this platform. `capture-exceptions.md:20-21` tells the agent to "Follow the docs and the reference example for this one", and `capture-exceptions.md:63-64` sets the success bar as capture "through the mechanism this SDK gives you rather than one you invented". Lines 57-59 name a Go mechanism the SDK does not give, so the agent that obeys those lines fails the file's own bar.
- **Found:** The path is reachable. A `go.mod` project is a supported target of this flow — `setup-error-tracking.md` maps `go.mod` to the `go` uploader variant, and the capture task's skill carries a `go` variant with the Go error-tracking page as its docs.
- **Impact:** A `recover()` at the process entry recovers only panics on its own goroutine. `net/http` runs each connection on its own goroutine and recovers handler panics itself, so a boundary in `main()` never sees them. On the most common Go target, a web service, the agent can finish, report a wired capture mechanism, and leave a build where no uncaught error reaches PostHog — a silent no-op, which is the one outcome the flow exists to prevent.
- **Impact:** The mitigating factor is that the correct docs travel with the task, so a careful agent may follow `NewSlogCaptureHandler` or explicit `NewDefaultException` captures instead of the sentence. That keeps the defect below `must_fix`, but the sentence still steers against the docs and costs one clause to correct.
- **Note on the fix:** Only part of the suggestion fits the file. Central capture through `NewSlogCaptureHandler` or an existing shared error path matches both the docs and the prompt's own "set it up in one place" rule (`capture-exceptions.md:21`). Adding recovery wrappers per worker goroutine pulls toward the hand-wrapping the same file forbids at `capture-exceptions.md:66-67`.
</issue_validation>

## Task
Investigate the issue and solve it

<potential_solution>
Direct the task to use existing central boundaries. Add recovery middleware around HTTP requests. Recover inside worker wrappers. Capture returned errors at central handlers with `NewDefaultException`. Report any paths that have no shared boundary.
</potential_solution>


## How you know you succeeded

An error the app does not catch reaches PostHog, through the mechanism this
SDK gives you rather than one you invented, and no global error listener of
your own sits beside the SDK's autocapture. You did not install anything, run
a build, lint, or tests, search outside the project, or read through the whole
app or hand-wrap individual components or routes. Your handoff names the files
you changed and the capture mechanism, so the report can explain it to the
user.
105 changes: 105 additions & 0 deletions context/agents/error-tracking/configure.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
---
type: configure
flow: error-tracking
label: Apply build-config changes
model_pi: openai/gpt-5.6-sol
effort_pi: medium
model_sdk: claude-sonnet-5
effort_sdk: medium
skills: []
allowedTools: [Read, Write, Edit, Glob, Grep, Bash, load_skill_menu, install_skill, check_env_keys]
disallowedTools: [enqueue_task]
dependsOn: [capture-exceptions]
---

## Goal

Make this project's production build emit and upload source maps (or, for Go
and Rust, native debug symbols). Install the skill your task input names
(`install_skill` with the `skillId`) and read it — it is the source of truth
for the per-framework build-config and the uploader wiring.

Two of the skill's steps are yours:

- **"Apply build-config changes"** — make the bundler / build-config edits the
skill instructs for this platform, so the build produces and injects the
chunk IDs PostHog needs and runs the uploader.
- **"Make credentials available at build time"** — do the skill's step so the
build can read the upload credentials from the environment. If it calls for a
loader (e.g. `dotenv`), install it SILENTLY with the project's package
manager. Skip this step entirely when the platform already auto-loads `.env`.

Install every dependency with the project's own package manager: call
`detect_package_manager` before the first install and use its answer,
translating any `npm install` the skill or docs show (`pnpm add -D …`,
`yarn add -D …`). In a pnpm or yarn workspace, npm fails outright on
`workspace:*` dependencies (`EUNSUPPORTEDPROTOCOL`) — that error means the
wrong manager, never a flag to retry with.

When your changes make the build emit a bundle to a new directory (`dist/`,
`build/`), check that some script actually runs that output. A project whose
`build` writes `dist/index.js` while `start` still runs the original source
never executes the bundle the maps were uploaded for, so every uploaded map
goes unused. Add or fix the script that serves the built output.

That run script has its own environment to satisfy, and it is not the build's.
The uploader's credentials are handled — your build step passes them itself. The
separate question is the variables **the application reads when it starts**: the
project token and host its init looks up. Those live in the same gitignored env
file, and a compiled binary or a bare interpreter loads nothing on its own.

So open the init point, note the variables it reads, and make the run script
provide exactly those — export the env file ahead of the command, pass the
runtime's own flag for it, whatever that platform offers. Skip it and the
artifact starts, prints its own "variable missing" guard, and reports nothing:
the build is green, the symbols are uploaded, and the single command you hand
the user to verify with is the one command that cannot capture.

Put a multi-step build somewhere the tool actually runs it — a script file, a
make target, the manifest's own scripts — never an alias mechanism you are
assuming exists, borrowed from a neighbouring tool.

Match the bundle's module format to the package's type while you write the
command — you cannot run it, so it has to be right by construction. `esbuild
--platform=node` emits CommonJS unless you pass `--format=esm`, so in a package
whose `package.json` sets `"type": "module"` the bundle dies at boot with
`ReferenceError: module is not defined in ES module scope`. Read the `type`
field before writing the command.

Types are the same kind of trap. In a config the compiler checks — a `.ts`
config, or JS under `checkJs` — `process.env.ANYTHING` is `string | undefined`,
while a plugin's options usually require `string`. Where your skill's example
asserts or defaults that lookup, keep that part exactly: dropping a `!` or a
`?? ''` turns a working example into a build that fails type checking. The
upload step often runs before the type check, so the maps land and the build
still exits non-zero — a broken build that looks half-successful in the log.

## The names are a contract

An env variable only works if the name the code reads is the name in the file.
You write the code that reads them; the `credentials` task writes the file, and
you two run in parallel. So never invent a name that already exists somewhere
else — look for the other half of the contract first, and adopt it:

- **Call `check_env_keys` before you write the config.** It returns names, never
values. If the PostHog upload variables are already there, make your config
read exactly those names, whatever they are, even when they are not the ones
your skill's example shows.
- **Only when they are absent yet** are you the one deciding. Use the names your
skill documents for the mechanism you are wiring, and name them in your
handoff in full so `credentials` and `wire-ci` can match them.
Comment on lines +79 to +90

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Parallel tasks cannot enforce the environment-variable contract

consider bug

Issue description

The two independent tasks can both observe that no upload variables exist. Each task can then select a different variable set before either task writes. Handoffs only reach dependent tasks, so the handoff cannot repair this race. The build can silently read different names from those in the env file.

Why we think it's a valid issue
  • Checked: the name-settling rules in both parallel tasks, the dependency edges the seed creates, and the skill both tasks install.
  • Found: the window is real. configure.md:84-87 adopts existing env names through check_env_keys, and configure.md:88-90 lets the agent decide when they are "absent yet". credentials.md:59-62 adopts existing build-config names, and credentials.md:63-64 lets that agent decide when "nothing references them yet". When neither task has written, both take the deciding branch.
  • Found: the two name sets that can diverge are concrete. The installed skill lists them at description.md:142 — POSTHOG_CLI_API_KEY/POSTHOG_CLI_PROJECT_ID/POSTHOG_CLI_HOST for direct CLI upload, and POSTHOG_API_KEY/POSTHOG_PROJECT_ID/POSTHOG_HOST for bundler plugins. description.md:143 resolves the choice with "re-read the config you just wrote and use the names it references", which credentials cannot do, because it writes no config.
  • Found: the handoff cannot close the gap in the configure→credentials direction. credentials.md:12 declares dependsOn: [], and the seed keeps it a root task on purpose so the key prompt reaches the user early (context/agents/error-tracking/setup-error-tracking.md). No edge runs from configure to credentials, and credentials may already be running, so configure.md:90 ("name them in your handoff in full so credentials and wire-ci can match them") names a recipient that cannot receive it.
  • Found: the design does not rely on handoffs for the repair, which the finding's framing misses. Each task reads the other's artifact. If credentials writes first, configure.md:84-87 adopts those names from the env file. If configure writes first, credentials.md:59-62 adopts those names from the build config. Only the exact simultaneous case is unprotected.
  • Found: a strong convergence force narrows the remaining case. Both tasks receive the same skillId and install the same per-variant skill, and the seed sets that variant once. The skill states one preferred mechanism per platform, for example description.md:41: "Prefer the bundler plugin from the reference over hand-rolling the CLI." Divergence needs two agents to read the same page for the same platform and pick different mechanisms.
  • Impact: on a divergence the build reads one name while the env file holds another. Both prompts describe that outcome themselves (configure.md:92-95, credentials.md:69-72): no error anywhere, a clean build, and every stack trace stays minified. Nothing downstream repairs it — wire-ci.md:24-26 tells the agent to carry the names from both handoffs without reconciling a disagreement.
  • Priority: lowered to consider. The unprotected window is narrow, the artifact-based adoption rules already cover the ordinary interleavings, and the residual failure needs two agents to disagree while reading one document. The suggested fix reverses a documented design decision — the seed makes credentials a root task so the human prompt lands early while the code tasks run — so it is a trade-off, not a correction. The clause about frontmatter dependencies being advisory for planner-created tasks is not something this repo states, so I could not confirm it.
Suggested fix

Make credentials the sole owner of the variable names. Queue configure after both credentials and capture-exceptions, then require it to use the credentials handoff. Update the seed graph because frontmatter dependencies are advisory for planner-created tasks.

Prompt to fix with AI (copy-paste)
## Context
@context/agents/error-tracking/configure.md#L79-90

<issue_description>
The two independent tasks can both observe that no upload variables exist. Each task can then select a different variable set before either task writes. Handoffs only reach dependent tasks, so the handoff cannot repair this race. The build can silently read different names from those in the env file.
</issue_description>

<issue_validation>
- **Checked:** the name-settling rules in both parallel tasks, the dependency edges the seed creates, and the skill both tasks install.
- **Found:** the window is real. configure.md:84-87 adopts existing env names through `check_env_keys`, and configure.md:88-90 lets the agent decide when they are "absent yet". credentials.md:59-62 adopts existing build-config names, and credentials.md:63-64 lets that agent decide when "nothing references them yet". When neither task has written, both take the deciding branch.
- **Found:** the two name sets that can diverge are concrete. The installed skill lists them at description.md:142 — `POSTHOG_CLI_API_KEY`/`POSTHOG_CLI_PROJECT_ID`/`POSTHOG_CLI_HOST` for direct CLI upload, and `POSTHOG_API_KEY`/`POSTHOG_PROJECT_ID`/`POSTHOG_HOST` for bundler plugins. description.md:143 resolves the choice with "re-read the config you just wrote and use the names it references", which `credentials` cannot do, because it writes no config.
- **Found:** the handoff cannot close the gap in the configure→credentials direction. credentials.md:12 declares `dependsOn: []`, and the seed keeps it a root task on purpose so the key prompt reaches the user early (context/agents/error-tracking/setup-error-tracking.md). No edge runs from `configure` to `credentials`, and `credentials` may already be running, so configure.md:90 ("name them in your handoff in full so `credentials` and `wire-ci` can match them") names a recipient that cannot receive it.
- **Found:** the design does not rely on handoffs for the repair, which the finding's framing misses. Each task reads the other's artifact. If `credentials` writes first, configure.md:84-87 adopts those names from the env file. If `configure` writes first, credentials.md:59-62 adopts those names from the build config. Only the exact simultaneous case is unprotected.
- **Found:** a strong convergence force narrows the remaining case. Both tasks receive the same `skillId` and install the same per-variant skill, and the seed sets that variant once. The skill states one preferred mechanism per platform, for example description.md:41: "Prefer the bundler plugin from the reference over hand-rolling the CLI." Divergence needs two agents to read the same page for the same platform and pick different mechanisms.
- **Impact:** on a divergence the build reads one name while the env file holds another. Both prompts describe that outcome themselves (configure.md:92-95, credentials.md:69-72): no error anywhere, a clean build, and every stack trace stays minified. Nothing downstream repairs it — wire-ci.md:24-26 tells the agent to carry the names from both handoffs without reconciling a disagreement.
- **Priority:** lowered to `consider`. The unprotected window is narrow, the artifact-based adoption rules already cover the ordinary interleavings, and the residual failure needs two agents to disagree while reading one document. The suggested fix reverses a documented design decision — the seed makes `credentials` a root task so the human prompt lands early while the code tasks run — so it is a trade-off, not a correction. The clause about frontmatter dependencies being advisory for planner-created tasks is not something this repo states, so I could not confirm it.
</issue_validation>

## Task
Investigate the issue and solve it

<potential_solution>
Make `credentials` the sole owner of the variable names. Queue `configure` after both `credentials` and `capture-exceptions`, then require it to use the credentials handoff. Update the seed graph because frontmatter dependencies are advisory for planner-created tasks.
</potential_solution>


Names that merely look plausible are the failure here. A build reading
`POSTHOG_API_KEY` beside an env file holding `POSTHOG_CLI_API_KEY` throws no
error anywhere — the upload is skipped silently, the build looks clean, and
every stack trace stays minified.

Do not write any credential values and do not create env files — the
`credentials` task owns that, in parallel with you. Do not run the build.

## How you know you succeeded

The build config carries the skill's source-map / debug-symbol changes and can
read its credentials from the environment at build time. Your handoff names
every file you changed and the exact build-config keys you added, so the CI
task can wire the same variables through the pipeline.
80 changes: 80 additions & 0 deletions context/agents/error-tracking/credentials.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
---
type: credentials
flow: error-tracking
label: Get and write the upload credentials
model_pi: openai/gpt-5.6-sol
effort_pi: medium
model_sdk: claude-sonnet-5
effort_sdk: medium
skills: []
allowedTools: [Read, Write, Edit, Glob, Grep, Bash, load_skill_menu, install_skill, wizard_ask]
disallowedTools: [enqueue_task]
dependsOn: []
---

## Goal

Put the PostHog source-map upload credentials into this project's environment.
Install the skill your task input names (`install_skill` with the `skillId`)
and follow its **"Write credentials to the env file"** step for the variable
names and the env file to pick.

The upload needs a PostHog **personal API key** at build time. Only the user can
mint one — never call the PostHog API or any tool to create it. Get it and
write it in this one task (the key never survives across tasks):
Comment on lines +22 to +24

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not reuse a public SDK variable for the personal API key

should_fix security

Issue description

The rule adopts any PostHog variable that the build config references. A client build can reference a public SDK token variable. This task can then write the personal API key under that public name. The next browser build can expose the personal key. It can also replace the SDK token and stop event ingestion.

Why we think it's a valid issue
  • Checked: the three sources that decide the variable names in this task — credentials.md itself, the parallel configure prompt, and the installed skill's "Write credentials to the env file" step that credentials.md:18-20 points the agent at.
  • Found: the adoption rule in credentials.md:59-62 has no scope on the kind of PostHog variable. It says "If it already references PostHog env variables, use exactly those names, whatever they are, even when they are not the ones your skill would suggest." The lead-in at credentials.md:56-57 reinforces it: "never invent a name that already exists somewhere else — look for the other half of the contract first, and adopt it".
  • Found: the value the agent holds at that moment is the personal API key. credentials.md:22-24 and the wizard_ask prompt at credentials.md:27 collect it, and step 3 at credentials.md:36-41 writes it with set_env_values under "whichever names you settled on".
  • Found: the parallel prompt uses the narrow wording for the same rule. configure.md:85 reads "If the PostHog upload variables are already there", not "PostHog env variables". The author already has the correct scope one file away, so credentials.md:59-60 is an inconsistency rather than a deliberate broad rule.
  • Found: the installed skill also narrows it. description.md:142 lists the two legal sets, POSTHOG_CLI_API_KEY/POSTHOG_CLI_PROJECT_ID/POSTHOG_CLI_HOST for direct CLI upload and POSTHOG_API_KEY/POSTHOG_PROJECT_ID/POSTHOG_HOST for bundler plugins. description.md:143 adds "Write one set only — the names the mechanism you actually wired reads" and "re-read the config you just wrote and use the names it references".
  • Found: a triggering repo is a first-class path in this flow, not a hypothetical. The seed states that "an already-integrated project still gets the upload subgraph when a variant matches" (context/agents/error-tracking/setup-error-tracking.md), and its variant list includes nuxt, nextjs, webpack, and vite. A Nuxt project that already has PostHog names its public token in nuxt.config.ts under runtimeConfig.public, and a webpack project names it in a DefinePlugin entry. Both are build configs that reference a PostHog env variable.
  • Found: the write lands in the file that holds the SDK's own variables. credentials.md:32-33 tells the agent to "reuse the one PostHog's SDK already writes its POSTHOG_* vars to, when there is one", so an adopted public name resolves to an existing key in that same file.
  • Impact: on that path the personal API key is written under a client-exposed name. The bundler then inlines it into the browser bundle, which publishes a key that carries error-tracking write scope. The public project token is also replaced, so the SDK initialises with the wrong value and event ingestion stops. Both failures are silent.
  • Priority: lowered to should_fix. The bad outcome needs the agent to read the bullet against several narrowing signals in the same run: the task goal at credentials.md:17 ("source-map upload credentials"), the upload-only example triple at credentials.md:39, the upload-only failure example at credentials.md:69-72, and the skill's "write one set only" rule. Nothing forces the mistake, and the fix is one clause that matches the wording configure.md:85 already uses.
Suggested fix

Adopt a name only when the source-map uploader already uses it for authentication. Reject client-exposed and SDK-init names. Otherwise, create an upload-only name and wire the uploader to it.

Prompt to fix with AI (copy-paste)
## Context
@context/agents/error-tracking/credentials.md#L22-24
@context/agents/error-tracking/credentials.md#L59-64

<issue_description>
The rule adopts any PostHog variable that the build config references. A client build can reference a public SDK token variable. This task can then write the personal API key under that public name. The next browser build can expose the personal key. It can also replace the SDK token and stop event ingestion.
</issue_description>

<issue_validation>
- **Checked:** the three sources that decide the variable names in this task — credentials.md itself, the parallel `configure` prompt, and the installed skill's "Write credentials to the env file" step that credentials.md:18-20 points the agent at.
- **Found:** the adoption rule in credentials.md:59-62 has no scope on the kind of PostHog variable. It says "If it already references PostHog env variables, use exactly those names, whatever they are, even when they are not the ones your skill would suggest." The lead-in at credentials.md:56-57 reinforces it: "never invent a name that already exists somewhere else — look for the other half of the contract first, and adopt it".
- **Found:** the value the agent holds at that moment is the personal API key. credentials.md:22-24 and the `wizard_ask` prompt at credentials.md:27 collect it, and step 3 at credentials.md:36-41 writes it with `set_env_values` under "whichever names you settled on".
- **Found:** the parallel prompt uses the narrow wording for the same rule. configure.md:85 reads "If the PostHog **upload** variables are already there", not "PostHog env variables". The author already has the correct scope one file away, so credentials.md:59-60 is an inconsistency rather than a deliberate broad rule.
- **Found:** the installed skill also narrows it. description.md:142 lists the two legal sets, `POSTHOG_CLI_API_KEY`/`POSTHOG_CLI_PROJECT_ID`/`POSTHOG_CLI_HOST` for direct CLI upload and `POSTHOG_API_KEY`/`POSTHOG_PROJECT_ID`/`POSTHOG_HOST` for bundler plugins. description.md:143 adds "Write one set only — the names the mechanism you actually wired reads" and "re-read the config you just wrote and use the names it references".
- **Found:** a triggering repo is a first-class path in this flow, not a hypothetical. The seed states that "an already-integrated project still gets the upload subgraph when a variant matches" (context/agents/error-tracking/setup-error-tracking.md), and its variant list includes `nuxt`, `nextjs`, `webpack`, and `vite`. A Nuxt project that already has PostHog names its public token in `nuxt.config.ts` under `runtimeConfig.public`, and a webpack project names it in a `DefinePlugin` entry. Both are build configs that reference a PostHog env variable.
- **Found:** the write lands in the file that holds the SDK's own variables. credentials.md:32-33 tells the agent to "reuse the one PostHog's SDK already writes its `POSTHOG_*` vars to, when there is one", so an adopted public name resolves to an existing key in that same file.
- **Impact:** on that path the personal API key is written under a client-exposed name. The bundler then inlines it into the browser bundle, which publishes a key that carries error-tracking write scope. The public project token is also replaced, so the SDK initialises with the wrong value and event ingestion stops. Both failures are silent.
- **Priority:** lowered to `should_fix`. The bad outcome needs the agent to read the bullet against several narrowing signals in the same run: the task goal at credentials.md:17 ("source-map upload credentials"), the upload-only example triple at credentials.md:39, the upload-only failure example at credentials.md:69-72, and the skill's "write one set only" rule. Nothing forces the mistake, and the fix is one clause that matches the wording configure.md:85 already uses.
</issue_validation>

## Task
Investigate the issue and solve it

<potential_solution>
Adopt a name only when the source-map uploader already uses it for authentication. Reject client-exposed and SDK-init names. Otherwise, create an upload-only name and wire the uploader to it.
</potential_solution>


1. Ask with `wizard_ask`, exactly:
`{ id: "api-key", prompt: "Paste your PostHog personal API key below.\n\nDon't have one yet? Create one here:\n<SETTINGS_URL>\n\nWhen creating the key, choose the 'Source map upload' preset, then come back and paste it here.", kind: "text", sensitive: true }`
You receive `{ secretRef: "secret:..." }` — a vaulted reference, never the raw
value. If `wizard_ask` is unavailable (non-interactive run), report this task
with status `not needed` and say in your handoff that the user must create
the key and set the variables themselves; do not block.
Comment on lines +26 to +31

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wizard_ask uses the wrong request and response shapes

should_fix bug

Issue description

wizard_ask accepts a questions array, but this prompt sends one question as the full tool input. The tool returns the value under answers["api-key"], not as a top-level secretRef. An agent that follows "exactly" gets a schema error or passes no valid reference to set_env_values.

Why we think it's a valid issue
  • Checked: every description of wizard_ask in this repo, and every literal payload written for it. I grepped context/ and scripts/ for wizard_ask, questions:, and __cancelled__.
  • Found: the repo documents an array input. context/skills/data-warehouse-source/description.md:28 states the tool "takes up to 8 questions and an optional subject tag", and line 34 repeats "The schema accepts up to 8 questions per call". No source describes a bare single-question object as the whole input.
  • Found: the repo documents a keyed result. context/skills/migrate/description.md:33 states the tool "returns the answers keyed by question id". context/skills/data-warehouse-source/description.md:36 states that a sensitive field's answer "comes back as { secretRef: ... }", so that object is the value under the question id, not the top-level result.
  • Found: credentials.md:26-27 writes the literal input as { id: "api-key", prompt: …, kind: "text", sensitive: true } and prefixes it with "exactly". The questions wrapper is absent. credentials.md:28 then says "You receive { secretRef: "secret:..." }", which describes the top-level result rather than the answer for api-key.
  • Found: the same abbreviated shape appears at context/agents/error-tracking/test-setup.md:24, also added by this PR. So this is the author's model of the tool, not a single typo.
  • Found: the escape hatch makes the mismatch worse. credentials.md:29-31 tells the agent that when wizard_ask is unavailable it must report the task not needed and hand the key to the user. An agent that reads a schema rejection as unavailability takes that branch, so the only interactive path to the personal API key is skipped and source-map upload never works.
  • Found: two parts of the suggested fix do not hold here. subject is documented as optional (context/skills/data-warehouse-source/description.md:28), so it is a nicety. The __cancelled__ sentinel appears nowhere in this repo, so I cannot confirm that clause.
  • Impact: the literal payload is rejected on the first call, or the agent reads result.secretRef, finds nothing, and passes an empty reference to set_env_values at credentials.md:37-39. Either way the key never lands in the env file, and every stack trace stays minified.
  • Priority: lowered to should_fix. The wizard's real tool schema lives in another repo that I cannot read here, so my evidence is this repo's own prose about the tool. The agent also holds the live tool definition at run time and normally corrects a wrapper mismatch after the first error, so the common outcome is one wasted call rather than a dead task.
Suggested fix

Send { questions: [{ ... }], subject: "source-map credentials" }. Read the reference from result.answers["api-key"]. Treat __cancelled__ like an unavailable tool and use the manual follow-up path.

Prompt to fix with AI (copy-paste)
## Context
@context/agents/error-tracking/credentials.md#L26-31
@context/agents/error-tracking/credentials.md#L37-40

<issue_description>
`wizard_ask` accepts a `questions` array, but this prompt sends one question as the full tool input. The tool returns the value under `answers["api-key"]`, not as a top-level `secretRef`. An agent that follows "exactly" gets a schema error or passes no valid reference to `set_env_values`.
</issue_description>

<issue_validation>
- **Checked:** every description of `wizard_ask` in this repo, and every literal payload written for it. I grepped `context/` and `scripts/` for `wizard_ask`, `questions:`, and `__cancelled__`.
- **Found:** the repo documents an array input. context/skills/data-warehouse-source/description.md:28 states the tool "takes up to 8 `questions` and an optional `subject` tag", and line 34 repeats "The schema accepts up to 8 questions per call". No source describes a bare single-question object as the whole input.
- **Found:** the repo documents a keyed result. context/skills/migrate/description.md:33 states the tool "returns the answers keyed by question id". context/skills/data-warehouse-source/description.md:36 states that a `sensitive` field's *answer* "comes back as `{ secretRef: ... }`", so that object is the value under the question id, not the top-level result.
- **Found:** credentials.md:26-27 writes the literal input as `{ id: "api-key", prompt: …, kind: "text", sensitive: true }` and prefixes it with "exactly". The `questions` wrapper is absent. credentials.md:28 then says "You receive `{ secretRef: "secret:..." }`", which describes the top-level result rather than the answer for `api-key`.
- **Found:** the same abbreviated shape appears at context/agents/error-tracking/test-setup.md:24, also added by this PR. So this is the author's model of the tool, not a single typo.
- **Found:** the escape hatch makes the mismatch worse. credentials.md:29-31 tells the agent that when `wizard_ask` is unavailable it must report the task `not needed` and hand the key to the user. An agent that reads a schema rejection as unavailability takes that branch, so the only interactive path to the personal API key is skipped and source-map upload never works.
- **Found:** two parts of the suggested fix do not hold here. `subject` is documented as optional (context/skills/data-warehouse-source/description.md:28), so it is a nicety. The `__cancelled__` sentinel appears nowhere in this repo, so I cannot confirm that clause.
- **Impact:** the literal payload is rejected on the first call, or the agent reads `result.secretRef`, finds nothing, and passes an empty reference to `set_env_values` at credentials.md:37-39. Either way the key never lands in the env file, and every stack trace stays minified.
- **Priority:** lowered to `should_fix`. The wizard's real tool schema lives in another repo that I cannot read here, so my evidence is this repo's own prose about the tool. The agent also holds the live tool definition at run time and normally corrects a wrapper mismatch after the first error, so the common outcome is one wasted call rather than a dead task.
</issue_validation>

## Task
Investigate the issue and solve it

<potential_solution>
Send `{ questions: [{ ... }], subject: "source-map credentials" }`. Read the reference from `result.answers["api-key"]`. Treat `__cancelled__` like an unavailable tool and use the manual follow-up path.
</potential_solution>

Comment on lines +26 to +31

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handle a canceled API-key question

should_fix bug

Issue description

A canceled or timed-out wizard_ask field returns "__cancelled__". These instructions handle only an unavailable tool. Cancellation can reach set_env_values as an invalid reference or literal value. The task then fails or stores an unusable API key.

Why we think it's a valid issue
  • Checked: how this repo's other wizard_ask consumers treat a cancelled prompt, what branches credentials.md provides, and what reads the resulting handoff.
  • Found: cancellation is a documented, first-class outcome in this repo, not an edge case. context/skills/data-warehouse-source/description.md:34 states "A cancelled or timed-out wizard_ask does not count against the per-run cap. Treat a cancelled ask as 'the user declined' for that source", and context/skills/integration-v2/warehouse/description.md:74 repeats it. A test pins that sentence (scripts/lib/tests/data-warehouse-source-skill.test.js:60).
  • Found: the same repo already defines the correct response. context/skills/data-warehouse-source/description.md:123 says to report not needed and "Use this when the user cancelled or declined the prompts".
  • Found: credentials.md has no such branch. credentials.md:29-31 covers one case only: "If wizard_ask is unavailable (non-interactive run), report this task with status not needed". A user who closes the modal, or who never returns, is a different case, and step 3 at credentials.md:36-41 then still says to call set_env_values with the secretRef.
  • Found: the trigger is ordinary, not rare. The prompt text at credentials.md:27 sends the user out of the terminal to create a personal API key in PostHog settings and come back. Abandoning or dismissing that prompt is a normal user action.
  • Found: a downstream task reads this handoff and branches on it. report.md:34 tells the user to create a personal API key and says to "skip when the credentials handoff says the key is already written". An ambiguous handoff after a cancelled prompt therefore drops the instruction, so the user is never told to create the key, uploads never run, and the report claims the setup is complete.
  • Found: two specifics in the suggested fix are not verifiable here. The string __cancelled__ appears nowhere in this repo, and no file mentions a notNeededReason field. The wizard's tool contract lives in another repo that I cannot read from here, so the sentinel and that field are unconfirmed.
  • Impact: with no branch, the agent improvises. The benign outcome is a task reported incomplete with a vague handoff, which already misleads the report. The harmful outcome is a call to set_env_values with a reference that resolves to nothing, which writes an unusable value under the API-key name. A later run cannot detect that, because credentials.md:32-35 uses check_env_keys, which reports presence only and never a value.
Suggested fix

Check the api-key answer for "__cancelled__". Report not needed with notNeededReason: "user_declined", and do not call set_env_values.

Prompt to fix with AI (copy-paste)
## Context
@context/agents/error-tracking/credentials.md#L26-31

<issue_description>
A canceled or timed-out `wizard_ask` field returns `"__cancelled__"`. These instructions handle only an unavailable tool. Cancellation can reach `set_env_values` as an invalid reference or literal value. The task then fails or stores an unusable API key.
</issue_description>

<issue_validation>
- **Checked:** how this repo's other `wizard_ask` consumers treat a cancelled prompt, what branches credentials.md provides, and what reads the resulting handoff.
- **Found:** cancellation is a documented, first-class outcome in this repo, not an edge case. context/skills/data-warehouse-source/description.md:34 states "A cancelled or timed-out `wizard_ask` does **not** count against the per-run cap. Treat a cancelled ask as 'the user declined' for that source", and context/skills/integration-v2/warehouse/description.md:74 repeats it. A test pins that sentence (scripts/lib/tests/data-warehouse-source-skill.test.js:60).
- **Found:** the same repo already defines the correct response. context/skills/data-warehouse-source/description.md:123 says to report `not needed` and "Use this when the user cancelled or declined the prompts".
- **Found:** credentials.md has no such branch. credentials.md:29-31 covers one case only: "If `wizard_ask` is unavailable (non-interactive run), report this task with status `not needed`". A user who closes the modal, or who never returns, is a different case, and step 3 at credentials.md:36-41 then still says to call `set_env_values` with the secretRef.
- **Found:** the trigger is ordinary, not rare. The prompt text at credentials.md:27 sends the user out of the terminal to create a personal API key in PostHog settings and come back. Abandoning or dismissing that prompt is a normal user action.
- **Found:** a downstream task reads this handoff and branches on it. report.md:34 tells the user to create a personal API key and says to "skip when the credentials handoff says the key is already written". An ambiguous handoff after a cancelled prompt therefore drops the instruction, so the user is never told to create the key, uploads never run, and the report claims the setup is complete.
- **Found:** two specifics in the suggested fix are not verifiable here. The string `__cancelled__` appears nowhere in this repo, and no file mentions a `notNeededReason` field. The wizard's tool contract lives in another repo that I cannot read from here, so the sentinel and that field are unconfirmed.
- **Impact:** with no branch, the agent improvises. The benign outcome is a task reported incomplete with a vague handoff, which already misleads the report. The harmful outcome is a call to `set_env_values` with a reference that resolves to nothing, which writes an unusable value under the API-key name. A later run cannot detect that, because credentials.md:32-35 uses `check_env_keys`, which reports presence only and never a value.
</issue_validation>

## Task
Investigate the issue and solve it

<potential_solution>
Check the `api-key` answer for `"__cancelled__"`. Report `not needed` with `notNeededReason: "user_declined"`, and do not call `set_env_values`.
</potential_solution>

2. Pick the env file per the skill (reuse the one PostHog's SDK already writes
its `POSTHOG_*` vars to, when there is one). Call `check_env_keys` on it
first (it returns present/absent, never values — never read the file
directly).
Comment on lines +26 to +35

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Repeat runs overwrite an existing personal API key

should_fix bug

Issue description

The task always asks for a new key before it checks the environment. A repeat run therefore replaces a present key even when uploads already work. It also forces unnecessary user input.

Why we think it's a valid issue
  • Checked: the step order in credentials.md, the flow's other env-writing task, and the seed's rule for queuing this task.
  • Found: the ask comes first and the check comes second. Step 1 at credentials.md:26-31 calls wizard_ask for the personal API key. Step 2 at credentials.md:32-35 then picks the env file and calls check_env_keys. Step 3 at credentials.md:36-41 writes the pasted secret with set_env_values. No line in the file tells the agent to skip the ask, or skip the write, when the key is already present.
  • Found: the success criterion carries no present/absent branch either. credentials.md:76-80 requires only that the env file holds the upload variables and that .env.example documents the names.
  • Found: the same flow uses the opposite order where it matters. init.md:36-42 calls check_env_keys first and branches: "Present — the pair is complete. Leave it alone and say so." So the check-then-act pattern is an established convention in this flow, and credentials.md departs from it.
  • Found: the re-run path is open. The seed (context/agents/error-tracking/setup-error-tracking.md) queues credentials whenever an uploader variant matched, with no precondition on existing credentials, and it states that an already-integrated project still gets the upload subgraph. A second wizard error-tracking run on a configured repo therefore reaches step 1 again.
  • Impact: the user is asked for a personal API key that the project already has. Neither the user nor the agent can read the stored value back, and PostHog does not show a personal key again after it is created, so answering means minting a fresh key on every run. Unused keys accumulate in the account.
  • Impact: the write is not idempotent. set_env_values replaces the present value with whatever the user pasted. If they paste a key with the wrong scope, or the public project token, a working upload stops. The flow's own framing at credentials.md:69-72 says this failure is silent: the upload never runs and every stack trace stays minified.
  • Impact: implementing the suggested check needs a frontmatter change too. credentials.md:10 lists allowedTools: [Read, Write, Edit, Glob, Grep, Bash, load_skill_menu, install_skill, wizard_ask], so check_env_keys is not granted to this task, unlike configure.md:10 and init.md:10, which grant it explicitly.
Suggested fix

Resolve the uploader mechanism first. Call check_env_keys for its API-key variable before wizard_ask. Keep a present key and ask only when that key is missing.

Prompt to fix with AI (copy-paste)
## Context
@context/agents/error-tracking/credentials.md#L26-35

<issue_description>
The task always asks for a new key before it checks the environment. A repeat run therefore replaces a present key even when uploads already work. It also forces unnecessary user input.
</issue_description>

<issue_validation>
- **Checked:** the step order in credentials.md, the flow's other env-writing task, and the seed's rule for queuing this task.
- **Found:** the ask comes first and the check comes second. Step 1 at credentials.md:26-31 calls `wizard_ask` for the personal API key. Step 2 at credentials.md:32-35 then picks the env file and calls `check_env_keys`. Step 3 at credentials.md:36-41 writes the pasted secret with `set_env_values`. No line in the file tells the agent to skip the ask, or skip the write, when the key is already present.
- **Found:** the success criterion carries no present/absent branch either. credentials.md:76-80 requires only that the env file holds the upload variables and that `.env.example` documents the names.
- **Found:** the same flow uses the opposite order where it matters. init.md:36-42 calls `check_env_keys` first and branches: "**Present** — the pair is complete. Leave it alone and say so." So the check-then-act pattern is an established convention in this flow, and credentials.md departs from it.
- **Found:** the re-run path is open. The seed (context/agents/error-tracking/setup-error-tracking.md) queues `credentials` whenever an uploader variant matched, with no precondition on existing credentials, and it states that an already-integrated project still gets the upload subgraph. A second `wizard error-tracking` run on a configured repo therefore reaches step 1 again.
- **Impact:** the user is asked for a personal API key that the project already has. Neither the user nor the agent can read the stored value back, and PostHog does not show a personal key again after it is created, so answering means minting a fresh key on every run. Unused keys accumulate in the account.
- **Impact:** the write is not idempotent. `set_env_values` replaces the present value with whatever the user pasted. If they paste a key with the wrong scope, or the public project token, a working upload stops. The flow's own framing at credentials.md:69-72 says this failure is silent: the upload never runs and every stack trace stays minified.
- **Impact:** implementing the suggested check needs a frontmatter change too. credentials.md:10 lists `allowedTools: [Read, Write, Edit, Glob, Grep, Bash, load_skill_menu, install_skill, wizard_ask]`, so `check_env_keys` is not granted to this task, unlike configure.md:10 and init.md:10, which grant it explicitly.
</issue_validation>

## Task
Investigate the issue and solve it

<potential_solution>
Resolve the uploader mechanism first. Call `check_env_keys` for its API-key variable before `wizard_ask`. Keep a present key and ask only when that key is missing.
</potential_solution>

3. Settle the variable names before you write them — see "The names are a
contract" below. Then call `set_env_values`, passing the secretRef as a
value object, not a literal string — e.g.
`values: { "POSTHOG_CLI_API_KEY": { secretRef: "<the ref>" }, "POSTHOG_CLI_PROJECT_ID": "<PROJECT_ID>", "POSTHOG_CLI_HOST": "<UI_HOST>" }`
with whichever names you settled on. The wizard resolves the ref locally, so
you never see the key value.
4. Document the same variable names for other developers: append them to
`.env.example` (create it if the project has none) with empty or
placeholder values — never a real value, and never the key itself. The
example file is committed and is the only `.env*` you may write directly;
it is how the next developer, and the next wizard run's `check_env_keys`,
learns the project expects these variables.

Replace `<SETTINGS_URL>`, `<PROJECT_ID>`, and `<UI_HOST>` from your project
context. Do not touch the build config — the `configure` task owns that.

## The names are a contract

An env variable only works if the name in the file is the name the code reads.
You write the file; the `configure` task writes the code that reads it, and you
two run in parallel. So never invent a name that already exists somewhere else
— look for the other half of the contract first, and adopt it:

- **Read the build config before you choose.** If it already references PostHog
env variables, use exactly those names, whatever they are, even when they are
not the ones your skill would suggest. Reading config is safe — it holds
names, not secrets.
- **Only when nothing references them yet** are you the one deciding. Use the
names your skill documents for the mechanism that was actually wired.
- **Either way, say which set you chose in your handoff, in full.** That is how
`configure` and `wire-ci` learn what to match. A handoff that says "the usual
variables" hands the next task the same guess you just made.

Names that merely look plausible are the failure here. A build reading
`POSTHOG_API_KEY` beside an env file holding `POSTHOG_CLI_API_KEY` throws no
error anywhere — the upload simply never runs, and every stack trace stays
minified.

## How you know you succeeded

The env file holds the upload variables (the key as a resolved secret, the
non-secret project id and host as literals), written through the wizard tools,
never hardcoded in source, and `.env.example` documents the same names with
placeholders. Your handoff names the env file and every variable name — never
a value — so the CI task carries the same names into the pipeline.
117 changes: 117 additions & 0 deletions context/agents/error-tracking/init.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
---
type: init
flow: error-tracking
label: Set up PostHog initialization
model_pi: openai/gpt-5.6-terra
effort_pi: low
model_sdk: claude-sonnet-5
effort_sdk: medium
skills: [integration-v2-init, posthog-best-practices]
allowedTools: [Read, Write, Edit, Glob, Grep, Bash, check_env_keys, set_env_values]
disallowedTools: [enqueue_task]
dependsOn: []
Comment on lines +10 to +12

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Parallel root tasks can overwrite shared files

must_fix bug

Issue description

The init task runs at the same time as other root tasks. It can install dotenv while install changes the same manifest and lockfile. It also edits .env.example while credentials edits that file. These concurrent operations can lose a dependency or an environment key.

Why we think it's a valid issue
  • Checked: the frontmatter of all three root tasks in this flow (init.md:11-12, install.md:11, credentials.md:12), the seeding rules that set the real edges (setup-error-tracking.md:83-92), and the wizard-side execution semantics — src/lib/agent/runner/sequence/orchestrator/executor.ts, queue.ts, and the write paths of set_env_values in both tool facades.
  • Found: The concurrency is real and unbounded by design. executor.ts:1-7 states the executor "starts every runnable task (dependencies satisfied) as soon as it becomes runnable — parallelism is decided by the task graph, not by an executor knob", and drainQueue (executor.ts:97-113) starts each runnable task's promise in the same tick. queue.ts:5-7 is explicit that "there is no locking" and that nextRunnable returns every task whose dependencies are satisfied. All three tasks declare dependsOn: [], and setup-error-tracking.md:83 makes the overlap deliberate: "init, independent of install".
  • Found: Both root tasks drive a package manager over the same manifest. install.md runs the project's add command for the SDK, and init.md:61-62 tells this task to "install dotenv with the project's own package manager (detect it from the lockfile)" — init.md:10 grants it Bash. On the flow's own target for that branch, a plain Express/Fastify/Koa app with no PostHog, both tasks are queued as roots and both invoke the manager against one package.json and one lockfile.
  • Found: .env.example has two concurrent writers. init.md:41-42 documents the variable there, and credentials.md:43 appends its own names to the same file "(create it if the project has none)". setup-error-tracking.md:88 keeps credentials a root on purpose so the API-key prompt reaches the user early, so the two writers overlap whenever an uploader variant matched and init was queued.
  • Found: The .env write path itself can lose a key on the harness this flow uses. These tasks run on the pi harness (model_pi/effort_pi frontmatter), whose set_env_values does await fs.promises.readFilemergeEnvValuesawait fs.promises.writeFile with no lock (src/lib/agent/runner/harness/pi/tools.ts:226-244). The await boundary between read and write lets two concurrent calls interleave, so the second write drops the first's key. The only mutex anywhere in the tool layer is auditMutex for the audit ledger (src/lib/wizard-tools/mcp.ts:472). init writes the project token and credentials writes the POSTHOG_CLI_* values, and credentials.md:32-33 points it at the same file "PostHog's SDK already writes its POSTHOG_* vars to".
  • Found: The seed already reasons this way elsewhere, which is what makes the omission a gap rather than a deliberate trade. setup-error-tracking.md:91-92 orders configure after capture-exceptions "after the code edits so the two never fight over the same files". That rule is applied to build-config files and not to the manifest, .env, or .env.example.
  • Impact: Two package-manager processes in one project directory can leave a half-written package.json or lockfile, so the run ends with a missing dependency or an unbuildable project. A lost set_env_values write is worse to diagnose: either the project token never lands, and nothing is captured, or the POSTHOG_CLI_* credentials never land, and every production stack trace stays minified — with no error in either case. The repair is edges in the seed, or dropping the dotenv install from init in favour of the --env-file option it already offers at init.md:64-65.
Suggested fix

Make init depend on install when both tasks exist. Serialize its environment file writes with credentials. Split the early credential prompt from its file writes if the prompt must remain early.

Prompt to fix with AI (copy-paste)
## Context
@context/agents/error-tracking/init.md#L10-12
@context/agents/error-tracking/init.md#L40-43
@context/agents/error-tracking/init.md#L61-65

<issue_description>
The init task runs at the same time as other root tasks. It can install `dotenv` while `install` changes the same manifest and lockfile. It also edits `.env.example` while `credentials` edits that file. These concurrent operations can lose a dependency or an environment key.
</issue_description>

<issue_validation>
- **Checked:** the frontmatter of all three root tasks in this flow (`init.md:11-12`, `install.md:11`, `credentials.md:12`), the seeding rules that set the real edges (`setup-error-tracking.md:83-92`), and the wizard-side execution semantics — `src/lib/agent/runner/sequence/orchestrator/executor.ts`, `queue.ts`, and the write paths of `set_env_values` in both tool facades.
- **Found:** The concurrency is real and unbounded by design. `executor.ts:1-7` states the executor "starts every runnable task (dependencies satisfied) as soon as it becomes runnable — parallelism is decided by the task graph, not by an executor knob", and `drainQueue` (`executor.ts:97-113`) starts each runnable task's promise in the same tick. `queue.ts:5-7` is explicit that "there is no locking" and that `nextRunnable` returns every task whose dependencies are satisfied. All three tasks declare `dependsOn: []`, and `setup-error-tracking.md:83` makes the overlap deliberate: "`init`, independent of `install`".
- **Found:** Both root tasks drive a package manager over the same manifest. `install.md` runs the project's add command for the SDK, and `init.md:61-62` tells this task to "install `dotenv` with the project's own package manager (detect it from the lockfile)" — `init.md:10` grants it `Bash`. On the flow's own target for that branch, a plain Express/Fastify/Koa app with no PostHog, both tasks are queued as roots and both invoke the manager against one `package.json` and one lockfile.
- **Found:** `.env.example` has two concurrent writers. `init.md:41-42` documents the variable there, and `credentials.md:43` appends its own names to the same file "(create it if the project has none)". `setup-error-tracking.md:88` keeps `credentials` a root on purpose so the API-key prompt reaches the user early, so the two writers overlap whenever an uploader variant matched and init was queued.
- **Found:** The `.env` write path itself can lose a key on the harness this flow uses. These tasks run on the pi harness (`model_pi`/`effort_pi` frontmatter), whose `set_env_values` does `await fs.promises.readFile` → `mergeEnvValues` → `await fs.promises.writeFile` with no lock (`src/lib/agent/runner/harness/pi/tools.ts:226-244`). The await boundary between read and write lets two concurrent calls interleave, so the second write drops the first's key. The only mutex anywhere in the tool layer is `auditMutex` for the audit ledger (`src/lib/wizard-tools/mcp.ts:472`). `init` writes the project token and `credentials` writes the `POSTHOG_CLI_*` values, and `credentials.md:32-33` points it at the same file "PostHog's SDK already writes its `POSTHOG_*` vars to".
- **Found:** The seed already reasons this way elsewhere, which is what makes the omission a gap rather than a deliberate trade. `setup-error-tracking.md:91-92` orders `configure` after `capture-exceptions` "after the code edits so the two never fight over the same files". That rule is applied to build-config files and not to the manifest, `.env`, or `.env.example`.
- **Impact:** Two package-manager processes in one project directory can leave a half-written `package.json` or lockfile, so the run ends with a missing dependency or an unbuildable project. A lost `set_env_values` write is worse to diagnose: either the project token never lands, and nothing is captured, or the `POSTHOG_CLI_*` credentials never land, and every production stack trace stays minified — with no error in either case. The repair is edges in the seed, or dropping the `dotenv` install from `init` in favour of the `--env-file` option it already offers at `init.md:64-65`.
</issue_validation>

## Task
Investigate the issue and solve it

<potential_solution>
Make `init` depend on `install` when both tasks exist. Serialize its environment file writes with `credentials`. Split the early credential prompt from its file writes if the prompt must remain early.
</potential_solution>

---

## Goal

Make sure PostHog is initialized. If the project already has a working
`posthog.init(...)` (or the framework's equivalent) with its env keys wired,
leave it alone and say so in your handoff. If it doesn't, create it following
your skill — it owns the how: the framework's init point, the env-var wiring
through the wizard tools, and `.env.example`.

You exist in this flow because the user asked for error tracking on a repo
whose PostHog init is missing or unproven. Initialize the SDK so exceptions can
flow and stop — no instrumentation, no extras. Don't set up exception capture
either way; the capture-exceptions task after you owns that.

## An existing init still needs its variable defined

"Already initialised" is a property of the pair, not of the call. An init point
that reads `process.env.SOMETHING` only works when `SOMETHING` is defined in the
env file the project loads. A repo can carry an init that has never once run:
the call is there, the variable it names is nowhere, and the client is built
from an empty string.

So before you leave an existing init alone, call `check_env_keys` on that env
file and look for the exact name the init reads. It returns names, never values.

- **Present** — the pair is complete. Leave it alone and say so.
- **Absent** — the init is not wired yet, whoever wrote it. Write that variable
with `set_env_values` under the name the code already reads, and document it
in `.env.example`. Do not rename the code to match a name you would rather
have written; the code is the half that already exists.

An empty key is the quiet failure here. A client constructed from `''` throws
nothing and logs nothing. The build is clean, the app starts, every capture call
returns — and no event ever arrives.
Comment on lines +36 to +47

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The names-only check cannot detect an empty key

must_fix

Issue description

The prompt treats present as proof that the variable has a value. check_env_keys only returns key names and file paths. It reports POSTHOG_PROJECT_TOKEN= as present even though the value is empty. The task can therefore leave error tracking disabled.

Why we think it's a valid issue
  • Checked: context/agents/error-tracking/init.md in full, the skill it delegates to (context/skills/integration-v2/init/description.md), the sibling callers of the same tool in this PR (credentials.md, configure.md), the seed's own integration test in setup-error-tracking.md, and the wizard-side implementation of the tool (PostHog/wizard src/utils/env-scan.ts and src/lib/wizard-tools/tools.ts).
  • Found: The premise holds against the real implementation. parseEnvKeyNames in src/utils/env-scan.ts matches ^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*= and discards everything right of the =, so a line POSTHOG_PROJECT_TOKEN= yields the key name. checkEnvKeys in src/lib/wizard-tools/tools.ts:675 builds its answer from that name set alone, and EnvKeyPresence.status (tools.ts:638-643) is documented as "present only when a real env file sets the key".
  • Found: The one refinement the tool makes is about files, not values. isTemplateEnvFileName marks .env.example, .env.sample, .env.template and .env.dist, and CHECK_ENV_KEYS_DESCRIPTION (tools.ts:569) states a key found only in a template answers missing. A blank key in a real .env or .env.local gets no such treatment, so it answers present. src/utils/env-scan.ts also carries an explicit rule against returning values at all: "this module reads KEY NAMES only ... Do not add a code path that does."
  • Found: init.md is the only place in the flow that loads this answer with a meaning it cannot carry. init.md:36-37 states the tool "returns names, never values", then init.md:39 turns Present into "the pair is complete. Leave it alone and say so". The two sibling callers use it correctly for discovery only — credentials.md:33-37 calls it to pick the env file and settle names, then writes with set_env_values regardless, and configure.md:84-88 calls it only to adopt existing names.
  • Found: The same file already names the outcome its rule cannot catch. init.md:45-47 describes the empty key as "the quiet failure here", with no step that detects it, and the success criterion at init.md:108-110 accepts "keys in the env file and confirmed there with check_env_keys".
  • Found: The agent has no second signal to fall back on. It cannot read .env directly (credentials.md:34-35 forbids it and the wizard gates the agent's Read/Write on .env*), and init.md:10 grants no posthog_exec, so the "project state shows real events arriving" check that setup-error-tracking.md uses for the same question is out of reach in this task.
  • Impact: On a repo whose .env was copied from a template and left with POSTHOG_PROJECT_TOKEN= blank beside an existing init — the common state after a partial manual setup — init reports the pair complete and changes nothing. capture-exceptions then wires capture onto a client built from '', and report.md tells the user error tracking is live. No exception ever arrives, and nothing in the run contradicts the report: test-setup is queued only when an uploader variant matched, so a readable-stack platform such as Django gets no end-to-end check at all.
  • Impact on the proposed fix: The repair does not need the tool change the suggestion asks for. mergeEnvValues (tools.ts:778) updates an existing KEY= line in place and swaps the value, so instructing this task to write the project token with set_env_values even on a present answer closes the hole inside this repo. The trade-off to weigh is that an unconditional write also replaces a valid token the user set for a different project, which is the likely reason the current branch leaves it alone.
Suggested fix

Extend check_env_keys with a host-computed state that distinguishes empty and non-empty values without returning them. Require non-empty states for both the token and host. Otherwise, write the known project values with set_env_values and check the states again.

Prompt to fix with AI (copy-paste)
## Context
@context/agents/error-tracking/init.md#L36-47

<issue_description>
The prompt treats `present` as proof that the variable has a value. `check_env_keys` only returns key names and file paths. It reports `POSTHOG_PROJECT_TOKEN=` as present even though the value is empty. The task can therefore leave error tracking disabled.
</issue_description>

<issue_validation>
- **Checked:** `context/agents/error-tracking/init.md` in full, the skill it delegates to (`context/skills/integration-v2/init/description.md`), the sibling callers of the same tool in this PR (`credentials.md`, `configure.md`), the seed's own integration test in `setup-error-tracking.md`, and the wizard-side implementation of the tool (`PostHog/wizard` `src/utils/env-scan.ts` and `src/lib/wizard-tools/tools.ts`).
- **Found:** The premise holds against the real implementation. `parseEnvKeyNames` in `src/utils/env-scan.ts` matches `^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=` and discards everything right of the `=`, so a line `POSTHOG_PROJECT_TOKEN=` yields the key name. `checkEnvKeys` in `src/lib/wizard-tools/tools.ts:675` builds its answer from that name set alone, and `EnvKeyPresence.status` (`tools.ts:638-643`) is documented as "`present` only when a real env file sets the key".
- **Found:** The one refinement the tool makes is about files, not values. `isTemplateEnvFileName` marks `.env.example`, `.env.sample`, `.env.template` and `.env.dist`, and `CHECK_ENV_KEYS_DESCRIPTION` (`tools.ts:569`) states a key found only in a template answers `missing`. A blank key in a real `.env` or `.env.local` gets no such treatment, so it answers `present`. `src/utils/env-scan.ts` also carries an explicit rule against returning values at all: "this module reads KEY NAMES only ... Do not add a code path that does."
- **Found:** `init.md` is the only place in the flow that loads this answer with a meaning it cannot carry. `init.md:36-37` states the tool "returns names, never values", then `init.md:39` turns **Present** into "the pair is complete. Leave it alone and say so". The two sibling callers use it correctly for discovery only — `credentials.md:33-37` calls it to pick the env file and settle names, then writes with `set_env_values` regardless, and `configure.md:84-88` calls it only to adopt existing names.
- **Found:** The same file already names the outcome its rule cannot catch. `init.md:45-47` describes the empty key as "the quiet failure here", with no step that detects it, and the success criterion at `init.md:108-110` accepts "keys in the env file and confirmed there with `check_env_keys`".
- **Found:** The agent has no second signal to fall back on. It cannot read `.env` directly (`credentials.md:34-35` forbids it and the wizard gates the agent's Read/Write on `.env*`), and `init.md:10` grants no `posthog_exec`, so the "project state shows real events arriving" check that `setup-error-tracking.md` uses for the same question is out of reach in this task.
- **Impact:** On a repo whose `.env` was copied from a template and left with `POSTHOG_PROJECT_TOKEN=` blank beside an existing init — the common state after a partial manual setup — `init` reports the pair complete and changes nothing. `capture-exceptions` then wires capture onto a client built from `''`, and `report.md` tells the user error tracking is live. No exception ever arrives, and nothing in the run contradicts the report: `test-setup` is queued only when an uploader variant matched, so a readable-stack platform such as Django gets no end-to-end check at all.
- **Impact on the proposed fix:** The repair does not need the tool change the suggestion asks for. `mergeEnvValues` (`tools.ts:778`) updates an existing `KEY=` line in place and swaps the value, so instructing this task to write the project token with `set_env_values` even on a `present` answer closes the hole inside this repo. The trade-off to weigh is that an unconditional write also replaces a valid token the user set for a different project, which is the likely reason the current branch leaves it alone.
</issue_validation>

## Task
Investigate the issue and solve it

<potential_solution>
Extend `check_env_keys` with a host-computed state that distinguishes empty and non-empty values without returning them. Require non-empty states for both the token and host. Otherwise, write the known project values with `set_env_values` and check the states again.
</potential_solution>


## Make the environment actually reachable

Writing the keys to `.env` is only half the job — something has to load that
file at runtime, or the app throws on boot and captures nothing.

Most frameworks do it for you: Next, Nuxt, Astro and SvelteKit auto-load `.env`,
and Vite auto-loads it for client code. Nothing to do there.

A plain Node backend does not — Express, Fastify, Hono, Koa, a raw `node:http`
server — and neither does a bare Rollup or webpack config. When your init point
reads `process.env` on one of those, wire the loading too, either way:

- install `dotenv` with the project's own package manager (detect it from the
lockfile) and import it above the PostHog init — `require('dotenv').config()`,
or `import 'dotenv/config'` for ESM; or
- add `--env-file=.env` to the `start` and `dev` scripts, when the project is on
Node 20.6+ and would rather not take a new dependency.
Comment on lines +57 to +65

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not load browser variables with Node runtime mechanisms

must_fix bug

Issue description

These lines treat browser bundlers like Node runtimes. dotenv/config does not populate process.env inside a browser bundle. --env-file only sets the Node process environment. Plain Rollup and webpack still need build-time replacement. This path can break the bundle or initialize PostHog with undefined.

Why we think it's a valid issue
  • Checked: the whole env-loading section of context/agents/error-tracking/init.md (lines 49-89) against its later rules and success criterion, the skill the task loads (context/skills/integration-v2/init/, whose variants come from context/skills/integration/config.yaml), the platform detection in setup-error-tracking.md, and the place this wording came from, context/skills/error-tracking-upload-source-maps/description.md.
  • Found: The remedy is correct in its source and wrong once moved. context/skills/error-tracking-upload-source-maps/description.md:83 and :109 give the same instruction for "Rollup, plain webpack, and plain Node scripts", but there the consumer is the build itself — it says to add the loader "at the top of the bundler/config file" so POSTHOG_CLI_* reach the CLI. init.md:62 retargets it at application code: "import it above the PostHog init". For a browser bundle that is a different consumer entirely.
  • Found: The surrounding text frames the section as client-side. init.md:54-55 contrasts frameworks that auto-load .env and notes "Vite auto-loads it for client code", then init.md:57-58 puts "a bare Rollup or webpack config" in the same group as Express, Fastify, Koa and node:http, and offers only the two Node-runtime options at init.md:61-65. Neither reaches a browser bundle: dotenv pulls fs/path into client code, and --env-file on the start/dev scripts sets the dev-server process env, which a bare webpack or Rollup build never substitutes into the bundle.
  • Found: The file already carries the rule that gives the right answer, and the flagged paragraph contradicts it. init.md:85-89 says "do not wire a lookup unless you have opened the thing it reads from and seen your key defined there", and to write the public project token as a literal when nothing populates it. A bare browser bundler is exactly that case, yet lines 57-59 assert it has loading to wire, and the success criterion at init.md:108-110 demands keys "never hardcoded", pushing the agent back to the lookup.
  • Found: The skill supplies no correction. context/skills/integration/config.yaml has no webpack or rollup variant at all, so a bare-bundler project resolves to javascript_web (config.yaml:222-231), whose docs are the posthog-js library pages — nothing about build-time value injection. These platforms are in scope by design: setup-error-tracking.md maps webpack and rollup to their own uploader variants.
  • Impact: On a browser app with a bare webpack or Rollup config, following the flagged lines breaks a project that built before the run. The dotenv branch fails the build on unresolvable Node built-ins. The --env-file branch compiles, then evaluates process.env in a bundle where process does not exist, so the init module throws at load and the page renders blank — a worse outcome than the missing-events failure the section exists to prevent. The correct mechanisms for this platform are build-time replacement (EnvironmentPlugin/DefinePlugin, @rollup/plugin-replace) or the literal public token the file already prescribes for Angular.
Suggested fix

Load .env in the Node build process, not in browser init code. For webpack, inject the public values with EnvironmentPlugin or DefinePlugin. For Rollup, use the project's replacement plugin or add @rollup/plugin-replace.

Prompt to fix with AI (copy-paste)
## Context
@context/agents/error-tracking/init.md#L57-65

<issue_description>
These lines treat browser bundlers like Node runtimes. `dotenv/config` does not populate `process.env` inside a browser bundle. `--env-file` only sets the Node process environment. Plain Rollup and webpack still need build-time replacement. This path can break the bundle or initialize PostHog with `undefined`.
</issue_description>

<issue_validation>
- **Checked:** the whole env-loading section of `context/agents/error-tracking/init.md` (lines 49-89) against its later rules and success criterion, the skill the task loads (`context/skills/integration-v2/init/`, whose variants come from `context/skills/integration/config.yaml`), the platform detection in `setup-error-tracking.md`, and the place this wording came from, `context/skills/error-tracking-upload-source-maps/description.md`.
- **Found:** The remedy is correct in its source and wrong once moved. `context/skills/error-tracking-upload-source-maps/description.md:83` and `:109` give the same instruction for "Rollup, plain webpack, and plain Node scripts", but there the consumer is the build itself — it says to add the loader "at the top of the bundler/config file" so `POSTHOG_CLI_*` reach the CLI. `init.md:62` retargets it at application code: "import it above the PostHog init". For a browser bundle that is a different consumer entirely.
- **Found:** The surrounding text frames the section as client-side. `init.md:54-55` contrasts frameworks that auto-load `.env` and notes "Vite auto-loads it for client code", then `init.md:57-58` puts "a bare Rollup or webpack config" in the same group as Express, Fastify, Koa and `node:http`, and offers only the two Node-runtime options at `init.md:61-65`. Neither reaches a browser bundle: `dotenv` pulls `fs`/`path` into client code, and `--env-file` on the `start`/`dev` scripts sets the dev-server process env, which a bare webpack or Rollup build never substitutes into the bundle.
- **Found:** The file already carries the rule that gives the right answer, and the flagged paragraph contradicts it. `init.md:85-89` says "do not wire a lookup unless you have opened the thing it reads from and seen your key defined there", and to write the public project token as a literal when nothing populates it. A bare browser bundler is exactly that case, yet lines 57-59 assert it has loading to wire, and the success criterion at `init.md:108-110` demands keys "never hardcoded", pushing the agent back to the lookup.
- **Found:** The skill supplies no correction. `context/skills/integration/config.yaml` has no `webpack` or `rollup` variant at all, so a bare-bundler project resolves to `javascript_web` (`config.yaml:222-231`), whose docs are the `posthog-js` library pages — nothing about build-time value injection. These platforms are in scope by design: `setup-error-tracking.md` maps `webpack` and `rollup` to their own uploader variants.
- **Impact:** On a browser app with a bare webpack or Rollup config, following the flagged lines breaks a project that built before the run. The `dotenv` branch fails the build on unresolvable Node built-ins. The `--env-file` branch compiles, then evaluates `process.env` in a bundle where `process` does not exist, so the init module throws at load and the page renders blank — a worse outcome than the missing-events failure the section exists to prevent. The correct mechanisms for this platform are build-time replacement (`EnvironmentPlugin`/`DefinePlugin`, `@rollup/plugin-replace`) or the literal public token the file already prescribes for Angular.
</issue_validation>

## Task
Investigate the issue and solve it

<potential_solution>
Load `.env` in the Node build process, not in browser init code. For webpack, inject the public values with `EnvironmentPlugin` or `DefinePlugin`. For Rollup, use the project's replacement plugin or add `@rollup/plugin-replace`.
</potential_solution>

Comment on lines +64 to +65

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The production start command can require a missing .env file

must_fix bug

Issue description

node --env-file=.env exits before startup when .env does not exist. Production deployments often inject variables and omit this file. Adding the flag to start can therefore stop a valid deployment.

Why we think it's a valid issue
  • Checked: the flagged instruction and its sibling option in context/agents/error-tracking/init.md:57-70, the real Node behaviour of both flags, whether .env reaches a deployed artifact (ensureGitignoreCoverage in the wizard's src/lib/wizard-tools/tools.ts:602-621, called from set_env_values at mcp.ts:318), and every later task that could catch the break (test-setup.md, report.md).
  • Found: I ran it. On Node v24.21.0 in an empty directory, node --env-file=.env s.js printed node: .env: not found and exited with code 9 without executing the script, while node --env-file-if-exists=.env s.js printed .env not found. Continuing without it. and ran normally. The failure is unconditional, not timing- or input-dependent.
  • Found: The instruction puts that flag on the production command. init.md:64-65 says to "add --env-file=.env to the start and dev scripts", and the only gate it applies is the Node version, "Node 20.6+" — nothing about whether .env will exist where the app runs.
  • Found: The file is designed not to ship. The wizard appends the env file to .gitignore when it writes it (tools.ts:602-621, called at mcp.ts:318), and this flow treats .env as the secret half and .env.example as the committed half (init.md:41-42, credentials.md:43). So on a platform that runs npm start and injects variables itself, the file the flag demands is absent by construction.
  • Found: The two options the prompt offers differ exactly here, and it says nothing about it. The dotenv branch at init.md:61-63 fails soft — config() no-ops on a missing file and the injected variables still apply — while the --env-file branch fails closed. The prompt presents them as interchangeable, distinguished only by "would rather not take a new dependency".
  • Found: Nothing in the run can surface it. No task builds or starts the project, and test-setup.md only offers a local check, gated on the user accepting and on dependsOn: [wire-ci], so it is absent whenever no uploader variant matched — and locally .env exists, so even when it runs the flag succeeds. report.md:30-38 scopes its follow-ups to the personal API key and the CI secret, with no mention of the runtime environment the deploy has to supply.
  • Impact: A user whose service booted before the run gets a service that exits at startup after it, with node: .env: not found and exit code 9 as the only signal, first seen in their own deploy. That is a worse outcome than any capture gap in this flow, and the repair is small: keep the flag on dev only, or use --env-file-if-exists=.env, which exists from Node 22.9.
Suggested fix

Keep start unchanged when deployment supplies environment variables. Use --env-file=.env only when the file always exists. On Node 22.9 or later, use --env-file-if-exists=.env for optional loading.

Prompt to fix with AI (copy-paste)
## Context
@context/agents/error-tracking/init.md#L64-65

<issue_description>
`node --env-file=.env` exits before startup when `.env` does not exist. Production deployments often inject variables and omit this file. Adding the flag to `start` can therefore stop a valid deployment.
</issue_description>

<issue_validation>
- **Checked:** the flagged instruction and its sibling option in `context/agents/error-tracking/init.md:57-70`, the real Node behaviour of both flags, whether `.env` reaches a deployed artifact (`ensureGitignoreCoverage` in the wizard's `src/lib/wizard-tools/tools.ts:602-621`, called from `set_env_values` at `mcp.ts:318`), and every later task that could catch the break (`test-setup.md`, `report.md`).
- **Found:** I ran it. On Node v24.21.0 in an empty directory, `node --env-file=.env s.js` printed `node: .env: not found` and exited with code 9 without executing the script, while `node --env-file-if-exists=.env s.js` printed `.env not found. Continuing without it.` and ran normally. The failure is unconditional, not timing- or input-dependent.
- **Found:** The instruction puts that flag on the production command. `init.md:64-65` says to "add `--env-file=.env` to the `start` and `dev` scripts", and the only gate it applies is the Node version, "Node 20.6+" — nothing about whether `.env` will exist where the app runs.
- **Found:** The file is designed not to ship. The wizard appends the env file to `.gitignore` when it writes it (`tools.ts:602-621`, called at `mcp.ts:318`), and this flow treats `.env` as the secret half and `.env.example` as the committed half (`init.md:41-42`, `credentials.md:43`). So on a platform that runs `npm start` and injects variables itself, the file the flag demands is absent by construction.
- **Found:** The two options the prompt offers differ exactly here, and it says nothing about it. The `dotenv` branch at `init.md:61-63` fails soft — `config()` no-ops on a missing file and the injected variables still apply — while the `--env-file` branch fails closed. The prompt presents them as interchangeable, distinguished only by "would rather not take a new dependency".
- **Found:** Nothing in the run can surface it. No task builds or starts the project, and `test-setup.md` only offers a local check, gated on the user accepting and on `dependsOn: [wire-ci]`, so it is absent whenever no uploader variant matched — and locally `.env` exists, so even when it runs the flag succeeds. `report.md:30-38` scopes its follow-ups to the personal API key and the CI secret, with no mention of the runtime environment the deploy has to supply.
- **Impact:** A user whose service booted before the run gets a service that exits at startup after it, with `node: .env: not found` and exit code 9 as the only signal, first seen in their own deploy. That is a worse outcome than any capture gap in this flow, and the repair is small: keep the flag on `dev` only, or use `--env-file-if-exists=.env`, which exists from Node 22.9.
</issue_validation>

## Task
Investigate the issue and solve it

<potential_solution>
Keep `start` unchanged when deployment supplies environment variables. Use `--env-file=.env` only when the file always exists. On Node 22.9 or later, use `--env-file-if-exists=.env` for optional loading.
</potential_solution>

Comment on lines +49 to +65

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-JavaScript runtimes never load the env file

should_fix bug

Issue description

These instructions explain .env loading only for JavaScript runtimes. Java and Elixir read process variables, but neither runtime loads .env by default. These variants skip configure, so the flow can report success with an empty token.

Why we think it's a valid issue
  • Checked: the whole "Make the environment actually reachable" section (init.md:49-89) and the success criterion it feeds (init.md:106-114), the variant matrix the init skill inherits (context/skills/integration/config.yaml), the seed's platform routing (setup-error-tracking.md:62), and the per-language rules that reach this task through posthog-best-practices (init.md:9, context/commandments.yaml).
  • Found: The section states a runtime-agnostic rule and then covers one ecosystem. init.md:51-52 says "something has to load that file at runtime, or the app throws on boot and captures nothing", after which every named platform is JavaScript: Next, Nuxt, Astro, SvelteKit and Vite at :54-55, Express/Fastify/Hono/Koa/node:http plus bare Rollup and webpack at :57-65, and Angular at :72-89. Python, Ruby, Java, Elixir and .NET get no rule.
  • Found: Those runtimes are first-class targets of this task. context/skills/integration/config.yaml ships django, flask, fastapi, python, ruby, ruby-on-rails, java (:250, "Java (Spring Boot)"), elixir, php and laravel, and setup-error-tracking.md:62 routes them away only from the upload subgraph, never from init.
  • Found: The two halves the agent is given do not meet on those runtimes. init.md:108-110 requires "keys in the env file and confirmed there with check_env_keys, never hardcoded", while the language rules tell the code to read the process environment — commandments.yaml:144 "Initialize PostHog in AppConfig.ready() with api_key and host from environment variables", :213 for Rails, :124 for Elixir "read secrets from environment or runtime config". Nothing populates that environment from the .env the wizard just wrote, and commandments.yaml has no java section at all.
  • Found: No later task closes it, though not for the reason given. configure owns build-config edits for source-map upload and is queued only when an uploader variant matched, so it would not wire runtime env loading even when present — the gap is that no task in any branch owns this for non-JS runtimes.
  • Found: The claim does not hold uniformly, which bounds the impact. Laravel loads .env natively and commandments.yaml:179-180 has it read through env(), so that chain works. Flask's CLI loads .env when python-dotenv is installed, and many Python and Ruby projects already carry python-dotenv, django-environ or dotenv-rails, in which case the written keys resolve unchanged.
  • Found: The file also carries a correct fallback that generalises, at init.md:85-86: "do not wire a lookup unless you have opened the thing it reads from and seen your key defined there." Applied to a Django or Phoenix project with no loader, that rule stops the broken lookup — it is simply never connected to these runtimes.
  • Impact: On a Django, Spring Boot or Phoenix repo with no existing loader, the run writes the token to .env, writes code reading the process environment, and finishes. The client is then constructed from an empty or missing value and captures nothing, which is the quiet failure init.md:45-47 exists to prevent — or the lookup raises and the app fails to boot.
  • Priority: Lowered to should_fix. The prompt is silent for these runtimes rather than wrong about them, several of the platforms named in the finding do load .env or commonly already have a loader, and init.md:85-86 plus the framework docs give a correct path when applied — so the failure is likely on a subset of these projects, not all of them.
Suggested fix

Add loader rules for every supported server runtime. Update the existing launch path or framework configuration to load the selected env file.

Prompt to fix with AI (copy-paste)
## Context
@context/agents/error-tracking/init.md#L49-65
@context/agents/error-tracking/init.md#L106-112

<issue_description>
These instructions explain `.env` loading only for JavaScript runtimes. Java and Elixir read process variables, but neither runtime loads `.env` by default. These variants skip `configure`, so the flow can report success with an empty token.
</issue_description>

<issue_validation>
- **Checked:** the whole "Make the environment actually reachable" section (`init.md:49-89`) and the success criterion it feeds (`init.md:106-114`), the variant matrix the init skill inherits (`context/skills/integration/config.yaml`), the seed's platform routing (`setup-error-tracking.md:62`), and the per-language rules that reach this task through `posthog-best-practices` (`init.md:9`, `context/commandments.yaml`).
- **Found:** The section states a runtime-agnostic rule and then covers one ecosystem. `init.md:51-52` says "something has to load that file at runtime, or the app throws on boot and captures nothing", after which every named platform is JavaScript: Next, Nuxt, Astro, SvelteKit and Vite at `:54-55`, Express/Fastify/Hono/Koa/`node:http` plus bare Rollup and webpack at `:57-65`, and Angular at `:72-89`. Python, Ruby, Java, Elixir and .NET get no rule.
- **Found:** Those runtimes are first-class targets of this task. `context/skills/integration/config.yaml` ships `django`, `flask`, `fastapi`, `python`, `ruby`, `ruby-on-rails`, `java` (`:250`, "Java (Spring Boot)"), `elixir`, `php` and `laravel`, and `setup-error-tracking.md:62` routes them away only from the *upload* subgraph, never from `init`.
- **Found:** The two halves the agent is given do not meet on those runtimes. `init.md:108-110` requires "keys in the env file and confirmed there with `check_env_keys`, never hardcoded", while the language rules tell the code to read the process environment — `commandments.yaml:144` "Initialize PostHog in AppConfig.ready() with api_key and host from environment variables", `:213` for Rails, `:124` for Elixir "read secrets from environment or runtime config". Nothing populates that environment from the `.env` the wizard just wrote, and `commandments.yaml` has no `java` section at all.
- **Found:** No later task closes it, though not for the reason given. `configure` owns build-config edits for source-map upload and is queued only when an uploader variant matched, so it would not wire runtime env loading even when present — the gap is that no task in any branch owns this for non-JS runtimes.
- **Found:** The claim does not hold uniformly, which bounds the impact. Laravel loads `.env` natively and `commandments.yaml:179-180` has it read through `env()`, so that chain works. Flask's CLI loads `.env` when python-dotenv is installed, and many Python and Ruby projects already carry `python-dotenv`, `django-environ` or `dotenv-rails`, in which case the written keys resolve unchanged.
- **Found:** The file also carries a correct fallback that generalises, at `init.md:85-86`: "do not wire a lookup unless you have opened the thing it reads from and seen your key defined there." Applied to a Django or Phoenix project with no loader, that rule stops the broken lookup — it is simply never connected to these runtimes.
- **Impact:** On a Django, Spring Boot or Phoenix repo with no existing loader, the run writes the token to `.env`, writes code reading the process environment, and finishes. The client is then constructed from an empty or missing value and captures nothing, which is the quiet failure `init.md:45-47` exists to prevent — or the lookup raises and the app fails to boot.
- **Priority:** Lowered to `should_fix`. The prompt is silent for these runtimes rather than wrong about them, several of the platforms named in the finding do load `.env` or commonly already have a loader, and `init.md:85-86` plus the framework docs give a correct path when applied — so the failure is likely on a subset of these projects, not all of them.
</issue_validation>

## Task
Investigate the issue and solve it

<potential_solution>
Add loader rules for every supported server runtime. Update the existing launch path or framework configuration to load the selected env file.
</potential_solution>


You cannot run the app, so this has to be right by construction. Writing the
guard without the loader is what produces
`POSTHOG_… variable required by PostHog is missing or un-configured` at module
load: the guard you wrote firing against an env file nothing reads.

Some platforms have no environment to read at all, and there the answer is not
a loader. Angular on the stock `@angular/build` builder is the common one:
nothing populates `process.env`, `import.meta.env`, or the project's own
`src/environments/.env.ts` with your keys.

The trap is that each of those *looks* like a mechanism. `import.meta.env` and
the `NG_APP_` prefix only exist with `@ngx-env/builder` installed, and a
generated `.env.ts` usually carries one unrelated key such as
`npm_package_version` and nothing else. Read from either and your key is
`undefined`: in development the guard throws while the module evaluates and the
app renders a blank page, and in a production build the guard returns quietly,
so the app looks fine while PostHog never initialises at all.

So do not wire a lookup unless you have opened the thing it reads from and seen
your key defined there. When nothing populates it, write the real public project
token as a literal in the committed `src/environments/*` files. This is the
skill's "no valid environment to read from" case, and the public token is
publishable — it ships inside the browser bundle either way.
Comment on lines +85 to +89

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Angular fallback omits the project host

should_fix bug

Issue description

The fallback writes only the project token. Angular initialization also passes environment.posthogHost to api_host. An undefined or US-default host sends EU and self-hosted events to the wrong deployment.

Why we think it's a valid issue
  • Checked: every mention of configuration values in context/agents/error-tracking/init.md (the env-var path at :49-70, the Angular fallback at :72-89, the type-check rule at :91-104, the success criterion at :106-114), the skill this task loads (context/skills/integration-v2/init/description.md), the Angular rules that reach it via posthog-best-practices (context/commandments.yaml:310-314), and the Angular page the variant supplies (https://posthog.com/docs/error-tracking/installation/angular.md).
  • Found: The host is absent from this file end to end. init.md:86-87 says to "write the real public project token as a literal in the committed src/environments/* files", init.md:111-112 checks only that "the token is a literal rather than a lookup into something that never defines it", and no line in the file names the API host, the region, or a <UI_HOST> substitution — unlike credentials.md and report.md, which tell the agent to replace <UI_HOST> from project context.
  • Found: The Angular init genuinely needs two values. The docs page initializes with posthog.init(environment.posthogKey, { api_host: environment.posthogHost, defaults: '2026-05-30' }), reading both properties from src/environments/environment.ts — the same file the fallback tells the agent to write. commandments.yaml:314 says only "Configure PostHog credentials in src/environments/environment.ts files" and names neither value.
  • Found: The requirement reaches the agent only through a path that does not cover this case. The init skill's §Environment variables lists both "the public project token" and "the PostHog host", but scopes them to values set "through the wizard tools (set_env_values)". Its own fallback sentence — "embed the real public token in the config the build ships" — omits the host exactly as init.md does, so on the no-environment branch nothing in context asks for it.
  • Found: Angular's type checking bounds the failure. Angular projects are TypeScript, and init.md:93-96 requires the init to compile by construction, so an environment.posthogHost reference with no matching property fails the build rather than shipping. The reachable failure is therefore a wrong host value — an empty string, or a hardcoded US address on a non-US project — not a missing property.
  • Impact: posthog-js falls back to the US ingestion host when api_host is empty or absent, so an EU-cloud or self-hosted project initialised this way posts events with a token that region does not know. Nothing throws, the app runs, and Error Tracking stays empty — the same quiet failure init.md:45-47 warns about, on the stock @angular/build builder this file itself calls "the common one" (init.md:73). A US-cloud project is unaffected because the default matches.
  • Priority: Lowered to should_fix. The docs snippet and the compile-by-construction rule force both properties to exist, so this needs the agent to source a wrong host value rather than skip it, and the consequence lands only on non-US-cloud projects. The repair is still small — name the host beside the token in init.md:86-87 and require both at :111-112.
Suggested fix

Write both the project token and client API host into each active Angular environment file. Require both values in the success criteria.

Prompt to fix with AI (copy-paste)
## Context
@context/agents/error-tracking/init.md#L85-89
@context/agents/error-tracking/init.md#L108-112

<issue_description>
The fallback writes only the project token. Angular initialization also passes `environment.posthogHost` to `api_host`. An undefined or US-default host sends EU and self-hosted events to the wrong deployment.
</issue_description>

<issue_validation>
- **Checked:** every mention of configuration values in `context/agents/error-tracking/init.md` (the env-var path at `:49-70`, the Angular fallback at `:72-89`, the type-check rule at `:91-104`, the success criterion at `:106-114`), the skill this task loads (`context/skills/integration-v2/init/description.md`), the Angular rules that reach it via `posthog-best-practices` (`context/commandments.yaml:310-314`), and the Angular page the variant supplies (`https://posthog.com/docs/error-tracking/installation/angular.md`).
- **Found:** The host is absent from this file end to end. `init.md:86-87` says to "write the real public project token as a literal in the committed `src/environments/*` files", `init.md:111-112` checks only that "the token is a literal rather than a lookup into something that never defines it", and no line in the file names the API host, the region, or a `<UI_HOST>` substitution — unlike `credentials.md` and `report.md`, which tell the agent to replace `<UI_HOST>` from project context.
- **Found:** The Angular init genuinely needs two values. The docs page initializes with `posthog.init(environment.posthogKey, { api_host: environment.posthogHost, defaults: '2026-05-30' })`, reading both properties from `src/environments/environment.ts` — the same file the fallback tells the agent to write. `commandments.yaml:314` says only "Configure PostHog credentials in src/environments/environment.ts files" and names neither value.
- **Found:** The requirement reaches the agent only through a path that does not cover this case. The init skill's §Environment variables lists both "the public project token" and "the PostHog host", but scopes them to values set "through the wizard tools (`set_env_values`)". Its own fallback sentence — "embed the real public token in the config the build ships" — omits the host exactly as `init.md` does, so on the no-environment branch nothing in context asks for it.
- **Found:** Angular's type checking bounds the failure. Angular projects are TypeScript, and `init.md:93-96` requires the init to compile by construction, so an `environment.posthogHost` reference with no matching property fails the build rather than shipping. The reachable failure is therefore a *wrong* host value — an empty string, or a hardcoded US address on a non-US project — not a missing property.
- **Impact:** `posthog-js` falls back to the US ingestion host when `api_host` is empty or absent, so an EU-cloud or self-hosted project initialised this way posts events with a token that region does not know. Nothing throws, the app runs, and Error Tracking stays empty — the same quiet failure `init.md:45-47` warns about, on the stock `@angular/build` builder this file itself calls "the common one" (`init.md:73`). A US-cloud project is unaffected because the default matches.
- **Priority:** Lowered to `should_fix`. The docs snippet and the compile-by-construction rule force both properties to exist, so this needs the agent to source a wrong host value rather than skip it, and the consequence lands only on non-US-cloud projects. The repair is still small — name the host beside the token in `init.md:86-87` and require both at `:111-112`.
</issue_validation>

## Task
Investigate the issue and solve it

<potential_solution>
Write both the project token and client API host into each active Angular environment file. Require both values in the success criteria.
</potential_solution>


## The init has to type-check

You cannot run the build either. So in a project the compiler checks —
TypeScript, or JavaScript under `checkJs` — the init has to compile by
construction. `process.env.ANYTHING` is `string | undefined`, and the SDK
constructor wants a `string`.

Pass the variable that you checked. `if (!key) { … } else { new PostHog(key, …) }`
narrows `key` to `string`. A check on a different variable does not: store the
missing name in `missing`, test `missing`, then pass `key`, and the compiler
still sees `string | undefined`. The build then stops with "Argument of type
'string | undefined' is not assignable to parameter of type 'string'", emits
no bundle, and uploads no source maps. When the existing code already passes
`process.env.KEY ?? ''` or `process.env.KEY!`, keep that part as it is.

## How you know you succeeded

An init point exists with the PostHog env keys present — whether it already
did or you just created it — keys in the env file and confirmed there with
`check_env_keys`, never hardcoded. On a platform that does not auto-load
`.env`, the loading is wired; on a platform with no environment at all, the
token is a literal rather than a lookup into something that never defines it.
In a type-checked project, the value you pass to the SDK is narrowed to a
`string`.
Your handoff names the files involved, how the client is constructed, and how
`.env` reaches it, so the capture-exceptions task can find the init options
without re-discovering them.
Loading
Loading