Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .changeset/feedback-channels.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@taskless/cli": patch
---

An agent can now send the Taskless team general feedback (`taskless agent feedback`) or a bug report (`taskless agent bug-report`) on the user's behalf, without a GitHub account. Both show the user the exact payload and send only on their explicit yes, and both appear in the `taskless agent` index. A bug report's version information is filled in by the CLI from local state. The invited rule-authoring survey's recipe is renamed to `taskless agent rule-feedback`; its questions, invite, and cadence are unchanged. `feedback send` payloads now carry a required `kind` (`rule`, `general`, or `bug`). With telemetry disabled, nothing is sent and the CLI points to https://github.com/taskless/cli/issues instead.
2 changes: 2 additions & 0 deletions openspec/changes/feedback-channels/.openspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-10-08
57 changes: 57 additions & 0 deletions openspec/changes/feedback-channels/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
## Context

The survey machinery assumes one survey throughout: `survey/constants.ts` exports a single `SURVEY_ID` and `SURVEY_QUESTIONS`, `buildSurveyResponse` maps one flat payload, and both `feedback` verbs advance one cadence file. The invite (`survey/invite.ts`) and its gate are already per-survey in shape (the cadence store is keyed by survey id), so they need no change beyond the topic name the invite text points at.

The three PostHog surveys are fixed inputs (verified 2026-10-08, all active):

| kind | survey id | questions (id → payload key) |
| --------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `rule` | `01a0c7b9-dfe4-0000-d05e-ce253e90a68c` | unchanged from today's seven |
| `general` | `01a11da4-3948-0000-4ae4-c9da9321801e` | `c71e52ee…` Feedback → `verbatim`; `ab0ceb25…` Attach any additional context → `context` |
| `bug` | `01a11da7-27a2-0000-0f4e-6d3e1f89f385` | `2dd63cf3…` → `summary`; `ba096b81…` Version Information → CLI-filled; `e59a87e9…` → `trying`; `73415b80…` → `expected`; `daa98d8d…` → `actual`; `597381f5…` → `context` |

Full question ids live in `constants.ts` only, as they do today.

## Goals / Non-Goals

**Goals:**

- One send path, three surveys, with the payload's shape checked before anything leaves the machine.
- The agent never handles a question UUID or a survey id.
- User-initiated channels never disturb the invite cadence.

**Non-Goals:**

- A cadence, invite, or `dismiss` for the general or bug channels. They exist because the user asked; there is nothing to throttle.
- Stripping identity from bug reports. The existing telemetry identity (anonymous id, or JWT subject when logged in, plus `ghOwner` and adoption dimensions) rides along as on every event. "Anonymous" here means no GitHub account is required.
- Attachments, logs, or file uploads in a bug report.
- Any PostHog survey change.

## Decisions

**A survey registry keyed by kind.** `constants.ts` exports `SURVEYS: Record<FeedbackKind, { id, questions }>`, where each question is `{ key, id, question }` as today. `buildSurveyResponse(input)` looks up `SURVEYS[input.kind]` and walks its questions. `RULE_SURVEY_ID` is kept as a named export because the invite, the gate, and `dismiss` refer to that survey specifically, and spelling it `SURVEYS.rule.id` at each site hides that they are survey-specific. Alternative: three parallel modules. Rejected: the mapping loop is identical, and three copies is the DRY threshold.

**One `feedback send`, a zod discriminated union on `kind`.** `inputSchema = z.discriminatedUnion("kind", [rule, general, bug])`, each branch a `z.strictObject`. Strict, so a `general` payload carrying `ruleKind` fails naming the key instead of the key being silently dropped. That also tightens the `rule` branch, which today strips unknown keys; the recipe that writes it ships in the same binary, so the only payloads affected are hand-written ones. Each recipe embeds its own branch (`rule-feedback` → `ruleSchema`, etc.) through `TOPIC_INPUT_SCHEMAS`, so an agent reading `bug-report` never sees the rule keys. Alternatives: a `--kind` flag (duplicates what the file must say anyway, and lets the two disagree), or three verbs (three copies of read/parse/validate/error handling).

