From f6822f48240cc803eb99430f94ac7e0eb5b97ff0 Mon Sep 17 00:00:00 2001 From: Jakob Heuser Date: Thu, 8 Oct 2026 16:48:02 -0700 Subject: [PATCH 1/3] docs(openspec): propose feedback-channels --- .changeset/feedback-channels.md | 5 + .../changes/feedback-channels/.openspec.yaml | 2 + openspec/changes/feedback-channels/design.md | 57 ++++ .../changes/feedback-channels/proposal.md | 40 +++ .../specs/cli-feedback-survey/spec.md | 248 ++++++++++++++++++ openspec/changes/feedback-channels/tasks.md | 33 +++ 6 files changed, 385 insertions(+) create mode 100644 .changeset/feedback-channels.md create mode 100644 openspec/changes/feedback-channels/.openspec.yaml create mode 100644 openspec/changes/feedback-channels/design.md create mode 100644 openspec/changes/feedback-channels/proposal.md create mode 100644 openspec/changes/feedback-channels/specs/cli-feedback-survey/spec.md create mode 100644 openspec/changes/feedback-channels/tasks.md diff --git a/.changeset/feedback-channels.md b/.changeset/feedback-channels.md new file mode 100644 index 00000000..b1475c22 --- /dev/null +++ b/.changeset/feedback-channels.md @@ -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. diff --git a/openspec/changes/feedback-channels/.openspec.yaml b/openspec/changes/feedback-channels/.openspec.yaml new file mode 100644 index 00000000..01660bab --- /dev/null +++ b/openspec/changes/feedback-channels/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-10-08 diff --git a/openspec/changes/feedback-channels/design.md b/openspec/changes/feedback-channels/design.md new file mode 100644 index 00000000..9eec7a20 --- /dev/null +++ b/openspec/changes/feedback-channels/design.md @@ -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`, 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. diff --git a/openspec/changes/feedback-channels/proposal.md b/openspec/changes/feedback-channels/proposal.md new file mode 100644 index 00000000..24cbed2a --- /dev/null +++ b/openspec/changes/feedback-channels/proposal.md @@ -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, merging down.** 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. So the stack lands on `main` in one protected merge, after each layer merges down into its parent. 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. diff --git a/openspec/changes/feedback-channels/specs/cli-feedback-survey/spec.md b/openspec/changes/feedback-channels/specs/cli-feedback-survey/spec.md new file mode 100644 index 00000000..8f347a21 --- /dev/null +++ b/openspec/changes/feedback-channels/specs/cli-feedback-survey/spec.md @@ -0,0 +1,248 @@ +## MODIFIED Requirements + +### Requirement: The cadence store is one epoch timestamp per survey + +The CLI SHALL keep the survey cadence at `/surveys//next_ask`, where the config directory is the same XDG location that holds the anonymous telemetry id and the survey id is the PostHog survey's UUID. The file SHALL contain a single decimal epoch-milliseconds value: the earliest time the next invite may be served. Serving an invite SHALL set it to now plus 10 days; `feedback dismiss` and a `feedback send` of a `rule` payload SHALL each set it to now plus 20 days. A `feedback send` of a `general` or `bug` payload SHALL NOT read or write any cadence file. Every CLI version SHALL share the file, so upgrading the CLI does not reset the cadence; a different survey id SHALL have its own file. + +#### Scenario: A CLI upgrade keeps the cadence + +- **WHEN** version A served an invite yesterday and the user runs version B for the first time +- **THEN** version B SHALL read the same `next_ask` and SHALL NOT serve the invite + +#### Scenario: A new survey is a new ask + +- **WHEN** the CLI ships with a different survey id than the one whose `next_ask` is on disk +- **THEN** the CLI SHALL find no `next_ask` for the new survey and SHALL serve the invite + +#### Scenario: Dismissal earns the longer gap + +- **WHEN** an invite was served and the agent runs `taskless feedback dismiss` +- **THEN** `next_ask` SHALL be about 20 days in the future, later than the value the invite wrote + +#### Scenario: A bug report leaves the cadence alone + +- **WHEN** `next_ask` holds a value and an agent sends a valid `bug` payload with `feedback send` +- **THEN** `next_ask` SHALL be unchanged +- **AND** no cadence file SHALL be created for the bug survey + +### Requirement: The feedback subcommand records dismissals and sends responses + +The CLI SHALL provide a `feedback` subcommand with two verbs. `feedback dismiss` SHALL capture `survey dismissed` with the rule survey's `$survey_id` and advance that survey's `next_ask`. `feedback send --from ` SHALL read a JSON file, validate it against the feedback payload schema, and capture `survey sent` with the `$survey_id` of the survey the payload's `kind` selects; for a `rule` payload it SHALL also advance `next_ask`. Both verbs SHALL accept `--dir`. Neither SHALL require `.taskless/` to exist, run migrations, or write into the project. `feedback send` SHALL NOT delete its input file. + +When the telemetry client is disabled, `feedback dismiss` SHALL print a single line saying nothing was sent and exit zero. `feedback send` SHALL still validate the payload first, and on a valid payload SHALL send nothing, print that telemetry is disabled so nothing was sent, name `https://github.com/taskless/cli/issues` as the place to report instead, and exit zero. + +The `taskless agent` topic index SHALL list `feedback` and `bug-report` and SHALL NOT list `rule-feedback`. `taskless agent rule-feedback` SHALL still serve the `rule-feedback` recipe. + +#### Scenario: Dismiss + +- **WHEN** an agent runs `taskless feedback dismiss` +- **THEN** PostHog SHALL receive `survey dismissed` with the rule survey's `$survey_id` +- **AND** the command SHALL exit zero + +#### Scenario: Send a valid payload + +- **WHEN** an agent runs `taskless feedback send --from .taskless/.tmp-feedback.json` with a payload that passes validation +- **THEN** PostHog SHALL receive `survey sent` carrying the `$survey_id` for the payload's `kind` and one `$survey_response_` per answered question, and no other survey-specific property +- **AND** the command SHALL exit zero and leave the input file in place + +#### Scenario: Each kind reaches its own survey + +- **WHEN** valid payloads of kind `rule`, `general`, and `bug` are each sent +- **THEN** their `survey sent` events SHALL carry `$survey_id` `01a0c7b9-dfe4-0000-d05e-ce253e90a68c`, `01a11da4-3948-0000-4ae4-c9da9321801e`, and `01a11da7-27a2-0000-0f4e-6d3e1f89f385` respectively + +#### Scenario: Send an invalid payload + +- **WHEN** the payload is missing a required key, has no `kind` or an unknown `kind`, or `completed` is not one of the allowed values +- **THEN** the command SHALL exit non-zero with `INVALID_INPUT`, naming the failing field +- **AND** no survey event SHALL be captured and `next_ask` SHALL be unchanged + +#### Scenario: Telemetry disabled + +- **WHEN** `DO_NOT_TRACK=1` and an agent runs `feedback dismiss`, or runs `feedback send` with a valid payload of any kind +- **THEN** the command SHALL send nothing and exit zero +- **AND** `feedback send` SHALL print that telemetry is disabled and name `https://github.com/taskless/cli/issues` + +#### Scenario: Telemetry disabled does not excuse an invalid payload + +- **WHEN** `DO_NOT_TRACK=1` and the payload fails validation +- **THEN** the command SHALL exit non-zero with `INVALID_INPUT` + +#### Scenario: Not in the index + +- **WHEN** an agent runs `taskless agent` +- **THEN** the printed index SHALL NOT list `rule-feedback` + +#### Scenario: The user-initiated channels are in the index + +- **WHEN** an agent runs `taskless agent` +- **THEN** the printed index SHALL list `feedback` and `bug-report` + +### Requirement: The feedback payload uses human keys mapped to survey questions by the CLI + +The feedback payload SHALL be a JSON object with a required `kind` of exactly `rule`, `general`, or `bug`, which selects the survey and the remaining keys. Keys belonging to another kind SHALL be rejected rather than ignored. + +For `kind: "rule"`: + +| key | required | value | +| ------------------ | -------- | ----------------------------------------------------------------------------------------------------- | +| `ruleKind` | yes | the kind of rule the user was trying to create, non-empty; `none (onboarding)` on the onboarding path | +| `verbatim` | no | the user's own words, unedited | +| `completed` | no | exactly `Yes`, `No`, or `Unknown` | +| `workedWell` | no | what worked well | +| `needsImprovement` | no | what could use improvement | +| `agents` | no | the open-source, publicly available agent(s) or framework(s) in use | +| `mostValuableRule` | no | the rule creating the most value for the team, and why | + +For `kind: "general"`: + +| key | required | value | +| ---------- | -------- | ------------------------------------------------------------------------ | +| `verbatim` | yes | the user's feedback in their own words, unedited | +| `context` | no | the agent's account of what led to the feedback, as approved by the user | + +For `kind: "bug"`: + +| key | required | value | +| ---------- | -------- | ------------------------------------ | +| `summary` | yes | a one-line summary of the bug | +| `trying` | yes | what the user was trying to do | +| `expected` | yes | the expected result | +| `actual` | yes | the actual result | +| `context` | no | anything else that would help fix it | + +The CLI SHALL own the map from these keys to each survey's question identifiers; a payload SHALL NOT contain `$survey_*` keys. An omitted optional key SHALL be omitted from the event rather than sent as an empty string, and a key that is present SHALL be non-blank. Each recipe SHALL embed the schema for its own kind through the recipe input-schema mechanism. + +#### Scenario: Optional answers are omitted, not blanked + +- **WHEN** a `rule` payload has no `workedWell` +- **THEN** the `survey sent` event SHALL carry no `$survey_response_` key for that question + +#### Scenario: Completion is one of three literals + +- **WHEN** a `rule` payload has `completed: "partially"` +- **THEN** validation SHALL fail naming `completed` + +#### Scenario: The rule kind alone is a complete response + +- **WHEN** a payload is `{ "kind": "rule", "ruleKind": "ast-grep, forbid eval in TypeScript" }` +- **THEN** validation SHALL pass +- **AND** the `survey sent` event SHALL carry `$survey_id` and exactly one `$survey_response_` key + +#### Scenario: A missing rule kind is rejected + +- **WHEN** a `rule` payload carries every optional key and no `ruleKind` +- **THEN** validation SHALL fail naming `ruleKind` + +#### Scenario: A payload without a kind is rejected + +- **WHEN** a payload is `{ "ruleKind": "ast-grep, forbid eval in TypeScript" }` +- **THEN** validation SHALL fail naming `kind` + +#### Scenario: Keys from another kind are rejected + +- **WHEN** a `general` payload carries `ruleKind` +- **THEN** validation SHALL fail naming `ruleKind` + +#### Scenario: A bug report needs its four required answers + +- **WHEN** a `bug` payload omits `expected` +- **THEN** validation SHALL fail naming `expected` + +## REMOVED Requirements + +### Requirement: The feedback and feedback-invite recipes + +**Reason**: The invited survey's recipe is renamed from `feedback` to `rule-feedback`, so that `feedback` can name the general channel. A MODIFIED block cannot rename a requirement. +**Migration**: Replaced by "The rule-feedback and feedback-invite recipes", which carries the same content under the new recipe name. + +## ADDED Requirements + +### Requirement: The rule-feedback and feedback-invite recipes + +The CLI SHALL embed a `rule-feedback` recipe that tells the agent it is the respondent: it records the user's reply verbatim when there is one, fills the remaining answers from its own observation of the session, writes the payload with `kind: "rule"` to `.taskless/.tmp-feedback.json`, runs `feedback send --from` that path, and deletes the file afterwards. When the user replied `review`, it SHALL show every answer in the payload in the chat, labelled by key and exactly as it will be sent, name the keys it omitted, and offer to correct anything before sending; after each round of corrections it SHALL show the corrected payload in full and ask again, and it SHALL run `feedback send` only on the user's go-ahead, and SHALL run `feedback dismiss` instead if the user then decides not to send. The recipe SHALL embed the `rule` payload schema, SHALL NOT ask the agent to put the survey's questions to the user one by one, and SHALL NOT permit a follow-up question other than those correction rounds. It SHALL tell the agent to answer `ruleKind` with `none (onboarding)` when the surveyed recipe was `onboard`, to name only open-source, publicly available software in `agents`, and to omit `mostValuableRule` rather than ask the user for it. + +The CLI SHALL embed a `feedback-invite` recipe carrying the text appended to surveyed recipes. Rendered header-less, it SHALL instruct the agent to ask the user exactly once, with the sentence "Taskless would like to know how this went. Anything you'd like to add in your own words? Reply `skip` if not, and I'll send my own notes on the session, or `review` to see what I'd send before it goes."; to treat a reply of `skip`, silence, or a reply unrelated to feedback as an omitted `verbatim` and still fetch `agent rule-feedback`; to treat a reply of `review`, alone or alongside the user's words, as a request to see the answers before they are sent, with any other words in it as `verbatim`, and fetch `agent rule-feedback`; to treat any other reply as the user's words and fetch `agent rule-feedback`; and, only when the user explicitly asks for nothing to be sent, to run `feedback dismiss` and name the telemetry opt-out environment variables in one line. Both recipes SHALL follow the recipe conventions: a `# Topic:` header, the CLI named by its rendered invocation, and commands that exist. + +#### Scenario: The appended invite carries no header + +- **WHEN** the gate is open and a surveyed recipe is served +- **THEN** the appended text SHALL NOT contain a second `# Topic:` line +- **AND** SHALL name the rendered CLI invocation for both `feedback dismiss` and `agent rule-feedback` + +#### Scenario: The rule-feedback recipe embeds the schema + +- **WHEN** an agent runs `taskless agent rule-feedback` +- **THEN** stdout SHALL open with `# Topic: rule-feedback` and contain the JSON Schema for the `rule` payload + +#### Scenario: An explicit refusal is honoured + +- **WHEN** the user replies to the invite asking that nothing be sent +- **THEN** the invite SHALL direct the agent to `feedback dismiss` +- **AND** SHALL direct it to name `DO_NOT_TRACK=1` or `TASKLESS_TELEMETRY_DISABLED=1` as the switch for the rest of telemetry + +#### Scenario: A skip still sends + +- **WHEN** the user replies `skip` to the invite +- **THEN** the invite SHALL direct the agent to `agent rule-feedback` rather than `feedback dismiss` +- **AND** the rule-feedback recipe SHALL direct the agent to omit `verbatim` and send the rest + +#### Scenario: A review shows the answers before they are sent + +- **WHEN** the user replies `review` to the invite +- **THEN** the invite SHALL direct the agent to `agent rule-feedback` rather than `feedback dismiss` +- **AND** the rule-feedback recipe SHALL direct the agent to show every answer in the chat and offer to correct them before running `feedback send` +- **AND** SHALL direct the agent to show the corrected payload again after each correction, sending only on the user's go-ahead +- **AND** SHALL direct the agent to `feedback dismiss` if the user then decides not to send + +### Requirement: The general feedback recipe + +The CLI SHALL embed a `feedback` recipe for feedback the user chooses to give about Taskless. It SHALL direct the agent to take the user's feedback in their own words as `verbatim`, to draft `context` from what it observed in the session that bears on that feedback, and to keep both free of secrets, credentials, absolute paths, and source code the user has not chosen to share. Before sending, the agent SHALL show the user the complete payload and SHALL send only on the user's explicit agreement; a refusal or an edit request SHALL be honoured before anything is sent. The recipe SHALL write the payload with `kind: "general"` to `.taskless/.tmp-feedback.json`, run `feedback send --from` that path, delete the file afterwards, and embed the `general` payload schema. It SHALL tell the agent that when the CLI reports telemetry is disabled, it relays that to the user along with `https://github.com/taskless/cli/issues`. + +#### Scenario: The general recipe embeds its schema + +- **WHEN** an agent runs `taskless agent feedback` +- **THEN** stdout SHALL open with `# Topic: feedback` and contain the JSON Schema for the `general` payload + +#### Scenario: Nothing is sent without the user's yes + +- **WHEN** the agent has drafted a `general` payload +- **THEN** the recipe SHALL direct it to show the user the payload and wait for explicit agreement before running `feedback send` + +#### Scenario: The general recipe carries no invite + +- **WHEN** the survey gate is open and an agent runs `taskless agent feedback` +- **THEN** stdout SHALL be the recipe with no survey invite appended + +### Requirement: The bug-report recipe + +The CLI SHALL embed a `bug-report` recipe for reporting a bug in Taskless without a GitHub account. It SHALL direct the agent to draft `summary`, `trying`, `expected`, `actual`, and `context` from the session, to ask the user only for what the session does not show, and to keep every answer free of secrets, credentials, absolute paths, and source code the user has not chosen to share. Before sending, the agent SHALL show the user the complete payload and SHALL send only on the user's explicit agreement. The recipe SHALL write the payload with `kind: "bug"` to `.taskless/.tmp-feedback.json`, run `feedback send --from` that path, delete the file afterwards, and embed the `bug` payload schema. It SHALL NOT ask the agent for version information, which the CLI supplies. It SHALL tell the agent that when the CLI reports telemetry is disabled, it relays that to the user along with `https://github.com/taskless/cli/issues`. + +#### Scenario: The bug-report recipe embeds its schema + +- **WHEN** an agent runs `taskless agent bug-report` +- **THEN** stdout SHALL open with `# Topic: bug-report` and contain the JSON Schema for the `bug` payload +- **AND** the schema SHALL NOT contain a version-information key + +#### Scenario: Nothing is sent without the user's yes + +- **WHEN** the agent has drafted a `bug` payload +- **THEN** the recipe SHALL direct it to show the user the payload and wait for explicit agreement before running `feedback send` + +### Requirement: The CLI supplies a bug report's version information + +When `feedback send` sends a `bug` payload, the CLI SHALL answer the bug survey's version-information question itself, from local state only: the running CLI version, the installed scaffold version and rules reconciliation marker from `.taskless/taskless.json` when present, the operating system platform and architecture, and the Node.js version. It SHALL NOT include the user's login, email, organizations, repository URL, or any absolute path, and SHALL NOT make a network call to build it. A missing or unreadable `.taskless/` SHALL produce the answer without those fields rather than an error. + +#### Scenario: Version information is filled in + +- **WHEN** an agent sends a valid `bug` payload from a directory with an initialised `.taskless/` +- **THEN** the `survey sent` event SHALL carry the version-information response, naming the running CLI version and the installed scaffold version + +#### Scenario: No project, still a report + +- **WHEN** an agent sends a valid `bug` payload from a directory with no `.taskless/` +- **THEN** the command SHALL succeed and the version-information response SHALL name the running CLI version + +#### Scenario: Nothing identifying in the version answer + +- **WHEN** the user is logged in and sends a `bug` payload +- **THEN** the version-information response SHALL NOT contain their login, email, organization names, or the repository URL diff --git a/openspec/changes/feedback-channels/tasks.md b/openspec/changes/feedback-channels/tasks.md new file mode 100644 index 00000000..b5e4ee50 --- /dev/null +++ b/openspec/changes/feedback-channels/tasks.md @@ -0,0 +1,33 @@ +## 1. Survey registry and payload schema + +- [x] 1.1 Restructure `packages/cli/src/survey/constants.ts` into a `SURVEYS` registry keyed by `rule` / `general` / `bug` (ids and question ids per design.md's table), keep a `RULE_SURVEY_ID` export, and point `survey/invite.ts` and the cadence calls at it; verify `pnpm --filter @taskless/cli typecheck` passes and `survey-invite.test.ts` / `survey-cadence.test.ts` still pass unchanged +- [x] 1.2 Rewrite `packages/cli/src/schemas/feedback.ts` as a `z.discriminatedUnion("kind", …)` of three `z.strictObject` branches, each declaring `kind` as a required `z.literal(…)` with no default, exporting each branch schema; verify with new cases in `feedback-schema.test.ts`: missing `kind` names `kind`, a `general` payload with `ruleKind` names `ruleKind`, a `bug` payload without `expected` names `expected`, each branch's rendered JSON Schema lists `kind` as required with a single `const`, and every existing rule-schema case passes with `kind: "rule"` added +- [x] 1.3 Make `buildSurveyResponse` map through `SURVEYS[input.kind]`; verify a `feedback-command.test.ts` case per kind asserts the `$survey_id` and the exact set of `$survey_response_` keys + +## 2. `feedback send` behavior + +- [ ] 2.1 Add the CLI-built bug version-information answer (CLI version, `install.cliVersion`, `rules.reconciledTo`, platform/arch, Node version; no network, no identity) and attach it to `bug` sends only; verify tests for an initialised project, a directory with no `.taskless/`, and a logged-in token whose login/email/org/repository URL do not appear in the answer +- [ ] 2.2 Advance `next_ask` only for `kind: "rule"`; verify a test that sends `general` and `bug` payloads and asserts `next_ask` is unchanged and no cadence file exists for either survey +- [ ] 2.3 Change the telemetry-off message for `feedback send` to say telemetry is disabled and name `https://github.com/taskless/cli/issues`, keeping validation first; verify tests under `DO_NOT_TRACK=1` for a valid payload of each kind (exit 0, URL printed, nothing captured) and an invalid one (exit 1, `INVALID_INPUT`) + +## 3. Recipes and the agent index + +- [ ] 3.1 `git mv packages/cli/src/agent/feedback.md packages/cli/src/agent/rule-feedback.md`, update its header/topic name and its payload instructions to include `kind: "rule"`, and repoint `feedback-invite.md` at `agent rule-feedback`; verify `pnpm cli agent rule-feedback` (after `pnpm build`) opens with `# Topic: rule-feedback` and embeds the rule schema +- [ ] 3.2 Write the new `packages/cli/src/agent/feedback.md` (general channel: user's words as `verbatim`, agent-drafted `context`, redaction, show-payload-and-wait-for-yes, telemetry-off relay with the issues URL); verify `pnpm cli agent feedback` embeds only the `general` schema +- [ ] 3.3 Write `packages/cli/src/agent/bug-report.md` (agent drafts from the session, asks only for gaps, redaction, show-payload-and-wait-for-yes, no version-information ask, telemetry-off relay); verify `pnpm cli agent bug-report` embeds only the `bug` schema and contains no version key +- [ ] 3.4 Wire `TOPIC_INPUT_SCHEMAS` in `prompts/recipes.ts` (`rule-feedback` → rule branch, `feedback` → general, `bug-report` → bug) and add `rule-feedback` and `bug-report` to `INTERNAL_TOPICS` in `prompts/index.ts`; verify `prompts.test.ts` and `feedback-recipes.test.ts` pass after updating them for the new topic names +- [ ] 3.5 Add a `FEEDBACK_TOPICS` section to the `taskless agent` index in `commands/agent.ts` listing `feedback` and `bug-report`, keep the `feedback` command in `UNLISTED_COMMANDS` with its comment rewritten per design.md; verify a test that the index lists both and does not list `rule-feedback` +- [ ] 3.6 Update the `feedback` command's `meta.description` and the comment above `feedbackCommand` to describe three channels; verify `pnpm cli feedback --help` reads correctly +- [ ] 3.7 Run Vale over the three recipes and fix findings; verify `pnpm lint` reports no recipe prose errors + +## 4. Skill, spec purpose, and changeset + +- [ ] 4.1 Add "send feedback to Taskless" and "report a Taskless bug" triggers to the `description` in `skills/taskless/SKILL.md` and two rows to its Topics table (`agent feedback`, `agent bug-report`); verify any skill parity/render test passes +- [ ] 4.2 Update the `## Purpose` of `openspec/specs/cli-feedback-survey/spec.md` to cover all three channels; verify `pnpm openspec validate feedback-channels --strict` passes +- [ ] 4.3 Add one `patch` changeset describing the general and bug-report channels, the `rule-feedback` rename, and the required `kind`; verify `.changeset/` contains exactly one new file + +## 5. Verification + +- [ ] 5.1 Run `pnpm typecheck`, `pnpm lint`, and `pnpm test`; all pass +- [ ] 5.2 Run the pre-archive scenario check from CLAUDE.md (commit, `pnpm openspec archive feedback-channels -y`, confirm every prior `#### Scenario` under the invite and cadence requirements is still present plus the new ones, reset to the saved SHA) +- [ ] 5.3 With telemetry enabled against the real project, send one payload of each kind via `pnpm cli feedback send` and confirm each response appears under its survey in PostHog with the right questions answered From 7230c0c7da05597dce20676b5b3830292bd72e42 Mon Sep 17 00:00:00 2001 From: Jakob Heuser Date: Thu, 8 Oct 2026 17:22:49 -0700 Subject: [PATCH 2/3] docs(openspec): land feedback-channels with an atomic stack merge --- openspec/changes/feedback-channels/proposal.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openspec/changes/feedback-channels/proposal.md b/openspec/changes/feedback-channels/proposal.md index 24cbed2a..f8dc9189 100644 --- a/openspec/changes/feedback-channels/proposal.md +++ b/openspec/changes/feedback-channels/proposal.md @@ -37,4 +37,4 @@ Feedback from the CLI today goes one way: the rule-authoring survey the CLI invi ## Delivery shape -**Stacked, merging down.** 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. So the stack lands on `main` in one protected merge, after each layer merges down into its parent. 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. +**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. From cb65006b171a58bed413e3d57e9bbed5a5bdb5f4 Mon Sep 17 00:00:00 2001 From: Jakob Heuser Date: Thu, 8 Oct 2026 17:26:56 -0700 Subject: [PATCH 3/3] docs(openspec): leave feedback-channels section 1 unchecked until the schema layer lands it --- openspec/changes/feedback-channels/tasks.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openspec/changes/feedback-channels/tasks.md b/openspec/changes/feedback-channels/tasks.md index b5e4ee50..0ebc3284 100644 --- a/openspec/changes/feedback-channels/tasks.md +++ b/openspec/changes/feedback-channels/tasks.md @@ -1,8 +1,8 @@ ## 1. Survey registry and payload schema -- [x] 1.1 Restructure `packages/cli/src/survey/constants.ts` into a `SURVEYS` registry keyed by `rule` / `general` / `bug` (ids and question ids per design.md's table), keep a `RULE_SURVEY_ID` export, and point `survey/invite.ts` and the cadence calls at it; verify `pnpm --filter @taskless/cli typecheck` passes and `survey-invite.test.ts` / `survey-cadence.test.ts` still pass unchanged -- [x] 1.2 Rewrite `packages/cli/src/schemas/feedback.ts` as a `z.discriminatedUnion("kind", …)` of three `z.strictObject` branches, each declaring `kind` as a required `z.literal(…)` with no default, exporting each branch schema; verify with new cases in `feedback-schema.test.ts`: missing `kind` names `kind`, a `general` payload with `ruleKind` names `ruleKind`, a `bug` payload without `expected` names `expected`, each branch's rendered JSON Schema lists `kind` as required with a single `const`, and every existing rule-schema case passes with `kind: "rule"` added -- [x] 1.3 Make `buildSurveyResponse` map through `SURVEYS[input.kind]`; verify a `feedback-command.test.ts` case per kind asserts the `$survey_id` and the exact set of `$survey_response_` keys +- [ ] 1.1 Restructure `packages/cli/src/survey/constants.ts` into a `SURVEYS` registry keyed by `rule` / `general` / `bug` (ids and question ids per design.md's table), keep a `RULE_SURVEY_ID` export, and point `survey/invite.ts` and the cadence calls at it; verify `pnpm --filter @taskless/cli typecheck` passes and `survey-invite.test.ts` / `survey-cadence.test.ts` still pass unchanged +- [ ] 1.2 Rewrite `packages/cli/src/schemas/feedback.ts` as a `z.discriminatedUnion("kind", …)` of three `z.strictObject` branches, each declaring `kind` as a required `z.literal(…)` with no default, exporting each branch schema; verify with new cases in `feedback-schema.test.ts`: missing `kind` names `kind`, a `general` payload with `ruleKind` names `ruleKind`, a `bug` payload without `expected` names `expected`, each branch's rendered JSON Schema lists `kind` as required with a single `const`, and every existing rule-schema case passes with `kind: "rule"` added +- [ ] 1.3 Make `buildSurveyResponse` map through `SURVEYS[input.kind]`; verify a `feedback-command.test.ts` case per kind asserts the `$survey_id` and the exact set of `$survey_response_` keys ## 2. `feedback send` behavior