# @kentcdodds/friction-log — agent notes Human setup and intent live in [`README.md`](./README.md). This file is for agents: imports, smoke/dryRun snippets, and edge cases. Secrets by **name** only — never paste token values. **Do not disable** the live `daily` job or related webhooks unless the user explicitly asks. ## Secrets | Name | Via | Required | Notes | | --- | --- | --- | --- | | `github-botAccessToken` | `@kentcdodds/github` account `bot` | Yes (github host) | Issues/comments on `target.repo` | | `cursorApiKey` | `@kentcdodds/cursor` | Yes (daily sweep) | Spawn Cloud Agents on sweep | | grok-bot wake secrets | `@kentcdodds/grok-bot` | Yes (kody host) | Stamp-aligned; do not mint URLs yourself | ## Qualify principles (soft skip) Before filing, `./create` / `./file` soft-skip when **any** of: 1. Missing/invalid `target` (`host` not `github`|`kody`, or `repo` empty; github needs `owner/repo`) 2. Structured filing missing `preliminaryInvestigation` (and no `body`) 3. Missing reproducible gap: no `howToReproduce` **and** no `body` **and** structured fields too thin 4. Looks transient/session-bound: heuristic — no `howToReproduce`, empty/missing `cost`, `whatHappened` under ~40 chars (and no `body`) 5. For github: title empty after `Friction:` normalize Bar: **durable, recurring, clear owner, reproducible contract/API/docs gap**. Do **not** invent a deny-list of titles. Skip returns `{ ok: true, skipped: true, reason, title }` (create) or `skipped[]` (file). Structured filings also pass `preliminaryInvestigation` (short notes on what was already looked at) and optional `relevantFiles` (omit when none known). Do not invent a root cause. Do not paste a full issue thread. ## Import paths | Export | Import | | --- | --- | | overview | `kody:@kentcdodds/friction-log` | | create | `kody:@kentcdodds/friction-log/create` | | file | `kody:@kentcdodds/friction-log/file` | | scan | `kody:@kentcdodds/friction-log/scan` | | sweep | `kody:@kentcdodds/friction-log/sweep` | | daily | `kody:@kentcdodds/friction-log/daily` | | record-outcome | `kody:@kentcdodds/friction-log/record-outcome` | | pause | `kody:@kentcdodds/friction-log/pause` | | resume | `kody:@kentcdodds/friction-log/resume` | | status | `kody:@kentcdodds/friction-log/status` | Prefer `npx @kodycodes/cli execute --local` (Node ≥22) with static `kody:@kentcdodds/friction-log/...` imports. Do **not** use hosted MCP `execute`. Do not lead with `packages.invoke`. If `--local` cannot run, fix the environment — Open API / MCP `api` cannot invoke these package exports. ## Target (required) ```ts type FrictionHost = 'github' | 'kody' type FrictionTarget = { host: FrictionHost /** github: "owner/repo"; kody: "@owner/leaf" or "leaf" */ repo: string } ``` - No aliases, no dual/compat paths, no silent default host. - Platform GitHub queue: pass `target: { host: 'github', repo: 'kody-hq/kody' }`. - Package friction: `target: { host: 'kody', repo: '@kentcdodds/leaf' }` → Patch wake only. ## Smoke / dryRun Operator snapshot (read-only): ```ts import status from 'kody:@kentcdodds/friction-log/status' export default async function main() { return await status() } ``` Create — GitHub dryRun: ```ts import create from 'kody:@kentcdodds/friction-log/create' export default async function main() { return await create({ dryRun: true, target: { host: 'github', repo: 'kody-hq/kody' }, title: 'Friction: example papercut', whatHappened: '…', whatYouWanted: '…', howToReproduce: '…', cost: '…', preliminaryInvestigation: '…', relevantFiles: ['path/when/known.ts'], }) // => { ok, dryRun: true, routed: 'github', title, body, labels: ['friction'], created: false, target } } ``` Create — would-skip qualify (no network write beyond soft skip): ```ts import create from 'kody:@kentcdodds/friction-log/create' export default async function main() { return await create({ target: { host: 'github', repo: 'kody-hq/kody' }, title: 'confused once', whatHappened: 'brief confusion', }) // => { ok: true, skipped: true, reason, title } } ``` Create — Kody Patch wake dryRun: ```ts import create from 'kody:@kentcdodds/friction-log/create' // Local-execute callers should also statically import wake so bundler provenance // includes the nested grok-bot dependency. import 'kody:@kentcdodds/grok-bot/wake' export default async function main() { return await create({ dryRun: true, target: { host: 'kody', repo: '@kentcdodds/friction-log' }, title: 'export map stale', whatHappened: 'docs still imply a silent default host', whatYouWanted: 'required target documented everywhere', howToReproduce: 'read create without target in AGENTS', cost: 'wrong routing', preliminaryInvestigation: 'Compared create export JSDoc with AGENTS.md smoke examples.', relevantFiles: ['AGENTS.md', 'src/create.ts'], }) // => { ok, routed: 'patch-wake', woke: false, dryRun: true, title, target } } ``` ## File (end-of-ship) One top-level `target` applies to **all** items. Papercuts do not carry their own target. Prefer `./file` for batches. ```ts import file from 'kody:@kentcdodds/friction-log/file' export default async function main() { return await file({ dryRun: true, target: { host: 'github', repo: 'kody-hq/kody' }, items: [ { title: 'Friction: example papercut', whatHappened: '…', whatYouWanted: '…', howToReproduce: '…', cost: '…', preliminaryInvestigation: '…', relevantFiles: ['path/when/known.ts'], }, ], }) // => { ok, dryRun, filed, reused, woke, skipped } } ``` Kody batch (results in `woke`): ```ts import file from 'kody:@kentcdodds/friction-log/file' export default async function main() { return await file({ dryRun: true, target: { host: 'kody', repo: '@kentcdodds/friction-log' }, items: [ { title: 'qualify soft-skip unclear', whatHappened: '…', whatYouWanted: '…', howToReproduce: '…', cost: '…', preliminaryInvestigation: '…', }, ], }) // => { ok, dryRun, filed: [], reused: [], woke: [{ action:'woke', bot:'patch', woke, title, target }], skipped } } ``` ## Edge cases - **Always** pass `target`. Never assume `kody-hq/kody`. Never use `gh issue create` for friction that should hit the daily sweep — missing `friction` label means the platform sweep never sees it. - Prefer `./file` for end-of-ship batches; empty input / bad target → `skipped`. - GitHub reuse matches the exact normalized title, and the "Seen again" comment is built only from the item you passed. If a reuse lists a title you did not send, the call received stale params: check the `items` actually passed (for example the file behind `--params "$(cat items.json)"`). Re-filing a papercut whose body equals the issue body reuses without commenting (`commentSkipped: 'same-as-issue-body'`); `dryRun` reports the same. - Daily sweep eligibility: open + `friction`, not `friction-skipped`, on **kody-hq/kody** only (hard-coded queue). Comments are not parsed. - At most **one** automatic Cloud Agent per Denver calendar day. - `./pause` / `./resume` are the kill switch. Prefer pause over disabling the `daily` job definition. - `./record-outcome` stores the agent's final report — keep summary short; never include secrets. - Kody host: package calls `import wake from 'kody:@kentcdodds/grok-bot/wake'` with `bot: 'patch'`. From local CLI execute (or any execute path), also statically import `kody:@kentcdodds/grok-bot/wake` alongside the friction-log export so bundler provenance includes the nested grok-bot dependency. This is a bundler-provenance requirement, not a secret-visibility or Discord workaround. Cole does not need to arm a separate routine for this path. - Issue text forwarded into the Cloud Agent prompt goes through `sanitizeForPrompt`, which breaks any literal `{{secret:…}}` placeholder (`{ {secret:…}}`). Kody expands placeholders in outbound request bodies, so a quoted placeholder would otherwise fail the run with "Secrets require host approval" for `api.cursor.com`. Never approve a secret for that host to "fix" it; that would leak the secret into the prompt. - Daily Cloud Agents call `./record-outcome` via `npx @kodycodes/cli execute --local` — never hosted MCP `execute`. ## Mention safety Friction issues and comments are public. Before posting, `./create` and `./file` run a deterministic pass (`src/mention-safe.ts`): scoped package names (`@scope/name`, `kody:@scope/name/export`) go in inline code and credit phrases like "by @someone" are dropped. Same-title dedupe matches both the original and the sanitized title, so issues filed before this pass still reuse. `@kentcdodds/github` refuses any remaining @-mention. The daily agent's skip comment is the only intended mention. It pings Kent with `allowedMentions: ['kentcdodds']` on that one comment and nowhere else. ## Dependency note Uses `@kentcdodds/github` (`createAuthenticatedFetch` on `github-bot` / `github-kent` OAuth integrations) and `@kentcdodds/grok-bot` for Patch wake.