**`kind` is required: it is the zod discriminator.** Each branch declares `kind` as a required literal (`z.literal("rule")`, `z.literal("general")`, `z.literal("bug")`), with no `.default()` and no preprocessing that fills it in. That is what makes it the discriminator, and it carries through to the recipes: each branch's rendered JSON Schema lists `kind` under `required` with a single `const` value, so the agent reading a recipe sees the exact literal to write. A missing or unknown `kind` fails as "`kind` …", rather than being read as `rule` and failing as "`ruleKind` is required", which would point the agent at the wrong recipe.

**The CLI writes the bug survey's version-information answer.** It is assembled in the command from `__VERSION__`, the manifest read (`install.cliVersion`, `rules.reconciledTo`), `process.platform`/`process.arch`, and `process.version`, rendered as a few `key: value` lines. It reuses `readManifest` and does not call `info`'s handler: `info` makes a network `whoami` call and reports identity, both of which this answer must not carry. The agent's schema has no key for it, so it cannot be wrong or omitted.

**The index gains a "Feedback recipes" section listing `feedback` and `bug-report`; `rule-feedback` stays unlisted.** The index prints CLI commands (every subcommand not in `UNLISTED_COMMANDS`) and then the explicit `RECIPE_TOPICS` list under "Authoring recipes". Neither new topic is an authoring recipe, and `bug-report` is not a command, so a third explicit list, `FEEDBACK_TOPICS`, prints under its own heading and joins the shared padding width. The `feedback` _command_ stays in `UNLISTED_COMMANDS`: what the agent should reach is the recipe, which tells it to get consent before running the command, and listing both would offer a path that skips the recipe. Its comment there is rewritten to say so. `rule-feedback` appears in no list, which is all it takes to keep it out: listed, an agent runs it unprompted and the funnel fills with uninvited `survey sent`. Alternative: append both to `RECIPE_TOPICS`. Rejected because "Authoring recipes" would then mislabel them.

**Telemetry off: validate, then redirect.** The order stays validate-first, so an agent still hears about a malformed payload. The message changes from "Nothing else to do" to naming `https://github.com/taskless/cli/issues`, for every kind, because the user is now often the one who asked to send. `dismiss` keeps its current line: a dismissal under the opt-out has nothing to redirect.

**Consent lives in the recipes, not the CLI.** The general and bug recipes require the agent to show the payload and get an explicit yes. A CLI-side `--yes` flag was considered and rejected: the agent would pass it reflexively, and the CLI cannot tell a human's yes from the agent's.

**Recipe file moves.** `agent/feedback.md` → `agent/rule-feedback.md` (git mv, so its history follows), then a new `agent/feedback.md`. `feedback-invite.md`'s two references change to `agent rule-feedback`. All three new and renamed topics join `INTERNAL_TOPICS` in `prompts/index.ts`: none has a reader outside the CLI that sends the response.

## Risks / Trade-offs

- [An agent mixes CLI versions: the invite comes from one CLI and `agent feedback` (now the general recipe) from an older or newer one.] → The invite and the recipe it names are served by the same binary in every documented flow (the skill pins one version). A mismatched agent following an old invite to the new `agent feedback` gets the general recipe, whose payload still validates and reaches a real survey: misfiled, not lost.
- [Free-text answers carry secrets or proprietary code to PostHog.] → Both recipes require redaction and a user-approved preview. The CLI does not try to detect secrets; a heuristic scanner would give false confidence.
- [The general and bug PostHog surveys carry the PMF description text.] → Cosmetic and not respondent-visible; noted to the survey owner, not fixed here.
- [Strict objects reject a key a future survey question adds before the CLI knows it.] → That is the intended failure: the CLI owns the map, and an unmapped key would otherwise be dropped silently.

## Migration Plan

No data migration. The rule survey id is unchanged, so every install's cadence carries over. Rollback is a revert; no state is written that an older CLI cannot read.
40 changes: 40 additions & 0 deletions openspec/changes/feedback-channels/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
## Why

Feedback from the CLI today goes one way: the rule-authoring survey the CLI invites after an onboarding or authoring recipe. A user who wants to tell the Taskless team something else, or report a bug, has no path from their agent short of a GitHub issue, which needs a GitHub account and leaves the agent's view of the session behind. The `taskless agent` index already anticipated this: `feedback` was held out of it "until a general feedback channel exists". This change builds that channel and a bug-report channel beside it, and renames the invited survey so the three do not share one name.

## What Changes

- The invited rule-authoring survey is renamed from `agent feedback` to `agent rule-feedback`. Its PostHog survey (`01a0c7b9-dfe4-0000-d05e-ce253e90a68c`), questions, invite, cadence, and `feedback dismiss` are unchanged. It stays out of the `taskless agent` index.
- New `agent feedback` recipe for general product feedback, sent to PostHog survey `01a11da4-3948-0000-4ae4-c9da9321801e` ("Feedback", "Attach any additional context"). Listed in the `taskless agent` index.
- New `agent bug-report` recipe for bug reports, sent to PostHog survey `01a11da7-27a2-0000-0f4e-6d3e1f89f385` (summary, version information, what the user was trying to do, expected result, actual result, additional context). Listed in the `taskless agent` index. No GitHub account is needed.
- `feedback send --from` accepts a payload discriminated by a required `kind` field (`rule`, `general`, `bug`), validated by zod, and maps each kind to its own survey and question identifiers. **BREAKING** for the payload only: a rule-feedback payload must now carry `kind: "rule"`. The recipe that writes the payload ships in the same binary, so no released agent flow writes the old shape against a new CLI.
- The bug survey's version-information answer is filled in by the CLI from local, non-identifying state, never written by the agent.
- General feedback and bug reports are user-initiated: they neither read nor write the invite cadence, and both recipes require the agent to show the user the payload and get an explicit yes before sending.
- With telemetry disabled, `feedback send` for any kind sends nothing, says that telemetry is off, and points the user to `https://github.com/taskless/cli/issues`. Both new recipes tell the agent to relay that.
- The Taskless skill (`skills/taskless/SKILL.md`) gains triggers and topic rows for sending feedback and reporting a bug.

## Capabilities

### New Capabilities

(none)

### Modified Capabilities

- `cli-feedback-survey`: grows from one invited survey to three feedback channels. The `feedback` subcommand, payload, and recipe requirements change; new requirements cover the general and bug channels, their consent step, and the telemetry-off redirect. Invite and cadence requirements are unchanged except for the recipe name the invite points to.

## Impact

- `packages/cli/src/survey/constants.ts`: one survey registry instead of a single `SURVEY_ID`.
- `packages/cli/src/schemas/feedback.ts`: discriminated union over `kind`.
- `packages/cli/src/commands/feedback.ts`: per-kind mapping, cadence only for `rule`, telemetry-off message, CLI-filled version information.
- `packages/cli/src/commands/agent.ts`: `UNLISTED_COMMANDS` drops `feedback`; `rule-feedback` is withheld from the index as a topic.
- `packages/cli/src/agent/`: `feedback.md` → `rule-feedback.md`; new `feedback.md` and `bug-report.md`; `feedback-invite.md` points at `rule-feedback`.
- `packages/cli/src/prompts/recipes.ts` and `prompts/index.ts`: input-schema map and `INTERNAL_TOPICS`.
- `skills/taskless/SKILL.md`.
- Tests: `feedback-command`, `feedback-schema`, `feedback-recipes`, `survey-invite`, `prompts`.
- PostHog: no survey changes. All three surveys exist and are active.

## Delivery shape

**Stacked, merged atomically with `gh stack merge`.** The rename, the schema discriminator, and the two new recipes are only coherent together: an intermediate layer would ship, for example, a `feedback send` that requires `kind` while the recipe the invite names still describes one survey, or a `taskless agent` index listing a `bug-report` topic before the skill routes to it. `gh stack merge` lands every layer on `main` in one all-or-nothing operation, so no intermediate layer can reach `main` on its own. The layers, bottom to top: this proposal and the changeset; the survey registry and payload schema; `feedback send` per-kind behavior; the recipes and agent index; the skill; the archive. One `patch` changeset on the bottom layer, since the package is pre-1.0 and the payload change is internal to one binary.
Loading
Loading